Skip to content

fix(dispatcher): multi-repo coordination — close the real holes#229

Merged
thejustinwalsh merged 8 commits into
mainfrom
middle-issue-211
Jun 4, 2026
Merged

fix(dispatcher): multi-repo coordination — close the real holes#229
thejustinwalsh merged 8 commits into
mainfrom
middle-issue-211

Conversation

@thejustinwalsh

@thejustinwalsh thejustinwalsh commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #211

Closes the three real multi-repo coordination holes the audit found: cross-repo Epic blockers were ignored at runtime, two repos could silently share one checkout, and a hung recommender on one repo stalled all the others. One Epic = one branch = one PR; the three commits map one-to-one to the closed sub-issues (#225, #226, #227).

Status

What changed

  • packages/dispatcher/src/blocker-resolution.ts (new) — pure ref-parse + reclassify: same-repo #n and cross-repo owner/repo#n blockers resolved against live state.
  • packages/dispatcher/src/workflows/recommender.ts — new resolve-blockers step (after the dispatcher-section reapply, before verify).
  • packages/dispatcher/src/github.ts / epic-store/* — new EpicGateway.getIssueState (github via gh issue view; file via the Epic file) + routing.
  • packages/state-issue/src/validate.ts + schemas/state-issue.v1.md — blocker grammar relaxed for cross-repo + title/stale annotations.
  • packages/dispatcher/src/repo-config.tsRepoPathCollisionError + assertNoRepoPathCollision (path-normalized); registerManagedRepo rejects a shared checkout.
  • packages/cli/src/{commands/init.ts,bootstrap/*} + index.tsmm init runs the guard before scaffolding; exits non-zero on collision.
  • packages/dispatcher/src/hook-server.ts/control/dispatch maps a collision to 400.
  • packages/dispatcher/src/recommender-cron.ts + packages/core/src/config.ts — concurrent per-repo cron pass behind a bounded pool + per-repo timeout; new [recommender] max_concurrent_repos / run_timeout_seconds.

Acceptance criteria

Why these changes

The audit found BlockedItem.blocker had no runtime consumer, so a cross-repo block was permanent. Resolution is deterministic (a workflow step), not the agent's judgment, because an LLM can't reliably reach repo B and a deterministic unblock is reproducible. The collision guard is one helper shared by the registry write and an early mm init hook so a rejected init scaffolds nothing. The cron parallelization stamps all due repos before fanning out (keeping the double-dispatch guard) and wraps each run in a hard timeout so one hang is isolated.

Verification

Internal review

Ran a clean-eyes adversarial review pass over the diff before marking ready. It found one real bug (an empty blocker title produced #42 (), which the verify step's validate then rejected) and two adjacent hardenings (untruncated annotation title; collision guard missing trailing-slash/dot-segment path variants) — all fixed with regression tests in 6ae8348, then re-reviewed clean.

Decisions

Distilled into inline review comments on this PR; full rationale in planning/issues/211/decisions.md.

Stumbling points

  • The feat(dispatcher): BlockedItem.blocker runtime resolution (cross-repo unblock) #225 integration test hung when each tick created a fresh bunqueue Engine — embedded engines share a process-singleton queue manager (per packages/dispatcher/CLAUDE.md). Fixed by registering once on a shared engine and ticking via engine.start (mirrors recommender-workflow.test.ts).
  • validate() rejected the resolution pass's own annotated output until the blocker grammar was relaxed — a reminder that the verify step validates the post-resolution body.

Suggested CLAUDE.md updates

None — the bunqueue single-engine gotcha is already documented in packages/dispatcher/CLAUDE.md.

Follow-up issues

None filed. Two items are documented design boundaries, not discovery: cross-repo Epic-file references (file mode) are explicitly out of scope for #225 (a schema-v2 step), and the cron's per-repo failure surface is the log + rolled-back watermark (the cron has no state-issue writer of its own).

Out of scope

  • Cross-repo file-mode Epic references; repos sharing a checkout via legitimate git worktrees; replacing bunqueue's cron; adaptive rate-limit-aware concurrency.

Summary by CodeRabbit

  • New Features

    • Cross-repo blocker resolution: closed cross-repo blockers move to ready-to-dispatch; resolved titles annotated.
    • Configurable recommender cron: max concurrency and per-repo timeouts.
    • Init guard: early failure when a repo checkout path collides (skipped in dry-run).
  • Bug Fixes

    • Per-repo failures/timeouts are isolated; one hang/throw no longer blocks others.
    • Dispatch/control route returns 400 for checkout-path collisions.
  • Documentation

    • Updated blocker schema and decision notes.
  • Tests

    • New end-to-end and unit tests for collisions, blocker resolution, and cron parallelism.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b0014bb-ab68-4807-bf72-92e86deded3f

📥 Commits

Reviewing files that changed from the base of the PR and between 26d649c and 1d37770.

📒 Files selected for processing (1)
  • packages/cli/test/db-scripts.test.ts

📝 Walkthrough

Walkthrough

Adds deterministic cross-repo blocker resolution (gateway + engine + workflow), a normalized shared-checkout collision guard wired into init/registration/dispatch with DB uniqueness migration, and a two-phase recommender cron with bounded concurrency and per-repo timeouts plus tests.

Changes

Cross-repo blocker resolution

Layer / File(s) Summary
IssueState & EpicGateway implementations
packages/dispatcher/src/github.ts, packages/dispatcher/src/epic-store/file-epic-gateway.ts, packages/dispatcher/src/epic-store/index.ts
Adds IssueState type, EpicGateway.getIssueState, mapGhIssueState, and file/routing gateway implementations.
Blocker parsing & resolution engine
packages/dispatcher/src/blocker-resolution.ts
Adds parseBlockerRef, resolveBlockers, title sanitization/truncation, and Ready-row construction for closed blockers.
Recommender workflow wiring
packages/dispatcher/src/workflows/recommender.ts, packages/dispatcher/src/recommender-run.ts, packages/dispatcher/src/main.ts
Injects epicGateway, inserts resolve-blockers step after reapply and before verify, optionally prefetches open epics, and applies resolved state writes.
Validator, schema & docs
packages/state-issue/src/validate.ts, schemas/state-issue.v1.md, planning/issues/211/*
Validator accepts cross-repo/annotated blocker formats; schema and decision docs updated.
Tests (unit & integration)
packages/dispatcher/test/*, packages/state-issue/test/*
Extensive tests for parsing, resolution, truncation, idempotency, gateway behavior, and end-to-end multi-repo blocker flows.

Shared checkout path collision guard

Layer / File(s) Summary
Collision primitives & normalization
packages/dispatcher/src/repo-config.ts
Adds RepoPathCollisionError, assertNoRepoPathCollision, and normalizes checkout_path via path.resolve.
Registration & DB migration
packages/dispatcher/src/repo-config.ts, packages/dispatcher/src/db/migrations/011_repo_checkout_path_unique.sql
Normalize checkoutPath before upsert, eagerly assert collisions, add migration 011 to dedupe and create partial UNIQUE INDEX on non-NULL checkout_path.
CLI init wiring
packages/cli/src/bootstrap/types.ts, packages/cli/src/bootstrap/init.ts, packages/cli/src/commands/init.ts, packages/cli/src/index.ts
Add checkCollision option, implement checkRepoCollisionInDaemonDb helper, and pass guard into mm init (skips in --dry-run).
Dispatch route mapping
packages/dispatcher/src/hook-server.ts
Map RepoPathCollisionError to HTTP 400 on POST /control/dispatch.
Tests
packages/cli/test/init-collision.test.ts, packages/dispatcher/test/repo-config.test.ts, packages/dispatcher/test/control-routes.test.ts
Tests for init collision behavior, repo-config uniqueness/normalization, control-route 400 mapping, and migration behavior.

Recommender cron parallelization

Layer / File(s) Summary
Config knobs
packages/core/src/config.ts, packages/core/test/config.test.ts
Add maxConcurrentRepos? and runTimeoutMs? to recommender settings; map TOML max_concurrent_repos and run_timeout_seconds → ms.
Two-phase cron pass & worker pool
packages/dispatcher/src/recommender-cron.ts
Add defaults, validate knobs, stamp due repos synchronously, then run bounded concurrent worker pool with per-repo withTimeout and rollback of stamps on timeout/error.
Daemon wiring & tests
packages/dispatcher/src/main.ts, packages/dispatcher/test/recommender-cron-parallel.test.ts
Forward config knobs into cron deps and add tests for timeout isolation, error isolation, bounded concurrency, single-repo success, and invalid-knob fallback behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Recommender as Recommender Workflow
  participant EpicGateway as EpicGateway.getIssueState
  participant GitHub as gh CLI / GitHub
  participant FileEpic as File Epic

  Recommender->>Recommender: reapply-dispatcher-sections
  Recommender->>EpicGateway: getIssueState(repoB, "123")
  EpicGateway->>FileEpic: check local epic file
  FileEpic-->>EpicGateway: {state,title} or null
  EpicGateway->>GitHub: gh issue view (if numeric)
  GitHub-->>EpicGateway: {state,title} or null
  EpicGateway-->>Recommender: IssueState | null
  Recommender->>Recommender: annotate open blocker or move closed → readyToDispatch
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(dispatcher): multi-repo coordination — close the real holes' directly corresponds to the main objective of closing three real coordination holes in the dispatcher.
Linked Issues check ✅ Passed The PR comprehensively addresses all three sub-issue requirements: cross-repo blocker resolution (blocker-resolution.ts, workflow step), collision guard (repo-config.ts, RepoPathCollisionError, assertNoRepoPathCollision), and recommender cron parallelization (recommender-cron.ts timestamps and bounded concurrency). Integration tests verify acceptance criteria including the two-repo blocker fixture exercise.
Out of Scope Changes check ✅ Passed All changes are within scope: blocker resolution (dispatcher, schema, validator), collision guard (repo-config, CLI wiring, hook-server mapping), recommender cron parallelization (stamping, bounded pool, per-repo timeouts), and supporting wiring/tests. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

…ime (#225)

Add a deterministic resolve-blockers step to the recommender workflow that
parses each BlockedItem.blocker, resolves same-repo (#n) and cross-repo
(owner/repo#n) issue references against live state via the routing EpicGateway,
and reclassifies the blocked item: a closed blocker unblocks it into Ready to
dispatch, an open one annotates the line with the blocker title, an unresolvable
one gets a (stale blocker: <ref>) suffix. Backticked non-issue blockers stay put.

- New EpicGateway.getIssueState (github via gh issue view; file via the Epic file)
- New pure blocker-resolution module (unit-tested) + workflow wiring
- Relax the state-issue validator + schema doc for cross-repo/annotated blockers
- Integration test drives the real workflow through the engine, observed via the
  live state body
Two repo slugs pointing at one checkout collide on git worktree add — the
second mm init silently overwrote the in-memory path map. Add a collision guard:

- registerManagedRepo throws RepoPathCollisionError when a DIFFERENT repo is
  already registered for the same checkout_path; same-slug re-register stays
  idempotent. New assertNoRepoPathCollision helper.
- mm init runs the guard (injected checkCollision hook) after the slug resolves
  but before any files are written, so a rejected init scaffolds nothing; it
  exits non-zero naming both repos + the shared path.
- The /control/dispatch route maps the collision to a 400 (naming both repos),
  re-throwing any other error.

Tests: repo-config (a/b/c cases), control-routes 400 mapping, init e2e asserting
the second repo's .middle/<slug>.toml is never written.
@thejustinwalsh

thejustinwalsh commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Verification gates — phase #225

All 4 verification gate(s) passed for phase #225.

Gate Result Duration
format ✅ pass 0.3s
lint ✅ pass 0.1s
typecheck ✅ pass 2.1s
test ✅ pass 87.3s
format — ✅ pass (0.3s)
$ bun run format
Finished in 204ms on 337 files using 24 threads.

[stderr]
$ oxfmt

lint — ✅ pass (0.1s)
$ bun run lint
Found 0 warnings and 0 errors.
Finished in 58ms on 303 files with 95 rules using 24 threads.

[stderr]
$ oxlint --fix --deny-warnings

typecheck — ✅ pass (2.1s)
[stderr]
$ tsc --noEmit

test — ✅ pass (87.3s)
$ bun test
bun test v1.3.14 (0d9b296a)

[stderr]

packages/docs/test/resolve.test.ts:
(pass) resolveDocsTarget — detection > detects Starlight from astro.config + @astrojs/starlight [0.40ms]
(pass) resolveDocsTarget — detection > Starlight wins over co-resident TypeDoc [0.05ms]
(pass) resolveDocsTarget — detection > detects Docusaurus from docusaurus.config.js [0.04ms]
(pass) resolveDocsTarget — detection > detects MkDocs and reads a custom docs_dir [0.09ms]
(pass) resolveDocsTarget — detection > detects MkDocs with the default docs_dir [0.06ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from typedoc.json and reads out [0.08ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from a package.json typedoc key [0.04ms]
(pass) resolveDocsTarget — markdown fallback > falls back to markdown in docs/ when nothing is detected [0.05ms]
(pass) resolveDocsTarget — markdown fallback > a bare Astro site (no Starlight signal) does not match Starlight [0.11ms]
(pass) resolveDocsTarget — markdown fallback > resolves to markdown on a nonexistent path [0.17ms]
(pass) resolveDocsTarget — config override > tool override forces the framework, ignoring detection [0.07ms]
(pass) resolveDocsTarget — config override > tool override beats a detected framework [0.01ms]
(pass) resolveDocsTarget — config override > tool + path override sets both framework and root [0.03ms]
(pass) resolveDocsTarget — config override > path override alone overrides a detected target's root [0.05ms]
(pass) resolveDocsTarget — config override > path override alone overrides the fallback root [0.05ms]
(pass) resolveDocsTarget — config override > an unknown tool override throws with the valid names [0.06ms]
(pass) resolveOutputPath — slug normalization > strips a leading slash and an existing .md/.mdx extension [0.05ms]
(pass) DOCS_TARGET_NAMES > lists every resolvable target [0.02ms]

packages/docs/test/util.test.ts:
(pass) makeTarget.resolveOutputPath — path safety > nested slugs route into subfolders (preserved behavior) [0.03ms]
(pass) makeTarget.resolveOutputPath — path safety > leading slashes are stripped, never absolute [0.01ms]
(pass) makeTarget.resolveOutputPath — path safety > an .md/.mdx extension on the slug is not doubled [0.01ms]
(pass) makeTarget.resolveOutputPath — path safety > traversal segments cannot escape docsRoot [0.02ms]
(pass) makeTarget.resolveOutputPath — path safety > interior traversal segments are dropped too
(pass) makeTarget.resolveOutputPath — path safety > backslashes are normalized to POSIX separators
(pass) makeTarget.resolveOutputPath — path safety > an empty docsRoot stays repo-relative (no leading slash) [0.02ms]
(pass) readJsonIfExists — contract > a JSON object is returned as a Record [0.08ms]
(pass) readJsonIfExists — contract > a JSON array is rejected (not a Record<string, unknown>) [0.05ms]
(pass) readJsonIfExists — contract > a JSON scalar is rejected [0.02ms]

packages/dashboard/test/guard.test.ts:
(pass) makeGuard > surfaces a rejection as an error keyed by source [0.18ms]
(pass) makeGuard > a non-Error rejection is stringified [0.05ms]
(pass) makeGuard > success clears only its own source's error, never another source's [0.08ms]
(pass) makeGuard > REGRESSION: a nested same-source guard masks the inner failure [0.06ms]
(pass) makeGuard > FIX: awaiting raw work inside one guard surfaces the failure [0.05ms]

packages/dashboard/test/server.test.ts:
(pass) createDashboardRoutes maps /api/* and /events/* to the deps seam [78.24ms]

packages/dashboard/test/runs-deps.test.ts:
(pass) createDbDeps.listRuns > returns only non-implementation kinds, newest-first within kind [84.10ms]
(pass) createDbDeps.listRuns > projects duration, active, transcript, and session fallback [74.54ms]
(pass) createDbDeps.listRuns > outputLink: recommender → state issue, documentation → PR, else null [78.60ms]
(pass) createDbDeps.listRuns > caps at 20 per kind [174.51ms]

packages/dashboard/test/epics-api.test.ts:
(pass) /api/epics > GET /api/epics/:repo returns the card list [0.39ms]
(pass) /api/epics > POST /api/epics/:repo/:n/dispatch forwards adapter + status/body [0.18ms]
(pass) /api/epics > dispatch 404s when no dispatch seam is wired [0.05ms]
(pass) /api/epics > dispatch rejects a missing adapter with 400 [0.05ms]
(pass) /api/epics > POST /api/epics/:repo/refresh forwards [0.05ms]

packages/dashboard/test/queue.test.tsx:
(pass) Queue shows an empty state with no data [3.87ms]
(pass) Queue renders nothing-in-flight row when live is empty [0.86ms]
(pass) Queue renders gauge tile labels and values from totals [0.67ms]
(pass) Queue renders epic as #N for a numeric epic and — for null [0.52ms]
(pass) Queue state cell carries the s-running class [0.41ms]
(pass) Queue renders rate-limit chip with adapter name, status, and chip class [0.34ms]
(pass) Queue sorts waiting-human rows before running rows [0.24ms]

packages/dashboard/test/epic-ref.test.tsx:
(pass) EpicRef > github mode renders plain `#N` text, no anchor (AC4: no behavior change) [0.19ms]
(pass) EpicRef > github mode renders `#N` even if a backfilled epic_ref is also present [0.05ms]
(pass) EpicRef > file mode renders the slug as a file:// link to the Epic file, no GitHub link [0.14ms]
(pass) EpicRef > no-Epic (both null) renders the caller's fallback [0.08ms]
(pass) EpicRef > a blank epicRef (empty / whitespace) falls through to the fallback, not an empty link [0.06ms]
(pass) EpicRef > a slug with surrounding whitespace is trimmed in both label and href [0.05ms]
(pass) EpicRef > a slug with URL-unsafe / traversal chars is encoded into one safe path segment [0.01ms]
(pass) RunnerRow Epic rendering > file-mode runner shows the slug file:// link [0.63ms]
(pass) RunnerRow Epic rendering > github-mode runner is unchanged (`#7`, no link) [0.19ms]
(pass) RunnerRow Epic rendering > no-Epic runner keeps the `#—` fallback [0.13ms]
(pass) Inspector Epic rendering > file-mode panel shows the slug file:// link in the header [0.44ms]
(pass) Inspector Epic rendering > github-mode panel is unchanged (`#7`, no link) [0.28ms]

packages/dashboard/test/sse.test.ts:
(pass) dashboard SSE channels > GET /events/global delivers a broadcast on the global channel [69.13ms]
(pass) dashboard SSE channels > GET /events/repos/:repo delivers only that repo's events [65.35ms]
(pass) dashboard SSE channels > GET /events/sessions/:session delivers session timeline frames [64.91ms]
(pass) dashboard SSE channels > a rate-limit detection pushes a fresh banner on the global channel (the ≤2s path) [71.02ms]
(pass) dashboard SSE channels > a workflow transition pushes a `workflow` nudge on that repo's channel [79.42ms]
(pass) dashboard SSE channels > a file-mode transition pushes the epic_ref slug alongside a null epic [80.89ms]
(pass) dashboard SSE channels > disposing the workflow bridge stops the repo-channel nudges [87.45ms]
(pass) dashboard SSE channels > a malformed percent-encoded channel segment is a 400, not a crash [91.60ms]
(pass) dashboard SSE channels > the /events/* routes 503 when no bus is wired [70.36ms]
(pass) DashboardEventBus channel pruning > drained (zero-subscriber) channels are swept out on the next serve [65.09ms]

packages/dashboard/test/activity.test.tsx:
(pass) Activity > renders Recommender and Documentation sections [0.79ms]
(pass) Activity > shows an output link when present and omits it otherwise [0.29ms]
(pass) Activity > empty state per section when no runs of that kind [0.13ms]
(pass) Activity > renders a state label for each run [0.12ms]
(pass) Activity > state pill tone: completed is ok, compensated/failed are bad [0.35ms]

packages/dashboard/test/epics-deps.test.ts:
(pass) createDbDeps.listEpics > joins cache progress + state-issue decision/recommendation + free slots [90.23ms]
(pass) createDbDeps.listEpics > an in-flight workflow surfaces as the runner and flips inFlight [84.57ms]
(pass) createDbDeps.listEpics > a blocked Epic with no needs-human entry gets a blocked decision callout [84.71ms]
(pass) createDbDeps.listEpics > surfaces a file-mode Epic (slug ref, null number) and resolves its runner by ref (#200) [79.64ms]
(pass) createDbDeps.listEpics > dispatchEpic + refreshEpics delegate to the injected callbacks [63.15ms]

packages/dashboard/test/control-client.test.ts:
(pass) fetchControlMetrics parses the /control/metrics snapshot [0.17ms]
(pass) fetchControlMetrics throws on a non-OK response [0.10ms]

packages/dashboard/test/api.test.ts:
(pass) dashboard JSON API > GET /api/repos returns a JSON array of repo summaries [82.62ms]
(pass) dashboard JSON API > GET /api/repos/:repo returns NEXT UP + IN FLIGHT for a known repo [76.97ms]
(pass) dashboard JSON API > github-mode IN FLIGHT row carries epicRef alongside the numeric epic (#187) [74.80ms]
(pass) dashboard JSON API > file-mode IN FLIGHT row surfaces epic_ref as epicRef with a null epic (#187) [74.16ms]
(pass) dashboard JSON API > GET /api/sessions/:session carries epicRef for a file-mode runner (#187) [79.24ms]
(pass) dashboard JSON API > GET /api/repos/:repo 404s an unknown repo [65.51ms]
(pass) dashboard JSON API > GET /api/banner reports per-adapter rate limits (UNKNOWN unobserved) [65.58ms]
(pass) dashboard JSON API > GET /api/sessions/:session returns the Inspector runner panel with attach commands [79.67ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach control flips controlled_by and spawns a terminal [87.85ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach rejects an invalid mode [77.05ms]
(pass) dashboard JSON API > POST /api/rate-limits/:adapter/clear sets the adapter AVAILABLE [70.97ms]
(pass) dashboard JSON API > GET /api/sessions/:session/events validates the limit param [74.35ms]
(pass) dashboard JSON API > POST /api/repos/:repo/pause validates untilMs [79.46ms]
(pass) dashboard JSON API > a runner with no session_name is reachable by its workflow id [72.56ms]
(pass) dashboard JSON API > a malformed percent-encoded path segment is a 400, not a 500 [65.78ms]
(pass) dashboard JSON API > unknown /api routes 404 as JSON [65.83ms]

packages/dashboard/test/window.test.ts:
(pass) dashboard window launcher > missing URL argument is a usage error (exit 2) [9.15ms]
(pass) dashboard window launcher > an unavailable webview-bun degrades to a logged exit 0 (HTTP still serves) [8.22ms]

packages/dashboard/test/runs-api.test.ts:
(pass) /api/runs > GET /api/runs returns the run list [0.17ms]
(pass) /api/runs > a non-GET method on /api/runs is a 404 miss [0.06ms]

packages/dashboard/test/epics.test.tsx:
(pass) Epics > renders an Epic card with title, progress, and an enabled dispatch button [0.90ms]
(pass) Epics > empty state when there are no Epics [0.10ms]
(pass) Epics > a file-mode Epic renders a file:// slug link and disables in-dashboard dispatch (#200) [0.32ms]
(pass) Epics > disables dispatch when in flight [0.23ms]
(pass) Epics > disables dispatch when the chosen adapter has no free slot [0.19ms]
(pass) Epics > shows a decision callout when present [0.49ms]
(pass) Epics > renders the decision link as an anchor when present [0.33ms]

packages/dashboard/test/app.test.tsx:
(pass) App nav includes a queue tab [0.99ms]
(pass) App nav includes an activity tab [0.30ms]
(pass) api.runs reads runs from a live server [71.27ms]
(pass) App defaults to the Epics view (nav tab + empty state render) [0.45ms]
(pass) api.epics reads Epic cards from a live server [82.54ms]
(pass) applyWorkflowFrame upserts non-terminal and drops terminal workflows [0.15ms]
(pass) dashboard views (static render) > GlobalBanner shows per-adapter rate limits + GitHub quota [0.35ms]
(pass) dashboard views (static render) > NeedsYou lists aggregated items and an empty state [0.34ms]
(pass) dashboard views (static render) > RepoRow expansion shows slot pills, NEXT UP, IN FLIGHT, and an accurate attach command [0.48ms]
(pass) dashboard views (static render) > Inspector renders the per-runner panel, links, affordances, and timeline [0.56ms]
(pass) api-client against a live server > api.repos() + RepoRow render the live repo [78.55ms]
(pass) api-client against a live server > api.attach(control) flips controlled_by; api.release reverts it [87.05ms]
(pass) api-client against a live server > api.runRecommender surfaces a non-2xx as an ApiError [91.45ms]

packages/dashboard/test/settings.test.tsx:
(pass) settings round-trip through the API > GET /api/settings returns global + per-repo config [72.22ms]
(pass) settings round-trip through the API > POST /api/settings/global persists and is reflected back [73.49ms]
(pass) settings round-trip through the API > POST /api/settings/global rejects a non-positive maxConcurrent [71.62ms]
(pass) settings round-trip through the API > pause/resume toggles a repo's auto-dispatch [81.58ms]
(pass) settings round-trip through the API > the rate-limit override button's endpoint sets the adapter AVAILABLE [76.45ms]
(pass) Settings view (static render) > renders global fields, rate-limit override, and per-repo auto toggle [69.81ms]

packages/dashboard/test/spa.test.ts:
Bundled page in 23ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > GET / serves the bundled HTML shell [88.99ms]
Bundled page in 41ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the bundled entry script transpiles the TSX app [109.19ms]
Bundled page in 21ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the JSON API coexists with the SPA fallback on the same server [95.56ms]

packages/state-issue/test/validate.test.ts:
(pass) validate > passes a schema-conforming state [0.19ms]
(pass) validate > fails when a Ready row uses an unconfigured adapter [0.04ms]
(pass) validate > fails when an In-flight item uses an unconfigured adapter [0.02ms]
(pass) validate > accepts a non-numeric file-mode Epic slug as an In-flight ref (rule 4 scopes the numeric check to Ready epics and blocked blockers, not In-flight) [0.01ms]
(pass) validate > fails when generated is not ISO 8601 [0.01ms]
(pass) validate > fails when an epic reference is malformed [0.01ms]
(pass) validate > fails when a Ready row epic has no title [0.01ms]
(pass) validate > fails when a blocked issue-blocker reference is malformed [0.01ms]
(pass) validate > accepts a non-issue blocker in backticks [0.01ms]
(pass) validate > accepts a cross-repo blocker reference (#225) [0.01ms]
(pass) validate > accepts a blocker annotated with a resolved title (#225) [0.03ms]
(pass) validate > accepts a blocker carrying a (stale blocker: <ref>) suffix (#225)
(pass) validate > fails when a cross-repo blocker reference is malformed
(pass) validate > collects multiple errors [0.02ms]

packages/state-issue/test/fuzz.test.ts:
(pass) parser/renderer round-trip fuzz > renders, parses, and re-renders 10000 random valid states byte-identically [295.92ms]

packages/state-issue/test/schema-path.test.ts:
(pass) STATE_ISSUE_SCHEMA_PATH > is an absolute path ending in the canonical schema filename [0.03ms]
(pass) STATE_ISSUE_SCHEMA_PATH > points at the real schema shipped in the middle install (not a target repo) [9.78ms]

packages/state-issue/test/fixture.test.ts:
(pass) hand-crafted state-issue fixture > parseStateIssue succeeds [0.02ms]
(pass) hand-crafted state-issue fixture > validate returns pass [0.07ms]
(pass) hand-crafted state-issue fixture > round-trips byte-identically [0.03ms]
(pass) hand-crafted state-issue fixture > exercises all seven sections with non-empty content [0.07ms]

packages/state-issue/test/parser.test.ts:
(pass) renderStateIssue > renders an empty state in canonical form [0.04ms]
(pass) renderStateIssue > renders a fully-populated state with all section content [0.06ms]
(pass) parseStateIssue > parses the canonical empty body back to the original state [0.04ms]
(pass) parseStateIssue > parses a fully-populated body back to the original state [0.04ms]
(pass) parseStateIssue > round-trips a file-mode in-flight ref, including a non-kebab slug (#200) [0.04ms]
(pass) parseStateIssue > returns ParseError when the open marker is missing [0.08ms]
(pass) parseStateIssue > returns ParseError when the close marker is missing [0.03ms]
(pass) parseStateIssue > returns ParseError when a section is out of order [0.03ms]
(pass) parseStateIssue > ignores content outside the markers [0.03ms]
(pass) parseStateIssue > ignores dispatcher-tick markers between sections [0.02ms]
(pass) parseStateIssue > returns ParseError when the Ready table omits the documented empty-state row [0.03ms]
(pass) parseStateIssue > an In-flight section with no bullet reads as empty (lenient empty-state) [0.02ms]
(pass) parseStateIssue > returns ParseError when a Ready row rank is below 1 [0.03ms]
(pass) parseStateIssue > returns ParseError when a Ready row sub-issue count is below 1 [0.02ms]
(pass) round-trip > render(parse(render(state))) is byte-identical to render(state) [0.05ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Needs human input accepts "- _none_" (the #84 failure) [0.02ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Blocked accepts "- _none_" [0.01ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Excluded accepts "- _none_"
(pass) lenient empty-state sentinels (agent-produced placeholders) > In-flight accepts a "- _none_" variant and an empty section [0.02ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > a real item alongside no sentinel still parses strictly (no over-loosening) [0.02ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > a genuinely malformed item (not a sentinel) still fails [0.02ms]

packages/cli/test/bootstrap-gitignore.test.ts:
(pass) addMiddleIgnore > writes the glob form with policy/verify exceptions into a new file [0.46ms]
(pass) addMiddleIgnore > preserves existing unrelated entries [0.22ms]
(pass) addMiddleIgnore > is idempotent — a second call makes no change [0.17ms]
(pass) addMiddleIgnore > upgrades a legacy bare `.middle/` entry to the glob form [0.21ms]
(pass) removeMiddleIgnore > strips the whole block, leaving other entries [0.29ms]
(pass) removeMiddleIgnore > deletes the file when it empties [0.31ms]
(pass) removeMiddleIgnore > also clears a legacy bare `.middle/` line [0.29ms]
(pass) removeMiddleIgnore > no-op when there's nothing middle-owned to remove [0.20ms]
(pass) removeMiddleIgnore > no-op leaves a file without a trailing newline untouched [0.17ms]
(pass) removeMiddleIgnore > no file at all is a no-op [0.13ms]

packages/cli/test/config.test.ts:
(pass) mm config auto_dispatch > flips an existing toggle in place, preserving comments and other keys [1.42ms]
(pass) mm config auto_dispatch > inserts the key when the [recommender] section lacks it [0.30ms]
(pass) mm config auto_dispatch > appends the section when it does not exist [0.42ms]
(pass) mm config auto_dispatch > matches a header with a trailing comment in place (no duplicate section) [0.31ms]
(pass) mm config auto_dispatch > matches a header with whitespace inside the brackets (no duplicate section) [0.28ms]
(pass) mm config auto_dispatch > rejects an unknown key and an invalid value [0.18ms]
(pass) mm config auto_dispatch > errors when the config file is missing [0.12ms]

packages/cli/test/init-file-store.test.ts:
(pass) mm init --epic-store=file > writes the four scaffold files and makes zero gh calls [11.39ms]
(pass) mm init --epic-store=file > the README template snippet is a parseable v1 Epic body [8.74ms]
(pass) mm init --epic-store=file > calls the setEpicStore callback with file mode + default paths [6.58ms]
(pass) mm init --epic-store=file > a setEpicStore write failure is best-effort — init still succeeds [10.95ms]
(pass) mm init --epic-store=file > --dry-run writes nothing and makes no gh calls [0.33ms]
(pass) mm init — github mode is unchanged > default mode creates the state issue and writes no file-store scaffold [6.68ms]
(pass) mm init — github mode is unchanged > setEpicStore is called with github mode in the default path [7.33ms]

packages/cli/test/pause-resume.test.ts:
(pass) mm pause / mm resume > pause sets paused_until; resume clears it (keyed by the resolved slug) [83.18ms]
(pass) mm pause / mm resume > a slug-resolution failure returns exit 1, not an unhandled rejection [0.51ms]
(pass) mm pause / mm resume > a non-git path is rejected with exit 1 [0.41ms]

packages/cli/test/status.test.ts:
(pass) runStatus > prints a per-repo, per-state summary of recorded workflows [85.50ms]
(pass) runStatus > reports cleanly when the database does not exist yet [0.34ms]
(pass) runStatus > reports cleanly when the database has no workflows [69.89ms]
(pass) runStatus > exits non-zero when the config file is malformed [0.60ms]

packages/cli/test/bootstrap-hook.test.ts:
(pass) bootstrap hook.sh asset > is byte-identical to the canonical HOOK_SH constant [0.78ms]
(pass) bootstrap hook.sh asset > is a POSIX sh script that takes the event name and never blocks the agent [0.05ms]
(pass) bootstrap hook.sh asset > the committed asset is marked executable [0.03ms]

packages/cli/test/file-mode-smoke.test.ts:
(pass) file-mode CLI smoke (#194) > mm dispatch --epic <slug> lands a workflow row with epic_ref=<slug> (file mode selected) [83.66ms]

packages/cli/test/db-scripts.test.ts:
(pass) backup.sh + reset-db.sh round-trip > backup → reset → restore preserves the db and its rows [122.44ms]
(pass) safety guards > backup.sh fails when there is no database [2.77ms]
(pass) safety guards > reset-db.sh is a no-op (exit 0) when there is no database [2.42ms]
(pass) safety guards > reset-db.sh refuses while the dispatcher pidfile is live [70.90ms]
(pass) safety guards > --db points both scripts at a relocated database [103.72ms]
(pass) safety guards > restore creates missing parent dirs for a relocated db and config [125.61ms]
(pass) safety guards > restore refuses while the dispatcher pidfile is live [100.69ms]

packages/cli/test/doctor.test.ts:
(pass) runDoctor — happy path > returns 0 and prints every check when the toolchain is healthy [1417.85ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + existing epics dir → epics_dir pass, no state-issue row [1035.41ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + missing epics dir → epics_dir fail, no state-issue row [1054.88ms]
(pass) runDoctor — mode-aware Epic-store check > github mode (no config row) → state-issue row, no epics_dir row [1285.15ms]
(pass) checkAdapterBinaries > null config (unparseable) → single warn, no throw [0.13ms]
(pass) checkAdapterBinaries > no enabled adapters → warn [0.06ms]
(pass) checkAdapterBinaries > reports a row per ENABLED adapter from the passed config — not a reloaded global one [0.17ms]
(pass) checkAdapterBinaries > enabled adapter with a missing binary → warn (never fail) [27.03ms]
(pass) formatAgo > renders sub-minute as seconds [0.08ms]
(pass) formatAgo > renders minutes, hours, and days at the boundaries [0.05ms]
(pass) formatAgo > clamps a future timestamp to 0s (never negative)
(pass) summarizeRetention > never-run → pass, reports counts [0.04ms]
(pass) summarizeRetention > clean last run → pass, reports the run [0.03ms]
(pass) summarizeRetention > failed last run → warn, surfaces FAILED [0.04ms]

packages/cli/test/run-recommender.test.ts:
(pass) runRecommender — local validation > rejects a path that is not a git repository [16.51ms]
(pass) runRecommender — thin client to the daemon > daemon already up: POSTs /trigger/recommender and returns 0 on 202 [6.68ms]
(pass) runRecommender — thin client to the daemon > daemon down: auto-starts it, waits for health, then triggers [6.42ms]
(pass) runRecommender — thin client to the daemon > relays a daemon rejection (non-202) as exit 1 [6.28ms]
(pass) runRecommender — thin client to the daemon > returns 1 when the daemon never becomes ready after an auto-start [57.83ms]
(pass) runRecommender — thin client to the daemon > returns 1 when the dispatcher is unreachable (the POST throws) [7.37ms]

packages/cli/test/state-issue-check.test.ts:
(pass) checkStateIssueRoundTrip > passes for the canonical conforming fixture [0.15ms]
(pass) checkStateIssueRoundTrip > fails when the body does not parse [0.05ms]
(pass) checkStateIssueRoundTrip > fails validate when a Ready row uses an unconfigured adapter [0.11ms]
(pass) checkStateIssue > passes against middle's own source tree [0.08ms]
(pass) checkStateIssue > returns a structured fail (never throws) when the fixture is unreadable [0.09ms]

packages/cli/test/daemon-entry.test.ts:
Bundled page in 27ms: packages/dashboard/src/index.html
(pass) dashboardHostExtras routes + the hook fetch fallback coexist on one port [35.65ms]
(pass) a dispatch POST reaches the host-context dispatch callback [5.44ms]
(pass) dispose clears the process-global rate-limit observer (no broadcast after teardown) [1.79ms]

packages/cli/test/issue-audit.test.ts:
(pass) isFeatureIssue > epics, docs and chore issues are out of scope [0.09ms]
(pass) auditIssues > filters to feature issues and applies the rubric [0.32ms]
(pass) runAuditIssues --issue mode > flags a weak issue, returns 1, and labels it when --label is set [0.43ms]
(pass) runAuditIssues --issue mode > a thrown fetch error is handled: returns 1 and logs, not an unhandled rejection [0.19ms]
(pass) runAuditIssues --issue mode > a label-application failure is surfaced (logged) but does not crash the command [0.14ms]
(pass) runAuditIssues --issue mode > a passing issue returns 0 and is never labelled [0.09ms]
(pass) runAuditIssues backlog mode > returns 1 when any feature issue fails; labels only failures [0.16ms]

packages/cli/test/init-register.test.ts:
(pass) mm init — managed-repo registration > registers the slug + resolved checkout path on a successful init [8.15ms]
(pass) mm init — managed-repo registration > does NOT register under --dry-run (no changes made) [0.33ms]
(pass) mm init — managed-repo registration > a registry write failure is best-effort — init still succeeds [7.99ms]

packages/cli/test/audit-issues-cli.test.ts:
(pass) mm audit-issues --body-file (real CLI) > flags a weak issue and suggests a concrete rewrite (exit 1) [149.29ms]
(pass) mm audit-issues --body-file (real CLI) > passes a well-formed issue carrying an integration criterion (exit 0) [150.36ms]
(pass) mm audit-issues --body-file (real CLI) > --json emits a machine-readable report [151.29ms]
(pass) mm audit-issues --body-file (real CLI) > rejects a non-positive-integer --issue with a clear error (exit 1) [741.32ms]

packages/cli/test/module-index.test.ts:
(pass) parseModuleIndexFrontmatter > accepts a well-formed frontmatter block [0.09ms]
(pass) parseModuleIndexFrontmatter > reads claude-md: true [0.03ms]
(pass) parseModuleIndexFrontmatter > tolerates a leading shebang before the block [0.03ms]
(pass) parseModuleIndexFrontmatter > rejects a file with no leading block comment [0.01ms]
(pass) parseModuleIndexFrontmatter > rejects a block missing @packageDocumentation [0.01ms]
(pass) parseModuleIndexFrontmatter > rejects a block missing the @module tag [0.01ms]
(pass) parseModuleIndexFrontmatter > rejects a missing required section [0.02ms]
(pass) parseModuleIndexFrontmatter > rejects a non-boolean claude-md value [0.01ms]
(pass) claudeMdPathForIndex > maps a package's src/index.ts to the package root CLAUDE.md [0.01ms]
(pass) claudeMdPathForIndex > maps a nested module's index.ts to its own dir
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > flags claude-md: true with no CLAUDE.md [0.54ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > flags claude-md: false with a stray CLAUDE.md [0.42ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > passes when flag and presence agree, and skips bootstrap-assets [0.76ms]
(pass) checkModuleIndex — the real middle packages tree > every src/index.ts(x) carries valid, consistent frontmatter [0.53ms]
(pass) checkModuleIndex — the real middle packages tree > finds every package's index front door [0.51ms]

packages/cli/test/bootstrap-init.test.ts:
(pass) mm init — fresh install > stages skills, hooks, config, state issue, and gitignore [9.14ms]
(pass) mm init — fresh install > the created state-issue body parses and validates [6.68ms]
(pass) mm init — idempotent re-init > a matching-version re-init refreshes assets but keeps config and issue [11.89ms]
(pass) mm init — idempotent re-init > re-init does not clobber a team's committed policy edits (AC #103) [11.38ms]
(pass) mm init — idempotent re-init > a fresh clone (committed policy, no local cache) reconciles the issue and keeps policy [6.51ms]
(pass) mm init — idempotent re-init > loadConfig reads init's two files via sibling derivation and merges them [6.71ms]
(pass) mm init — dry run > writes nothing and makes no GitHub calls [0.32ms]
(pass) mm init — validation > rejects a dirty working tree [0.31ms]
(pass) mm init — validation > rejects a repo with no origin remote [0.29ms]
(pass) mm init — validation > fails fast on a malformed existing config instead of re-initializing fresh [0.46ms]
(pass) mm init — existing config without a usable state issue > a matching-version re-init with no issue number mints one and persists it [7.93ms]
(pass) mm init — reconciles the state issue against GitHub > a fresh local install reuses the repo's existing state issue instead of creating one [6.20ms]
(pass) mm init — reconciles the state issue against GitHub > warns and reuses the oldest when GitHub has duplicate state issues [8.18ms]
(pass) mm init — reconciles the state issue against GitHub > creates a state issue only when GitHub has none [6.22ms]
(pass) mm uninit > closes the issue and removes everything init staged [10.12ms]
(pass) mm uninit > closes the state issue even when [repo] metadata is missing (deps fallback) [0.64ms]
(pass) mm uninit > closes the state issue offline by reading [repo] from committed policy (#103) [0.85ms]
(pass) mm uninit > falls back to default_branch 'main' when committed policy has a non-string value (#103) [0.62ms]
(pass) mm uninit > dry run removes nothing [6.49ms]
(pass) mm uninit > strips only middle's hook entries, preserving foreign ones [9.43ms]

packages/cli/test/dispatch.test.ts:
(pass) runDispatch — input validation > rejects a malformed numeric epic (digit-leading but not a whole number) [16.52ms]
(pass) runDispatch — input validation > rejects an epic number below 1 [6.48ms]
(pass) runDispatch — input validation > rejects a path that is not a git repository [0.28ms]
(pass) runDispatch — control client > health already up: dispatches and exits 0 on completed, without spawning a daemon [122.44ms]
(pass) runDispatch — control client > a file-mode slug dispatches with epicRef and skips the gh label fetch [11.86ms]
(pass) runDispatch — control client > subscribes to /control/events BEFORE POSTing /control/dispatch [117.96ms]
(pass) runDispatch — control client > exits 0 when the workflow parks for review (waiting-human) [123.11ms]
(pass) runDispatch — control client > exits 1 when the workflow fails [110.33ms]
(pass) runDispatch — control client > reconnects when the event stream drops mid-flight and follows to completion [111.75ms]
(pass) runDispatch — control client > --adapter overrides the agent label and the default, and is sent to the daemon [11.51ms]
(pass) runDispatch — control client > an agent:<name> label on the Epic selects that adapter [10.73ms]
(pass) runDispatch — control client > no agent label falls back to the default adapter [11.06ms]
(pass) runDispatch — control client > a disabled adapter is rejected (exit 1), even via --adapter, before any dispatch [8.84ms]
(pass) runDispatch — control client > an unconfigured --adapter is rejected (exit 1) before any dispatch [11.29ms]
(pass) runDispatch — control client > friendly failure (exit 1) when the daemon can't be reached or started [539.61ms]

packages/cli/test/state-issue-body.test.ts:
(pass) buildInitialStateIssueBody > parses and validates against the schema (configured adapters) [0.13ms]
(pass) buildInitialStateIssueBody > is empty in every section [0.07ms]
(pass) buildInitialStateIssueBody > round-trips byte-identically (the keystone invariant) [0.03ms]
(pass) buildInitialStateIssueBody > carries the markers and the generated timestamp [0.02ms]
(pass) parseRepoSlug > parses git@github.com:acme/widget.git [0.09ms]
(pass) parseRepoSlug > parses https://github.com/acme/widget.git
(pass) parseRepoSlug > parses https://github.com/acme/widget
(pass) parseRepoSlug > parses ssh://git@github.com/acme/widget.git
(pass) parseRepoSlug > parses https://github.com/acme/widget/
(pass) parseRepoSlug > returns null for an unparseable URL

packages/cli/test/start-stop.test.ts:
(pass) runStart / runStop lifecycle > start spawns a detached process and records its pid; stop kills it [301.63ms]
(pass) runStart / runStop lifecycle > start refuses when a live dispatcher is already recorded [101.02ms]
(pass) runStart / runStop lifecycle > start clears a stale pid file and launches fresh [0.61ms]
(pass) runStart / runStop lifecycle > stop exits non-zero when no dispatcher is running [0.21ms]
(pass) runStartCommand --window > opens the dashboard window once /health is ready [0.70ms]
(pass) runStartCommand --window > does not open the window when /health never becomes ready (but start still succeeds) [0.55ms]
(pass) runStartCommand --window > a throwing opener (or health probe) never fails the start — window step is best-effort [0.60ms]
(pass) runStartCommand --window > no --window and no windowed config → never opens, never polls health [0.46ms]

packages/cli/test/tsdoc-coverage.test.ts:
(pass) checkTsdocCoverage > counts a documented local export as documented [326.92ms]
(pass) checkTsdocCoverage > flags an undocumented local export [299.66ms]
(pass) checkTsdocCoverage > resolves a re-export to the original declaration's doc comment [274.91ms]
(pass) checkTsdocCoverage > a bare `export {}` module contributes no exports [300.62ms]
(pass) checkTsdocCoverage > analyzes the real middle tree without throwing [511.52ms]

packages/cli/test/init-collision.test.ts:
(pass) mm init — shared-checkout collision guard (#226) > a second init at the same path with a different slug exits non-zero and writes nothing [19.44ms]
(pass) mm init — shared-checkout collision guard (#226) > re-initializing the SAME slug at the same path is allowed (idempotent, no collision) [14.19ms]
(pass) mm init — shared-checkout collision guard (#226) > --dry-run skips the collision guard (it writes nothing anyway) [1.95ms]

packages/cli/test/docs.test.ts:
(pass) runDocs — input validation > rejects a path that is not a git repository [16.99ms]
(pass) runDocs — input validation > rejects an unknown [docs] tool override [6.87ms]
(pass) runDocs — enqueues a documentation run for the repo > resolves the markdown fallback target and dispatches a read-only run [8.82ms]
(pass) runDocs — enqueues a documentation run for the repo > a [docs] tool/path override flows through to the resolved target [7.88ms]
(pass) runDocs — enqueues a documentation run for the repo > returns 1 when the dispatched run does not complete [9.21ms]

packages/cli/test/bun-path.test.ts:
(pass) isDirOnPath > true when present [0.09ms]
(pass) isDirOnPath > false when absent [0.02ms]
(pass) isDirOnPath > tolerates trailing slashes on either side [0.01ms]
(pass) isDirOnPath > false on empty PATH
(pass) resolveShellRc > zsh (platform-independent) [0.04ms]
(pass) resolveShellRc > bash on macOS targets .bash_profile (login shells don't source .bashrc)
(pass) resolveShellRc > bash elsewhere targets .bashrc
(pass) resolveShellRc > unknown shell [0.01ms]
(pass) bunPathSnippet > HOME-relative form when dir is the canonical ~/.bun/bin [0.03ms]
(pass) bunPathSnippet > literal form when dir is non-canonical [0.01ms]
(pass) rcAlreadyConfigured > detects literal bin dir [0.01ms]
(pass) rcAlreadyConfigured > detects BUN_INSTALL form
(pass) rcAlreadyConfigured > false on unrelated rc
(pass) applyPathFix > appends once and is idempotent [0.28ms]
(pass) applyPathFix > creates content when the rc file is absent [0.17ms]

packages/cli/test/skills-sync.test.ts:
(pass) syncSkills > copies every canonical file into the mirror byte-for-byte [1.17ms]
(pass) syncSkills > a second sync is a no-op (inSync, no changes) [1.60ms]
(pass) syncSkills > removes stale files the canonical no longer has [1.09ms]
(pass) syncSkills > detects and removes an orphaned skill DIRECTORY present only in the mirror [1.14ms]
(pass) diffSkills / check mode > check mode reports drift without writing [0.66ms]
(pass) diffSkills / check mode > check mode reports in-sync once synced [0.97ms]
(pass) diffSkills / check mode > check mode catches a single-byte edit in the mirror [1.05ms]
(pass) default repo paths > the shipped canonical and mirror are in sync [0.88ms]
(pass) default repo paths > the shipped skill set includes the three bootstrapped skills [0.52ms]

packages/dispatcher/test/epic-143-demo.test.ts:
(pass) Epic #143 — integration-verified requirements + freshness > 1. the requirements auditor flags a deliberately weak issue [0.07ms]
(pass) Epic #143 — integration-verified requirements + freshness > 2. a unit-only feature cannot reach PR-ready [0.51ms]
[staleness] o/r#50 landed in merged PR #88 → closed
[staleness] o/r: filed reconcile task #900 for Phase 9
(pass) Epic #143 — integration-verified requirements + freshness > 3. reconciliation surfaces a landed-but-open issue and a drifted spec line [0.68ms]

packages/dispatcher/test/watchdog.test.ts:
(pass) watchdog — launch timeout > a launching workflow past the window is failed 'stuck-launching' [79.61ms]
(pass) watchdog — launch timeout > a launching workflow within the window is left alone [70.26ms]
(pass) watchdog — prompt not accepted > a running session that went ready but never started a turn is failed 'prompt-not-accepted' [86.25ms]
(pass) watchdog — prompt not accepted > a running session whose prompt landed (turn.started present) is not failed [79.25ms]
(pass) watchdog — prompt not accepted > a running session still within the launch window is not yet failed [73.95ms]
(pass) watchdog — tmux liveness > a running workflow with a dead session is failed + compensation triggered [79.33ms]
(pass) watchdog — tmux liveness > a running workflow with a live session is not failed for liveness [72.76ms]
[watchdog] status check failed for middle-14, skipping liveness this pass: tmux server not running
(pass) watchdog — tmux liveness > a status() error is inconclusive — liveness is skipped, fresh row not failed [70.30ms]
[watchdog] status check failed for middle-14, skipping liveness this pass: tmux server not running
(pass) watchdog — tmux liveness > a persistent status() error does NOT block rule 3 — a stale row still idle-times-out [81.49ms]
[watchdog] status check failed for middle-bad, skipping liveness this pass: tmux error
(pass) watchdog — tmux liveness > a status() error on one row does not abort reconciliation of others [86.02ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — tmux liveness > a killSession() error still records the failure decision [78.87ms]
(pass) watchdog — activity freshness > idle ≥ threshold marks one idle event but does not kill [73.51ms]
(pass) watchdog — activity freshness > idle ≥ kill-threshold kills the session and fails 'idle-timeout' [78.32ms]
(pass) watchdog — activity freshness > freshness is skipped while controlled_by = 'human' [71.25ms]
(pass) watchdog — activity freshness > a stale heartbeat is rescued by fresh transcript activity (cross-check) [71.43ms]
(pass) watchdog — sentinel re-arm > a blocked.json with no armed signal arms one, idempotently [75.60ms]
(pass) watchdog — sentinel re-arm > no sentinel file → no signal armed [72.43ms]
(pass) watchdog — blocked sentinel self-heal > idle ≥ kill-threshold with a blocked sentinel hands off to the drive, not compensation [83.98ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — blocked sentinel self-heal > a failed kill does not record the handoff — it retries next pass [71.17ms]
(pass) watchdog — blocked sentinel self-heal > the handoff is recorded once, not every idle tick [79.27ms]
(pass) reconcileTranscriptDrift > advances last_heartbeat when the transcript is newer than the recorded beat [76.00ms]
(pass) reconcileTranscriptDrift > leaves the heartbeat alone when the transcript is older [71.17ms]
(pass) notification failsafe — detect + capture + intervene > a notification still within the grace window is left alone [76.45ms]
(pass) notification failsafe — detect + capture + intervene > a notification past the grace window captures the pane, classifies, and nudges [85.59ms]
(pass) notification failsafe — detect + capture + intervene > classifies a plain 'waiting for input' notification as a question (kind=input) [100.28ms]
(pass) notification failsafe — detect + capture + intervene > an agent that resumed after the notification (newer activity) is left alone [78.68ms]
(pass) notification failsafe — detect + capture + intervene > a human-controlled session is never rescued (a human will answer) [78.87ms]
(pass) notification failsafe — detect + capture + intervene > no-op when the tmux surface lacks the failsafe methods [76.43ms]
(pass) notification failsafe — detect + capture + intervene > a capture-only notification (no message payload) still classifies + nudges [84.31ms]
(pass) notification failsafe — fast-fail backstop > still idle past the kill-grace → fast-fails with the captured kind and kills the session [90.64ms]
(pass) notification failsafe — fast-fail backstop > two captures sharing a ts → the latest-by-id kind wins (contract lock) [94.94ms]
(pass) notification failsafe — fast-fail backstop > within the kill-grace → not yet failed (the nudge still has time to take) [87.85ms]
(pass) notification failsafe — fast-fail backstop > a repeat notification with no activity does NOT reset the kill clock — still fast-fails [100.06ms]
(pass) notification failsafe — fast-fail backstop > a fresh notification AFTER genuine activity re-arms the failsafe (re-captures) [98.02ms]

packages/dispatcher/test/recommender-run.test.ts:
[recommender-run] workflow wf_1780555141506_33v1mrdy enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > runs to completion and records a kind:'recommender' workflow row for the repo [379.55ms]
[recommender-run] workflow wf_1780555141882_s5hmwkva enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > read-only by default: with no triggerAutoDispatch wired, a clean run dispatches nothing [372.16ms]
[recommender-run] workflow wf_1780555142259_83fuwa4p enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > fires triggerAutoDispatch on a clean run when wired and auto_dispatch is on (trigger #1) [377.37ms]
[recommender-run] workflow wf_1780555142633_1dvhquhe enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > forwards epicStore so a file-mode run frames the prompt for the file store (#200) [374.72ms]
[recommender-run] workflow wf_1780555143010_ufmy0969 enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > does not fire triggerAutoDispatch when auto_dispatch is off, even if wired [376.52ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [7.60ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > file mode resolves without a state issue — sentinel 0 + epicStore carried (#200) [7.54ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > github mode still requires a configured state issue number [6.33ms]
(pass) resolveRecommenderOptions — schema resolution (issue #107) > resolves schemaPath from the middle install, not from repoPath [7.29ms]

packages/dispatcher/test/state-issue.test.ts:
(pass) applyDispatcherSections > replaces only the three owned sections, keeps the rest [0.04ms]
(pass) updateDispatcherSections > recommender-owned sections come back byte-identical [0.52ms]
(pass) updateDispatcherSections > the owned sections actually changed [0.23ms]
(pass) updateDispatcherSections > a partial patch leaves the unspecified owned sections intact [0.12ms]
(pass) updateDispatcherSections > a dispatcher-tick marker is ignored by the parser and preserves sections [0.25ms]
(pass) updateDispatcherSections > ticks do not accumulate across repeated updates [0.19ms]
(pass) readState > parses a valid body [0.11ms]
(pass) readState > throws on a malformed body [0.08ms]
(pass) insertDispatcherTick > leaves a non-canonical body untouched [0.02ms]

packages/dispatcher/test/stop-wait.test.ts:
(pass) awaitStopOrSessionEnd > resolves via 'stop' when the Stop hook arrives first [5.32ms]
(pass) awaitStopOrSessionEnd > resolves via 'session-ended' when liveness goes false while Stop is pending [11.37ms]
(pass) awaitStopOrSessionEnd > resolves via 'timeout' when the Stop wait rejects and the session stays alive [5.21ms]
(pass) awaitStopOrSessionEnd > without a liveness probe, a rejected Stop wait surfaces as 'timeout' [6.02ms]
(pass) awaitStopOrSessionEnd > liveness-probe errors are ignored — a later Stop still wins [21.30ms]

packages/dispatcher/test/build-deps.test.ts:
(pass) buildImplementationDeps > wires deps from the injected collaborators and returns the gate it built [64.96ms]
(pass) buildImplementationDeps > epicPrReadiness reports a missing PR as { exists: false, isDraft: false } [62.74ms]
(pass) buildImplementationDeps > the factory module imports no engine (no bunqueue construction) [11.05ms]
(pass) buildImplementationDeps > the default postQuestion posts a gh comment framed by pause kind [63.52ms]
(pass) buildImplementationDeps > the default postQuestion is idempotent on a repeated identical question (#205) [63.17ms]
(pass) postQuestionComment (idempotent pause poster, #205) > skips when the latest agent-comment already has the identical body [0.32ms]
(pass) postQuestionComment (idempotent pause poster, #205) > a different body posts a fresh comment (questions are a history) [0.18ms]
(pass) postQuestionComment (idempotent pause poster, #205) > ignores non-agent comments — only the marker-prefixed latest counts [0.22ms]
(pass) formatPauseComment > a complexity pause carries the `complexity pause` label vocabulary [0.15ms]
(pass) formatPauseComment > a plain question reads as an agent question, not a complexity pause [0.14ms]
(pass) formatPauseComment > both kinds start with the hidden agent-comment marker so the poller skips them (#178) [0.12ms]

packages/dispatcher/test/staleness.test.ts:
(pass) detectSpecDrift > flags future-phase lines whose phase has merged [0.07ms]
(pass) detectSpecDrift > does not flag a future phase that has not merged [0.02ms]
(pass) detectSpecDrift > matches the verb-less 'planned for phase N' phrasing [0.03ms]
[staleness] o/r#50 landed in merged PR #88 → closed
[staleness] o/r: filed reconcile task #1001 for Phase 9
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > closes a landed-but-open issue and files a drift task for its phase [0.27ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > does not close an issue no merged PR references, and dedupes an existing reconcile task [0.10ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > maxPerPass caps the TOTAL of closes + filed tasks, not each bucket [0.07ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > no spec → still reconciles landed issues, no drift [0.06ms]

packages/dispatcher/test/hook-store.test.ts:
(pass) DbHookStore — resolveSessionToken > returns the token of the active workflow owning the session [71.52ms]
(pass) DbHookStore — resolveSessionToken > returns null for an unknown session [65.09ms]
(pass) DbHookStore — resolveSessionToken > ignores terminal workflows that previously held the deterministic session name [90.42ms]
(pass) DbHookStore — record > writes an events row for every hook [88.70ms]
(pass) DbHookStore — record > tool.pre and tool.post advance last_heartbeat [102.26ms]
(pass) DbHookStore — record > a non-tool event records but does not advance last_heartbeat [92.10ms]
(pass) DbHookStore — record > session.started writes session_id + transcript_path onto the workflow [93.73ms]
[hook-store] dropping tool.pre: no active workflow for session middle-GHOST
(pass) DbHookStore — record > an unmatchable session is dropped, not crashed on, and writes nothing [77.99ms]
(pass) DbHookStore — record > oversized payloads are truncated before storage [87.97ms]
[hook-server] received tool.post:middle-14
(pass) HookServer wired to DbHookStore — end to end into SQLite > an authenticated POST flows through the server into the events table + heartbeat [95.49ms]
(pass) serializePayload > returns compact JSON for a small payload [79.47ms]
(pass) serializePayload > clips and marks a payload over 16KB [75.58ms]

packages/dispatcher/test/event-hub.test.ts:
(pass) EventHub > serve emits a `connected` frame first, with SSE content-type [0.48ms]
(pass) EventHub > serve replays caller-supplied init events after `connected` [0.16ms]
(pass) EventHub > a broadcast reaches a live subscriber [0.12ms]
(pass) EventHub > a heartbeat keeps the stream alive (injectable interval) [21.61ms]
(pass) EventHub > an aborted client is unsubscribed cleanly [11.76ms]
(pass) EventHub > a slow consumer that overflows its buffer is dropped without throwing [0.46ms]

packages/dispatcher/test/notification-classify.test.ts:
(pass) classifyNotification — permission blocks > message "Claude needs your permission to use Bash" → permission [0.02ms]
(pass) classifyNotification — permission blocks > message "Claude needs permission to run a command" → permission
(pass) classifyNotification — permission blocks > message "This action requires your approval" → permission
(pass) classifyNotification — permission blocks > message "Claude wants to use the Edit tool" → permission
(pass) classifyNotification — permission blocks > message "Allow Claude to run `chmod +x`?" → permission
(pass) classifyNotification — permission blocks > pane "Do you want to proceed?" → permission even with a generic message [0.01ms]
(pass) classifyNotification — permission blocks > pane "Do you want to allow this?" → permission even with a generic message
(pass) classifyNotification — permission blocks > pane "❯ 1. Yes" → permission even with a generic message
(pass) classifyNotification — permission blocks > pane "❯ 2. Allow" → permission even with a generic message
(pass) classifyNotification — permission blocks > permission outranks an input-shaped message when the pane shows a dialog [0.01ms]
(pass) classifyNotification — input (genuine question) > message "Claude is waiting for your input" → input
(pass) classifyNotification — input (genuine question) > message "Waiting for input" → input
(pass) classifyNotification — input (genuine question) > message "Claude needs your input to continue" → input
(pass) classifyNotification — input (genuine question) > message "Awaiting your input" → input
(pass) classifyNotification — idle/unknown > unattributable message "" → idle-unknown
(pass) classifyNotification — idle/unknown > unattributable message "Some unrelated notification" → idle-unknown
(pass) classifyNotification — idle/unknown > unattributable message "Task finished" → idle-unknown
(pass) classifyNotification — idle/unknown > a long whitespace-laden 'allow …' message classifies fast (no catastrophic backtracking) [0.09ms]
(pass) classifyNotification — idle/unknown > still matches a legitimate 'allow … to' permission request [0.01ms]
(pass) classifyNotification — idle/unknown > tolerates missing message/pane (undefined-safe)

packages/dispatcher/test/multi-repo-blockers.test.ts:
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > an open cross-repo blocker keeps the Epic blocked, annotated with Repo B's title [259.50ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > closing the cross-repo blocker moves the Epic to Ready within one tick [323.79ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > an unresolvable (404) blocker stays blocked with a (stale blocker: <ref>) suffix [249.85ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > the resolved state body round-trips byte-identically [251.73ms]

packages/dispatcher/test/poller-gateway.test.ts:
(pass) deriveCiStatus > no checks configured → none (nothing to gate on) [0.13ms]
(pass) deriveCiStatus > all check runs succeeded (incl. neutral/skipped) → passing [0.05ms]
(pass) deriveCiStatus > any failed/errored/cancelled/timed-out check → failing [0.03ms]
(pass) deriveCiStatus > an unfinished check run (not COMPLETED) → pending [0.01ms]
(pass) deriveCiStatus > a failure outranks a still-running check → failing
(pass) deriveCiStatus > legacy StatusContext entries (state) are read too [0.02ms]
(pass) deriveCiStatus > EXPECTED is pending, not passing — a green gate requires an actual SUCCESS
(pass) ghPollGateway.prSnapshot failure isolation > a transient reviews-fetch failure degrades to null, not a thrown pass [2.20ms]
(pass) ghPollGateway.prSnapshot failure isolation > a `pr view` failure also degrades to null (the symmetric branch) [0.98ms]
(pass) ghPollGateway.prSnapshot failure isolation > both fetches succeed → a populated snapshot [1.45ms]

packages/dispatcher/test/backlog-audit.test.ts:
[backlog-audit] o/r#2 fails the integration rubric → needs-design
(pass) runBacklogAudit > flags rubric-failing feature issues; passes the good one; skips epics [0.39ms]
(pass) runBacklogAudit > does not re-label an issue already marked needs-design [0.07ms]
[backlog-audit] o/r#10 fails the integration rubric → needs-design
[backlog-audit] o/r#11 fails the integration rubric → needs-design
(pass) runBacklogAudit > respects the per-pass cap [0.12ms]
[backlog-audit] failed to label o/r#1 (continuing): boom
[backlog-audit] o/r#2 fails the integration rubric → needs-design
(pass) runBacklogAudit > an addLabel failure is isolated — the sweep continues [0.19ms]
[backlog-audit] o/active#1 fails the integration rubric → needs-design
(pass) runAuditCronPass > sweeps managed repos, skips paused ones [2.88ms]

packages/dispatcher/test/db-migrations.test.ts:
(pass) migration 007 — repo_config epic-store columns > adds epic_store TEXT NOT NULL DEFAULT 'github' [72.81ms]
(pass) migration 007 — repo_config epic-store columns > adds epics_dir TEXT (nullable — only set in file mode) [62.65ms]
(pass) migration 007 — repo_config epic-store columns > adds state_file TEXT (nullable — only set in file mode) [61.86ms]
(pass) migration 007 — repo_config epic-store columns > workflows table gains a nullable epic_ref TEXT column [61.28ms]
(pass) migration 007 — repo_config epic-store columns > backfill: existing implementation rows get epic_ref = stringified epic_number [74.62ms]
(pass) migration 007 — repo_config epic-store columns > a freshly-inserted row defaults epic_store to 'github' [67.07ms]

packages/dispatcher/test/epics-cache.test.ts:
(pass) epics-cache > refreshEpics upserts open Epics and readEpics returns them newest-first [66.89ms]
(pass) epics-cache > an Epic that vanishes from the open set is marked closed and dropped from readEpics [71.55ms]
(pass) epics-cache > a closed Epic that reappears is reopened and visible again [78.57ms]
(pass) epics-cache > caches a file-mode Epic (slug ref, null number) and surfaces it in readEpics (#200) [66.86ms]
(pass) epics-cache > mixed github + file Epics: github (by number desc) first, file (null number) after [68.11ms]
(pass) epics-cache > a file Epic that vanishes is marked closed by its slug ref [71.78ms]
(pass) epics-cache > refresh is repo-scoped — another repo's rows are untouched [73.08ms]

packages/dispatcher/test/metrics.test.ts:
(pass) collectMetrics > empty db → zeroed snapshot [63.58ms]
(pass) collectMetrics > groups workflows by (repo, kind, state) and rolls up totals [103.03ms]
(pass) collectMetrics > a completed implementation frees its slot but stays counted in totals [72.43ms]
(pass) collectMetrics > surfaces rate-limit standing per adapter [67.16ms]
(pass) renderPrometheus > emits gauges with HELP/TYPE and a trailing newline [78.95ms]
(pass) renderPrometheus > an AVAILABLE adapter renders rate_limited 0 [72.53ms]
(pass) renderPrometheus > escapes special characters in label values [81.60ms]

packages/dispatcher/test/implementation-workflow.test.ts:
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-LkVjAx/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-LkVjAx/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=failed
(pass) implementation workflow — terminal stops fall through the waitFor > a 'failed' classifyStop ends 'failed', destroys the worktree, leaks no session [270.52ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-yT7Mny/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-yT7Mny/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — terminal stops fall through the waitFor > a 'bare-stop' ends 'completed' without parking [265.40ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-TsmZxJ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-TsmZxJ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=rate-limited
(pass) implementation workflow — terminal stops fall through the waitFor > a rate-limited classifyStop ends 'rate-limited' and records rate_limit_state [269.55ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-25fkfZ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-25fkfZ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] prompt-first launch: dismissing boot dialogs before prompt
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=s
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > prompt-first adapter sends the prompt BEFORE awaiting SessionStart (codex; no deadlock) [275.32ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-RiPFtd/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-RiPFtd/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=s
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > boot-first adapter awaits SessionStart BEFORE sending the prompt (Claude path, unchanged) [264.16ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-vZAyGT/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-vZAyGT/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 250ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: timed out waiting for session.started
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > await-first ordering deadlocks a prompt-triggered CLI — why the flag exists [514.58ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-yK4hYK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-yK4hYK/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — prepare-worktree survives a step retry (#108) > a transient createWorktree failure retries to success — the re-INSERT is a no-op, not a masking UNIQUE [872.21ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-cmCHnY/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-cmCHnY/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > a hung agent whose session dies parks for resume; worktree preserved [282.92ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-2nolbv/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-2nolbv/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > parkForResume keeps a pre-armed blocked signal (no duplicate) [281.09ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-IdvIjG/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-IdvIjG/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: session ended before Stop hook
(pass) implementation workflow — blocked sentinel self-heal > a hung agent with NO sentinel still fails (compensates, worktree pruned) [295.87ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ER5lVL/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ER5lVL/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > parkForResume removes the consumed blocked.json sentinel (#205) [284.40ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-zo9QsA/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-zo9QsA/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
(pass) implementation workflow — blocked sentinel self-heal > a session that dies mid-nudge with a blocked sentinel parks, not compensates [285.09ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-oF1Jyn/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-oF1Jyn/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-oF1Jyn/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-oF1Jyn/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-oF1Jyn/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-oF1Jyn/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[recommender-run] engine.close drain timed out after 10s — proceeding
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — question-spam integration (#205) > three consecutive dispatch ticks on a stale sentinel grow the Epic by ≤1 comment [405.40ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-gt7QxZ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-gt7QxZ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > a complexity-kind pause routes to waiting-human and surfaces with kind 'complexity' [257.35ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-99bnCF/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-99bnCF/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > a plain question pause surfaces with kind 'question' (the default) [204.50ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-iIzSgL/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-iIzSgL/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > the dispatch brief carries the repo's complexity_ceiling as the agent's fork budget [256.05ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-v3U6R6/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-v3U6R6/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > an in-ceiling decision never surfaces a complexity pause [309.47ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-3MJCSB/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-3MJCSB/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > an approved Epic's brief authorizes proceeding past a complexity overrun (#53) [270.09ms]
[recommender-run] engine.close drain timed out after 10s — proceeding
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] brief-context resolution failed, using defaults (ceiling=3, approved=false): gh rate limited
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-5XfsCt/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-5XfsCt/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > a flaky brief-context read falls back to safe defaults, never failing the dispatch [279.27ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-FrJRqE/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-FrJRqE/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-99] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-99] installing hooks in /tmp/middle-wf-FrJRqE/worktrees/thejustinwalsh/middle/issue-99
[workflow:middle-thejustinwalsh-middle-99] launching tmux session: true (cwd=/tmp/middle-wf-FrJRqE/worktrees/thejustinwalsh/middle/issue-99)
[workflow:middle-thejustinwalsh-middle-99] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-99] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-99] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-99] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-99] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-99] Stop received — classification=asked-question
(pass) implementation workflow — dispatch source (#53) > records source 'manual' for a manual dispatch and 'auto' by default [319.90ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-bncOJm/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-bncOJm/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-bncOJm/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-bncOJm/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (answer): "@.middle/prompt.md (answer)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — asked-question park → answer → resume (e2e) > parks on asked-question, a human reply resumes a fresh continuation with the answer injected [341.32ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-uA6DVq/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-uA6DVq/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-uA6DVq/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-uA6DVq/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a CHANGES_REQUESTED pass resumes a continuation with the address-review brief; APPROVED ends the loop [331.07ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-90I5rD/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-90I5rD/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-90I5rD/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-90I5rD/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a CI_FAILED verdict resumes a continuation with the fix-CI brief (not the address-review one) [311.66ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-653Ajb/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-653Ajb/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a resolved review reverts a previously RATE_LIMITED adapter to AVAILABLE [282.79ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Ce01To/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Ce01To/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Ce01To/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Ce01To/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Ce01To/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Ce01To/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — review-round cap > after the configured cap of CHANGES_REQUESTED passes without APPROVED, it parks in waiting-human and stops auto-resuming [353.41ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-vAjpdu
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-vAjpdu)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] plan-comment guard: Plan-comment guard: no plan comment found on Epic #6
(pass) implementation workflow — plan-comment completion gate > a 'done' drive with no plan comment ends 'failed' (guard fires) [254.21ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-UoaAgk
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-UoaAgk)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — plan-comment completion gate > a 'done' with a matching plan comment passes the guard and parks for review [249.05ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-FQdTNW/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-FQdTNW/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — plan-comment completion gate > without a planCommentReader wired, a 'done' parks unguarded (back-compat) [258.24ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-kaAJzC/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-kaAJzC/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/2
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 2/2
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 2 nudges — parking for a human
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a bare-stop with no ready Epic PR nudges, then parks in waiting-human [253.86ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-rSXT4K/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-rSXT4K/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] positive done-signal: ready Epic PR — completing
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a ready, non-draft Epic PR is the positive done-signal — done (no nudge), parks for review [257.78ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-sLHjtl/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-sLHjtl/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/1
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 1 nudges — parking for a human
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a draft Epic PR is not a positive done-signal — it still nudges [253.98ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-f7TG3E/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-f7TG3E/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > without an epicPrReadiness seam, a bare-stop keeps the legacy completion (back-compat) [263.34ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-AlxYIj/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-AlxYIj/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: launch timeout
(pass) implementation workflow — compensation > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [263.81ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-CaG0WD/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-CaG0WD/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: all gates pass — done stands
(pass) implementation workflow — verify-on-stop gate > a `done` whose verify fails then passes nudges in-session, then parks for review [265.03ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-zK98OV/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-zK98OV/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/1
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: still failing after 1 rounds — parking for a human
(pass) implementation workflow — verify-on-stop gate > a `done` whose verify never passes parks for a human and keeps the worktree [260.11ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-9OYw8n/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-9OYw8n/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 0 nudges — parking for a human
(pass) implementation workflow — verify-on-stop gate > a verify re-stop classified `bare-stop` can't bypass the done-signal [263.30ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-SxPssz/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-SxPssz/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — verify-on-stop gate > no runVerifyGates seam → a `done` parks for review unchanged (verify is opt-in) [287.32ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-BpH5yJ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-BpH5yJ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-BpH5yJ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-BpH5yJ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — durable recovery across daemon restart (#116) > a workflow parked on .waitFor(RESUME_EVENT) survives a restart; a review verdict resumes it [897.05ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-VEQEbF/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-VEQEbF/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — durable recovery across daemon restart (#116) > an orphaned parked signal (store lost the execution) is reconciled, not left for the poller [685.98ms]

packages/dispatcher/test/pr-divergence-integration.test.ts:
(pass) tryRebaseOntoMain — fixture repo > clean fast-forward: feature has no commits past old main; main advanced → rebase FFs [149.05ms]
(pass) tryRebaseOntoMain — fixture repo > non-FF, no conflict: feature edits A, main edits B, no shared paths → rebase replays cleanly [151.10ms]
(pass) tryRebaseOntoMain — fixture repo > conflict: feature + main both edit shared.txt → rebase aborts, paths reported, worktree clean [184.22ms]
(pass) tryRebaseOntoMain — fixture repo > data-loss guard (#201): a rebase that drops ALL of the PR's commits → restore worktree, droppedAllCommits, branch not emptied [194.10ms]
(pass) tryRebaseOntoMain — fixture repo > gitOps.revListCount: counts a resolvable range and falls back to 0 on an unresolvable one (the guard's conservative semantics) [121.93ms]
(pass) tryRebaseOntoMain — fixture repo > a non-managed head ref (not middle-issue-*) → ok:false with empty paths (skip signal) [109.71ms]
(pass) tryRebaseOntoMain — fixture repo > a missing PR (gateway returns null) → ok:false with empty paths (skip signal) [103.49ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict rebase failure (missing upstream) THROWS — not shaped as a path-less conflict [109.68ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict merge failure (missing ref) THROWS — symmetric to the rebase hardening [108.81ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > rebase would loop but merge -X ours lands cleanly (same line, feature wins) [193.57ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > residual conflict -X ours can't auto-resolve (rename/rename) → abort, paths reported [173.17ms]
(pass) applySuccess — fixture repo > pushes the rebased branch, posts one PR comment, and records CLEAN — twice = idempotent [202.91ms]
(pass) applySuccess — fixture repo > a different mainCommitSha allows a fresh announcement (the marker is sha-keyed) [259.59ms]
(pass) applySuccess — fixture repo > null mainCommitSha skips the comment but still pushes and records CLEAN (self-review hardening) [182.08ms]
(pass) applySuccess — fixture repo > keystone data-loss guard (#201): refuses to push when local HEAD is emptied but the remote branch has commits [190.20ms]
(pass) applySuccess — fixture repo > a non-managed head ref is a no-op (no push, no comment, no row) [117.29ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > BEHIND PR rebases cleanly on the next tick, applies success, and a re-tick is idempotent [195.84ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR rebase-fails → merge fallback lands → applySuccess('merged-new-work-as-base') [256.88ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR both attempts fail (rename/rename) → applyDemoteToWork fires [245.27ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > data-loss regression (#201): rebase that would empty the branch → escalation fires; branch NOT reset to main, PR NOT closed [213.50ms]
[pr-divergence] GitHub budget low (10 < 100); skipping pass — resets 2026-06-04T06:40:23.654Z
(pass) reconcileOpenPRs — end-to-end against the fixture repo > rate-limit floor short-circuits the pass; no listing happens [108.18ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CLEAN PR → walked but unchanged; nothing posted, no state advance [120.58ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > two open managed PRs in one pass — both walked, mix of CLEAN + BEHIND→rebased [185.94ms]
[pr-divergence] o/r PR #300 reconciliation failed: transient classify boom
(pass) reconcileOpenPRs — end-to-end against the fixture repo > per-PR throw increments `failed` and the pass continues on subsequent PRs (self-review hardening) [124.16ms]
[pr-divergence] list open managed PRs for o/r failed: transient gh outage
(pass) reconcileOpenPRs — end-to-end against the fixture repo > listOpenManagedPrs throws → pass returns 0s and logs, no orchestration [110.65ms]

packages/dispatcher/test/documentation-workflow.test.ts:
(pass) documentation workflow — shell: step order + dedicated slot > declares the six steps in order [182.58ms]
(pass) documentation workflow — shell: step order + dedicated slot > runs the steps in order at runtime and completes [275.90ms]
(pass) documentation workflow — shell: step order + dedicated slot > records its row with kind 'documentation' — its own dedicated slot, off maxConcurrent [267.12ms]
(pass) documentation workflow — shell: step order + dedicated slot > claims the 'docs' worktree unit, distinct from the recommender's [272.14ms]
(pass) documentation workflow — shell: step order + dedicated slot > spawn-docs-agent has the spec's 5-minute hard cap [187.08ms]
(pass) documentation workflow — shell: step order + dedicated slot > prepare-docs-worktree registers a compensation handler [172.09ms]
(pass) documentation workflow — shell: step order + dedicated slot > check-rate-limit does not retry [170.29ms]
(pass) documentation workflow — shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' [235.07ms]
[documentation:middle-docs-thejustinwalsh-middle-84683439] spawn failed: launch timeout
(pass) documentation workflow — shell: step order + dedicated slot > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [280.69ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=false: persist seam is never invoked [275.54ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true but persistDocs UNWIRED: still persists nothing (read-only first) [270.58ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true and persistDocs wired: persist runs after the agent, before cleanup [275.75ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports the resolved target, audit mode, and config; invokes the skill via @-reference [348.23ms]
(pass) documentation workflow — assembleDocumentationPrompt > includes the llms.txt audit line only when the target supports it [213.52ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports write=true to the agent when configured [210.91ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=true selects write mode: discover-or-author, agent does not commit [205.10ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=false stays in audit mode (read-only), never write mode [211.12ms]
(pass) documentation workflow — assembleDocumentationPrompt > write mode keeps the llms.txt instruction only when the target supports it [197.63ms]
(pass) documentation workflow — sessionNameFor collision-resistance > is deterministic for a given repo [194.52ms]
(pass) documentation workflow — sessionNameFor collision-resistance > produces a tmux-safe session name under the docs namespace [186.50ms]
(pass) documentation workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [187.52ms]

packages/dispatcher/test/recommender-cron-parallel.test.ts:
[recommender-cron] acme/b run timed out after 500ms — abandoned (retries next tick)
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > a hung repo times out without blocking the others; A+C succeed, B fails [504.24ms]
[recommender-cron] bad/b run failed: boom
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > a throwing run is isolated the same way (stamp rolled back, others succeed) [23.96ms]
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > concurrency is bounded by maxConcurrentRepos [95.24ms]
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > the pass still works (and is sequential-equivalent) with a single due repo [12.09ms]

packages/dispatcher/test/host-context.test.ts:
(pass) DaemonHostContext exposes dispatch + refreshEpics callbacks [0.03ms]

packages/dispatcher/test/control-routes.test.ts:
(pass) HookServer control routes > GET /health reports liveness, port, and version [2.20ms]
(pass) HookServer control routes > the server idle-timeout exceeds the SSE heartbeat (else /control/events streams drop) [0.03ms]
(pass) HookServer control routes > POST /control/dispatch starts the workflow and returns its id [2.06ms]
(pass) HookServer control routes > POST /control/dispatch rejects invalid bodies with 400 and starts nothing [3.04ms]
(pass) HookServer control routes > POST /control/dispatch surfaces the disabled-vs-unknown distinction in the 400 body [1.82ms]
(pass) HookServer control routes > POST /control/dispatch refuses with 429 when no slot is available (manual respects limits) [2.06ms]
(pass) HookServer control routes > POST /control/dispatch proceeds when a slot is available [1.42ms]
[hook-server] afterDispatch failed for o/r: scheduler boom
(pass) HookServer control routes > POST /control/dispatch survives a throwing afterDispatch (best-effort, still 200) [3.09ms]
(pass) HookServer control routes > POST /control/dispatch rejects a colliding Epic with 409 [1.68ms]
(pass) HookServer control routes > POST /control/dispatch maps a shared-checkout collision to 400 (#226) [1.62ms]
(pass) HookServer control routes > two concurrent dispatches of the same Epic: exactly one 200, one 409 [6.91ms]
(pass) HookServer control routes > GET /control/events opens an SSE stream with a connected frame [2.73ms]
(pass) HookServer control routes > GET /control/events replays the injected init events [3.86ms]
(pass) HookServer control routes > GET / 404s in the bare server (the status page is gone; the SPA mounts via extraRoutes) [1.95ms]
(pass) HookServer control routes > GET /metrics renders Prometheus text from the metrics seam [1.66ms]
(pass) HookServer control routes > GET /control/metrics returns the raw snapshot as JSON [1.86ms]
(pass) HookServer control routes > metric routes 404 without a metrics seam [1.51ms]
(pass) HookServer control routes > POST /control/resume fires the parked Epic's resume and returns its id [2.00ms]
(pass) HookServer control routes > POST /control/resume 404s when no parked workflow owns the ref [1.19ms]
(pass) HookServer control routes > POST /control/resume 400s on a missing epicRef or answer [1.93ms]
(pass) HookServer control routes > control routes 404 in gate-only mode (no control plane wired) [1.98ms]

packages/dispatcher/test/tmux.test.ts:
(pass) tmux session lifecycle > launch → has-session → send-text → capture-pane → kill [266.45ms]
(pass) tmux session lifecycle > newSession injects env via -e KEY=val [257.37ms]
(pass) tmux session lifecycle > hasSession is false for an unknown session [1.32ms]
(pass) tmux session lifecycle > status reports not-alive for an unknown session [1.21ms]
(pass) tmux session lifecycle > killSession on an already-gone session is a no-op, not a throw [2.43ms]
(pass) tmux session lifecycle > newSession rejects a duplicate session name with a TmuxError [5.47ms]
(pass) tmux session lifecycle > getTmuxVersion parses the installed tmux's version [1.03ms]
(pass) parseTmuxVersion > parses release versions [0.05ms]
(pass) parseTmuxVersion > parses pre-release builds (next-X.Y, X.Ya) [0.03ms]
(pass) parseTmuxVersion > returns null on garbage input [0.01ms]
(pass) tmuxVersionAtLeast > compares major then minor against the threshold [0.05ms]

packages/dispatcher/test/workflow-record.test.ts:
(pass) getWorkflow epic_ref (#187) > reads back epic_ref straight from the column (slug, number-string, or null) [138.69ms]
(pass) dispatch source (#53) > records and reads back source 'manual' / 'auto'; null when unset [117.52ms]
(pass) workflow meta_json accessors > readWorkflowMeta returns {} for a missing row, a null meta, and malformed JSON [115.20ms]
(pass) workflow meta_json accessors > patchWorkflowMeta merges, preserving keys it does not set [94.22ms]
(pass) workflow meta_json accessors > patchWorkflowMeta does not bump updated_at — meta is scratch, not an activity signal [86.95ms]
(pass) workflow meta_json accessors > checkbox-reconcile state round-trips; defaults when unset [72.02ms]
(pass) workflow meta_json accessors > getCheckboxReconcileState sanitizes malformed nested meta back to the contract [99.51ms]
(pass) listRunningImplementationWorkflows > returns only running implementation rows that own both an epic and a worktree [120.13ms]
(pass) createWorkflowRecord > inserts a pending implementation row carrying epic_number [69.42ms]
(pass) createWorkflowRecord > a second create with the same id is a no-op (idempotent on retry), not a UNIQUE error [71.44ms]
(pass) createWorkflowRecord > a non-PK constraint violation (bad kind) still throws — not swallowed [64.94ms]
(pass) countActiveImplementationSlots > counts non-terminal implementation rows, grouped by adapter [76.82ms]
(pass) countActiveImplementationSlots > excludes terminal implementation rows [75.77ms]
(pass) countActiveImplementationSlots > excludes the recommender's own row — its dedicated slot is not a dispatch slot [71.58ms]
(pass) updateWorkflow > transitions state and bumps updated_at [74.47ms]
(pass) updateWorkflow > patches session fields without disturbing others [78.23ms]
(pass) updateWorkflow > a no-op patch leaves the row intact [70.06ms]
(pass) getWorkflow > returns null for an unknown id [63.03ms]
(pass) hasNonTerminalEpicWorkflow > true while an implementation Epic workflow is non-terminal, false once terminal [70.67ms]
(pass) hasNonTerminalEpicWorkflow > scopes by repo and epic; a recommender row never collides [71.01ms]
(pass) findParkedWorkflowByRef > finds the waiting-human workflow for a ref (slug or number); null otherwise [73.87ms]
(pass) listActiveImplementationWorkflows (#180) > returns lastHeartbeat (null when none observed, the touched epoch otherwise) [75.11ms]
(pass) listNonTerminalWorkflows > returns id/repo/epic/state for non-terminal implementation rows only [87.74ms]
(pass) workflow observers > notifies the observer of each patch, and stops after dispose [79.06ms]
[workflow-record] update observer threw: observer boom
(pass) workflow observers > a throwing observer does not break the DB write [72.08ms]
(pass) workflow observers > addWorkflowObserver fans out to every observer; disposers independent [74.24ms]
(pass) workflow observers > the finalize path notifies observers on a real transition only [79.85ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > flips a still-pending row to failed and reports the transition [70.62ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a row already past pending (e.g. a later step's compensated failure) [69.54ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a launching row — the launch step already advanced it [72.52ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on an unknown id [64.98ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > does NOT touch a pending recommender row — it legitimately sits at pending through build-prompt, where compensation owns the terminal state [69.66ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > does NOT touch a pending documentation row (same reason as recommender) [66.86ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > notifies observers only on a real transition [71.92ms]

packages/dispatcher/test/recovery.test.ts:
(pass) reconcileOrphanedSignals > an armed signal with no recoverable execution is finalized failed, consumed, and surfaced [84.25ms]
(pass) reconcileOrphanedSignals > a recoverable parked execution is left untouched (not an orphan) [87.46ms]
(pass) reconcileOrphanedSignals > only the orphaned rows are reconciled when alive and orphaned parks coexist [94.98ms]
(pass) reconcileOrphanedSignals > respects a custom finalState and tolerates a missing surface callback [82.46ms]
[recover] surfacing orphaned signal a32be1b6-e1ec-47a3-87d6-25318a312fc2 (epic-9-answered) failed: comment failed
(pass) reconcileOrphanedSignals > a surface callback that throws never aborts the reconcile (still finalized + consumed) [97.80ms]
(pass) reconcileOrphanedSignals > an orphaned signal with a null epicNumber still reconciles [83.84ms]
(pass) reconcileOrphanedSignals > a non-parked (terminal) workflow's stale signal is ignored — only waiting-human rows are pollable [76.80ms]
(pass) reconcileOrphanedSignals > finalState is typed to terminal states only (compile-time guard) [64.71ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BUNQUEUE_DATA_PATH) when it is set [62.52ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BQ_DATA_PATH) when it is set [62.65ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming DATA_PATH) when it is set [66.32ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming SQLITE_PATH) when it is set [62.29ms]
(pass) createDurableEngine (transient-queue env guard) > an empty-string env var still trips the guard (bunqueue coalesces with ??) [62.23ms]
(pass) createDurableEngine (transient-queue env guard) > names every offending var when several are set at once [61.37ms]
(pass) recoverEngine (durable engine across restart) > re-arms a parked waiting execution so a later signal resumes it [453.74ms]
(pass) recoverEngine (durable engine across restart) > drops a mid-drive (running) execution instead of re-driving it [369.71ms]

packages/dispatcher/test/hook-server.test.ts:
[hook-server] received session.started:middle-6
(pass) HookServer — SessionStart > awaitSessionStart resolves with the posted payload [2.28ms]
[hook-server] received session.started:middle-7
(pass) HookServer — SessionStart > a payload that arrives before anyone awaits is stashed and delivered [1.64ms]
[hook-server] received session.started:middle-9
[hook-server] received session.started:middle-9
(pass) HookServer — SessionStart > duplicate pre-await arrivals keep the FIRST payload, not the last [2.06ms]
[hook-server] received session.started:middle-DIFFERENT
(pass) HookServer — SessionStart > waiters are keyed by session — one session's event does not satisfy another [301.63ms]
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > awaitStop resolves on an agent.stopped POST [2.47ms]
[hook-server] received agent.subagent-stopped:middle-6
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > a subagent stop does NOT resolve awaitStop — only the main agent's Stop does [300.73ms]
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > a re-registered awaitStop is not evicted by an abandoned waiter's stale timeout [63.67ms]
[hook-server] received tool.pre:middle-42
(pass) HookServer — HMAC auth + event validation (with store) > a valid POST (correct token, known event) is accepted and recorded [3.26ms]
[hook-server] rejected tool.pre:middle-42 — bad or unknown token
(pass) HookServer — HMAC auth + event validation (with store) > a bad-HMAC POST is rejected 401 and never recorded [3.20ms]
[hook-server] rejected tool.pre:middle-DOES-NOT-EXIST — bad or unknown token
(pass) HookServer — HMAC auth + event validation (with store) > a POST for an unknown session is rejected 401 (no token resolvable) [3.00ms]
[hook-server] rejected unknown event "not.a.real.event"
(pass) HookServer — HMAC auth + event validation (with store) > an unknown event name is rejected 400 before auth or recording [3.11ms]
[hook-server] received session.started:middle-42
(pass) HookServer — HMAC auth + event validation (with store) > session.started with a valid token resolves the SessionGate awaiter [2.89ms]
(pass) HookServer — lifecycle > awaitSessionStart rejects on timeout [52.77ms]
(pass) HookServer — lifecycle > non-POST and unknown paths return 404 [2.40ms]
(pass) HookServer — lifecycle > stop() rejects outstanding waiters [1.41ms]
(pass) HookServer — recommender trigger endpoint > 404s when no trigger is wired (gate-only mode) [2.03ms]
(pass) HookServer — recommender trigger endpoint > wired trigger receives the posted repo and returns its status/body verbatim [2.89ms]
(pass) HookServer — recommender trigger endpoint > tolerates a garbled body — the trigger validates its own inputs [2.84ms]
(pass) HookServer — recommender trigger endpoint > coerces non-string repoSlug/repoPath to undefined before forwarding [2.73ms]
(pass) HookServer — recommender trigger endpoint > a non-object JSON body (null, primitive, array) is treated as empty, not a 500 [3.33ms]
(pass) HookServer — recommender trigger endpoint > passes a string field through while dropping a non-string sibling [3.55ms]
(pass) HookServer — merged routes > extraRoutes are served, and the fetch fallback still answers /health [3.15ms]
(pass) HookServer — merged routes > GET / no longer returns the status page (404 with no SPA route) [3.22ms]

packages/dispatcher/test/docs-persist.test.ts:
(pass) commitDocs > stages and commits authored docs; returns the sha + sorted file list [33.19ms]
(pass) commitDocs > returns null on a clean worktree — no empty commit [16.88ms]
(pass) commitDocs > excludes middle's .middle/ scratch even when the repo does not gitignore it [27.72ms]
(pass) commitDocs > honors a custom commit message [20.17ms]
(pass) makeGhPersistDocs > commits, then invokes the push seam with the commit it produced [20.56ms]
(pass) makeGhPersistDocs > clean worktree: the push seam is never invoked (no empty PR) [13.74ms]
(pass) pushDocsBranch > first run creates the branch on origin at the authored commit [37.71ms]
(pass) pushDocsBranch > re-run force-pushes a divergent commit (rebuilt branch is non-fast-forward) [56.80ms]
(pass) pushDocsBranch > surfaces a push failure rather than swallowing it (no origin configured) [21.13ms]
(pass) docsPrBody > lists the committed files, the commit sha, and the draft notice [9.64ms]

packages/dispatcher/test/documentation-run.test.ts:
[documentation-run] workflow wf_1780555175967_2ek4146j enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > runs to completion and records a kind:'documentation' row for the repo [381.35ms]
[documentation-run] workflow wf_1780555176351_qbmlgc0l enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > write=true but a clean worktree: the wired seam opens no PR (no empty commit) [382.30ms]
[documentation-run] workflow wf_1780555176734_3v0wp30f enqueued
(pass) dispatchDocumentation — integration: authors markdown into docs/ and persists it > no docs surface + write=true: the agent authors docs/, the run commits + pushes it [383.66ms]
(pass) resolveDocumentationOptions > accepts a configured non-default adapter (e.g. codex) [11.52ms]
(pass) resolveDocumentationOptions > rejects an adapter the registry doesn't know [10.74ms]
(pass) resolveDocumentationOptions > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [11.47ms]
(pass) resolveDocumentationOptions > resolves the markdown fallback target for a plain repo [11.20ms]
(pass) resolveDocumentationOptions > honors a [docs] tool/path override [12.48ms]
(pass) resolveDocumentationOptions > surfaces an unknown tool override as an error rather than falling back [11.66ms]

packages/dispatcher/test/recommender-cron.test.ts:
(pass) runRecommenderCronPass > fires a due, enabled, unpaused repo and stamps last_recommender_run [2.18ms]
(pass) runRecommenderCronPass > does not re-fire a repo whose interval hasn't elapsed [1.61ms]
(pass) runRecommenderCronPass > fires once the interval has elapsed [1.51ms]
(pass) runRecommenderCronPass > skips a paused repo [1.59ms]
(pass) runRecommenderCronPass > skips a repo whose recommender is disabled or unconfigured [1.50ms]
(pass) runRecommenderCronPass > skips a repo with a non-positive interval (never auto-runs) [1.49ms]
[recommender-cron] bad/repo run failed: recommender run boom
[recommender-cron] bad/repo run failed: recommender run boom
(pass) runRecommenderCronPass > a failed launch rolls the stamp back (retries next tick) and is isolated [1.76ms]
(pass) runRecommenderCronPass > ignores unmanaged rows (no checkout path) [1.48ms]

packages/dispatcher/test/poller.test.ts:
(pass) reasonFromSignalName > maps the durable signal names to resume reasons [66.12ms]
(pass) classifyNewHumanReply > returns the newest non-bot reply posted after the wait armed [66.37ms]
(pass) classifyNewHumanReply > returns null when only bot/stale comments exist [63.71ms]
(pass) classifyNewHumanReply > skips the dispatcher's own marked pause comment (posted as a non-bot human identity) [65.61ms]
(pass) classifyNewHumanReply > a genuine human reply that quote-replies the pause comment still resumes [62.25ms]
(pass) classifyReviewOutcome > a fresh CHANGES_REQUESTED review → changes-requested [63.09ms]
(pass) classifyReviewOutcome > a fresh APPROVED review → resolved [63.69ms]
(pass) classifyReviewOutcome > a fresh 0-actionable re-review → resolved even while decision stays CHANGES_REQUESTED [62.82ms]
(pass) classifyReviewOutcome > the `changes-requested` label alone (no fresh review) → changes-requested [64.15ms]
(pass) classifyReviewOutcome > only stale reviews and no actionable label → null (nothing changed) [62.91ms]
(pass) classifyReviewOutcome > a stale standing CHANGES_REQUESTED decision (no fresh review, no label) → null [66.35ms]
(pass) classifyReviewOutcome — CI gate > failing CI with no review feedback → resume to fix CI (CI_FAILED) [75.14ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review while CI is still pending is held (null) — don't end on un-built CI [64.74ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review with passing CI resolves [61.19ms]
(pass) classifyReviewOutcome — CI gate > explicit review feedback wins over red CI (address the review, which greens CI) [59.02ms]
(pass) classifyReviewOutcome — CI gate > absent CI (`none`) is non-blocking — the pre-CI review loop is unchanged [61.18ms]
(pass) classifyReviewOutcome — CI gate > failing CI but no PR change and no review → still CI_FAILED (red build is actionable) [63.21ms]
(pass) runPoller — answered-question > a new human reply fires epic-<n>-answered exactly once (idempotent across passes) [80.40ms]
(pass) runPoller — answered-question > a bot-only reply does not fire [80.40ms]
(pass) runPoller — answered-question > the dispatcher's own pause comment does not self-resume (#178) [76.62ms]
(pass) runPoller — review-changes > CHANGES_REQUESTED fires review-resolved with outcome 'changes-requested' [82.07ms]
(pass) runPoller — review-changes > APPROVED fires review-resolved as resolved [85.03ms]
(pass) runPoller — review-changes > a 0-actionable re-review fires review-resolved as resolved [84.51ms]
(pass) runPoller — review-changes > no PR yet → no fire [77.18ms]
[poller] poll failed for workflow efbe64fc-c58d-4e26-aa23-767db0d7a26b (epic-200-answered): API rate limit exceeded
(pass) runPoller — resilience > a gateway error for one workflow is isolated; others still fire [98.20ms]
[poller] GitHub budget low (50 < 100); skipping pass — resets 1970-01-01T00:17:40.000Z
(pass) runPoller — GitHub rate-limit guards > skips the whole pass when remaining budget is below the buffer [79.32ms]
(pass) runPoller — GitHub rate-limit guards > a healthy budget proceeds (the guard isn't always-on) [84.04ms]
(pass) runPoller — GitHub rate-limit guards > caps the workflows polled per pass (burst protection) [118.93ms]

packages/dispatcher/test/github-epics.test.ts:
(pass) parseEpicsList > maps sub_issues_summary into Epic rows [1.20ms]
(pass) parseEpicsList > tolerates blank lines and ignores rows missing a summary [0.04ms]
(pass) parseEpicsList > parses with labels: [] when labels key is wholly absent [0.02ms]

packages/dispatcher/test/reconcile.test.ts:
[reconcile] thejustinwalsh/middle#50 PR MERGED → completed (workflow 4505ae8d-fa1d-45b7-9400-30013a2c619f)
(pass) reconcileMergedParks > a merged PR finalizes the parked workflow to `completed` and tears down its worktree [75.03ms]
[reconcile] thejustinwalsh/middle#51 PR CLOSED → cancelled (workflow 6bfc4005-630d-4434-a283-6343bd9a679a)
(pass) reconcileMergedParks > a closed-unmerged PR finalizes to `cancelled` [74.53ms]
(pass) reconcileMergedParks > an open PR (a live review park) is left alone [71.72ms]
(pass) reconcileMergedParks > no PR for the Epic (a pending question) is left alone [69.33ms]
[reconcile] thejustinwalsh/middle#54 PR MERGED → completed (workflow e5736f47-e9c0-4623-a4db-5a17bdb05ab7)
[reconcile] worktree cleanup failed for e5736f47-e9c0-4623-a4db-5a17bdb05ab7 (continuing): git worktree remove failed
(pass) reconcileMergedParks > finalizes the row even when worktree teardown throws (best-effort) [72.36ms]
(pass) reconcileMergedParks > only walks `waiting-human` rows — running/terminal rows are untouched [88.30ms]
[reconcile] GitHub budget low (10 < 100); skipping pass — resets 1970-01-01T00:00:00.000Z
(pass) reconcileMergedParks > skips the whole pass when the GitHub budget is below the buffer [74.76ms]
[reconcile] thejustinwalsh/middle#70 PR MERGED → completed (workflow 4cbf7a01-d214-424a-b0d6-b6803b638e23)
[reconcile] thejustinwalsh/middle#71 PR CLOSED → cancelled (workflow 54c7ab26-35c2-4035-bd0e-5489ba714c0f)
[reconcile] thejustinwalsh/middle#72 PR MERGED → completed (workflow 9263c301-41d2-4878-8c50-5fd72e51bf07)
(pass) reconcileMergedParks > fires onMergedTransition at most once per repo per pass (Epic #168 wiring) [102.09ms]
[reconcile] thejustinwalsh/middle#75 PR MERGED → completed (workflow ef70061c-fcbb-4af7-a884-db17fa3d451a)
[reconcile] onMergedTransition for thejustinwalsh/middle failed (continuing): downstream sweep boom
[reconcile] thejustinwalsh/middle#76 PR MERGED → completed (workflow ac3f9c29-2c53-4ba9-adee-59791332c34b)
(pass) reconcileMergedParks > a thrown onMergedTransition is isolated — the merged-parks pass still finishes [89.09ms]
[reconcile] thejustinwalsh/middle#60 PR MERGED → completed (workflow d9cd6d46-76a8-41bd-80a6-ae5a62425a6c)
[reconcile] thejustinwalsh/middle#61 PR MERGED → completed (workflow bbe045b9-b9b8-43cb-aa63-ec7a28eaa8fb)
(pass) reconcileMergedParks > honors the per-pass burst cap [96.07ms]
(pass) reconcileMergedParks > does not tear down the worktree when it loses the race to a concurrent resume [75.33ms]
(pass) finalizeParkedWorkflow > transitions a still-parked row and reports the change [75.64ms]
(pass) finalizeParkedWorkflow > no-ops (returns false) a row that already left waiting-human [74.72ms]

packages/dispatcher/test/recommender-workflow.test.ts:
(pass) recommender workflow — #43 shell: step order + dedicated slot > declares the eight spec steps in order [176.08ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > runs the steps in spec order at runtime and completes [268.56ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > records its row with kind 'recommender' — its own dedicated slot, off maxConcurrent [271.30ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > spawn-recommender-agent's step backstop is sized for the per-repo ceiling [171.26ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > prepare-shallow-worktree registers a compensation handler [175.65ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > check-rate-limit does not retry — it creates the row then may throw, and a retry would re-INSERT [174.69ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' (not a UNIQUE error) [235.26ms]
[recommender:middle-rec-thejustinwalsh-middle-84683439] spawn failed: launch timeout
(pass) recommender workflow — #43 shell: step order + dedicated slot > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [267.26ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > assembles all eight Phase-1 inputs, with dispatcher-owned context verbatim [173.15ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > file mode reframes the prompt for the file-backed store (#200) [171.97ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > writes the assembled prompt to .middle/prompt.md and launches it via the @-reference [266.31ms]
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a valid produced body verifies ok and the workflow proceeds to trigger-auto-dispatch [263.53ms]
[recommender] reapply skipped — agent body for #99 does not parse: missing open marker
[recommender] state issue #99 does not parse: missing open marker
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a malformed produced body does NOT proceed to auto-dispatch and surfaces the problem [216.40ms]
[recommender] state issue #99 failed validation: Ready row uses unconfigured adapter: "ghost"
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a body that parses but fails validation is also gated and surfaced [268.53ms]
[recommender] reapply skipped — agent body for #99 does not parse: missing open marker
[recommender] state issue #99 does not parse: missing open marker
[recommender] surfaceProblem failed: gh comment failed
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a failed surfaceProblem callback does not abort cleanup (best-effort surfacing) [271.23ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > heartbeatRel formats epoch deltas; null → 'unknown' [175.35ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > dispatcherSectionsFromContext builds canonical sections (heartbeat, null-issue dropped, null-session→pending) [172.21ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > self-heal: agent emits empty In-flight; dispatcher overwrites with the canonical 5-field line [267.39ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > no-op: when the agent body already matches the dispatcher's sections, reapply skips the write [266.25ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
[documentation-run] engine.close drain timed out after 10s — proceeding
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > a throwing reapply write compensates (worktree rolled back, no dispatch) [2444.45ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
[recommender] reapply skipped — agent body for #99 does not parse: malformed "In-flight" item: "- **#60** · claude · running · [tmux: middle-thejustinwalsh-middle-60]"
[recommender] state issue #99 does not parse: malformed "In-flight" item: "- **#60** · claude · running · [tmux: middle-thejustinwalsh-middle-60]"
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > exact bug shape: agent body with a 4-field In-flight line is left to verify, which surfaces it [264.32ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > derives rate_limits, in_flight, and slots from db + config [198.72ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > excludes the recommender's own row from in_flight and slots [184.41ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > scopes per-repo slots/in_flight to the repo, but global_used spans all repos [188.25ms]
(pass) recommender workflow — sessionNameFor collision-resistance > is deterministic for a given repo [170.44ms]
(pass) recommender workflow — sessionNameFor collision-resistance > produces a tmux-safe session name (no separators survive) [202.21ms]
(pass) recommender workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [176.47ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > runs on the engine via per-repo resolveRunSettings and creates the recommender row [220.00ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > a clear wiring error when neither resolveRunSettings nor static settings are provided [2314.36ms]

packages/dispatcher/test/staleness-cron.test.ts:
[staleness] o/active#50 landed in merged PR #88 → closed
[staleness] o/active: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass > reads the repo's spec from its checkout, closes + flags; skips paused [3.20ms]
(pass) runStalenessCronPass > a non-ENOENT spec read error surfaces (not silently treated as missing spec) [2.35ms]
[staleness] o/custom#50 landed in merged PR #88 → closed
[staleness] o/custom: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — per-repo spec path > a repo's [staleness] spec_path points the drift check at a non-default location [2.53ms]
[staleness] o/defaulted#50 landed in merged PR #88 → closed
[staleness] o/defaulted: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — per-repo spec path > a repo with no configured spec_path falls back to the default path [2.93ms]
[staleness] o/nospec#50 landed in merged PR #88 → closed
(pass) runStalenessCronPass — per-repo spec path > a repo with no spec file still reconciles landed issues (no drift) [2.03ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a `..` traversal spec_path is rejected — the pass never reads outside the checkout [2.44ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a deeper `../../` traversal is rejected too [2.29ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > an absolute spec_path is rejected (the field is repo-relative by contract) [2.20ms]
[staleness] o/dotdotname#50 landed in merged PR #88 → closed
[staleness] o/dotdotname: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a filename whose segment merely starts with `..` is allowed (not a traversal) [2.17ms]

packages/dispatcher/test/rate-limits.test.ts:
(pass) rate_limit_state > getRateLimitState is null until observed [74.81ms]
(pass) rate_limit_state > setRateLimited records status, reset_at, and source [66.41ms]
(pass) rate_limit_state > setRateLimited upserts an existing adapter row [70.45ms]
(pass) rate_limit_state > markAvailable clears the reset time [71.14ms]
(pass) rate_limit_state > markAvailableOnSuccess flips RATE_LIMITED → AVAILABLE and reports it [70.04ms]
(pass) rate_limit_state > markAvailableOnSuccess is a no-op when not rate-limited [68.71ms]
(pass) rate-limit observer fan-out > addRateLimitObserver fans out to every observer; disposers are independent [73.57ms]
[rate-limits] observer threw: boom
(pass) rate-limit observer fan-out > a throwing observer does not stop the others or the write path [67.51ms]
(pass) parseResetAt > parses an ISO timestamp to unix ms [61.18ms]
(pass) parseResetAt > returns null for unrecognized text [61.83ms]

packages/dispatcher/test/poller-cron.test.ts:
(pass) POLLER_INTERVAL_MS matches the dispatcher CLAUDE.md cadence contract (60s) [0.92ms]

packages/dispatcher/test/blocker-resolution.test.ts:
(pass) parseBlockerRef > same-repo #<n> [0.08ms]
(pass) parseBlockerRef > cross-repo <owner>/<repo>#<n> [0.02ms]
(pass) parseBlockerRef > strips a trailing title annotation when extracting the ref [0.02ms]
(pass) parseBlockerRef > backticked non-issue blocker is non-resolvable [0.01ms]
(pass) parseBlockerRef > free text without a #<n> is non-issue
(pass) resolveBlockers > a closed same-repo blocker moves the item to Ready to dispatch [0.27ms]
(pass) resolveBlockers > an open blocker stays Blocked, annotated with the resolved title [0.09ms]
(pass) resolveBlockers > an unresolvable (404) blocker stays Blocked with a (stale blocker: <ref>) suffix [0.05ms]
(pass) resolveBlockers > a backticked non-issue blocker is left untouched [0.07ms]
(pass) resolveBlockers > an open blocker with an empty title falls back to the bare ref (never `#42 ()`) [0.07ms]
(pass) resolveBlockers > a long open-blocker title is truncated to 60 chars in the annotation [0.05ms]
(pass) resolveBlockers > re-resolving is idempotent — a re-annotated open blocker does not accumulate [0.04ms]
(pass) resolveBlockers > re-resolving a now-closed previously-stale blocker unblocks it [0.04ms]
(pass) resolveBlockers > appended Ready rows are re-ranked after existing rows [0.11ms]
(pass) resolveBlockers > falls back to resolveIssue for the title when selfEpic has no entry [0.07ms]
(pass) resolveBlockers > the produced state still round-trips through render/parse [0.12ms]
(pass) resolveBlockers > no resolvable blockers → state is returned structurally unchanged [0.06ms]
(pass) resolveBlockers > a long blocker title is truncated to 60 chars with an ellipsis in the Ready epic [0.09ms]

packages/dispatcher/test/hook-server-gates.test.ts:
(pass) HookServer — /gates/pr-ready > returns 200 when the gate allows [2.48ms]
[hook-server] pr-ready gate DENY for middle-27: criteria X and Y lack evidence
(pass) HookServer — /gates/pr-ready > returns 403 with the reason in the body when the gate denies [1.65ms]
(pass) HookServer — /gates/pr-ready > forwards the session name and payload to the gate handler [2.10ms]
(pass) HookServer — /gates/pr-ready > 404s the gate route when no gate handler is wired [2.62ms]

packages/dispatcher/test/repo-config.test.ts:
(pass) repo pause/resume > an unpaused repo (no row) reads as not paused [1.73ms]
(pass) repo pause/resume > mm pause (indefinite) suspends the repo [1.47ms]
(pass) repo pause/resume > a paused_until in the future reads as paused; in the past auto-expires [1.36ms]
(pass) repo pause/resume > mm resume clears the pause [1.42ms]
(pass) repo pause/resume > pausing is idempotent and re-pausing updates the timestamp [1.39ms]
(pass) repo pause/resume > resume on a never-paused repo is a harmless no-op [1.33ms]
(pass) managed-repo registry (#135) > an unregistered repo has no path and isn't listed [1.42ms]
(pass) managed-repo registry (#135) > registerManagedRepo records the checkout path and lists it [1.80ms]
(pass) managed-repo registry (#135) > registering is idempotent and updates the path in place (one row) [1.55ms]
(pass) managed-repo registry (#135) > registering preserves an existing pause (doesn't clobber paused_until) [1.59ms]
(pass) managed-repo registry (#135) > listManagedRepos excludes rows with no checkout path (e.g. a pause-only row) [1.44ms]
(pass) managed-repo registry (#135) > setLastRecommenderRun writes a value and clears it with null (cron rollback) [1.38ms]
(pass) managed-repo registry (#135) > markRecommenderRun stamps and reads back last_recommender_run [1.40ms]
(pass) shared-checkout collision guard (#226) > (a) registering acme/a at /tmp/X succeeds [1.38ms]
(pass) shared-checkout collision guard (#226) > (b) re-registering the SAME repo at the same path is idempotent and succeeds [1.42ms]
(pass) shared-checkout collision guard (#226) > (c) registering a DIFFERENT repo at the same path rejects, naming both repos + the path [1.42ms]
(pass) shared-checkout collision guard (#226) > the rejected repo is NOT written (the collision guard runs before the insert) [1.43ms]
(pass) shared-checkout collision guard (#226) > the same repo can move to a new path (no self-collision) [1.51ms]
(pass) shared-checkout collision guard (#226) > assertNoRepoPathCollision is a standalone guard (used by mm init before scaffolding) [1.39ms]
(pass) shared-checkout collision guard (#226) > trailing-slash / dot-segment spellings of the same path still collide (normalized) [1.74ms]

packages/dispatcher/test/worktree.test.ts:
(pass) createWorktree → listWorktrees → destroyWorktree > create places the worktree under <root>/<repo>/issue-<n> on a fresh branch [15.28ms]
(pass) createWorktree → listWorktrees → destroyWorktree > the recommender unit is named 'recommender' [13.04ms]
(pass) createWorktree → listWorktrees → destroyWorktree > list enumerates active worktrees under the root [21.97ms]
(pass) createWorktree → listWorktrees → destroyWorktree > destroy removes the worktree directory and its branch [21.41ms]
(pass) idempotency > creating an already-existing worktree returns the handle without throwing [14.29ms]
(pass) idempotency > destroying an already-removed worktree is a no-op, not a throw [20.11ms]
(pass) branch reuse (issue #179) > reuses an existing branch — does not pass -b, so it doesn't error [15.33ms]
(pass) branch reuse (issue #179) > reuse checks out the existing branch's own tip, not a fresh branch from HEAD [20.76ms]
(pass) branch reuse (issue #179) > still creates a fresh branch when none exists (first dispatch unchanged) [16.38ms]
(pass) branch reuse (issue #179) > dispatch → prune (branch survives) → re-dispatch all succeed [24.06ms]
(pass) failure surfacing > create against a non-git directory throws WorktreeError [7.25ms]

packages/dispatcher/test/adapter-conformance.test.ts:
(pass) the registry knows all three adapters [0.19ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("toString") throws unknown-adapter [0.17ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("toString") is false [0.12ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("constructor") throws unknown-adapter [0.10ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("constructor") is false [0.08ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("hasOwnProperty") throws unknown-adapter [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("hasOwnProperty") is false [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("__proto__") throws unknown-adapter [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("__proto__") is false [0.08ms]
(pass) AgentAdapter contract — claude > resolveTranscriptPath yields a path from this adapter's own ready payload [0.14ms]
(pass) AgentAdapter contract — claude > identity: name matches its registry key and readyEvent is a normalized event [0.14ms]
(pass) AgentAdapter contract — claude > buildLaunchCommand yields a non-empty argv and the session env [0.15ms]
(pass) AgentAdapter contract — claude > buildPromptText: initial is the skill slash-command on the Epic [0.12ms]
(pass) AgentAdapter contract — claude > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.10ms]
(pass) AgentAdapter contract — claude > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [2.63ms]
(pass) AgentAdapter contract — claude > classifyStop: blocked.json → asked-question [0.46ms]
(pass) AgentAdapter contract — claude > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.53ms]
(pass) AgentAdapter contract — claude > detectRateLimit is implemented and returns null on a clean transcript [0.17ms]
(pass) AgentAdapter contract — codex > resolveTranscriptPath yields a path from this adapter's own ready payload [0.14ms]
(pass) AgentAdapter contract — codex > identity: name matches its registry key and readyEvent is a normalized event [0.09ms]
(pass) AgentAdapter contract — codex > buildLaunchCommand yields a non-empty argv and the session env [0.11ms]
(pass) AgentAdapter contract — codex > buildPromptText: initial is the skill slash-command on the Epic [0.08ms]
(pass) AgentAdapter contract — codex > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.08ms]
(pass) AgentAdapter contract — codex > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.35ms]
(pass) AgentAdapter contract — codex > classifyStop: blocked.json → asked-question [0.41ms]
(pass) AgentAdapter contract — codex > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.45ms]
(pass) AgentAdapter contract — codex > detectRateLimit is implemented and returns null on a clean transcript [0.14ms]
(pass) AgentAdapter contract — copilot > resolveTranscriptPath yields a path from this adapter's own ready payload [0.23ms]
(pass) AgentAdapter contract — copilot > identity: name matches its registry key and readyEvent is a normalized event [0.09ms]
(pass) AgentAdapter contract — copilot > buildLaunchCommand yields a non-empty argv and the session env [0.17ms]
(pass) AgentAdapter contract — copilot > buildPromptText: initial is the skill slash-command on the Epic [0.10ms]
(pass) AgentAdapter contract — copilot > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.09ms]
(pass) AgentAdapter contract — copilot > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.13ms]
(pass) AgentAdapter contract — copilot > classifyStop: blocked.json → asked-question [0.41ms]
(pass) AgentAdapter contract — copilot > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.37ms]
(pass) AgentAdapter contract — copilot > detectRateLimit is implemented and returns null on a clean transcript [0.13ms]

packages/dispatcher/test/main.test.ts:
(pass) dispatcher main > starts the hook server, announces readiness, and exits 0 on SIGTERM [1464.02ms]
(pass) dispatcher main > hosts a dispatch on its own engine and broadcasts a workflow SSE event [1218.34ms]
(pass) dispatcher main > a terminal prepare-worktree failure marks the row failed, so the next dispatch isn't 409-blocked (issue #179) [3097.03ms]
(pass) dispatcher main > daemon rejects a disabled adapter on /control/dispatch (configured+enabled+implemented gate) [1215.71ms]
(pass) dispatcher main > two concurrent dispatches of the same Epic: exactly one starts, the other 409s [1465.23ms]

packages/dispatcher/test/db.test.ts:
(pass) openDb > opens a file database in WAL mode [12.40ms]
(pass) runMigrations > a fresh db starts at schema version 0 [12.21ms]
(pass) runMigrations > applies every migration and reports the latest version [62.11ms]
(pass) runMigrations > 001_initial creates every documented table [62.94ms]
(pass) runMigrations > 001_initial creates every documented index [63.67ms]
(pass) runMigrations > is idempotent — running twice leaves version at the latest and does not throw [70.03ms]
(pass) runMigrations > 002 adds the waitfor_signals.fired_at column [63.02ms]
(pass) runMigrations > workflows.state CHECK rejects an unknown state [63.59ms]
(pass) runMigrations > workflows.state CHECK accepts 'launching' [66.71ms]
(pass) runMigrations > 003 widens workflows.kind to accept 'documentation' but still rejects unknown kinds [66.91ms]
(pass) runMigrations > 003 preserves existing rows and child FK references through the table rebuild [72.47ms]
(pass) openAndMigrate > opens, migrates, and returns a ready database [65.29ms]

packages/dispatcher/test/retention.test.ts:
(pass) runRetentionPass — events cutoff (14d) > deletes events older than 14 days, keeps newer ones [89.74ms]
(pass) runRetentionPass — events cutoff (14d) > an event exactly at the cutoff age is kept (strict `< cutoff`) [77.05ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > archives completed workflows older than 30 days; drops their events, preserves the row [79.87ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > does not archive completed workflows inside the 30-day window [72.59ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > does not archive old non-completed workflows (failed/running/etc.) [76.49ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > is idempotent — a second pass archives nothing new [81.73ms]
(pass) retention_runs recording > records each pass (even a no-op) with ok=true [70.34ms]
(pass) retention_runs recording > recordRetentionRun with a detail marks ok=false [67.36ms]
(pass) retention_runs recording > an empty-string detail still marks ok=false (failure presence, not truthiness) [67.10ms]
(pass) retention_runs recording > getLatestRetentionRun returns the most recent by ran_at [72.05ms]
(pass) collectRetentionStatus > reports row counts (incl. archived) and the last run [81.69ms]
(pass) collectRetentionStatus > lastRun is null before any retention has run [62.46ms]

packages/dispatcher/test/slots.test.ts:
(pass) getSlotState > free-slot: no active work reports full availability across every dimension [1.81ms]
(pass) getSlotState > at-capacity: a full repo reports zero availability and the guard refuses [1.69ms]
(pass) getSlotState > per-adapter cap binds before the repo cap [1.59ms]
(pass) getSlotState > global cap binds across repos even when this repo has room [1.54ms]
(pass) getSlotState > the recommender's own row is never counted against dispatch slots [1.49ms]
(pass) getSlotState > used over max clamps available to 0 (a tightened cap never goes negative) [1.51ms]
(pass) getSlotState > an adapter with no per-adapter cap is gated only by the repo and global dims [1.45ms]
(pass) reserveSlot > decrements the adapter, repo, and global dimensions for the loop's local view [1.41ms]
(pass) reserveSlot > reserving down to capacity flips the guard to refuse [1.97ms]
(pass) reserveSlot > reserving an adapter with no cap still decrements repo + global [1.41ms]

packages/dispatcher/test/auto-dispatch.test.ts:
(pass) autoDispatch > normal pass: enqueues every ready row that has a free slot [0.42ms]
(pass) autoDispatch > does nothing for a repo whose auto-dispatch is disabled [0.06ms]
(pass) autoDispatch > skips a rate-limited adapter but keeps dispatching others [0.06ms]
(pass) autoDispatch > skips a row whose per-adapter slot is exhausted, continues to the next adapter [0.05ms]
(pass) autoDispatch > stops entirely when the repo total is exhausted (slots-exhausted) [0.06ms]
(pass) autoDispatch > stops when the global total is exhausted even if the repo has room [0.04ms]
(pass) autoDispatch > decrements local counters as it enqueues so a shared cap stops mid-pass [0.07ms]
(pass) autoDispatch > a refused enqueue (collision/null) does not consume a local slot [0.11ms]
(pass) autoDispatch > dispatches a file-mode Epic by its slug ref (#200) [0.07ms]
(pass) autoDispatch > extracts a non-kebab slug ref up to the first space (#200) [0.19ms]
(pass) autoDispatch > ignores the empty-state (no ready rows) without enqueuing [0.09ms]
(pass) autoDispatch > no pre-dispatch complexity gate: a large-sub-issue Epic still dispatches (#52) [0.08ms]
(pass) createParseFailureSurfacer (#180) > surfaces a parse failure on the state issue, with the underlying message [0.16ms]
(pass) createParseFailureSurfacer (#180) > dedupes an identical message across a burst — one comment, not N [0.06ms]
(pass) createParseFailureSurfacer (#180) > reset() re-arms surfacing after a healthy read [0.05ms]
(pass) createParseFailureSurfacer (#180) > a different parse message surfaces even without a reset [0.04ms]
(pass) createParseFailureSurfacer (#180) > ignores non-parse errors so transient gh/network failures never spam [0.02ms]
(pass) createParseFailureSurfacer (#180) > a failed comment is not recorded — the next tick retries (no silent suppression) [0.09ms]
(pass) createParseFailureSurfacer (#180) > dedup is per-repo — two repos with the same message each surface once [0.04ms]
(pass) didReadState (#180) — gate re-arming on an actual read > a `disabled` pass did not read — must NOT re-arm surfacing [0.02ms]
(pass) didReadState (#180) — gate re-arming on an actual read > every reason that runs after readState counts as a read [0.01ms]
(pass) didReadState (#180) — gate re-arming on an actual read > disabled tick does not re-arm; a healthy (drained) read does [0.08ms]

packages/dispatcher/test/pr-divergence.test.ts:
(pass) classifyMergeability > DIRTY → CONFLICTED regardless of mergeable [62.63ms]
(pass) classifyMergeability > BEHIND → BEHIND [62.39ms]
(pass) classifyMergeability > CLEAN + MERGEABLE → CLEAN [63.03ms]
(pass) classifyMergeability > CLEAN but not MERGEABLE → UNKNOWN (CI gating, secondary signals) [62.23ms]
(pass) classifyMergeability > BLOCKED / HAS_HOOKS / UNSTABLE / UNKNOWN → UNKNOWN [62.73ms]
(pass) classifyMergeability > a null view (PR doesn't exist) → UNKNOWN [62.08ms]
(pass) classifyMergeability > missing fields → UNKNOWN (legacy fixtures don't tip the classifier) [63.56ms]
(pass) classifyDivergence > classifies BEHIND and persists the row with the supplied clock [65.75ms]
(pass) classifyDivergence > classifies CONFLICTED and overwrites a prior row (upsert keeps the row fresh) [70.15ms]
(pass) classifyDivergence > classifies CLEAN [68.01ms]
(pass) classifyDivergence > classifies UNKNOWN for a PR with no mergeability view (gone / 404) [71.27ms]
(pass) parseEpicFromHeadRef > parses `middle-issue-<N>` to the integer N [61.79ms]
(pass) parseEpicFromHeadRef > a non-managed head ref → null (the helper skips it) [65.79ms]
(pass) parseEpicFromHeadRef > a malformed managed ref → null (defends against an inadvertent rename) [63.74ms]
(pass) worktreePathFor > uses <root>/<repo>/issue-<n> — the same layout createWorktree writes [64.54ms]
(pass) recordDivergenceState > accepts terminal-ish states (DEMOTED, SKIPPED) written by sibling phases [75.38ms]
(pass) recordDivergenceState > the CHECK constraint rejects an out-of-vocabulary state — defends against a reconciler typo [82.95ms]
(pass) recordDivergenceState > the (repo, pr_number) PK lets the same pr_number coexist across repos [72.15ms]
(pass) applyDemoteToWork > flips PR draft, reopens sub-issue, posts dual-surface comment, re-enqueues, state→DEMOTED [69.36ms]
(pass) applyDemoteToWork > per-step idempotency: a second call skips draft-flip + reopen + comments via markers (but still re-enqueues) [97.33ms]
(pass) applyDemoteToWork > partial-retry: prior attempt left the PR drafted but did not reopen / comment / enqueue — second pass completes remediation [78.36ms]
(pass) applyDemoteToWork > partial-retry safety: existing marker on PR skips the duplicate PR comment, still posts on Epic [68.65ms]
(pass) applyDemoteToWork > Epic with no closed sub-issues: still demotes + comments + enqueues; no reopen call [65.66ms]
(pass) applyDemoteToWork > non-managed head ref → no-op (no draft, no comments, no enqueue, no row) [67.48ms]
(pass) applyDemoteToWork > manual recovery: an Epic that already carries the demote marker skips the reopen call (self-review hardening) [66.06ms]
(pass) applyDemoteToWork > a supplied reason (#201 data-loss) replaces the conflict narrative in the escalation comment [66.70ms]
(pass) applyDemoteToWork > PR doesn't exist (gateway returns null) → no-op [66.23ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a PullRequest with " [66.72ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a Branch with the n" [62.16ms]
(pass) ghStderrIsNotFound > recognizes not-found: "HTTP 404: Not Found (https://api.github." [66.19ms]
(pass) ghStderrIsNotFound > recognizes not-found: "graphql: Could not resolve to a Reposito" [64.56ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "error connecting to api.github.com: dial" [63.55ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 401: Bad credentials" [65.28ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 403: API rate limit exceeded" [64.76ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 502: Bad Gateway" [62.62ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "gh: command failed (oauth token expired)" [64.87ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "could not deserialize response" [74.03ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "remote: secret not found, push declined" [70.29ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "Not Found" [75.84ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "" [62.69ms]

packages/core/test/config.test.ts:
(pass) loadConfig — [docs] section > parses a full docs block [2.05ms]
(pass) loadConfig — [docs] section > a tool/path-only override block is valid; bot fields default [0.33ms]
(pass) loadConfig — [docs] section > absent override fields stay undefined so the resolver auto-detects [0.25ms]
(pass) loadConfig — [docs] section > no [docs] section leaves docs undefined [0.20ms]
(pass) loadConfig — [staleness] section > reads spec_path [0.22ms]
(pass) loadConfig — [staleness] section > no [staleness] section leaves staleness undefined [0.18ms]
(pass) loadConfig — [staleness] section > an empty [staleness] block leaves specPath undefined (falls back to the default) [0.21ms]
(pass) loadConfig — [staleness] section > the local cache overrides committed policy spec_path [0.22ms]
(pass) loadConfig — global only > parses the global sections and leaves per-repo sections undefined [0.23ms]
(pass) loadConfig — global only > expands ~ in path values [0.20ms]
(pass) loadConfig — per-repo merge > populates per-repo sections alongside global [0.40ms]
(pass) loadConfig — per-repo merge > per-repo values override global on a colliding key [0.30ms]
(pass) loadConfig — missing files > missing global file falls back to documented defaults without throwing [0.14ms]
(pass) loadConfig — missing files > missing per-repo file leaves per-repo sections undefined [0.20ms]
(pass) loadConfig — missing files > no paths at all yields an all-defaults config [0.16ms]
(pass) loadConfig — committed policy layer > reads policy.toml as the sibling of repoPath, merged with the local cache [0.27ms]
(pass) loadConfig — committed policy layer > a fresh clone with committed policy but no local cache still reads policy [0.25ms]
(pass) loadConfig — committed policy layer > local cache overrides committed policy on a colliding key [0.30ms]
(pass) loadConfig — committed policy layer > policy overrides the global file on a colliding key [0.30ms]
(pass) loadConfig — committed policy layer > an explicit repoPolicyPath overrides the sibling derivation [0.36ms]
(pass) loadConfig — committed policy layer > no repoPath means no policy is derived (global-only callers unaffected) [0.22ms]

packages/core/test/integration-rubric.test.ts:
(pass) parseAcceptanceCriteria > collects list items under the first acceptance heading, stops at next heading [0.06ms]
(pass) parseAcceptanceCriteria > returns [] when there is no acceptance section [0.01ms]
(pass) parseAcceptanceCriteria > only the first acceptance section counts — a later one does not reopen it [0.01ms]
(pass) isIntegrationCriterion > the spec's worked example is an integration criterion [0.04ms]
(pass) isIntegrationCriterion > 'unit tests pass' alone is not an integration criterion [0.01ms]
(pass) isIntegrationCriterion > wiring without a real-path test fails (behavior, not test) [0.02ms]
(pass) isIntegrationCriterion > a real-path test without wiring fails
(pass) isIntegrationCriterion > prose 'get' does not trip the uppercase HTTP-verb signal
(pass) isIntegrationCriterion > served + e2e qualifies
(pass) isIntegrationCriterion > plural 'integration tests' / 'smoke tests' phrasing still qualifies [0.02ms]
(pass) detectExemption > reads an inline annotation and a comment form [0.02ms]
(pass) auditIssueBody > passes a body with an integration criterion [0.03ms]
(pass) auditIssueBody > flags a weak body and suggests a concrete rewrite naming the feature [0.03ms]
(pass) auditIssueBody > flags a body with no acceptance section, suggestion says so [0.02ms]
(pass) auditIssueBody > a declared exemption passes and surfaces the reason [0.01ms]

packages/core/test/hook-script.test.ts:
(pass) PR_READY_GATE_SH exit-code contract > HTTP 200 → exit 0 (allow) [2.57ms]
(pass) PR_READY_GATE_SH exit-code contract > curl failure emitting no http code → exit 0 (fails OPEN, not closed) [2.11ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 403 from a reachable dispatcher → exit 2 (blocks) [2.31ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 404 (no gate wired — e.g. a recommender/docs session) → exit 0 (allow, never wedge) [1.93ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 401 (reachable bad-token/missing-session) → exit 2 (surface, don't silently disable the guard) [2.17ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 500 (reachable dispatcher fault) → exit 2 (surface, not a silent allow) [2.49ms]

packages/core/test/select-adapter.test.ts:
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > an agent:<name> label pins that adapter over the default [0.08ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > whitespace around the label and name is tolerated [0.03ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > conflicting agent labels throw [0.07ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > duplicate agent labels for the same name are not a conflict [0.01ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > a label naming an unconfigured adapter throws [0.02ms]
(pass) selectAdapter — rule 2: default adapter > with no agent label, the default adapter is chosen [0.01ms]
(pass) selectAdapter — rule 2: default adapter > a default adapter that isn't configured throws [0.03ms]
(pass) selectAdapter — rule 3: switch away from a rate-limited adapter when portable > a rate-limited default switches to an available adapter for a portable task [0.03ms]
(pass) selectAdapter — rule 3: switch away from a rate-limited adapter when portable > a label pin is never switched away from, even when rate-limited and portable [0.02ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a rate-limited default with a non-portable task is left and marked skip [0.02ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a portable task with no non-rate-limited alternative is left and marked skip [0.02ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a non-rate-limited choice is never marked skip [0.01ms]

packages/core/test/tmux-tui.test.ts:
(pass) capturePane > returns the visible pane contents of a live session [155.08ms]
(pass) capturePane > returns null for an unknown session [1.29ms]
(pass) sendText and sendKeys > sendText writes literal text into the pane [158.38ms]
(pass) sendText and sendKeys > sendKeys with delayBetweenMs sends each key in its own call [223.08ms]
(pass) pollPaneFor > resolves with the predicate's value when the pane matches [313.81ms]
(pass) pollPaneFor > returns null on timeout when the pane never matches [415.03ms]
(pass) pollPaneFor > returns null and bails when the session disappears [1.52ms]
(pass) pollPaneFor > when `tag` is set, writes one stderr line per iteration [4.33ms]

packages/adapters/codex/test/adapter.test.ts:
(pass) codexAdapter identity > name is 'codex' and readyEvent is session.started [0.29ms]
(pass) buildLaunchCommand > argv launches interactive codex (no exec, no prompt) [0.20ms]
(pass) buildLaunchCommand > env sets CODEX_HOME to the worktree-local .codex so the config is loaded [0.15ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.12ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.12ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.15ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.11ms]
(pass) buildPromptText > recommender force-invokes the recommender skill with the @-referenced context [0.11ms]
(pass) buildPromptText > docs force-invokes the documenting-the-repo skill with the @-referenced context [0.22ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.17ms]
(pass) resolveTranscriptPath > returns transcript_path from the SessionStart payload [0.14ms]
(pass) resolveTranscriptPath > falls back to rollout_path when transcript_path is absent [0.20ms]
(pass) resolveTranscriptPath > throws when the payload carries no session-file path [5.72ms]
(pass) readTranscriptState > parses a real-shaped rollout: activity, turn count, last tool use, context tokens [0.51ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.26ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.41ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.38ms]
(pass) classifyStop > asked-question tolerates a malformed blocked.json (sentinel → null) [0.35ms]
(pass) classifyStop > structured rate_limits with rate_limit_reached_type → rate-limited, resetAt from resets_at [0.41ms]
(pass) classifyStop > structured rate_limits at/over 100% used → rate-limited even without reached_type [0.32ms]
(pass) classifyStop > a healthy structured block is authoritative → bare-stop, even with a stray '429' in text [0.45ms]
(pass) classifyStop > text fallback (no structured block): "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.47ms]
(pass) classifyStop > text fallback (no structured block): "Error 429: Too Many Requests" → rate-limited (429 status) [0.29ms]
(pass) classifyStop > text fallback (no structured block): "too many requests — slow down" → rate-limited (too many requests phrase) [0.37ms]
(pass) classifyStop > text fallback (no structured block): "ratelimit exceeded" → rate-limited (ratelimit no-space) [0.38ms]
(pass) classifyStop > a bare "line 4290 of the file" is NOT a rate-limit signal → bare-stop (4290 — a line number) [0.50ms]
(pass) classifyStop > a bare "commit 4291abcdef" is NOT a rate-limit signal → bare-stop (4291 in a hash) [0.29ms]
(pass) classifyStop > a bare "listening on port 14290" is NOT a rate-limit signal → bare-stop (embedded 4290) [0.29ms]
(pass) classifyStop > a bare "processed 42900 rows" is NOT a rate-limit signal → bare-stop (42900) [0.26ms]
(pass) classifyStop > done.json sentinel → done [0.33ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.33ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.39ms]
(pass) classifyStop > nothing notable → bare-stop [0.29ms]
(pass) detectRateLimit > structured block at the limit → detection with the real reset time [0.21ms]
(pass) detectRateLimit > text fallback matches a rate-limit signal when no structured block exists [0.17ms]
(pass) detectRateLimit > returns null when a healthy structured block is present [0.15ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present at all [0.17ms]
(pass) installHooks > writes .codex/config.toml with auto-mode + sandbox_mode (NOT the rejected 'sandbox' key) [3.18ms]
(pass) installHooks > pre-trusts the worktree directory so codex skips the directory-trust dialog [1.20ms]
(pass) installHooks > maps each real Codex event to the normalized taxonomy via the absolute hook path [1.15ms]
(pass) installHooks > registers exactly the real Codex event set (PascalCase, no fictional names) [1.02ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.08ms]
(pass) installHooks > registers the PR-ready gate as a SECOND PreToolUse matcher group scoped to Bash [1.01ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.07ms]
(pass) installHooks > symlinks the operator's auth.json into the worktree CODEX_HOME [1.05ms]
(pass) installHooks > does not throw or create a link when the operator has no auth.json [1.16ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.28ms]
(pass) detectNeedsLogin > does not match normal pane content [0.15ms]
(pass) detectHooksTrustPrompt > matches the real 'Hooks need review' dialog text [0.23ms]
(pass) detectHooksTrustPrompt > does not match a normal pane or the directory-trust dialog [0.18ms]
(pass) detectDirTrustPrompt > matches the real first-run directory-trust dialog text [0.15ms]
(pass) detectDirTrustPrompt > does not match a normal pane or the hooks-trust dialog [0.12ms]
(pass) detectReadyForInput > matches the live composer-ready welcome banner (codex 0.133.0) [0.12ms]
(pass) detectReadyForInput > does not match a boot dialog (so a dialog is answered before we treat it as ready) [0.11ms]
(pass) startsSessionOnFirstPrompt > codex sets the prompt-triggered-session flag (it fires no SessionStart until a prompt) [0.09ms]
(pass) enterAutoMode > returns immediately when the target session does not exist [1.93ms]

packages/adapters/claude/test/adapter.test.ts:
(pass) claudeAdapter identity > name is 'claude' and readyEvent is session.started [0.22ms]
(pass) claudeAdapter identity > does NOT set startsSessionOnFirstPrompt — Claude fires SessionStart at boot, so the dispatcher keeps await-first order (#183 regression) [0.11ms]
(pass) buildLaunchCommand > argv launches interactive claude in auto mode via --dangerously-skip-permissions [0.14ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.14ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.11ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.12ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.18ms]
(pass) buildPromptText > recommender force-invokes the recommender skill with the @-referenced context [0.18ms]
(pass) buildPromptText > docs force-invokes the documenting-the-repo skill with the @-referenced context [0.11ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.11ms]
(pass) resolveTranscriptPath > returns transcript_path from the SessionStart payload [0.19ms]
(pass) resolveTranscriptPath > throws when the payload has no transcript_path [0.13ms]
(pass) readTranscriptState > parses activity, turn count, last tool use, and context tokens [0.32ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.24ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.40ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.32ms]
(pass) classifyStop > an unrecognized kind falls back to a plain question (kind omitted) [0.33ms]
(pass) classifyStop > asked-question tolerates a malformed/contentless blocked.json (sentinel → null) [0.39ms]
(pass) classifyStop > usage-limit message in the transcript tail → rate-limited [0.32ms]
(pass) classifyStop > done.json sentinel → done [0.31ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.35ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.38ms]
(pass) classifyStop > nothing notable → bare-stop [0.28ms]
(pass) detectRateLimit > matches a usage-limit message in the transcript tail [0.15ms]
(pass) detectRateLimit > returns null when no usage-limit message is present [0.16ms]
(pass) installHooks > registers the full Claude hook event set in .claude/settings.json [1.09ms]
(pass) installHooks > each entry maps its Claude event to the normalized taxonomy via the absolute hook path [1.00ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [0.94ms]
(pass) installHooks > registers the PR-ready gate as a second Bash-matched PreToolUse hook [0.89ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [0.94ms]
(pass) detectBypassPrompt > matches representative bypass-mode confirmation strings [0.18ms]
(pass) detectBypassPrompt > does not match normal Claude pane content [0.11ms]
(pass) detectTrustPrompt > matches the first-run folder-trust dialog [0.12ms]
(pass) detectTrustPrompt > does not match the bypass dialog or normal content [0.14ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.26ms]
(pass) detectNeedsLogin > does not match the bypass prompt or normal pane content [0.16ms]
(pass) enterAutoMode > returns immediately when the target session does not exist [1.87ms]

packages/adapters/copilot/test/adapter.test.ts:
(pass) copilotAdapter identity > name is 'copilot' and readyEvent is session.started [0.23ms]
(pass) copilotAdapter identity > sets the prompt-triggered-session flag (fires no sessionStart until a prompt) [0.13ms]
(pass) buildLaunchCommand > argv launches interactive copilot in auto mode (no -p, no prompt) [0.16ms]
(pass) buildLaunchCommand > env sets COPILOT_HOME to the worktree-local .copilot so the config + hooks load [0.14ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.13ms]
(pass) buildLaunchCommand > forwards an exported gh token so token-auth keeps working under the repointed home [0.14ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.12ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.11ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.11ms]
(pass) buildPromptText > recommender / docs force-invoke their skill with the @-referenced context [0.12ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.12ms]
(pass) resolveTranscriptPath > derives <cwd>/.copilot/session-state/<sessionId>/events.jsonl from the payload [0.13ms]
(pass) resolveTranscriptPath > falls back to snake_case session_id defensively [0.11ms]
(pass) resolveTranscriptPath > throws when the payload carries no sessionId [0.15ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "../../../../etc/passwd" (defense-in-depth against path escape) [0.13ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "a/b" (defense-in-depth against path escape) [0.09ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId ".." (defense-in-depth against path escape) [0.09ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "id with spaces" (defense-in-depth against path escape) [0.09ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "id;rm -rf" (defense-in-depth against path escape) [0.08ms]
(pass) readTranscriptState > parses a real-shaped events.jsonl: activity, turn count, last tool use, context tokens [0.28ms]
(pass) readTranscriptState > counts each assistant.turn_end as a turn [0.22ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.18ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.38ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.33ms]
(pass) classifyStop > asked-question tolerates a malformed blocked.json (sentinel → null) [0.34ms]
(pass) classifyStop > rate-limit text "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.31ms]
(pass) classifyStop > rate-limit text "Error 429: Too Many Requests" → rate-limited (429 status) [0.28ms]
(pass) classifyStop > rate-limit text "too many requests — slow down" → rate-limited (too many requests phrase) [0.29ms]
(pass) classifyStop > rate-limit text "ratelimit exceeded" → rate-limited (ratelimit no-space) [0.29ms]
(pass) classifyStop > rate-limit text "weekly quota exceeded for this model" → rate-limited (quota exceeded) [0.27ms]
(pass) classifyStop > rate-limit text "You have reached your usage limit" → rate-limited (usage limit) [0.27ms]
(pass) classifyStop > a bare "line 4290 of the file" is NOT a rate-limit signal → bare-stop (4290 — a line number) [0.32ms]
(pass) classifyStop > a bare "commit 4291abcdef" is NOT a rate-limit signal → bare-stop (4291 in a hash) [0.27ms]
(pass) classifyStop > a bare "listening on port 14290" is NOT a rate-limit signal → bare-stop (embedded 4290) [0.29ms]
(pass) classifyStop > a bare "processed 42900 rows" is NOT a rate-limit signal → bare-stop (42900) [0.27ms]
(pass) classifyStop > done.json sentinel → done [0.32ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.32ms]
(pass) classifyStop > done.json outranks stale rate-limit text in the transcript → done [0.32ms]
(pass) classifyStop > failed.json outranks stale rate-limit text in the transcript → failed [0.33ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.39ms]
(pass) classifyStop > nothing notable → bare-stop [0.34ms]
(pass) detectRateLimit > text rate-limit signal → detection with unknown reset (no structured block on disk) [0.28ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present [0.20ms]
(pass) installHooks > writes .copilot/hooks/middle.json with version 1 and the camelCase event keys [1.24ms]
(pass) installHooks > maps each Copilot event to the normalized taxonomy via the absolute hook path [1.08ms]
(pass) installHooks > registers the PR-ready gate as a SECOND preToolUse hook scoped to the bash tool [0.98ms]
(pass) installHooks > pre-trusts the worktree in config.json so copilot skips the folder-trust dialog [1.02ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [2.46ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.44ms]
(pass) installHooks > writes NO auth file (copilot authenticates via gh, unlike codex) [0.94ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.22ms]
(pass) detectNeedsLogin > does not match normal pane content [0.11ms]
(pass) detectReadyForInput > matches the live composer-ready footer / prompt (copilot 1.0.54) [0.14ms]
(pass) detectReadyForInput > does not match a bare boot screen with no composer [0.12ms]
(pass) detectTrustPrompt > matches a folder-trust dialog (defense-in-depth; pre-empted by trustedFolders) [0.12ms]
(pass) detectTrustPrompt > does not match a normal pane [0.10ms]
(pass) enterAutoMode > throws fast when the target session does not exist (never treated as ready) [1.88ms]

packages/dispatcher/test/epic-store/file-state-gateway.test.ts:
(pass) fileStateGateway > readBody returns the state file contents verbatim [0.40ms]
(pass) fileStateGateway > readBody throws a clear error when the state file is absent [0.17ms]
(pass) fileStateGateway > writeBody creates the parent directory and round-trips [0.32ms]
(pass) fileStateGateway > writeBody is atomic: leaves no `.tmp` sibling after a successful write [0.34ms]
(pass) fileStateGateway > writeBody derives the temp sibling from the filename via `basename` (separator-safe) [0.27ms]
(pass) fileStateGateway > writeBody overwrites an existing file [0.23ms]

packages/dispatcher/test/epic-store/file-poll-gateway.test.ts:
(pass) filePollGateway > listIssueComments derives authorIsBot structurally from the marker kind [0.79ms]
(pass) filePollGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.20ms]
(pass) filePollGateway > findPrForEpic resolves a slug via meta.pr; delegates a numeric ref to gh's finder [0.36ms]
(pass) filePollGateway > findPrForEpic returns null for a slug whose Epic file has no stamped meta.pr [0.23ms]
(pass) filePollGateway > findEpicPrLifecycle resolves a slug via meta.pr; delegates a numeric ref to gh [0.29ms]
(pass) filePollGateway > findEpicPrLifecycle returns null for a slug with no stamped meta.pr [0.23ms]
(pass) filePollGateway > a numeric-named file Epic (e.g. 42.md) resolves via meta.pr, not gh's #42 finder (#200) [0.29ms]
(pass) filePollGateway > prSnapshot / prLifecycle delegate straight to gh by PR number [0.19ms]
(pass) filePollGateway > getRateLimit delegates straight to gh [0.20ms]

packages/dispatcher/test/epic-store/file-epic-gateway.test.ts:
(pass) fileEpicGateway > listOpenEpics scans the dir, derives sub-issue progress, skips closed [0.80ms]
(pass) fileEpicGateway > listIssueComments maps the conversation; answer is attributed to the human [0.54ms]
(pass) fileEpicGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.17ms]
(pass) fileEpicGateway > getCommentAuthor discriminates human (answer) from agent by the file:// fragment [0.16ms]
(pass) fileEpicGateway > getCommentAuthor delegates a github.com URL to gh [0.14ms]
(pass) fileEpicGateway > getIssueLabels reads the Epic meta labels [0.21ms]
(pass) fileEpicGateway > postComment appends a re-parseable dispatch-event block [0.49ms]
(pass) fileEpicGateway > postComment delegates a PR-number ref to gh (no Epic file for it) [0.16ms]
(pass) fileEpicGateway > findEpicPr returns null without a stamped pr, and delegates to gh when present [0.32ms]
(pass) fileEpicGateway > findEpicPr returns null when the Epic file is absent [0.14ms]
(pass) fileEpicGateway > addLabel appends to meta labels and is a no-op if already present [0.78ms]
(pass) fileEpicGateway > a present-but-malformed Epic file surfaces the parser's named error [0.23ms]
(pass) fileEpicGateway > postComment writes atomically — no `.tmp` sibling left behind [0.33ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > re-asking the identical open question is a no-op [0.49ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > a different question (or different kind/context) appends a new entry [0.94ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > round-trip purity survives the skip (renderer remains the sole marker writer) [0.36ms]

packages/dispatcher/test/epic-store/round-trip.test.ts:
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(empty-epic.md)) === empty-epic.md [0.07ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(all-closed.md)) === all-closed.md [0.13ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(codex-adapter.md)) === codex-adapter.md [0.08ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(mid-question.md)) === mid-question.md [0.07ms]

packages/dispatcher/test/epic-store/mode-commands-mirror.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-mirror-O07de2/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-O07de2/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) dispatch brief — mode-commands mirror (#195) > a file-mode dispatch mirrors file-mode-commands.md into the worktree, byte-identical [238.08ms]
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-mirror-Yqk377/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-Yqk377/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) dispatch brief — mode-commands mirror (#195) > a github-mode dispatch does not mirror the file-mode reference [284.36ms]

packages/dispatcher/test/epic-store/file-dispatch-integration.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fdisp-q3sIIw/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fdisp-q3sIIw/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) file-mode dispatch — Test A: real workflow drive > a file-mode Epic parks asking a question → row carries the slug, Epic file gains a question block [297.19ms]
(pass) file-mode dispatch — Test B: real buildImplementationDeps selector > postQuestion routes to the Epic file for a file repo, and to gh for a github repo [184.01ms]

packages/dispatcher/test/epic-store/parity.test.ts:
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-4hHTKF/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-4hHTKF/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] Stop received — classification=bare-stop
(pass) implementation parity — github mode > happy-path dispatch reaches completed [276.14ms]
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-4QzqAW/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-4QzqAW/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-6] Stop received — classification=asked-question
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-4QzqAW/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-4QzqAW/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-6] Stop received — classification=bare-stop
(pass) implementation parity — github mode > park → resume-answer → continuation reaches completed [308.07ms]
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-Snt2JA/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-Snt2JA/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=bare-stop
(pass) implementation parity — file mode > happy-path dispatch reaches completed [219.61ms]
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-9NOlDr/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-9NOlDr/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=asked-question
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-9NOlDr/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-9NOlDr/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=bare-stop
(pass) implementation parity — file mode > park → resume-answer → continuation reaches completed [310.32ms]

packages/dispatcher/test/epic-store/file-watcher-integration.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fw-GrAjhG/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-GrAjhG/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fw-GrAjhG/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-GrAjhG/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) file-watcher Q&A loop (#197) > poller cron detects a non-empty answer edit and resumes the parked Epic to completion [431.33ms]

packages/dispatcher/test/epic-store/watcher.test.ts:
(pass) collectChangedSince > includes files with mtime > sinceMs, excludes older + dotfiles/.tmp [0.34ms]
(pass) collectChangedSince > missing dir → empty [0.14ms]
(pass) pollFileSignals > emits an open question that has a non-empty answer [0.21ms]
(pass) pollFileSignals > an unanswered question (placeholder) does NOT trigger [0.25ms]
(pass) pollFileSignals > a resolved question does NOT trigger (only the first non-empty edit fires) [0.18ms]
(pass) pollFileSignals > the mtime gate skips unchanged files [0.14ms]
(pass) resolveQuestion > flips an open question to resolved (the dedup write); idempotent [0.30ms]
(pass) resolveQuestion > a missing file/question is a no-op [0.15ms]
(pass) runFileWatcherTick > fires the resume + resolves the question for an answered-question park [81.01ms]
(pass) runFileWatcherTick > does NOT resume a workflow parked on a non-answered signal (reason guard) [74.94ms]

packages/dispatcher/test/epic-store/selector.test.ts:
(pass) buildGitHubGateways / buildFileGateways > buildGitHubGateways defaults to the real gh-backed trio [0.06ms]
(pass) buildGitHubGateways / buildFileGateways > buildFileGateways returns file-backed implementations (not the gh trio) [0.21ms]
(pass) makeRoutingEpicGateway > routes per-repo: file repo → file backend, github repo → gh backend [71.89ms]
(pass) makeRoutingPollGateway > a file-mode slug never reaches gh's numeric PR-finders; github delegates [76.26ms]
(pass) appendQuestion > appends an open question block that re-parses; ids increment [0.67ms]
(pass) appendQuestion > throws a clear error when the Epic file is absent [0.22ms]

packages/dispatcher/test/epic-store/file-gateways-integration.test.ts:
(pass) file gateways — Phase-1 lifecycle integration > dispatch-event record, human answer on disk, poll surfaces the human reply [0.94ms]
(pass) file gateways — Phase-1 lifecycle integration > state gateway round-trips the recommender state file atomically [0.30ms]

packages/dispatcher/test/epic-store/file-review-resume-integration.test.ts:
(pass) file-mode PR-review resume (real poller path) > a CHANGES_REQUESTED review on the stamped PR resumes the parked file-mode Epic [88.43ms]
(pass) file-mode PR-review resume (real poller path) > no resume while the Epic file has no stamped meta.pr (PR not opened yet) [82.21ms]

packages/dispatcher/test/epic-store/parser.test.ts:
(pass) parseEpicFile — document structure > parses the document marker, title, and minimal meta from an empty Epic [1.42ms]
(pass) parseEpicFile — document structure > throws when the document marker is missing [0.08ms]
(pass) parseEpicFile — document structure > throws when the meta block has no slug key [0.03ms]
(pass) parseEpicFile — meta > parses every recognized meta key from codex-adapter fixture [0.11ms]
(pass) parseEpicFile — meta > parses closed=true [0.09ms]
(pass) parseEpicFile — acceptance criteria > parses unchecked criteria from codex-adapter [0.06ms]
(pass) parseEpicFile — acceptance criteria > parses checked criteria from all-closed [0.05ms]
(pass) parseEpicFile — sub-issues > parses sub-issues with stable IDs + body [0.05ms]
(pass) parseEpicFile — sub-issues > parses checked sub-issues + provenance suffix [0.05ms]
(pass) parseEpicFile — conversation > parses dispatch-event + question entries; empty answer block stays absent [0.11ms]
(pass) parseEpicFile — conversation > treats a non-empty answer block as the resolved reply [0.07ms]
(pass) parseEpicFile — conversation > empty conversation block yields empty conversation array [0.03ms]

packages/dispatcher/test/epic-store/file-auto-dispatch-integration.test.ts:
(pass) file-mode auto-dispatch (real readState path) > reads the state_file and enqueues a file Epic by its slug ref [67.16ms]
(pass) file-mode auto-dispatch (real readState path) > a github-mode repo still routes readState to the gh state issue gateway [63.65ms]

packages/dispatcher/test/gates/verify-config.test.ts:
(pass) parseVerifyConfig — valid > parses gates in declared order and applies the default timeout [0.07ms]
(pass) parseVerifyConfig — valid > carries an optional phases scope [0.05ms]
(pass) parseVerifyConfig — valid > category defaults to unit and accepts integration; integrationGates filters [0.06ms]
(pass) gatesForPhase — per-phase addressing > an unscoped gate runs for every phase [0.03ms]
(pass) gatesForPhase — per-phase addressing > a scoped gate runs only for its listed phases, preserving declared order [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: no gates [0.05ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: missing name [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty name
(pass) parseVerifyConfig — malformed fails loudly > rejects: missing command
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty command [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: duplicate name [0.04ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: non-positive timeout [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: non-int phases [0.06ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: negative phases [0.03ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty phases
(pass) parseVerifyConfig — malformed fails loudly > rejects: unknown key [0.06ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: invalid category [0.05ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: invalid toml [0.08ms]
(pass) parseVerifyConfig — malformed fails loudly > the unknown-key message lists the live key set (incl. `category`) [0.06ms]
(pass) loadVerifyConfig — file IO > loads a valid file from disk [0.32ms]
(pass) loadVerifyConfig — file IO > a missing file fails loudly with the path in the message [0.09ms]
(pass) loadVerifyConfig — file IO > verifyConfigPath resolves the worktree's .middle/verify.toml [0.02ms]

packages/dispatcher/test/gates/plan-comment.test.ts:
(pass) verifyPlanComment > passes when a comment by the agent's account contains the plan body [0.10ms]
(pass) verifyPlanComment > fails with the exact reason when no comment contains the plan body [0.05ms]
(pass) verifyPlanComment > fails when the plan body was posted by a different account [0.04ms]
(pass) verifyPlanComment > tolerates CRLF and trailing-whitespace differences between comment and plan [0.04ms]
(pass) verifyPlanComment > matches regardless of author when no agentLogin filter is supplied [0.03ms]
(pass) verifyPlanComment > an empty plan body never vacuously passes [0.02ms]

packages/dispatcher/test/gates/checkbox-revert.test.ts:
(pass) parseStatusCheckboxes > extracts one entry per Status line carrying a #N reference, stopping at the next heading [0.21ms]
(pass) parseStatusCheckboxes > returns [] when there is no Status section [0.02ms]
(pass) parseStatusCheckboxes > a lookalike heading (## Status notes) does not shadow the real ## Status [0.02ms]
(pass) parseStatusCheckboxes > only a level-2 ## Status heading starts the section (# / ### Status ignored)
(pass) parseStatusCheckboxes > a ## Status / checkbox inside a fenced code block does not shadow the real section [0.03ms]
(pass) parseStatusCheckboxes > mixed fence delimiters: a ~~~ inside a ``` block does not reopen real parsing [0.02ms]
(pass) parseStatusCheckboxes > only the FIRST ## Status section is parsed; a later one is ignored [0.02ms]
(pass) reconcileCheckboxes > a passing [ ]→[x] transition is left checked, no comment, state recorded [0.27ms]
(pass) reconcileCheckboxes > a failing [ ]→[x] transition is reverted and a comment names the failed gate [0.17ms]
(pass) reconcileCheckboxes > a box already checked on the previous pass is not re-run [0.07ms]
(pass) reconcileCheckboxes > a revert touches only the Status section, not the same #N checkbox elsewhere [0.07ms]
(pass) reconcileCheckboxes > with several transitions, only the failing sub-issue is reverted [0.06ms]

packages/dispatcher/test/gates/pr-ready-handler.test.ts:
(pass) pr-ready gate handler > allows a non-`gh pr ready` command without touching GitHub [0.24ms]
(pass) pr-ready gate handler > allows when the Epic PR's criteria are all evidenced [0.14ms]
(pass) pr-ready gate handler > denies when the Epic PR has unevidenced criteria [0.09ms]
(pass) pr-ready gate handler > denies when no open Epic PR can be found [0.05ms]
(pass) pr-ready gate handler > denies when the session maps to no active workflow [0.03ms]

packages/dispatcher/test/gates/gate-runner.test.ts:
(pass) runGate > a passing gate captures stdout and exit 0 [0.98ms]
(pass) runGate > a failing gate captures the non-zero exit and stderr [0.65ms]
(pass) runGate > a gate that exceeds its timeout is killed and reported as timed out [700.66ms]
(pass) runGate > runs in the given cwd [1.91ms]
(pass) runGates > runs every gate in declared order; aggregate ok when all pass [1.33ms]
(pass) runGates > a failing gate makes the aggregate fail and names the first failure; later gates still run [1.62ms]
(pass) runGates > an empty gate list is a vacuous pass [0.07ms]

packages/dispatcher/test/gates/verify.test.ts:
(pass) verification gates wired into checkbox-revert (end to end) > a failing phase's box is reverted; a passing phase's box stays checked [2.06ms]
(pass) verification gates wired into checkbox-revert (end to end) > evidence is posted for both phases and a revert notice names the failed gate [1.51ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > an evidence-upsert failure yields ok:false (not a throw), preserving a real gate failure [1.24ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > a gate-runner failure (worktree gone) yields ok:false instead of throwing [0.43ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > reconcileCheckboxes still processes every transition + persists state when evidence fails [1.58ms]
(pass) verification gates wired into checkbox-revert (end to end) > re-running after a fix keeps the box checked and updates evidence in place [2.33ms]

packages/dispatcher/test/gates/pr-ready.test.ts:
(pass) parseAcceptanceCriteria > extracts the list items under the acceptance-criteria heading only [0.07ms]
(pass) parseAcceptanceCriteria > returns [] when there is no acceptance-criteria section [0.03ms]
(pass) commandIsPrReady > matches a bare and an argumented `gh pr ready` [0.02ms]
(pass) commandIsPrReady > does not match other gh commands
(pass) extractCommand > reads tool_input.command from a Claude/Codex PreToolUse payload [0.01ms]
(pass) extractCommand > parses Copilot's string-encoded toolArgs (else the gate never fires for copilot) [0.03ms]
(pass) extractCommand > accepts a tool_args object as a defensive snake_case variant
(pass) extractCommand > returns null on malformed toolArgs JSON rather than throwing [0.03ms]
(pass) extractCommand > returns null when there is no command [0.01ms]
(pass) evaluatePrReady > allows when every criterion carries an evidence link or a non-bot deferral [0.10ms]
(pass) evaluatePrReady > denies and names the criterion that has no evidence [0.07ms]
(pass) evaluatePrReady > a `#N` reference counts as an evidence link [0.05ms]
(pass) evaluatePrReady > a stakeholder-deferred criterion (non-bot comment) is allowed [0.05ms]
(pass) evaluatePrReady > a deferral pointing at a bot comment is denied [0.07ms]
(pass) evaluatePrReady > evidence still satisfies a criterion whose deferral is invalid (OR semantics) [0.07ms]
(pass) evaluatePrReady > two bot deferrals and no real evidence is denied (no second-annotation leak) [0.05ms]
(pass) evaluatePrReady > denies when there is no acceptance-criteria section (no bypass by deletion) [0.03ms]
(pass) evaluatePrReady — integration evidence > denies a unit-only PR: every criterion evidenced, none an integration test [0.04ms]
(pass) evaluatePrReady — integration evidence > allows when an integration criterion is evidenced by a named test file [0.04ms]
(pass) evaluatePrReady — integration evidence > a human-authored integration-exempt annotation allows [0.04ms]
(pass) evaluatePrReady — integration evidence > a bot-authored integration-exempt annotation is denied [0.04ms]
(pass) evaluatePrReady — integration evidence > an evidenced integration criterion allows even if a stray bot exemption is present [0.09ms]
(pass) evaluatePrReady — integration evidence > a deferred integration criterion does not count as integration evidence [0.06ms]

packages/dispatcher/test/gates/gate-evidence.test.ts:
(pass) renderEvidence > carries the per-phase marker so re-runs can find it [0.02ms]
(pass) renderEvidence > summarizes each gate's pass/fail in a table [0.05ms]
(pass) renderEvidence > puts full gate output inside collapsed <details> blocks [0.01ms]
(pass) renderEvidence > fences output that itself contains backticks without breaking the block [0.04ms]
(pass) upsertEvidenceComment > posts a fresh comment when none exists for the phase [0.14ms]
(pass) upsertEvidenceComment > re-runs update the same comment in place rather than posting a duplicate [0.16ms]
(pass) upsertEvidenceComment > a different phase's evidence gets its own comment [0.10ms]

packages/dispatcher/test/gates/checkbox-revert-pass.test.ts:
(pass) runCheckboxRevertPass > reverts a failing-gate checkbox after a push: body, comment, persisted state [86.96ms]
(pass) runCheckboxRevertPass > a passing-gate checkbox stays checked; SHA + state persisted [76.10ms]
(pass) runCheckboxRevertPass > head-SHA gate: an unchanged SHA skips a would-be transition entirely [73.87ms]
(pass) runCheckboxRevertPass > an advanced SHA re-processes: the new transition's gate runs and reverts [80.77ms]
(pass) runCheckboxRevertPass > undefined gateway SHA falls through to the reconciler's checkbox-state diff [78.97ms]
(pass) runCheckboxRevertPass > no usable verify.toml → the workflow is skipped (nothing to enforce) [71.13ms]
[checkbox-revert] GitHub budget low (10 < 100); skipping pass — resets 1970-01-01T00:00:00.000Z
(pass) runCheckboxRevertPass > rate-limit ceiling skips the whole pass before any GitHub call [70.03ms]
[checkbox-revert] pass failed for workflow bad (o/r#1): GitHub down
(pass) runCheckboxRevertPass > a per-workflow failure is isolated — other workflows still process [86.35ms]
(pass) runCheckboxRevertPass > a parked (non-running) workflow is not processed [70.46ms]

 1411 pass
 0 fail
 3550 expect() calls
Ran 1411 tests across 127 files. [87.24s]

@thejustinwalsh

thejustinwalsh commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Verification gates — phase #226

All 4 verification gate(s) passed for phase #226.

Gate Result Duration
format ✅ pass 0.3s
lint ✅ pass 0.1s
typecheck ✅ pass 2.1s
test ✅ pass 86.5s
format — ✅ pass (0.3s)
$ bun run format
Finished in 184ms on 337 files using 24 threads.

[stderr]
$ oxfmt

lint — ✅ pass (0.1s)
$ bun run lint
Found 0 warnings and 0 errors.
Finished in 22ms on 303 files with 95 rules using 24 threads.

[stderr]
$ oxlint --fix --deny-warnings

typecheck — ✅ pass (2.1s)
[stderr]
$ tsc --noEmit

test — ✅ pass (86.5s)
$ bun test
bun test v1.3.14 (0d9b296a)

[stderr]

packages/docs/test/resolve.test.ts:
(pass) resolveDocsTarget — detection > detects Starlight from astro.config + @astrojs/starlight [0.35ms]
(pass) resolveDocsTarget — detection > Starlight wins over co-resident TypeDoc [0.05ms]
(pass) resolveDocsTarget — detection > detects Docusaurus from docusaurus.config.js [0.04ms]
(pass) resolveDocsTarget — detection > detects MkDocs and reads a custom docs_dir [0.09ms]
(pass) resolveDocsTarget — detection > detects MkDocs with the default docs_dir [0.05ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from typedoc.json and reads out [0.08ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from a package.json typedoc key [0.04ms]
(pass) resolveDocsTarget — markdown fallback > falls back to markdown in docs/ when nothing is detected [0.05ms]
(pass) resolveDocsTarget — markdown fallback > a bare Astro site (no Starlight signal) does not match Starlight [0.09ms]
(pass) resolveDocsTarget — markdown fallback > resolves to markdown on a nonexistent path [0.15ms]
(pass) resolveDocsTarget — config override > tool override forces the framework, ignoring detection [0.06ms]
(pass) resolveDocsTarget — config override > tool override beats a detected framework [0.01ms]
(pass) resolveDocsTarget — config override > tool + path override sets both framework and root [0.02ms]
(pass) resolveDocsTarget — config override > path override alone overrides a detected target's root [0.04ms]
(pass) resolveDocsTarget — config override > path override alone overrides the fallback root [0.03ms]
(pass) resolveDocsTarget — config override > an unknown tool override throws with the valid names [0.05ms]
(pass) resolveOutputPath — slug normalization > strips a leading slash and an existing .md/.mdx extension [0.04ms]
(pass) DOCS_TARGET_NAMES > lists every resolvable target [0.02ms]

packages/docs/test/util.test.ts:
(pass) makeTarget.resolveOutputPath — path safety > nested slugs route into subfolders (preserved behavior) [0.02ms]
(pass) makeTarget.resolveOutputPath — path safety > leading slashes are stripped, never absolute [0.01ms]
(pass) makeTarget.resolveOutputPath — path safety > an .md/.mdx extension on the slug is not doubled [0.01ms]
(pass) makeTarget.resolveOutputPath — path safety > traversal segments cannot escape docsRoot [0.02ms]
(pass) makeTarget.resolveOutputPath — path safety > interior traversal segments are dropped too
(pass) makeTarget.resolveOutputPath — path safety > backslashes are normalized to POSIX separators
(pass) makeTarget.resolveOutputPath — path safety > an empty docsRoot stays repo-relative (no leading slash) [0.02ms]
(pass) readJsonIfExists — contract > a JSON object is returned as a Record [0.07ms]
(pass) readJsonIfExists — contract > a JSON array is rejected (not a Record<string, unknown>) [0.04ms]
(pass) readJsonIfExists — contract > a JSON scalar is rejected [0.02ms]

packages/dashboard/test/guard.test.ts:
(pass) makeGuard > surfaces a rejection as an error keyed by source [0.17ms]
(pass) makeGuard > a non-Error rejection is stringified [0.06ms]
(pass) makeGuard > success clears only its own source's error, never another source's [0.16ms]
(pass) makeGuard > REGRESSION: a nested same-source guard masks the inner failure [0.07ms]
(pass) makeGuard > FIX: awaiting raw work inside one guard surfaces the failure [0.06ms]

packages/dashboard/test/server.test.ts:
(pass) createDashboardRoutes maps /api/* and /events/* to the deps seam [73.33ms]

packages/dashboard/test/runs-deps.test.ts:
(pass) createDbDeps.listRuns > returns only non-implementation kinds, newest-first within kind [82.98ms]
(pass) createDbDeps.listRuns > projects duration, active, transcript, and session fallback [73.09ms]
(pass) createDbDeps.listRuns > outputLink: recommender → state issue, documentation → PR, else null [79.18ms]
(pass) createDbDeps.listRuns > caps at 20 per kind [163.91ms]

packages/dashboard/test/epics-api.test.ts:
(pass) /api/epics > GET /api/epics/:repo returns the card list [0.36ms]
(pass) /api/epics > POST /api/epics/:repo/:n/dispatch forwards adapter + status/body [0.18ms]
(pass) /api/epics > dispatch 404s when no dispatch seam is wired [0.05ms]
(pass) /api/epics > dispatch rejects a missing adapter with 400 [0.05ms]
(pass) /api/epics > POST /api/epics/:repo/refresh forwards [0.06ms]

packages/dashboard/test/queue.test.tsx:
(pass) Queue shows an empty state with no data [3.61ms]
(pass) Queue renders nothing-in-flight row when live is empty [0.79ms]
(pass) Queue renders gauge tile labels and values from totals [0.62ms]
(pass) Queue renders epic as #N for a numeric epic and — for null [0.50ms]
(pass) Queue state cell carries the s-running class [0.31ms]
(pass) Queue renders rate-limit chip with adapter name, status, and chip class [0.30ms]
(pass) Queue sorts waiting-human rows before running rows [0.22ms]

packages/dashboard/test/epic-ref.test.tsx:
(pass) EpicRef > github mode renders plain `#N` text, no anchor (AC4: no behavior change) [0.21ms]
(pass) EpicRef > github mode renders `#N` even if a backfilled epic_ref is also present [0.06ms]
(pass) EpicRef > file mode renders the slug as a file:// link to the Epic file, no GitHub link [0.15ms]
(pass) EpicRef > no-Epic (both null) renders the caller's fallback [0.09ms]
(pass) EpicRef > a blank epicRef (empty / whitespace) falls through to the fallback, not an empty link [0.07ms]
(pass) EpicRef > a slug with surrounding whitespace is trimmed in both label and href [0.07ms]
(pass) EpicRef > a slug with URL-unsafe / traversal chars is encoded into one safe path segment [0.02ms]
(pass) RunnerRow Epic rendering > file-mode runner shows the slug file:// link [0.57ms]
(pass) RunnerRow Epic rendering > github-mode runner is unchanged (`#7`, no link) [0.21ms]
(pass) RunnerRow Epic rendering > no-Epic runner keeps the `#—` fallback [0.13ms]
(pass) Inspector Epic rendering > file-mode panel shows the slug file:// link in the header [0.43ms]
(pass) Inspector Epic rendering > github-mode panel is unchanged (`#7`, no link) [0.27ms]

packages/dashboard/test/sse.test.ts:
(pass) dashboard SSE channels > GET /events/global delivers a broadcast on the global channel [69.10ms]
(pass) dashboard SSE channels > GET /events/repos/:repo delivers only that repo's events [66.68ms]
(pass) dashboard SSE channels > GET /events/sessions/:session delivers session timeline frames [66.73ms]
(pass) dashboard SSE channels > a rate-limit detection pushes a fresh banner on the global channel (the ≤2s path) [70.83ms]
(pass) dashboard SSE channels > a workflow transition pushes a `workflow` nudge on that repo's channel [77.64ms]
(pass) dashboard SSE channels > a file-mode transition pushes the epic_ref slug alongside a null epic [78.34ms]
(pass) dashboard SSE channels > disposing the workflow bridge stops the repo-channel nudges [80.99ms]
(pass) dashboard SSE channels > a malformed percent-encoded channel segment is a 400, not a crash [64.96ms]
(pass) dashboard SSE channels > the /events/* routes 503 when no bus is wired [65.93ms]
(pass) DashboardEventBus channel pruning > drained (zero-subscriber) channels are swept out on the next serve [67.21ms]

packages/dashboard/test/activity.test.tsx:
(pass) Activity > renders Recommender and Documentation sections [0.84ms]
(pass) Activity > shows an output link when present and omits it otherwise [0.32ms]
(pass) Activity > empty state per section when no runs of that kind [0.14ms]
(pass) Activity > renders a state label for each run [0.12ms]
(pass) Activity > state pill tone: completed is ok, compensated/failed are bad [0.33ms]

packages/dashboard/test/epics-deps.test.ts:
(pass) createDbDeps.listEpics > joins cache progress + state-issue decision/recommendation + free slots [73.16ms]
(pass) createDbDeps.listEpics > an in-flight workflow surfaces as the runner and flips inFlight [77.91ms]
(pass) createDbDeps.listEpics > a blocked Epic with no needs-human entry gets a blocked decision callout [71.09ms]
(pass) createDbDeps.listEpics > surfaces a file-mode Epic (slug ref, null number) and resolves its runner by ref (#200) [78.71ms]
(pass) createDbDeps.listEpics > dispatchEpic + refreshEpics delegate to the injected callbacks [65.83ms]

packages/dashboard/test/control-client.test.ts:
(pass) fetchControlMetrics parses the /control/metrics snapshot [0.16ms]
(pass) fetchControlMetrics throws on a non-OK response [0.08ms]

packages/dashboard/test/api.test.ts:
(pass) dashboard JSON API > GET /api/repos returns a JSON array of repo summaries [84.18ms]
(pass) dashboard JSON API > GET /api/repos/:repo returns NEXT UP + IN FLIGHT for a known repo [77.72ms]
(pass) dashboard JSON API > github-mode IN FLIGHT row carries epicRef alongside the numeric epic (#187) [73.58ms]
(pass) dashboard JSON API > file-mode IN FLIGHT row surfaces epic_ref as epicRef with a null epic (#187) [73.08ms]
(pass) dashboard JSON API > GET /api/sessions/:session carries epicRef for a file-mode runner (#187) [73.41ms]
(pass) dashboard JSON API > GET /api/repos/:repo 404s an unknown repo [64.40ms]
(pass) dashboard JSON API > GET /api/banner reports per-adapter rate limits (UNKNOWN unobserved) [64.70ms]
(pass) dashboard JSON API > GET /api/sessions/:session returns the Inspector runner panel with attach commands [77.79ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach control flips controlled_by and spawns a terminal [81.31ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach rejects an invalid mode [74.36ms]
(pass) dashboard JSON API > POST /api/rate-limits/:adapter/clear sets the adapter AVAILABLE [70.22ms]
(pass) dashboard JSON API > GET /api/sessions/:session/events validates the limit param [73.86ms]
(pass) dashboard JSON API > POST /api/repos/:repo/pause validates untilMs [80.86ms]
(pass) dashboard JSON API > a runner with no session_name is reachable by its workflow id [76.44ms]
(pass) dashboard JSON API > a malformed percent-encoded path segment is a 400, not a 500 [65.93ms]
(pass) dashboard JSON API > unknown /api routes 404 as JSON [65.74ms]

packages/dashboard/test/window.test.ts:
(pass) dashboard window launcher > missing URL argument is a usage error (exit 2) [8.94ms]
(pass) dashboard window launcher > an unavailable webview-bun degrades to a logged exit 0 (HTTP still serves) [8.84ms]

packages/dashboard/test/runs-api.test.ts:
(pass) /api/runs > GET /api/runs returns the run list [0.16ms]
(pass) /api/runs > a non-GET method on /api/runs is a 404 miss [0.06ms]

packages/dashboard/test/epics.test.tsx:
(pass) Epics > renders an Epic card with title, progress, and an enabled dispatch button [0.89ms]
(pass) Epics > empty state when there are no Epics [0.14ms]
(pass) Epics > a file-mode Epic renders a file:// slug link and disables in-dashboard dispatch (#200) [0.27ms]
(pass) Epics > disables dispatch when in flight [0.29ms]
(pass) Epics > disables dispatch when the chosen adapter has no free slot [0.19ms]
(pass) Epics > shows a decision callout when present [0.32ms]
(pass) Epics > renders the decision link as an anchor when present [0.29ms]

packages/dashboard/test/app.test.tsx:
(pass) App nav includes a queue tab [1.00ms]
(pass) App nav includes an activity tab [0.38ms]
(pass) api.runs reads runs from a live server [70.02ms]
(pass) App defaults to the Epics view (nav tab + empty state render) [0.44ms]
(pass) api.epics reads Epic cards from a live server [81.27ms]
(pass) applyWorkflowFrame upserts non-terminal and drops terminal workflows [0.17ms]
(pass) dashboard views (static render) > GlobalBanner shows per-adapter rate limits + GitHub quota [0.41ms]
(pass) dashboard views (static render) > NeedsYou lists aggregated items and an empty state [0.35ms]
(pass) dashboard views (static render) > RepoRow expansion shows slot pills, NEXT UP, IN FLIGHT, and an accurate attach command [0.50ms]
(pass) dashboard views (static render) > Inspector renders the per-runner panel, links, affordances, and timeline [0.66ms]
(pass) api-client against a live server > api.repos() + RepoRow render the live repo [79.61ms]
(pass) api-client against a live server > api.attach(control) flips controlled_by; api.release reverts it [86.27ms]
(pass) api-client against a live server > api.runRecommender surfaces a non-2xx as an ApiError [78.03ms]

packages/dashboard/test/settings.test.tsx:
(pass) settings round-trip through the API > GET /api/settings returns global + per-repo config [73.14ms]
(pass) settings round-trip through the API > POST /api/settings/global persists and is reflected back [74.36ms]
(pass) settings round-trip through the API > POST /api/settings/global rejects a non-positive maxConcurrent [72.91ms]
(pass) settings round-trip through the API > pause/resume toggles a repo's auto-dispatch [91.07ms]
(pass) settings round-trip through the API > the rate-limit override button's endpoint sets the adapter AVAILABLE [76.89ms]
(pass) Settings view (static render) > renders global fields, rate-limit override, and per-repo auto toggle [72.51ms]

packages/dashboard/test/spa.test.ts:
Bundled page in 22ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > GET / serves the bundled HTML shell [87.88ms]
Bundled page in 42ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the bundled entry script transpiles the TSX app [110.30ms]
Bundled page in 19ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the JSON API coexists with the SPA fallback on the same server [85.92ms]

packages/state-issue/test/validate.test.ts:
(pass) validate > passes a schema-conforming state [0.17ms]
(pass) validate > fails when a Ready row uses an unconfigured adapter [0.04ms]
(pass) validate > fails when an In-flight item uses an unconfigured adapter [0.02ms]
(pass) validate > accepts a non-numeric file-mode Epic slug as an In-flight ref (rule 4 scopes the numeric check to Ready epics and blocked blockers, not In-flight) [0.01ms]
(pass) validate > fails when generated is not ISO 8601 [0.01ms]
(pass) validate > fails when an epic reference is malformed [0.01ms]
(pass) validate > fails when a Ready row epic has no title [0.01ms]
(pass) validate > fails when a blocked issue-blocker reference is malformed [0.01ms]
(pass) validate > accepts a non-issue blocker in backticks
(pass) validate > accepts a cross-repo blocker reference (#225)
(pass) validate > accepts a blocker annotated with a resolved title (#225) [0.01ms]
(pass) validate > accepts a blocker carrying a (stale blocker: <ref>) suffix (#225) [0.03ms]
(pass) validate > fails when a cross-repo blocker reference is malformed [0.01ms]
(pass) validate > collects multiple errors [0.02ms]

packages/state-issue/test/fuzz.test.ts:
(pass) parser/renderer round-trip fuzz > renders, parses, and re-renders 10000 random valid states byte-identically [297.66ms]

packages/state-issue/test/schema-path.test.ts:
(pass) STATE_ISSUE_SCHEMA_PATH > is an absolute path ending in the canonical schema filename [0.03ms]
(pass) STATE_ISSUE_SCHEMA_PATH > points at the real schema shipped in the middle install (not a target repo) [0.05ms]

packages/state-issue/test/fixture.test.ts:
(pass) hand-crafted state-issue fixture > parseStateIssue succeeds [0.02ms]
(pass) hand-crafted state-issue fixture > validate returns pass [0.13ms]
(pass) hand-crafted state-issue fixture > round-trips byte-identically [0.05ms]
(pass) hand-crafted state-issue fixture > exercises all seven sections with non-empty content [0.14ms]

packages/state-issue/test/parser.test.ts:
(pass) renderStateIssue > renders an empty state in canonical form [0.03ms]
(pass) renderStateIssue > renders a fully-populated state with all section content [0.04ms]
(pass) parseStateIssue > parses the canonical empty body back to the original state [0.04ms]
(pass) parseStateIssue > parses a fully-populated body back to the original state [0.04ms]
(pass) parseStateIssue > round-trips a file-mode in-flight ref, including a non-kebab slug (#200) [0.05ms]
(pass) parseStateIssue > returns ParseError when the open marker is missing [0.09ms]
(pass) parseStateIssue > returns ParseError when the close marker is missing [0.04ms]
(pass) parseStateIssue > returns ParseError when a section is out of order [0.04ms]
(pass) parseStateIssue > ignores content outside the markers [0.03ms]
(pass) parseStateIssue > ignores dispatcher-tick markers between sections [0.03ms]
(pass) parseStateIssue > returns ParseError when the Ready table omits the documented empty-state row [0.03ms]
(pass) parseStateIssue > an In-flight section with no bullet reads as empty (lenient empty-state) [0.02ms]
(pass) parseStateIssue > returns ParseError when a Ready row rank is below 1 [0.03ms]
(pass) parseStateIssue > returns ParseError when a Ready row sub-issue count is below 1 [0.02ms]
(pass) round-trip > render(parse(render(state))) is byte-identical to render(state) [0.05ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Needs human input accepts "- _none_" (the #84 failure) [0.03ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Blocked accepts "- _none_" [0.02ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Excluded accepts "- _none_" [0.01ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > In-flight accepts a "- _none_" variant and an empty section [0.02ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > a real item alongside no sentinel still parses strictly (no over-loosening) [0.02ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > a genuinely malformed item (not a sentinel) still fails [0.02ms]

packages/cli/test/bootstrap-gitignore.test.ts:
(pass) addMiddleIgnore > writes the glob form with policy/verify exceptions into a new file [0.48ms]
(pass) addMiddleIgnore > preserves existing unrelated entries [0.25ms]
(pass) addMiddleIgnore > is idempotent — a second call makes no change [0.20ms]
(pass) addMiddleIgnore > upgrades a legacy bare `.middle/` entry to the glob form [0.20ms]
(pass) removeMiddleIgnore > strips the whole block, leaving other entries [0.29ms]
(pass) removeMiddleIgnore > deletes the file when it empties [0.28ms]
(pass) removeMiddleIgnore > also clears a legacy bare `.middle/` line [0.25ms]
(pass) removeMiddleIgnore > no-op when there's nothing middle-owned to remove [0.18ms]
(pass) removeMiddleIgnore > no-op leaves a file without a trailing newline untouched [0.17ms]
(pass) removeMiddleIgnore > no file at all is a no-op [0.12ms]

packages/cli/test/config.test.ts:
(pass) mm config auto_dispatch > flips an existing toggle in place, preserving comments and other keys [1.22ms]
(pass) mm config auto_dispatch > inserts the key when the [recommender] section lacks it [0.32ms]
(pass) mm config auto_dispatch > appends the section when it does not exist [0.40ms]
(pass) mm config auto_dispatch > matches a header with a trailing comment in place (no duplicate section) [0.29ms]
(pass) mm config auto_dispatch > matches a header with whitespace inside the brackets (no duplicate section) [0.36ms]
(pass) mm config auto_dispatch > rejects an unknown key and an invalid value [0.19ms]
(pass) mm config auto_dispatch > errors when the config file is missing [0.16ms]

packages/cli/test/init-file-store.test.ts:
(pass) mm init --epic-store=file > writes the four scaffold files and makes zero gh calls [9.11ms]
(pass) mm init --epic-store=file > the README template snippet is a parseable v1 Epic body [9.57ms]
(pass) mm init --epic-store=file > calls the setEpicStore callback with file mode + default paths [6.87ms]
(pass) mm init --epic-store=file > a setEpicStore write failure is best-effort — init still succeeds [8.53ms]
(pass) mm init --epic-store=file > --dry-run writes nothing and makes no gh calls [0.35ms]
(pass) mm init — github mode is unchanged > default mode creates the state issue and writes no file-store scaffold [6.69ms]
(pass) mm init — github mode is unchanged > setEpicStore is called with github mode in the default path [8.03ms]

packages/cli/test/pause-resume.test.ts:
(pass) mm pause / mm resume > pause sets paused_until; resume clears it (keyed by the resolved slug) [89.78ms]
(pass) mm pause / mm resume > a slug-resolution failure returns exit 1, not an unhandled rejection [0.41ms]
(pass) mm pause / mm resume > a non-git path is rejected with exit 1 [0.38ms]

packages/cli/test/status.test.ts:
(pass) runStatus > prints a per-repo, per-state summary of recorded workflows [76.24ms]
(pass) runStatus > reports cleanly when the database does not exist yet [0.35ms]
(pass) runStatus > reports cleanly when the database has no workflows [62.07ms]
(pass) runStatus > exits non-zero when the config file is malformed [0.57ms]

packages/cli/test/bootstrap-hook.test.ts:
(pass) bootstrap hook.sh asset > is byte-identical to the canonical HOOK_SH constant [0.25ms]
(pass) bootstrap hook.sh asset > is a POSIX sh script that takes the event name and never blocks the agent [0.07ms]
(pass) bootstrap hook.sh asset > the committed asset is marked executable [0.03ms]

packages/cli/test/file-mode-smoke.test.ts:
(pass) file-mode CLI smoke (#194) > mm dispatch --epic <slug> lands a workflow row with epic_ref=<slug> (file mode selected) [80.21ms]

packages/cli/test/db-scripts.test.ts:
(pass) backup.sh + reset-db.sh round-trip > backup → reset → restore preserves the db and its rows [121.16ms]
(pass) safety guards > backup.sh fails when there is no database [2.73ms]
(pass) safety guards > reset-db.sh is a no-op (exit 0) when there is no database [2.41ms]
(pass) safety guards > reset-db.sh refuses while the dispatcher pidfile is live [69.68ms]
(pass) safety guards > --db points both scripts at a relocated database [100.72ms]
(pass) safety guards > restore creates missing parent dirs for a relocated db and config [120.33ms]
(pass) safety guards > restore refuses while the dispatcher pidfile is live [102.52ms]

packages/cli/test/doctor.test.ts:
(pass) runDoctor — happy path > returns 0 and prints every check when the toolchain is healthy [1148.88ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + existing epics dir → epics_dir pass, no state-issue row [1042.14ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + missing epics dir → epics_dir fail, no state-issue row [1300.83ms]
(pass) runDoctor — mode-aware Epic-store check > github mode (no config row) → state-issue row, no epics_dir row [1012.80ms]
(pass) checkAdapterBinaries > null config (unparseable) → single warn, no throw [0.15ms]
(pass) checkAdapterBinaries > no enabled adapters → warn [0.05ms]
(pass) checkAdapterBinaries > reports a row per ENABLED adapter from the passed config — not a reloaded global one [0.11ms]
(pass) checkAdapterBinaries > enabled adapter with a missing binary → warn (never fail) [23.37ms]
(pass) formatAgo > renders sub-minute as seconds [0.06ms]
(pass) formatAgo > renders minutes, hours, and days at the boundaries [0.03ms]
(pass) formatAgo > clamps a future timestamp to 0s (never negative)
(pass) summarizeRetention > never-run → pass, reports counts [0.04ms]
(pass) summarizeRetention > clean last run → pass, reports the run [0.04ms]
(pass) summarizeRetention > failed last run → warn, surfaces FAILED [0.02ms]

packages/cli/test/run-recommender.test.ts:
(pass) runRecommender — local validation > rejects a path that is not a git repository [16.94ms]
(pass) runRecommender — thin client to the daemon > daemon already up: POSTs /trigger/recommender and returns 0 on 202 [6.69ms]
(pass) runRecommender — thin client to the daemon > daemon down: auto-starts it, waits for health, then triggers [6.27ms]
(pass) runRecommender — thin client to the daemon > relays a daemon rejection (non-202) as exit 1 [6.23ms]
(pass) runRecommender — thin client to the daemon > returns 1 when the daemon never becomes ready after an auto-start [57.55ms]
(pass) runRecommender — thin client to the daemon > returns 1 when the dispatcher is unreachable (the POST throws) [7.41ms]

packages/cli/test/state-issue-check.test.ts:
(pass) checkStateIssueRoundTrip > passes for the canonical conforming fixture [0.15ms]
(pass) checkStateIssueRoundTrip > fails when the body does not parse [0.06ms]
(pass) checkStateIssueRoundTrip > fails validate when a Ready row uses an unconfigured adapter [0.09ms]
(pass) checkStateIssue > passes against middle's own source tree [0.07ms]
(pass) checkStateIssue > returns a structured fail (never throws) when the fixture is unreadable [0.09ms]

packages/cli/test/daemon-entry.test.ts:
Bundled page in 43ms: packages/dashboard/src/index.html
(pass) dashboardHostExtras routes + the hook fetch fallback coexist on one port [50.96ms]
(pass) a dispatch POST reaches the host-context dispatch callback [5.82ms]
(pass) dispose clears the process-global rate-limit observer (no broadcast after teardown) [1.83ms]

packages/cli/test/issue-audit.test.ts:
(pass) isFeatureIssue > epics, docs and chore issues are out of scope [0.09ms]
(pass) auditIssues > filters to feature issues and applies the rubric [0.32ms]
(pass) runAuditIssues --issue mode > flags a weak issue, returns 1, and labels it when --label is set [0.42ms]
(pass) runAuditIssues --issue mode > a thrown fetch error is handled: returns 1 and logs, not an unhandled rejection [0.18ms]
(pass) runAuditIssues --issue mode > a label-application failure is surfaced (logged) but does not crash the command [0.13ms]
(pass) runAuditIssues --issue mode > a passing issue returns 0 and is never labelled [0.09ms]
(pass) runAuditIssues backlog mode > returns 1 when any feature issue fails; labels only failures [0.19ms]

packages/cli/test/init-register.test.ts:
(pass) mm init — managed-repo registration > registers the slug + resolved checkout path on a successful init [7.45ms]
(pass) mm init — managed-repo registration > does NOT register under --dry-run (no changes made) [0.30ms]
(pass) mm init — managed-repo registration > a registry write failure is best-effort — init still succeeds [7.81ms]

packages/cli/test/audit-issues-cli.test.ts:
(pass) mm audit-issues --body-file (real CLI) > flags a weak issue and suggests a concrete rewrite (exit 1) [147.76ms]
(pass) mm audit-issues --body-file (real CLI) > passes a well-formed issue carrying an integration criterion (exit 0) [146.62ms]
(pass) mm audit-issues --body-file (real CLI) > --json emits a machine-readable report [144.94ms]
(pass) mm audit-issues --body-file (real CLI) > rejects a non-positive-integer --issue with a clear error (exit 1) [738.34ms]

packages/cli/test/module-index.test.ts:
(pass) parseModuleIndexFrontmatter > accepts a well-formed frontmatter block [0.05ms]
(pass) parseModuleIndexFrontmatter > reads claude-md: true [0.05ms]
(pass) parseModuleIndexFrontmatter > tolerates a leading shebang before the block [0.02ms]
(pass) parseModuleIndexFrontmatter > rejects a file with no leading block comment [0.01ms]
(pass) parseModuleIndexFrontmatter > rejects a block missing @packageDocumentation [0.01ms]
(pass) parseModuleIndexFrontmatter > rejects a block missing the @module tag
(pass) parseModuleIndexFrontmatter > rejects a missing required section [0.02ms]
(pass) parseModuleIndexFrontmatter > rejects a non-boolean claude-md value
(pass) claudeMdPathForIndex > maps a package's src/index.ts to the package root CLAUDE.md [0.01ms]
(pass) claudeMdPathForIndex > maps a nested module's index.ts to its own dir
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > flags claude-md: true with no CLAUDE.md [0.50ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > flags claude-md: false with a stray CLAUDE.md [0.42ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > passes when flag and presence agree, and skips bootstrap-assets [0.87ms]
(pass) checkModuleIndex — the real middle packages tree > every src/index.ts(x) carries valid, consistent frontmatter [0.53ms]
(pass) checkModuleIndex — the real middle packages tree > finds every package's index front door [0.42ms]

packages/cli/test/bootstrap-init.test.ts:
(pass) mm init — fresh install > stages skills, hooks, config, state issue, and gitignore [8.69ms]
(pass) mm init — fresh install > the created state-issue body parses and validates [6.42ms]
(pass) mm init — idempotent re-init > a matching-version re-init refreshes assets but keeps config and issue [11.95ms]
(pass) mm init — idempotent re-init > re-init does not clobber a team's committed policy edits (AC #103) [11.79ms]
(pass) mm init — idempotent re-init > a fresh clone (committed policy, no local cache) reconciles the issue and keeps policy [6.72ms]
(pass) mm init — idempotent re-init > loadConfig reads init's two files via sibling derivation and merges them [8.51ms]
(pass) mm init — dry run > writes nothing and makes no GitHub calls [0.34ms]
(pass) mm init — validation > rejects a dirty working tree [0.30ms]
(pass) mm init — validation > rejects a repo with no origin remote [0.27ms]
(pass) mm init — validation > fails fast on a malformed existing config instead of re-initializing fresh [0.45ms]
(pass) mm init — existing config without a usable state issue > a matching-version re-init with no issue number mints one and persists it [6.27ms]
(pass) mm init — reconciles the state issue against GitHub > a fresh local install reuses the repo's existing state issue instead of creating one [7.68ms]
(pass) mm init — reconciles the state issue against GitHub > warns and reuses the oldest when GitHub has duplicate state issues [6.62ms]
(pass) mm init — reconciles the state issue against GitHub > creates a state issue only when GitHub has none [6.51ms]
(pass) mm uninit > closes the issue and removes everything init staged [9.41ms]
(pass) mm uninit > closes the state issue even when [repo] metadata is missing (deps fallback) [0.51ms]
(pass) mm uninit > closes the state issue offline by reading [repo] from committed policy (#103) [0.53ms]
(pass) mm uninit > falls back to default_branch 'main' when committed policy has a non-string value (#103) [0.53ms]
(pass) mm uninit > dry run removes nothing [8.03ms]
(pass) mm uninit > strips only middle's hook entries, preserving foreign ones [7.32ms]

packages/cli/test/dispatch.test.ts:
(pass) runDispatch — input validation > rejects a malformed numeric epic (digit-leading but not a whole number) [15.60ms]
(pass) runDispatch — input validation > rejects an epic number below 1 [6.04ms]
(pass) runDispatch — input validation > rejects a path that is not a git repository [0.23ms]
(pass) runDispatch — control client > health already up: dispatches and exits 0 on completed, without spawning a daemon [114.78ms]
(pass) runDispatch — control client > a file-mode slug dispatches with epicRef and skips the gh label fetch [11.29ms]
(pass) runDispatch — control client > subscribes to /control/events BEFORE POSTing /control/dispatch [106.26ms]
(pass) runDispatch — control client > exits 0 when the workflow parks for review (waiting-human) [106.89ms]
(pass) runDispatch — control client > exits 1 when the workflow fails [114.48ms]
(pass) runDispatch — control client > reconnects when the event stream drops mid-flight and follows to completion [111.48ms]
(pass) runDispatch — control client > --adapter overrides the agent label and the default, and is sent to the daemon [10.85ms]
(pass) runDispatch — control client > an agent:<name> label on the Epic selects that adapter [11.20ms]
(pass) runDispatch — control client > no agent label falls back to the default adapter [11.04ms]
(pass) runDispatch — control client > a disabled adapter is rejected (exit 1), even via --adapter, before any dispatch [10.00ms]
(pass) runDispatch — control client > an unconfigured --adapter is rejected (exit 1) before any dispatch [9.47ms]
(pass) runDispatch — control client > friendly failure (exit 1) when the daemon can't be reached or started [517.39ms]

packages/cli/test/state-issue-body.test.ts:
(pass) buildInitialStateIssueBody > parses and validates against the schema (configured adapters) [0.14ms]
(pass) buildInitialStateIssueBody > is empty in every section [0.07ms]
(pass) buildInitialStateIssueBody > round-trips byte-identically (the keystone invariant) [0.03ms]
(pass) buildInitialStateIssueBody > carries the markers and the generated timestamp [0.02ms]
(pass) parseRepoSlug > parses git@github.com:acme/widget.git [0.09ms]
(pass) parseRepoSlug > parses https://github.com/acme/widget.git
(pass) parseRepoSlug > parses https://github.com/acme/widget
(pass) parseRepoSlug > parses ssh://git@github.com/acme/widget.git
(pass) parseRepoSlug > parses https://github.com/acme/widget/
(pass) parseRepoSlug > returns null for an unparseable URL [0.01ms]

packages/cli/test/start-stop.test.ts:
(pass) runStart / runStop lifecycle > start spawns a detached process and records its pid; stop kills it [303.09ms]
(pass) runStart / runStop lifecycle > start refuses when a live dispatcher is already recorded [102.04ms]
(pass) runStart / runStop lifecycle > start clears a stale pid file and launches fresh [0.63ms]
(pass) runStart / runStop lifecycle > stop exits non-zero when no dispatcher is running [0.19ms]
(pass) runStartCommand --window > opens the dashboard window once /health is ready [0.71ms]
(pass) runStartCommand --window > does not open the window when /health never becomes ready (but start still succeeds) [0.52ms]
(pass) runStartCommand --window > a throwing opener (or health probe) never fails the start — window step is best-effort [0.54ms]
(pass) runStartCommand --window > no --window and no windowed config → never opens, never polls health [0.44ms]

packages/cli/test/tsdoc-coverage.test.ts:
(pass) checkTsdocCoverage > counts a documented local export as documented [328.05ms]
(pass) checkTsdocCoverage > flags an undocumented local export [297.88ms]
(pass) checkTsdocCoverage > resolves a re-export to the original declaration's doc comment [268.77ms]
(pass) checkTsdocCoverage > a bare `export {}` module contributes no exports [289.92ms]
(pass) checkTsdocCoverage > analyzes the real middle tree without throwing [455.90ms]

packages/cli/test/init-collision.test.ts:
(pass) mm init — shared-checkout collision guard (#226) > a second init at the same path with a different slug exits non-zero and writes nothing [19.09ms]
(pass) mm init — shared-checkout collision guard (#226) > re-initializing the SAME slug at the same path is allowed (idempotent, no collision) [14.04ms]
(pass) mm init — shared-checkout collision guard (#226) > --dry-run skips the collision guard (it writes nothing anyway) [1.92ms]

packages/cli/test/docs.test.ts:
(pass) runDocs — input validation > rejects a path that is not a git repository [16.21ms]
(pass) runDocs — input validation > rejects an unknown [docs] tool override [6.51ms]
(pass) runDocs — enqueues a documentation run for the repo > resolves the markdown fallback target and dispatches a read-only run [7.48ms]
(pass) runDocs — enqueues a documentation run for the repo > a [docs] tool/path override flows through to the resolved target [7.36ms]
(pass) runDocs — enqueues a documentation run for the repo > returns 1 when the dispatched run does not complete [8.72ms]

packages/cli/test/bun-path.test.ts:
(pass) isDirOnPath > true when present [0.08ms]
(pass) isDirOnPath > false when absent [0.02ms]
(pass) isDirOnPath > tolerates trailing slashes on either side [0.02ms]
(pass) isDirOnPath > false on empty PATH
(pass) resolveShellRc > zsh (platform-independent) [0.06ms]
(pass) resolveShellRc > bash on macOS targets .bash_profile (login shells don't source .bashrc) [0.02ms]
(pass) resolveShellRc > bash elsewhere targets .bashrc [0.01ms]
(pass) resolveShellRc > unknown shell [0.02ms]
(pass) bunPathSnippet > HOME-relative form when dir is the canonical ~/.bun/bin [0.04ms]
(pass) bunPathSnippet > literal form when dir is non-canonical [0.02ms]
(pass) rcAlreadyConfigured > detects literal bin dir [0.02ms]
(pass) rcAlreadyConfigured > detects BUN_INSTALL form [0.01ms]
(pass) rcAlreadyConfigured > false on unrelated rc [0.01ms]
(pass) applyPathFix > appends once and is idempotent [0.31ms]
(pass) applyPathFix > creates content when the rc file is absent [0.19ms]

packages/cli/test/skills-sync.test.ts:
(pass) syncSkills > copies every canonical file into the mirror byte-for-byte [1.12ms]
(pass) syncSkills > a second sync is a no-op (inSync, no changes) [1.01ms]
(pass) syncSkills > removes stale files the canonical no longer has [1.04ms]
(pass) syncSkills > detects and removes an orphaned skill DIRECTORY present only in the mirror [1.11ms]
(pass) diffSkills / check mode > check mode reports drift without writing [0.53ms]
(pass) diffSkills / check mode > check mode reports in-sync once synced [1.02ms]
(pass) diffSkills / check mode > check mode catches a single-byte edit in the mirror [0.98ms]
(pass) default repo paths > the shipped canonical and mirror are in sync [0.88ms]
(pass) default repo paths > the shipped skill set includes the three bootstrapped skills [0.61ms]

packages/dispatcher/test/epic-143-demo.test.ts:
(pass) Epic #143 — integration-verified requirements + freshness > 1. the requirements auditor flags a deliberately weak issue [0.06ms]
(pass) Epic #143 — integration-verified requirements + freshness > 2. a unit-only feature cannot reach PR-ready [0.79ms]
[staleness] o/r#50 landed in merged PR #88 → closed
[staleness] o/r: filed reconcile task #900 for Phase 9
(pass) Epic #143 — integration-verified requirements + freshness > 3. reconciliation surfaces a landed-but-open issue and a drifted spec line [0.99ms]

packages/dispatcher/test/watchdog.test.ts:
(pass) watchdog — launch timeout > a launching workflow past the window is failed 'stuck-launching' [81.14ms]
(pass) watchdog — launch timeout > a launching workflow within the window is left alone [76.24ms]
(pass) watchdog — prompt not accepted > a running session that went ready but never started a turn is failed 'prompt-not-accepted' [85.82ms]
(pass) watchdog — prompt not accepted > a running session whose prompt landed (turn.started present) is not failed [81.34ms]
(pass) watchdog — prompt not accepted > a running session still within the launch window is not yet failed [76.94ms]
(pass) watchdog — tmux liveness > a running workflow with a dead session is failed + compensation triggered [84.09ms]
(pass) watchdog — tmux liveness > a running workflow with a live session is not failed for liveness [71.99ms]
[watchdog] status check failed for middle-14, skipping liveness this pass: tmux server not running
(pass) watchdog — tmux liveness > a status() error is inconclusive — liveness is skipped, fresh row not failed [72.50ms]
[watchdog] status check failed for middle-14, skipping liveness this pass: tmux server not running
(pass) watchdog — tmux liveness > a persistent status() error does NOT block rule 3 — a stale row still idle-times-out [81.14ms]
[watchdog] status check failed for middle-bad, skipping liveness this pass: tmux error
(pass) watchdog — tmux liveness > a status() error on one row does not abort reconciliation of others [88.45ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — tmux liveness > a killSession() error still records the failure decision [81.98ms]
(pass) watchdog — activity freshness > idle ≥ threshold marks one idle event but does not kill [75.87ms]
(pass) watchdog — activity freshness > idle ≥ kill-threshold kills the session and fails 'idle-timeout' [81.88ms]
(pass) watchdog — activity freshness > freshness is skipped while controlled_by = 'human' [86.73ms]
(pass) watchdog — activity freshness > a stale heartbeat is rescued by fresh transcript activity (cross-check) [77.81ms]
(pass) watchdog — sentinel re-arm > a blocked.json with no armed signal arms one, idempotently [78.46ms]
(pass) watchdog — sentinel re-arm > no sentinel file → no signal armed [70.09ms]
(pass) watchdog — blocked sentinel self-heal > idle ≥ kill-threshold with a blocked sentinel hands off to the drive, not compensation [78.74ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — blocked sentinel self-heal > a failed kill does not record the handoff — it retries next pass [70.28ms]
(pass) watchdog — blocked sentinel self-heal > the handoff is recorded once, not every idle tick [77.72ms]
(pass) reconcileTranscriptDrift > advances last_heartbeat when the transcript is newer than the recorded beat [74.58ms]
(pass) reconcileTranscriptDrift > leaves the heartbeat alone when the transcript is older [71.81ms]
(pass) notification failsafe — detect + capture + intervene > a notification still within the grace window is left alone [79.41ms]
(pass) notification failsafe — detect + capture + intervene > a notification past the grace window captures the pane, classifies, and nudges [82.40ms]
(pass) notification failsafe — detect + capture + intervene > classifies a plain 'waiting for input' notification as a question (kind=input) [84.95ms]
(pass) notification failsafe — detect + capture + intervene > an agent that resumed after the notification (newer activity) is left alone [72.54ms]
(pass) notification failsafe — detect + capture + intervene > a human-controlled session is never rescued (a human will answer) [73.13ms]
(pass) notification failsafe — detect + capture + intervene > no-op when the tmux surface lacks the failsafe methods [75.92ms]
(pass) notification failsafe — detect + capture + intervene > a capture-only notification (no message payload) still classifies + nudges [84.06ms]
(pass) notification failsafe — fast-fail backstop > still idle past the kill-grace → fast-fails with the captured kind and kills the session [91.79ms]
(pass) notification failsafe — fast-fail backstop > two captures sharing a ts → the latest-by-id kind wins (contract lock) [104.05ms]
(pass) notification failsafe — fast-fail backstop > within the kill-grace → not yet failed (the nudge still has time to take) [82.88ms]
(pass) notification failsafe — fast-fail backstop > a repeat notification with no activity does NOT reset the kill clock — still fast-fails [96.80ms]
(pass) notification failsafe — fast-fail backstop > a fresh notification AFTER genuine activity re-arms the failsafe (re-captures) [95.09ms]

packages/dispatcher/test/recommender-run.test.ts:
[recommender-run] workflow wf_1780555233247_gd1r7sqn enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > runs to completion and records a kind:'recommender' workflow row for the repo [383.07ms]
[recommender-run] workflow wf_1780555233625_cfwfi9hv enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > read-only by default: with no triggerAutoDispatch wired, a clean run dispatches nothing [374.09ms]
[recommender-run] workflow wf_1780555234001_2s81oeyr enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > fires triggerAutoDispatch on a clean run when wired and auto_dispatch is on (trigger #1) [383.44ms]
[recommender-run] workflow wf_1780555234390_fz6hm99i enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > forwards epicStore so a file-mode run frames the prompt for the file store (#200) [381.44ms]
[recommender-run] workflow wf_1780555234763_xdufxb6z enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > does not fire triggerAutoDispatch when auto_dispatch is off, even if wired [373.44ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [7.42ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > file mode resolves without a state issue — sentinel 0 + epicStore carried (#200) [7.67ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > github mode still requires a configured state issue number [6.10ms]
(pass) resolveRecommenderOptions — schema resolution (issue #107) > resolves schemaPath from the middle install, not from repoPath [7.21ms]

packages/dispatcher/test/state-issue.test.ts:
(pass) applyDispatcherSections > replaces only the three owned sections, keeps the rest [0.04ms]
(pass) updateDispatcherSections > recommender-owned sections come back byte-identical [0.47ms]
(pass) updateDispatcherSections > the owned sections actually changed [0.17ms]
(pass) updateDispatcherSections > a partial patch leaves the unspecified owned sections intact [0.11ms]
(pass) updateDispatcherSections > a dispatcher-tick marker is ignored by the parser and preserves sections [0.34ms]
(pass) updateDispatcherSections > ticks do not accumulate across repeated updates [0.18ms]
(pass) readState > parses a valid body [0.14ms]
(pass) readState > throws on a malformed body [0.08ms]
(pass) insertDispatcherTick > leaves a non-canonical body untouched [0.02ms]

packages/dispatcher/test/stop-wait.test.ts:
(pass) awaitStopOrSessionEnd > resolves via 'stop' when the Stop hook arrives first [5.36ms]
(pass) awaitStopOrSessionEnd > resolves via 'session-ended' when liveness goes false while Stop is pending [11.64ms]
(pass) awaitStopOrSessionEnd > resolves via 'timeout' when the Stop wait rejects and the session stays alive [5.22ms]
(pass) awaitStopOrSessionEnd > without a liveness probe, a rejected Stop wait surfaces as 'timeout' [5.13ms]
(pass) awaitStopOrSessionEnd > liveness-probe errors are ignored — a later Stop still wins [21.14ms]

packages/dispatcher/test/build-deps.test.ts:
(pass) buildImplementationDeps > wires deps from the injected collaborators and returns the gate it built [65.68ms]
(pass) buildImplementationDeps > epicPrReadiness reports a missing PR as { exists: false, isDraft: false } [62.13ms]
(pass) buildImplementationDeps > the factory module imports no engine (no bunqueue construction) [11.29ms]
(pass) buildImplementationDeps > the default postQuestion posts a gh comment framed by pause kind [62.57ms]
(pass) buildImplementationDeps > the default postQuestion is idempotent on a repeated identical question (#205) [62.87ms]
(pass) postQuestionComment (idempotent pause poster, #205) > skips when the latest agent-comment already has the identical body [0.32ms]
(pass) postQuestionComment (idempotent pause poster, #205) > a different body posts a fresh comment (questions are a history) [0.19ms]
(pass) postQuestionComment (idempotent pause poster, #205) > ignores non-agent comments — only the marker-prefixed latest counts [0.22ms]
(pass) formatPauseComment > a complexity pause carries the `complexity pause` label vocabulary [0.15ms]
(pass) formatPauseComment > a plain question reads as an agent question, not a complexity pause [0.14ms]
(pass) formatPauseComment > both kinds start with the hidden agent-comment marker so the poller skips them (#178) [0.12ms]

packages/dispatcher/test/staleness.test.ts:
(pass) detectSpecDrift > flags future-phase lines whose phase has merged [0.06ms]
(pass) detectSpecDrift > does not flag a future phase that has not merged [0.03ms]
(pass) detectSpecDrift > matches the verb-less 'planned for phase N' phrasing [0.03ms]
[staleness] o/r#50 landed in merged PR #88 → closed
[staleness] o/r: filed reconcile task #1001 for Phase 9
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > closes a landed-but-open issue and files a drift task for its phase [0.27ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > does not close an issue no merged PR references, and dedupes an existing reconcile task [0.11ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > maxPerPass caps the TOTAL of closes + filed tasks, not each bucket [0.10ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > no spec → still reconciles landed issues, no drift [0.05ms]

packages/dispatcher/test/hook-store.test.ts:
(pass) DbHookStore — resolveSessionToken > returns the token of the active workflow owning the session [75.86ms]
(pass) DbHookStore — resolveSessionToken > returns null for an unknown session [72.90ms]
(pass) DbHookStore — resolveSessionToken > ignores terminal workflows that previously held the deterministic session name [85.92ms]
(pass) DbHookStore — record > writes an events row for every hook [88.57ms]
(pass) DbHookStore — record > tool.pre and tool.post advance last_heartbeat [88.45ms]
(pass) DbHookStore — record > a non-tool event records but does not advance last_heartbeat [87.12ms]
(pass) DbHookStore — record > session.started writes session_id + transcript_path onto the workflow [84.92ms]
[hook-store] dropping tool.pre: no active workflow for session middle-GHOST
(pass) DbHookStore — record > an unmatchable session is dropped, not crashed on, and writes nothing [78.42ms]
(pass) DbHookStore — record > oversized payloads are truncated before storage [88.83ms]
[hook-server] received tool.post:middle-14
(pass) HookServer wired to DbHookStore — end to end into SQLite > an authenticated POST flows through the server into the events table + heartbeat [87.33ms]
(pass) serializePayload > returns compact JSON for a small payload [66.80ms]
(pass) serializePayload > clips and marks a payload over 16KB [65.47ms]

packages/dispatcher/test/event-hub.test.ts:
(pass) EventHub > serve emits a `connected` frame first, with SSE content-type [0.49ms]
(pass) EventHub > serve replays caller-supplied init events after `connected` [0.20ms]
(pass) EventHub > a broadcast reaches a live subscriber [0.14ms]
(pass) EventHub > a heartbeat keeps the stream alive (injectable interval) [21.47ms]
(pass) EventHub > an aborted client is unsubscribed cleanly [11.67ms]
(pass) EventHub > a slow consumer that overflows its buffer is dropped without throwing [0.34ms]

packages/dispatcher/test/notification-classify.test.ts:
(pass) classifyNotification — permission blocks > message "Claude needs your permission to use Bash" → permission [0.02ms]
(pass) classifyNotification — permission blocks > message "Claude needs permission to run a command" → permission
(pass) classifyNotification — permission blocks > message "This action requires your approval" → permission
(pass) classifyNotification — permission blocks > message "Claude wants to use the Edit tool" → permission
(pass) classifyNotification — permission blocks > message "Allow Claude to run `chmod +x`?" → permission
(pass) classifyNotification — permission blocks > pane "Do you want to proceed?" → permission even with a generic message [0.01ms]
(pass) classifyNotification — permission blocks > pane "Do you want to allow this?" → permission even with a generic message
(pass) classifyNotification — permission blocks > pane "❯ 1. Yes" → permission even with a generic message
(pass) classifyNotification — permission blocks > pane "❯ 2. Allow" → permission even with a generic message
(pass) classifyNotification — permission blocks > permission outranks an input-shaped message when the pane shows a dialog [0.01ms]
(pass) classifyNotification — input (genuine question) > message "Claude is waiting for your input" → input
(pass) classifyNotification — input (genuine question) > message "Waiting for input" → input
(pass) classifyNotification — input (genuine question) > message "Claude needs your input to continue" → input
(pass) classifyNotification — input (genuine question) > message "Awaiting your input" → input
(pass) classifyNotification — idle/unknown > unattributable message "" → idle-unknown
(pass) classifyNotification — idle/unknown > unattributable message "Some unrelated notification" → idle-unknown
(pass) classifyNotification — idle/unknown > unattributable message "Task finished" → idle-unknown
(pass) classifyNotification — idle/unknown > a long whitespace-laden 'allow …' message classifies fast (no catastrophic backtracking) [0.08ms]
(pass) classifyNotification — idle/unknown > still matches a legitimate 'allow … to' permission request [0.01ms]
(pass) classifyNotification — idle/unknown > tolerates missing message/pane (undefined-safe)

packages/dispatcher/test/multi-repo-blockers.test.ts:
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > an open cross-repo blocker keeps the Epic blocked, annotated with Repo B's title [257.11ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > closing the cross-repo blocker moves the Epic to Ready within one tick [335.06ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > an unresolvable (404) blocker stays blocked with a (stale blocker: <ref>) suffix [248.61ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > the resolved state body round-trips byte-identically [260.44ms]

packages/dispatcher/test/poller-gateway.test.ts:
(pass) deriveCiStatus > no checks configured → none (nothing to gate on) [0.11ms]
(pass) deriveCiStatus > all check runs succeeded (incl. neutral/skipped) → passing [0.04ms]
(pass) deriveCiStatus > any failed/errored/cancelled/timed-out check → failing [0.02ms]
(pass) deriveCiStatus > an unfinished check run (not COMPLETED) → pending [0.01ms]
(pass) deriveCiStatus > a failure outranks a still-running check → failing
(pass) deriveCiStatus > legacy StatusContext entries (state) are read too [0.02ms]
(pass) deriveCiStatus > EXPECTED is pending, not passing — a green gate requires an actual SUCCESS
(pass) ghPollGateway.prSnapshot failure isolation > a transient reviews-fetch failure degrades to null, not a thrown pass [2.03ms]
(pass) ghPollGateway.prSnapshot failure isolation > a `pr view` failure also degrades to null (the symmetric branch) [0.77ms]
(pass) ghPollGateway.prSnapshot failure isolation > both fetches succeed → a populated snapshot [1.33ms]

packages/dispatcher/test/backlog-audit.test.ts:
[backlog-audit] o/r#2 fails the integration rubric → needs-design
(pass) runBacklogAudit > flags rubric-failing feature issues; passes the good one; skips epics [0.37ms]
(pass) runBacklogAudit > does not re-label an issue already marked needs-design [0.06ms]
[backlog-audit] o/r#10 fails the integration rubric → needs-design
[backlog-audit] o/r#11 fails the integration rubric → needs-design
(pass) runBacklogAudit > respects the per-pass cap [0.12ms]
[backlog-audit] failed to label o/r#1 (continuing): boom
[backlog-audit] o/r#2 fails the integration rubric → needs-design
(pass) runBacklogAudit > an addLabel failure is isolated — the sweep continues [0.19ms]
[backlog-audit] o/active#1 fails the integration rubric → needs-design
(pass) runAuditCronPass > sweeps managed repos, skips paused ones [2.29ms]

packages/dispatcher/test/db-migrations.test.ts:
(pass) migration 007 — repo_config epic-store columns > adds epic_store TEXT NOT NULL DEFAULT 'github' [72.65ms]
(pass) migration 007 — repo_config epic-store columns > adds epics_dir TEXT (nullable — only set in file mode) [67.91ms]
(pass) migration 007 — repo_config epic-store columns > adds state_file TEXT (nullable — only set in file mode) [64.70ms]
(pass) migration 007 — repo_config epic-store columns > workflows table gains a nullable epic_ref TEXT column [66.65ms]
(pass) migration 007 — repo_config epic-store columns > backfill: existing implementation rows get epic_ref = stringified epic_number [71.38ms]
(pass) migration 007 — repo_config epic-store columns > a freshly-inserted row defaults epic_store to 'github' [67.41ms]

packages/dispatcher/test/epics-cache.test.ts:
(pass) epics-cache > refreshEpics upserts open Epics and readEpics returns them newest-first [66.78ms]
(pass) epics-cache > an Epic that vanishes from the open set is marked closed and dropped from readEpics [75.43ms]
(pass) epics-cache > a closed Epic that reappears is reopened and visible again [82.05ms]
(pass) epics-cache > caches a file-mode Epic (slug ref, null number) and surfaces it in readEpics (#200) [66.88ms]
(pass) epics-cache > mixed github + file Epics: github (by number desc) first, file (null number) after [78.92ms]
(pass) epics-cache > a file Epic that vanishes is marked closed by its slug ref [83.18ms]
(pass) epics-cache > refresh is repo-scoped — another repo's rows are untouched [69.63ms]

packages/dispatcher/test/metrics.test.ts:
(pass) collectMetrics > empty db → zeroed snapshot [65.90ms]
(pass) collectMetrics > groups workflows by (repo, kind, state) and rolls up totals [98.15ms]
(pass) collectMetrics > a completed implementation frees its slot but stays counted in totals [71.17ms]
(pass) collectMetrics > surfaces rate-limit standing per adapter [70.95ms]
(pass) renderPrometheus > emits gauges with HELP/TYPE and a trailing newline [80.15ms]
(pass) renderPrometheus > an AVAILABLE adapter renders rate_limited 0 [75.62ms]
(pass) renderPrometheus > escapes special characters in label values [76.16ms]

packages/dispatcher/test/implementation-workflow.test.ts:
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-MF2UV0/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-MF2UV0/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=failed
(pass) implementation workflow — terminal stops fall through the waitFor > a 'failed' classifyStop ends 'failed', destroys the worktree, leaks no session [274.01ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Qz5Cb2/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Qz5Cb2/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — terminal stops fall through the waitFor > a 'bare-stop' ends 'completed' without parking [274.38ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-BFLg0n/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-BFLg0n/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=rate-limited
(pass) implementation workflow — terminal stops fall through the waitFor > a rate-limited classifyStop ends 'rate-limited' and records rate_limit_state [271.46ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-aDzytS/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-aDzytS/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] prompt-first launch: dismissing boot dialogs before prompt
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=s
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > prompt-first adapter sends the prompt BEFORE awaiting SessionStart (codex; no deadlock) [271.67ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-6a9g2w/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-6a9g2w/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=s
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > boot-first adapter awaits SessionStart BEFORE sending the prompt (Claude path, unchanged) [269.82ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-b04xYD/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-b04xYD/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 250ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: timed out waiting for session.started
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > await-first ordering deadlocks a prompt-triggered CLI — why the flag exists [521.86ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-e4mWEU/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-e4mWEU/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — prepare-worktree survives a step retry (#108) > a transient createWorktree failure retries to success — the re-INSERT is a no-op, not a masking UNIQUE [935.50ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-hOL8dM/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-hOL8dM/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > a hung agent whose session dies parks for resume; worktree preserved [294.01ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-iOJgdc/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-iOJgdc/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > parkForResume keeps a pre-armed blocked signal (no duplicate) [297.67ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-DV6Uvr/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-DV6Uvr/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: session ended before Stop hook
(pass) implementation workflow — blocked sentinel self-heal > a hung agent with NO sentinel still fails (compensates, worktree pruned) [256.94ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Nxkze2/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Nxkze2/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > parkForResume removes the consumed blocked.json sentinel (#205) [282.43ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-66pZp0/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-66pZp0/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
(pass) implementation workflow — blocked sentinel self-heal > a session that dies mid-nudge with a blocked sentinel parks, not compensates [295.01ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-V6Zwex/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-V6Zwex/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[recommender-run] engine.close drain timed out after 10s — proceeding
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-V6Zwex/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-V6Zwex/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-V6Zwex/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-V6Zwex/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — question-spam integration (#205) > three consecutive dispatch ticks on a stale sentinel grow the Epic by ≤1 comment [409.10ms]
[recommender-run] engine.close drain timed out after 10s — proceeding
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ya4nL9/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ya4nL9/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > a complexity-kind pause routes to waiting-human and surfaces with kind 'complexity' [263.73ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-pTqR4m/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-pTqR4m/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > a plain question pause surfaces with kind 'question' (the default) [270.27ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-M8tAuK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-M8tAuK/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > the dispatch brief carries the repo's complexity_ceiling as the agent's fork budget [261.66ms]
[recommender-run] engine.close drain timed out after 10s — proceeding
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-TuALRx/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-TuALRx/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — complexity pause (#52) > an in-ceiling decision never surfaces a complexity pause [315.56ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ZFa0rI/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ZFa0rI/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > an approved Epic's brief authorizes proceeding past a complexity overrun (#53) [212.07ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] brief-context resolution failed, using defaults (ceiling=3, approved=false): gh rate limited
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ZCvuq8/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ZCvuq8/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > a flaky brief-context read falls back to safe defaults, never failing the dispatch [261.90ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-kXKlxE/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-kXKlxE/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-99] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-99] installing hooks in /tmp/middle-wf-kXKlxE/worktrees/thejustinwalsh/middle/issue-99
[workflow:middle-thejustinwalsh-middle-99] launching tmux session: true (cwd=/tmp/middle-wf-kXKlxE/worktrees/thejustinwalsh/middle/issue-99)
[workflow:middle-thejustinwalsh-middle-99] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-99] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-99] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-99] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-99] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-99] Stop received — classification=asked-question
(pass) implementation workflow — dispatch source (#53) > records source 'manual' for a manual dispatch and 'auto' by default [303.38ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-pk79Db/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-pk79Db/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-pk79Db/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-pk79Db/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (answer): "@.middle/prompt.md (answer)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — asked-question park → answer → resume (e2e) > parks on asked-question, a human reply resumes a fresh continuation with the answer injected [336.80ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-RqpdhZ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-RqpdhZ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-RqpdhZ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-RqpdhZ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a CHANGES_REQUESTED pass resumes a continuation with the address-review brief; APPROVED ends the loop [338.04ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-MjmhRb/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-MjmhRb/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-MjmhRb/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-MjmhRb/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a CI_FAILED verdict resumes a continuation with the fix-CI brief (not the address-review one) [319.09ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-fXF2jD/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-fXF2jD/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a resolved review reverts a previously RATE_LIMITED adapter to AVAILABLE [287.42ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-PPUios/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-PPUios/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-PPUios/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-PPUios/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-PPUios/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-PPUios/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — review-round cap > after the configured cap of CHANGES_REQUESTED passes without APPROVED, it parks in waiting-human and stops auto-resuming [364.21ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-VQ5HPj
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-VQ5HPj)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] plan-comment guard: Plan-comment guard: no plan comment found on Epic #6
(pass) implementation workflow — plan-comment completion gate > a 'done' drive with no plan comment ends 'failed' (guard fires) [253.24ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-1qKeAZ
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-1qKeAZ)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — plan-comment completion gate > a 'done' with a matching plan comment passes the guard and parks for review [283.98ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-PxZBIO/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-PxZBIO/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — plan-comment completion gate > without a planCommentReader wired, a 'done' parks unguarded (back-compat) [301.84ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-QGp7kJ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-QGp7kJ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/2
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 2/2
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 2 nudges — parking for a human
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a bare-stop with no ready Epic PR nudges, then parks in waiting-human [294.69ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-hmRT9L/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-hmRT9L/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] positive done-signal: ready Epic PR — completing
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a ready, non-draft Epic PR is the positive done-signal — done (no nudge), parks for review [305.14ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-40qKA1/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-40qKA1/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/1
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 1 nudges — parking for a human
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a draft Epic PR is not a positive done-signal — it still nudges [287.29ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-6upnyF/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-6upnyF/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > without an epicPrReadiness seam, a bare-stop keeps the legacy completion (back-compat) [274.53ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-irj9YT/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-irj9YT/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: launch timeout
(pass) implementation workflow — compensation > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [277.85ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-xOE1Td/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-xOE1Td/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: all gates pass — done stands
(pass) implementation workflow — verify-on-stop gate > a `done` whose verify fails then passes nudges in-session, then parks for review [261.33ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-kWXktb/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-kWXktb/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/1
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: still failing after 1 rounds — parking for a human
(pass) implementation workflow — verify-on-stop gate > a `done` whose verify never passes parks for a human and keeps the worktree [261.59ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-8ggb01/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-8ggb01/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 0 nudges — parking for a human
(pass) implementation workflow — verify-on-stop gate > a verify re-stop classified `bare-stop` can't bypass the done-signal [260.72ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-0pwugN/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-0pwugN/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — verify-on-stop gate > no runVerifyGates seam → a `done` parks for review unchanged (verify is opt-in) [270.60ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-rbp6VJ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-rbp6VJ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-rbp6VJ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-rbp6VJ/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — durable recovery across daemon restart (#116) > a workflow parked on .waitFor(RESUME_EVENT) survives a restart; a review verdict resumes it [941.10ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-2rQ2Ac/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-2rQ2Ac/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — durable recovery across daemon restart (#116) > an orphaned parked signal (store lost the execution) is reconciled, not left for the poller [699.44ms]

packages/dispatcher/test/pr-divergence-integration.test.ts:
(pass) tryRebaseOntoMain — fixture repo > clean fast-forward: feature has no commits past old main; main advanced → rebase FFs [149.97ms]
(pass) tryRebaseOntoMain — fixture repo > non-FF, no conflict: feature edits A, main edits B, no shared paths → rebase replays cleanly [148.45ms]
(pass) tryRebaseOntoMain — fixture repo > conflict: feature + main both edit shared.txt → rebase aborts, paths reported, worktree clean [182.50ms]
(pass) tryRebaseOntoMain — fixture repo > data-loss guard (#201): a rebase that drops ALL of the PR's commits → restore worktree, droppedAllCommits, branch not emptied [199.79ms]
(pass) tryRebaseOntoMain — fixture repo > gitOps.revListCount: counts a resolvable range and falls back to 0 on an unresolvable one (the guard's conservative semantics) [114.56ms]
(pass) tryRebaseOntoMain — fixture repo > a non-managed head ref (not middle-issue-*) → ok:false with empty paths (skip signal) [101.96ms]
(pass) tryRebaseOntoMain — fixture repo > a missing PR (gateway returns null) → ok:false with empty paths (skip signal) [102.20ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict rebase failure (missing upstream) THROWS — not shaped as a path-less conflict [111.99ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict merge failure (missing ref) THROWS — symmetric to the rebase hardening [107.44ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > rebase would loop but merge -X ours lands cleanly (same line, feature wins) [195.99ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > residual conflict -X ours can't auto-resolve (rename/rename) → abort, paths reported [182.42ms]
(pass) applySuccess — fixture repo > pushes the rebased branch, posts one PR comment, and records CLEAN — twice = idempotent [194.92ms]
(pass) applySuccess — fixture repo > a different mainCommitSha allows a fresh announcement (the marker is sha-keyed) [169.51ms]
(pass) applySuccess — fixture repo > null mainCommitSha skips the comment but still pushes and records CLEAN (self-review hardening) [173.37ms]
(pass) applySuccess — fixture repo > keystone data-loss guard (#201): refuses to push when local HEAD is emptied but the remote branch has commits [175.51ms]
(pass) applySuccess — fixture repo > a non-managed head ref is a no-op (no push, no comment, no row) [110.84ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > BEHIND PR rebases cleanly on the next tick, applies success, and a re-tick is idempotent [185.19ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR rebase-fails → merge fallback lands → applySuccess('merged-new-work-as-base') [236.38ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR both attempts fail (rename/rename) → applyDemoteToWork fires [217.05ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > data-loss regression (#201): rebase that would empty the branch → escalation fires; branch NOT reset to main, PR NOT closed [196.81ms]
[pr-divergence] GitHub budget low (10 < 100); skipping pass — resets 2026-06-04T06:41:55.562Z
(pass) reconcileOpenPRs — end-to-end against the fixture repo > rate-limit floor short-circuits the pass; no listing happens [106.44ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CLEAN PR → walked but unchanged; nothing posted, no state advance [111.83ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > two open managed PRs in one pass — both walked, mix of CLEAN + BEHIND→rebased [182.03ms]
[pr-divergence] o/r PR #300 reconciliation failed: transient classify boom
(pass) reconcileOpenPRs — end-to-end against the fixture repo > per-PR throw increments `failed` and the pass continues on subsequent PRs (self-review hardening) [120.54ms]
[pr-divergence] list open managed PRs for o/r failed: transient gh outage
(pass) reconcileOpenPRs — end-to-end against the fixture repo > listOpenManagedPrs throws → pass returns 0s and logs, no orchestration [103.20ms]

packages/dispatcher/test/documentation-workflow.test.ts:
(pass) documentation workflow — shell: step order + dedicated slot > declares the six steps in order [176.05ms]
(pass) documentation workflow — shell: step order + dedicated slot > runs the steps in order at runtime and completes [280.45ms]
(pass) documentation workflow — shell: step order + dedicated slot > records its row with kind 'documentation' — its own dedicated slot, off maxConcurrent [273.11ms]
(pass) documentation workflow — shell: step order + dedicated slot > claims the 'docs' worktree unit, distinct from the recommender's [276.08ms]
(pass) documentation workflow — shell: step order + dedicated slot > spawn-docs-agent has the spec's 5-minute hard cap [182.38ms]
(pass) documentation workflow — shell: step order + dedicated slot > prepare-docs-worktree registers a compensation handler [180.58ms]
(pass) documentation workflow — shell: step order + dedicated slot > check-rate-limit does not retry [177.21ms]
(pass) documentation workflow — shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' [237.29ms]
[documentation:middle-docs-thejustinwalsh-middle-84683439] spawn failed: launch timeout
(pass) documentation workflow — shell: step order + dedicated slot > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [277.37ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=false: persist seam is never invoked [278.14ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true but persistDocs UNWIRED: still persists nothing (read-only first) [325.73ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true and persistDocs wired: persist runs after the agent, before cleanup [270.96ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports the resolved target, audit mode, and config; invokes the skill via @-reference [268.97ms]
(pass) documentation workflow — assembleDocumentationPrompt > includes the llms.txt audit line only when the target supports it [174.12ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports write=true to the agent when configured [182.29ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=true selects write mode: discover-or-author, agent does not commit [175.88ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=false stays in audit mode (read-only), never write mode [176.77ms]
(pass) documentation workflow — assembleDocumentationPrompt > write mode keeps the llms.txt instruction only when the target supports it [177.28ms]
(pass) documentation workflow — sessionNameFor collision-resistance > is deterministic for a given repo [172.76ms]
(pass) documentation workflow — sessionNameFor collision-resistance > produces a tmux-safe session name under the docs namespace [176.08ms]
(pass) documentation workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [176.60ms]

packages/dispatcher/test/recommender-cron-parallel.test.ts:
[recommender-cron] acme/b run timed out after 500ms — abandoned (retries next tick)
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > a hung repo times out without blocking the others; A+C succeed, B fails [503.05ms]
[recommender-cron] bad/b run failed: boom
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > a throwing run is isolated the same way (stamp rolled back, others succeed) [24.15ms]
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > concurrency is bounded by maxConcurrentRepos [94.20ms]
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > the pass still works (and is sequential-equivalent) with a single due repo [12.01ms]

packages/dispatcher/test/host-context.test.ts:
(pass) DaemonHostContext exposes dispatch + refreshEpics callbacks [0.03ms]

packages/dispatcher/test/control-routes.test.ts:
(pass) HookServer control routes > GET /health reports liveness, port, and version [2.85ms]
(pass) HookServer control routes > the server idle-timeout exceeds the SSE heartbeat (else /control/events streams drop) [0.03ms]
(pass) HookServer control routes > POST /control/dispatch starts the workflow and returns its id [1.34ms]
(pass) HookServer control routes > POST /control/dispatch rejects invalid bodies with 400 and starts nothing [2.80ms]
(pass) HookServer control routes > POST /control/dispatch surfaces the disabled-vs-unknown distinction in the 400 body [1.71ms]
(pass) HookServer control routes > POST /control/dispatch refuses with 429 when no slot is available (manual respects limits) [2.19ms]
(pass) HookServer control routes > POST /control/dispatch proceeds when a slot is available [1.47ms]
[hook-server] afterDispatch failed for o/r: scheduler boom
(pass) HookServer control routes > POST /control/dispatch survives a throwing afterDispatch (best-effort, still 200) [1.88ms]
(pass) HookServer control routes > POST /control/dispatch rejects a colliding Epic with 409 [2.16ms]
(pass) HookServer control routes > POST /control/dispatch maps a shared-checkout collision to 400 (#226) [2.20ms]
(pass) HookServer control routes > two concurrent dispatches of the same Epic: exactly one 200, one 409 [6.63ms]
(pass) HookServer control routes > GET /control/events opens an SSE stream with a connected frame [2.53ms]
(pass) HookServer control routes > GET /control/events replays the injected init events [1.82ms]
(pass) HookServer control routes > GET / 404s in the bare server (the status page is gone; the SPA mounts via extraRoutes) [3.14ms]
(pass) HookServer control routes > GET /metrics renders Prometheus text from the metrics seam [1.78ms]
(pass) HookServer control routes > GET /control/metrics returns the raw snapshot as JSON [1.26ms]
(pass) HookServer control routes > metric routes 404 without a metrics seam [1.95ms]
(pass) HookServer control routes > POST /control/resume fires the parked Epic's resume and returns its id [1.66ms]
(pass) HookServer control routes > POST /control/resume 404s when no parked workflow owns the ref [1.96ms]
(pass) HookServer control routes > POST /control/resume 400s on a missing epicRef or answer [1.80ms]
(pass) HookServer control routes > control routes 404 in gate-only mode (no control plane wired) [2.12ms]

packages/dispatcher/test/tmux.test.ts:
(pass) tmux session lifecycle > launch → has-session → send-text → capture-pane → kill [265.75ms]
(pass) tmux session lifecycle > newSession injects env via -e KEY=val [257.01ms]
(pass) tmux session lifecycle > hasSession is false for an unknown session [1.37ms]
(pass) tmux session lifecycle > status reports not-alive for an unknown session [1.25ms]
(pass) tmux session lifecycle > killSession on an already-gone session is a no-op, not a throw [2.64ms]
(pass) tmux session lifecycle > newSession rejects a duplicate session name with a TmuxError [5.15ms]
(pass) tmux session lifecycle > getTmuxVersion parses the installed tmux's version [0.94ms]
(pass) parseTmuxVersion > parses release versions [0.05ms]
(pass) parseTmuxVersion > parses pre-release builds (next-X.Y, X.Ya) [0.03ms]
(pass) parseTmuxVersion > returns null on garbage input [0.01ms]
(pass) tmuxVersionAtLeast > compares major then minor against the threshold [0.08ms]

packages/dispatcher/test/workflow-record.test.ts:
(pass) getWorkflow epic_ref (#187) > reads back epic_ref straight from the column (slug, number-string, or null) [90.16ms]
(pass) dispatch source (#53) > records and reads back source 'manual' / 'auto'; null when unset [78.46ms]
(pass) workflow meta_json accessors > readWorkflowMeta returns {} for a missing row, a null meta, and malformed JSON [79.89ms]
(pass) workflow meta_json accessors > patchWorkflowMeta merges, preserving keys it does not set [77.00ms]
(pass) workflow meta_json accessors > patchWorkflowMeta does not bump updated_at — meta is scratch, not an activity signal [76.76ms]
(pass) workflow meta_json accessors > checkbox-reconcile state round-trips; defaults when unset [73.78ms]
(pass) workflow meta_json accessors > getCheckboxReconcileState sanitizes malformed nested meta back to the contract [91.53ms]
(pass) listRunningImplementationWorkflows > returns only running implementation rows that own both an epic and a worktree [124.32ms]
(pass) createWorkflowRecord > inserts a pending implementation row carrying epic_number [73.06ms]
(pass) createWorkflowRecord > a second create with the same id is a no-op (idempotent on retry), not a UNIQUE error [74.44ms]
(pass) createWorkflowRecord > a non-PK constraint violation (bad kind) still throws — not swallowed [63.35ms]
(pass) countActiveImplementationSlots > counts non-terminal implementation rows, grouped by adapter [73.20ms]
(pass) countActiveImplementationSlots > excludes terminal implementation rows [75.61ms]
(pass) countActiveImplementationSlots > excludes the recommender's own row — its dedicated slot is not a dispatch slot [74.11ms]
(pass) updateWorkflow > transitions state and bumps updated_at [78.98ms]
(pass) updateWorkflow > patches session fields without disturbing others [74.40ms]
(pass) updateWorkflow > a no-op patch leaves the row intact [70.70ms]
(pass) getWorkflow > returns null for an unknown id [67.04ms]
(pass) hasNonTerminalEpicWorkflow > true while an implementation Epic workflow is non-terminal, false once terminal [72.29ms]
(pass) hasNonTerminalEpicWorkflow > scopes by repo and epic; a recommender row never collides [75.05ms]
(pass) findParkedWorkflowByRef > finds the waiting-human workflow for a ref (slug or number); null otherwise [79.89ms]
(pass) listActiveImplementationWorkflows (#180) > returns lastHeartbeat (null when none observed, the touched epoch otherwise) [73.35ms]
(pass) listNonTerminalWorkflows > returns id/repo/epic/state for non-terminal implementation rows only [86.50ms]
(pass) workflow observers > notifies the observer of each patch, and stops after dispose [84.88ms]
[workflow-record] update observer threw: observer boom
(pass) workflow observers > a throwing observer does not break the DB write [75.56ms]
(pass) workflow observers > addWorkflowObserver fans out to every observer; disposers independent [81.27ms]
(pass) workflow observers > the finalize path notifies observers on a real transition only [78.87ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > flips a still-pending row to failed and reports the transition [72.53ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a row already past pending (e.g. a later step's compensated failure) [73.63ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a launching row — the launch step already advanced it [74.59ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on an unknown id [67.66ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > does NOT touch a pending recommender row — it legitimately sits at pending through build-prompt, where compensation owns the terminal state [69.72ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > does NOT touch a pending documentation row (same reason as recommender) [71.20ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > notifies observers only on a real transition [77.87ms]

packages/dispatcher/test/recovery.test.ts:
(pass) reconcileOrphanedSignals > an armed signal with no recoverable execution is finalized failed, consumed, and surfaced [84.41ms]
(pass) reconcileOrphanedSignals > a recoverable parked execution is left untouched (not an orphan) [84.38ms]
(pass) reconcileOrphanedSignals > only the orphaned rows are reconciled when alive and orphaned parks coexist [109.38ms]
(pass) reconcileOrphanedSignals > respects a custom finalState and tolerates a missing surface callback [86.18ms]
[recover] surfacing orphaned signal 361e1db9-e379-419c-a082-a2b68cade915 (epic-9-answered) failed: comment failed
(pass) reconcileOrphanedSignals > a surface callback that throws never aborts the reconcile (still finalized + consumed) [82.85ms]
(pass) reconcileOrphanedSignals > an orphaned signal with a null epicNumber still reconciles [88.66ms]
(pass) reconcileOrphanedSignals > a non-parked (terminal) workflow's stale signal is ignored — only waiting-human rows are pollable [77.87ms]
(pass) reconcileOrphanedSignals > finalState is typed to terminal states only (compile-time guard) [64.01ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BUNQUEUE_DATA_PATH) when it is set [62.70ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BQ_DATA_PATH) when it is set [62.92ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming DATA_PATH) when it is set [67.56ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming SQLITE_PATH) when it is set [66.60ms]
(pass) createDurableEngine (transient-queue env guard) > an empty-string env var still trips the guard (bunqueue coalesces with ??) [69.57ms]
(pass) createDurableEngine (transient-queue env guard) > names every offending var when several are set at once [65.22ms]
(pass) recoverEngine (durable engine across restart) > re-arms a parked waiting execution so a later signal resumes it [404.01ms]
(pass) recoverEngine (durable engine across restart) > drops a mid-drive (running) execution instead of re-driving it [370.14ms]

packages/dispatcher/test/hook-server.test.ts:
[hook-server] received session.started:middle-6
(pass) HookServer — SessionStart > awaitSessionStart resolves with the posted payload [2.34ms]
[hook-server] received session.started:middle-7
(pass) HookServer — SessionStart > a payload that arrives before anyone awaits is stashed and delivered [1.45ms]
[hook-server] received session.started:middle-9
[hook-server] received session.started:middle-9
(pass) HookServer — SessionStart > duplicate pre-await arrivals keep the FIRST payload, not the last [2.86ms]
[hook-server] received session.started:middle-DIFFERENT
(pass) HookServer — SessionStart > waiters are keyed by session — one session's event does not satisfy another [303.59ms]
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > awaitStop resolves on an agent.stopped POST [2.17ms]
[hook-server] received agent.subagent-stopped:middle-6
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > a subagent stop does NOT resolve awaitStop — only the main agent's Stop does [300.81ms]
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > a re-registered awaitStop is not evicted by an abandoned waiter's stale timeout [63.50ms]
[hook-server] received tool.pre:middle-42
(pass) HookServer — HMAC auth + event validation (with store) > a valid POST (correct token, known event) is accepted and recorded [3.76ms]
[hook-server] rejected tool.pre:middle-42 — bad or unknown token
(pass) HookServer — HMAC auth + event validation (with store) > a bad-HMAC POST is rejected 401 and never recorded [3.19ms]
[hook-server] rejected tool.pre:middle-DOES-NOT-EXIST — bad or unknown token
(pass) HookServer — HMAC auth + event validation (with store) > a POST for an unknown session is rejected 401 (no token resolvable) [2.78ms]
[hook-server] rejected unknown event "not.a.real.event"
(pass) HookServer — HMAC auth + event validation (with store) > an unknown event name is rejected 400 before auth or recording [3.17ms]
[hook-server] received session.started:middle-42
(pass) HookServer — HMAC auth + event validation (with store) > session.started with a valid token resolves the SessionGate awaiter [4.89ms]
(pass) HookServer — lifecycle > awaitSessionStart rejects on timeout [52.74ms]
(pass) HookServer — lifecycle > non-POST and unknown paths return 404 [1.86ms]
(pass) HookServer — lifecycle > stop() rejects outstanding waiters [1.40ms]
(pass) HookServer — recommender trigger endpoint > 404s when no trigger is wired (gate-only mode) [1.92ms]
(pass) HookServer — recommender trigger endpoint > wired trigger receives the posted repo and returns its status/body verbatim [3.33ms]
(pass) HookServer — recommender trigger endpoint > tolerates a garbled body — the trigger validates its own inputs [3.03ms]
(pass) HookServer — recommender trigger endpoint > coerces non-string repoSlug/repoPath to undefined before forwarding [2.87ms]
(pass) HookServer — recommender trigger endpoint > a non-object JSON body (null, primitive, array) is treated as empty, not a 500 [5.01ms]
(pass) HookServer — recommender trigger endpoint > passes a string field through while dropping a non-string sibling [3.27ms]
(pass) HookServer — merged routes > extraRoutes are served, and the fetch fallback still answers /health [3.29ms]
(pass) HookServer — merged routes > GET / no longer returns the status page (404 with no SPA route) [2.83ms]

packages/dispatcher/test/docs-persist.test.ts:
(pass) commitDocs > stages and commits authored docs; returns the sha + sorted file list [32.52ms]
(pass) commitDocs > returns null on a clean worktree — no empty commit [16.00ms]
(pass) commitDocs > excludes middle's .middle/ scratch even when the repo does not gitignore it [21.06ms]
(pass) commitDocs > honors a custom commit message [19.80ms]
(pass) makeGhPersistDocs > commits, then invokes the push seam with the commit it produced [20.65ms]
(pass) makeGhPersistDocs > clean worktree: the push seam is never invoked (no empty PR) [14.27ms]
(pass) pushDocsBranch > first run creates the branch on origin at the authored commit [34.12ms]
(pass) pushDocsBranch > re-run force-pushes a divergent commit (rebuilt branch is non-fast-forward) [57.47ms]
(pass) pushDocsBranch > surfaces a push failure rather than swallowing it (no origin configured) [21.37ms]
(pass) docsPrBody > lists the committed files, the commit sha, and the draft notice [9.41ms]

packages/dispatcher/test/documentation-run.test.ts:
[documentation-run] workflow wf_1780555267513_7xjl1p71 enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > runs to completion and records a kind:'documentation' row for the repo [381.83ms]
[documentation-run] workflow wf_1780555267894_3l3hmq2i enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > write=true but a clean worktree: the wired seam opens no PR (no empty commit) [379.26ms]
[documentation-run] workflow wf_1780555268273_dvv9joci enqueued
(pass) dispatchDocumentation — integration: authors markdown into docs/ and persists it > no docs surface + write=true: the agent authors docs/, the run commits + pushes it [380.47ms]
(pass) resolveDocumentationOptions > accepts a configured non-default adapter (e.g. codex) [11.57ms]
(pass) resolveDocumentationOptions > rejects an adapter the registry doesn't know [10.52ms]
(pass) resolveDocumentationOptions > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [10.93ms]
(pass) resolveDocumentationOptions > resolves the markdown fallback target for a plain repo [10.80ms]
(pass) resolveDocumentationOptions > honors a [docs] tool/path override [12.35ms]
(pass) resolveDocumentationOptions > surfaces an unknown tool override as an error rather than falling back [9.79ms]

packages/dispatcher/test/recommender-cron.test.ts:
(pass) runRecommenderCronPass > fires a due, enabled, unpaused repo and stamps last_recommender_run [2.13ms]
(pass) runRecommenderCronPass > does not re-fire a repo whose interval hasn't elapsed [1.83ms]
(pass) runRecommenderCronPass > fires once the interval has elapsed [1.63ms]
(pass) runRecommenderCronPass > skips a paused repo [1.57ms]
(pass) runRecommenderCronPass > skips a repo whose recommender is disabled or unconfigured [1.52ms]
(pass) runRecommenderCronPass > skips a repo with a non-positive interval (never auto-runs) [1.51ms]
[recommender-cron] bad/repo run failed: recommender run boom
[recommender-cron] bad/repo run failed: recommender run boom
(pass) runRecommenderCronPass > a failed launch rolls the stamp back (retries next tick) and is isolated [1.81ms]
(pass) runRecommenderCronPass > ignores unmanaged rows (no checkout path) [1.50ms]

packages/dispatcher/test/poller.test.ts:
(pass) reasonFromSignalName > maps the durable signal names to resume reasons [66.89ms]
(pass) classifyNewHumanReply > returns the newest non-bot reply posted after the wait armed [66.21ms]
(pass) classifyNewHumanReply > returns null when only bot/stale comments exist [65.19ms]
(pass) classifyNewHumanReply > skips the dispatcher's own marked pause comment (posted as a non-bot human identity) [65.07ms]
(pass) classifyNewHumanReply > a genuine human reply that quote-replies the pause comment still resumes [66.26ms]
(pass) classifyReviewOutcome > a fresh CHANGES_REQUESTED review → changes-requested [73.35ms]
(pass) classifyReviewOutcome > a fresh APPROVED review → resolved [66.52ms]
(pass) classifyReviewOutcome > a fresh 0-actionable re-review → resolved even while decision stays CHANGES_REQUESTED [65.46ms]
(pass) classifyReviewOutcome > the `changes-requested` label alone (no fresh review) → changes-requested [68.23ms]
(pass) classifyReviewOutcome > only stale reviews and no actionable label → null (nothing changed) [68.86ms]
(pass) classifyReviewOutcome > a stale standing CHANGES_REQUESTED decision (no fresh review, no label) → null [66.60ms]
(pass) classifyReviewOutcome — CI gate > failing CI with no review feedback → resume to fix CI (CI_FAILED) [62.48ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review while CI is still pending is held (null) — don't end on un-built CI [66.65ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review with passing CI resolves [64.33ms]
(pass) classifyReviewOutcome — CI gate > explicit review feedback wins over red CI (address the review, which greens CI) [62.76ms]
(pass) classifyReviewOutcome — CI gate > absent CI (`none`) is non-blocking — the pre-CI review loop is unchanged [65.15ms]
(pass) classifyReviewOutcome — CI gate > failing CI but no PR change and no review → still CI_FAILED (red build is actionable) [70.55ms]
(pass) runPoller — answered-question > a new human reply fires epic-<n>-answered exactly once (idempotent across passes) [92.05ms]
(pass) runPoller — answered-question > a bot-only reply does not fire [82.53ms]
(pass) runPoller — answered-question > the dispatcher's own pause comment does not self-resume (#178) [79.99ms]
(pass) runPoller — review-changes > CHANGES_REQUESTED fires review-resolved with outcome 'changes-requested' [82.63ms]
(pass) runPoller — review-changes > APPROVED fires review-resolved as resolved [87.34ms]
(pass) runPoller — review-changes > a 0-actionable re-review fires review-resolved as resolved [82.90ms]
(pass) runPoller — review-changes > no PR yet → no fire [80.00ms]
[poller] poll failed for workflow 586112c7-3338-485a-989c-79fed55856a2 (epic-200-answered): API rate limit exceeded
(pass) runPoller — resilience > a gateway error for one workflow is isolated; others still fire [99.67ms]
[poller] GitHub budget low (50 < 100); skipping pass — resets 1970-01-01T00:17:40.000Z
(pass) runPoller — GitHub rate-limit guards > skips the whole pass when remaining budget is below the buffer [84.78ms]
(pass) runPoller — GitHub rate-limit guards > a healthy budget proceeds (the guard isn't always-on) [92.76ms]
(pass) runPoller — GitHub rate-limit guards > caps the workflows polled per pass (burst protection) [129.00ms]

packages/dispatcher/test/github-epics.test.ts:
(pass) parseEpicsList > maps sub_issues_summary into Epic rows [1.76ms]
(pass) parseEpicsList > tolerates blank lines and ignores rows missing a summary [0.03ms]
(pass) parseEpicsList > parses with labels: [] when labels key is wholly absent [0.02ms]

packages/dispatcher/test/reconcile.test.ts:
[reconcile] thejustinwalsh/middle#50 PR MERGED → completed (workflow 047127de-e9ce-47b5-906a-4ba881fdd0a5)
(pass) reconcileMergedParks > a merged PR finalizes the parked workflow to `completed` and tears down its worktree [80.89ms]
[reconcile] thejustinwalsh/middle#51 PR CLOSED → cancelled (workflow 96379856-c0ab-42bf-9a4d-2a65ea6bf469)
(pass) reconcileMergedParks > a closed-unmerged PR finalizes to `cancelled` [82.93ms]
(pass) reconcileMergedParks > an open PR (a live review park) is left alone [83.32ms]
(pass) reconcileMergedParks > no PR for the Epic (a pending question) is left alone [81.23ms]
[reconcile] thejustinwalsh/middle#54 PR MERGED → completed (workflow dba9428b-0c70-405c-8058-93f372debc42)
[reconcile] worktree cleanup failed for dba9428b-0c70-405c-8058-93f372debc42 (continuing): git worktree remove failed
(pass) reconcileMergedParks > finalizes the row even when worktree teardown throws (best-effort) [95.75ms]
(pass) reconcileMergedParks > only walks `waiting-human` rows — running/terminal rows are untouched [103.86ms]
[reconcile] GitHub budget low (10 < 100); skipping pass — resets 1970-01-01T00:00:00.000Z
(pass) reconcileMergedParks > skips the whole pass when the GitHub budget is below the buffer [77.79ms]
[reconcile] thejustinwalsh/middle#70 PR MERGED → completed (workflow c12f426a-bf25-4768-9cd4-346704411de6)
[reconcile] thejustinwalsh/middle#71 PR CLOSED → cancelled (workflow 55e91596-ef8f-48e2-827f-6996dc011acf)
[reconcile] thejustinwalsh/middle#72 PR MERGED → completed (workflow 3237efa9-a91f-461c-a3a1-b1e0b2fc6b90)
(pass) reconcileMergedParks > fires onMergedTransition at most once per repo per pass (Epic #168 wiring) [105.81ms]
[reconcile] thejustinwalsh/middle#75 PR MERGED → completed (workflow 04640aad-121f-47b3-94e9-117379223106)
[reconcile] onMergedTransition for thejustinwalsh/middle failed (continuing): downstream sweep boom
[reconcile] thejustinwalsh/middle#76 PR MERGED → completed (workflow da3de4b0-40b8-45ad-9218-a8bbb9338cd7)
(pass) reconcileMergedParks > a thrown onMergedTransition is isolated — the merged-parks pass still finishes [89.71ms]
[reconcile] thejustinwalsh/middle#60 PR MERGED → completed (workflow 122ae19b-a82a-42ac-9f76-189a3eaf5a3a)
[reconcile] thejustinwalsh/middle#61 PR MERGED → completed (workflow 42ff2de8-41bc-4778-b5cb-dc8c59f1be2b)
(pass) reconcileMergedParks > honors the per-pass burst cap [112.04ms]
(pass) reconcileMergedParks > does not tear down the worktree when it loses the race to a concurrent resume [78.29ms]
(pass) finalizeParkedWorkflow > transitions a still-parked row and reports the change [82.01ms]
(pass) finalizeParkedWorkflow > no-ops (returns false) a row that already left waiting-human [78.60ms]

packages/dispatcher/test/recommender-workflow.test.ts:
(pass) recommender workflow — #43 shell: step order + dedicated slot > declares the eight spec steps in order [175.49ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > runs the steps in spec order at runtime and completes [279.48ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > records its row with kind 'recommender' — its own dedicated slot, off maxConcurrent [266.76ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > spawn-recommender-agent's step backstop is sized for the per-repo ceiling [177.07ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > prepare-shallow-worktree registers a compensation handler [184.34ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > check-rate-limit does not retry — it creates the row then may throw, and a retry would re-INSERT [174.38ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' (not a UNIQUE error) [236.05ms]
[recommender:middle-rec-thejustinwalsh-middle-84683439] spawn failed: launch timeout
(pass) recommender workflow — #43 shell: step order + dedicated slot > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [274.52ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > assembles all eight Phase-1 inputs, with dispatcher-owned context verbatim [174.64ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > file mode reframes the prompt for the file-backed store (#200) [173.40ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > writes the assembled prompt to .middle/prompt.md and launches it via the @-reference [267.70ms]
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a valid produced body verifies ok and the workflow proceeds to trigger-auto-dispatch [276.24ms]
[recommender] reapply skipped — agent body for #99 does not parse: missing open marker
[recommender] state issue #99 does not parse: missing open marker
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a malformed produced body does NOT proceed to auto-dispatch and surfaces the problem [273.34ms]
[recommender] state issue #99 failed validation: Ready row uses unconfigured adapter: "ghost"
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a body that parses but fails validation is also gated and surfaced [270.61ms]
[recommender] reapply skipped — agent body for #99 does not parse: missing open marker
[recommender] state issue #99 does not parse: missing open marker
[recommender] surfaceProblem failed: gh comment failed
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a failed surfaceProblem callback does not abort cleanup (best-effort surfacing) [280.44ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > heartbeatRel formats epoch deltas; null → 'unknown' [175.02ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > dispatcherSectionsFromContext builds canonical sections (heartbeat, null-issue dropped, null-session→pending) [172.68ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > self-heal: agent emits empty In-flight; dispatcher overwrites with the canonical 5-field line [271.36ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > no-op: when the agent body already matches the dispatcher's sections, reapply skips the write [277.89ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
[documentation-run] engine.close drain timed out after 10s — proceeding
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > a throwing reapply write compensates (worktree rolled back, no dispatch) [1932.01ms]
[recommender] reapply skipped — agent body for #99 does not parse: malformed "In-flight" item: "- **#60** · claude · running · [tmux: middle-thejustinwalsh-middle-60]"
[recommender] state issue #99 does not parse: malformed "In-flight" item: "- **#60** · claude · running · [tmux: middle-thejustinwalsh-middle-60]"
[documentation-run] engine.close drain timed out after 10s — proceeding
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > exact bug shape: agent body with a 4-field In-flight line is left to verify, which surfaces it [268.24ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > derives rate_limits, in_flight, and slots from db + config [198.98ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > excludes the recommender's own row from in_flight and slots [191.93ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > scopes per-repo slots/in_flight to the repo, but global_used spans all repos [191.81ms]
(pass) recommender workflow — sessionNameFor collision-resistance > is deterministic for a given repo [176.27ms]
(pass) recommender workflow — sessionNameFor collision-resistance > produces a tmux-safe session name (no separators survive) [178.24ms]
(pass) recommender workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [176.05ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > runs on the engine via per-repo resolveRunSettings and creates the recommender row [266.96ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > a clear wiring error when neither resolveRunSettings nor static settings are provided [2293.25ms]

packages/dispatcher/test/staleness-cron.test.ts:
[staleness] o/active#50 landed in merged PR #88 → closed
[staleness] o/active: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass > reads the repo's spec from its checkout, closes + flags; skips paused [3.02ms]
(pass) runStalenessCronPass > a non-ENOENT spec read error surfaces (not silently treated as missing spec) [2.35ms]
[staleness] o/custom#50 landed in merged PR #88 → closed
[staleness] o/custom: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — per-repo spec path > a repo's [staleness] spec_path points the drift check at a non-default location [2.29ms]
[staleness] o/defaulted#50 landed in merged PR #88 → closed
[staleness] o/defaulted: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — per-repo spec path > a repo with no configured spec_path falls back to the default path [2.20ms]
[staleness] o/nospec#50 landed in merged PR #88 → closed
(pass) runStalenessCronPass — per-repo spec path > a repo with no spec file still reconciles landed issues (no drift) [1.71ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a `..` traversal spec_path is rejected — the pass never reads outside the checkout [2.09ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a deeper `../../` traversal is rejected too [2.40ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > an absolute spec_path is rejected (the field is repo-relative by contract) [3.08ms]
[staleness] o/dotdotname#50 landed in merged PR #88 → closed
[staleness] o/dotdotname: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a filename whose segment merely starts with `..` is allowed (not a traversal) [2.24ms]

packages/dispatcher/test/rate-limits.test.ts:
(pass) rate_limit_state > getRateLimitState is null until observed [62.73ms]
(pass) rate_limit_state > setRateLimited records status, reset_at, and source [78.72ms]
(pass) rate_limit_state > setRateLimited upserts an existing adapter row [81.58ms]
(pass) rate_limit_state > markAvailable clears the reset time [77.01ms]
(pass) rate_limit_state > markAvailableOnSuccess flips RATE_LIMITED → AVAILABLE and reports it [75.07ms]
(pass) rate_limit_state > markAvailableOnSuccess is a no-op when not rate-limited [71.23ms]
(pass) rate-limit observer fan-out > addRateLimitObserver fans out to every observer; disposers are independent [72.83ms]
[rate-limits] observer threw: boom
(pass) rate-limit observer fan-out > a throwing observer does not stop the others or the write path [67.24ms]
(pass) parseResetAt > parses an ISO timestamp to unix ms [65.91ms]
(pass) parseResetAt > returns null for unrecognized text [70.49ms]

packages/dispatcher/test/poller-cron.test.ts:
(pass) POLLER_INTERVAL_MS matches the dispatcher CLAUDE.md cadence contract (60s) [1.34ms]

packages/dispatcher/test/blocker-resolution.test.ts:
(pass) parseBlockerRef > same-repo #<n> [0.09ms]
(pass) parseBlockerRef > cross-repo <owner>/<repo>#<n> [0.02ms]
(pass) parseBlockerRef > strips a trailing title annotation when extracting the ref [0.02ms]
(pass) parseBlockerRef > backticked non-issue blocker is non-resolvable [0.01ms]
(pass) parseBlockerRef > free text without a #<n> is non-issue
(pass) resolveBlockers > a closed same-repo blocker moves the item to Ready to dispatch [0.28ms]
(pass) resolveBlockers > an open blocker stays Blocked, annotated with the resolved title [0.08ms]
(pass) resolveBlockers > an unresolvable (404) blocker stays Blocked with a (stale blocker: <ref>) suffix [0.10ms]
(pass) resolveBlockers > a backticked non-issue blocker is left untouched [0.07ms]
(pass) resolveBlockers > an open blocker with an empty title falls back to the bare ref (never `#42 ()`) [0.07ms]
(pass) resolveBlockers > a long open-blocker title is truncated to 60 chars in the annotation [0.06ms]
(pass) resolveBlockers > re-resolving is idempotent — a re-annotated open blocker does not accumulate [0.04ms]
(pass) resolveBlockers > re-resolving a now-closed previously-stale blocker unblocks it [0.05ms]
(pass) resolveBlockers > appended Ready rows are re-ranked after existing rows [0.11ms]
(pass) resolveBlockers > falls back to resolveIssue for the title when selfEpic has no entry [0.09ms]
(pass) resolveBlockers > the produced state still round-trips through render/parse [0.16ms]
(pass) resolveBlockers > no resolvable blockers → state is returned structurally unchanged [0.06ms]
(pass) resolveBlockers > a long blocker title is truncated to 60 chars with an ellipsis in the Ready epic [0.08ms]

packages/dispatcher/test/hook-server-gates.test.ts:
(pass) HookServer — /gates/pr-ready > returns 200 when the gate allows [2.40ms]
[hook-server] pr-ready gate DENY for middle-27: criteria X and Y lack evidence
(pass) HookServer — /gates/pr-ready > returns 403 with the reason in the body when the gate denies [1.54ms]
(pass) HookServer — /gates/pr-ready > forwards the session name and payload to the gate handler [1.60ms]
(pass) HookServer — /gates/pr-ready > 404s the gate route when no gate handler is wired [1.81ms]

packages/dispatcher/test/repo-config.test.ts:
(pass) repo pause/resume > an unpaused repo (no row) reads as not paused [1.69ms]
(pass) repo pause/resume > mm pause (indefinite) suspends the repo [1.48ms]
(pass) repo pause/resume > a paused_until in the future reads as paused; in the past auto-expires [1.42ms]
(pass) repo pause/resume > mm resume clears the pause [1.55ms]
(pass) repo pause/resume > pausing is idempotent and re-pausing updates the timestamp [1.41ms]
(pass) repo pause/resume > resume on a never-paused repo is a harmless no-op [1.34ms]
(pass) managed-repo registry (#135) > an unregistered repo has no path and isn't listed [1.36ms]
(pass) managed-repo registry (#135) > registerManagedRepo records the checkout path and lists it [1.47ms]
(pass) managed-repo registry (#135) > registering is idempotent and updates the path in place (one row) [1.40ms]
(pass) managed-repo registry (#135) > registering preserves an existing pause (doesn't clobber paused_until) [1.84ms]
(pass) managed-repo registry (#135) > listManagedRepos excludes rows with no checkout path (e.g. a pause-only row) [1.50ms]
(pass) managed-repo registry (#135) > setLastRecommenderRun writes a value and clears it with null (cron rollback) [1.48ms]
(pass) managed-repo registry (#135) > markRecommenderRun stamps and reads back last_recommender_run [1.51ms]
(pass) shared-checkout collision guard (#226) > (a) registering acme/a at /tmp/X succeeds [1.58ms]
(pass) shared-checkout collision guard (#226) > (b) re-registering the SAME repo at the same path is idempotent and succeeds [1.57ms]
(pass) shared-checkout collision guard (#226) > (c) registering a DIFFERENT repo at the same path rejects, naming both repos + the path [1.51ms]
(pass) shared-checkout collision guard (#226) > the rejected repo is NOT written (the collision guard runs before the insert) [1.50ms]
(pass) shared-checkout collision guard (#226) > the same repo can move to a new path (no self-collision) [1.44ms]
(pass) shared-checkout collision guard (#226) > assertNoRepoPathCollision is a standalone guard (used by mm init before scaffolding) [1.44ms]
(pass) shared-checkout collision guard (#226) > trailing-slash / dot-segment spellings of the same path still collide (normalized) [1.53ms]

packages/dispatcher/test/worktree.test.ts:
(pass) createWorktree → listWorktrees → destroyWorktree > create places the worktree under <root>/<repo>/issue-<n> on a fresh branch [15.72ms]
(pass) createWorktree → listWorktrees → destroyWorktree > the recommender unit is named 'recommender' [13.15ms]
(pass) createWorktree → listWorktrees → destroyWorktree > list enumerates active worktrees under the root [21.42ms]
(pass) createWorktree → listWorktrees → destroyWorktree > destroy removes the worktree directory and its branch [19.99ms]
(pass) idempotency > creating an already-existing worktree returns the handle without throwing [14.06ms]
(pass) idempotency > destroying an already-removed worktree is a no-op, not a throw [20.33ms]
(pass) branch reuse (issue #179) > reuses an existing branch — does not pass -b, so it doesn't error [15.27ms]
(pass) branch reuse (issue #179) > reuse checks out the existing branch's own tip, not a fresh branch from HEAD [19.02ms]
(pass) branch reuse (issue #179) > still creates a fresh branch when none exists (first dispatch unchanged) [16.17ms]
(pass) branch reuse (issue #179) > dispatch → prune (branch survives) → re-dispatch all succeed [22.98ms]
(pass) failure surfacing > create against a non-git directory throws WorktreeError [7.14ms]

packages/dispatcher/test/adapter-conformance.test.ts:
(pass) the registry knows all three adapters [0.20ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("toString") throws unknown-adapter [0.16ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("toString") is false [0.11ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("constructor") throws unknown-adapter [0.10ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("constructor") is false [0.08ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("hasOwnProperty") throws unknown-adapter [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("hasOwnProperty") is false [0.08ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("__proto__") throws unknown-adapter [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("__proto__") is false [0.09ms]
(pass) AgentAdapter contract — claude > resolveTranscriptPath yields a path from this adapter's own ready payload [0.14ms]
(pass) AgentAdapter contract — claude > identity: name matches its registry key and readyEvent is a normalized event [0.11ms]
(pass) AgentAdapter contract — claude > buildLaunchCommand yields a non-empty argv and the session env [0.14ms]
(pass) AgentAdapter contract — claude > buildPromptText: initial is the skill slash-command on the Epic [0.12ms]
(pass) AgentAdapter contract — claude > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.10ms]
(pass) AgentAdapter contract — claude > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.28ms]
(pass) AgentAdapter contract — claude > classifyStop: blocked.json → asked-question [0.41ms]
(pass) AgentAdapter contract — claude > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.82ms]
(pass) AgentAdapter contract — claude > detectRateLimit is implemented and returns null on a clean transcript [0.28ms]
(pass) AgentAdapter contract — codex > resolveTranscriptPath yields a path from this adapter's own ready payload [0.21ms]
(pass) AgentAdapter contract — codex > identity: name matches its registry key and readyEvent is a normalized event [0.17ms]
(pass) AgentAdapter contract — codex > buildLaunchCommand yields a non-empty argv and the session env [0.20ms]
(pass) AgentAdapter contract — codex > buildPromptText: initial is the skill slash-command on the Epic [0.19ms]
(pass) AgentAdapter contract — codex > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.10ms]
(pass) AgentAdapter contract — codex > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [2.70ms]
(pass) AgentAdapter contract — codex > classifyStop: blocked.json → asked-question [0.37ms]
(pass) AgentAdapter contract — codex > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.47ms]
(pass) AgentAdapter contract — codex > detectRateLimit is implemented and returns null on a clean transcript [0.14ms]
(pass) AgentAdapter contract — copilot > resolveTranscriptPath yields a path from this adapter's own ready payload [0.14ms]
(pass) AgentAdapter contract — copilot > identity: name matches its registry key and readyEvent is a normalized event [0.08ms]
(pass) AgentAdapter contract — copilot > buildLaunchCommand yields a non-empty argv and the session env [0.14ms]
(pass) AgentAdapter contract — copilot > buildPromptText: initial is the skill slash-command on the Epic [0.09ms]
(pass) AgentAdapter contract — copilot > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.09ms]
(pass) AgentAdapter contract — copilot > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.11ms]
(pass) AgentAdapter contract — copilot > classifyStop: blocked.json → asked-question [0.39ms]
(pass) AgentAdapter contract — copilot > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.40ms]
(pass) AgentAdapter contract — copilot > detectRateLimit is implemented and returns null on a clean transcript [0.14ms]

packages/dispatcher/test/main.test.ts:
(pass) dispatcher main > starts the hook server, announces readiness, and exits 0 on SIGTERM [1228.80ms]
(pass) dispatcher main > hosts a dispatch on its own engine and broadcasts a workflow SSE event [1231.35ms]
(pass) dispatcher main > a terminal prepare-worktree failure marks the row failed, so the next dispatch isn't 409-blocked (issue #179) [3460.62ms]
(pass) dispatcher main > daemon rejects a disabled adapter on /control/dispatch (configured+enabled+implemented gate) [1209.67ms]
(pass) dispatcher main > two concurrent dispatches of the same Epic: exactly one starts, the other 409s [1263.54ms]

packages/dispatcher/test/db.test.ts:
(pass) openDb > opens a file database in WAL mode [12.59ms]
(pass) runMigrations > a fresh db starts at schema version 0 [13.02ms]
(pass) runMigrations > applies every migration and reports the latest version [64.28ms]
(pass) runMigrations > 001_initial creates every documented table [76.50ms]
(pass) runMigrations > 001_initial creates every documented index [66.43ms]
(pass) runMigrations > is idempotent — running twice leaves version at the latest and does not throw [66.77ms]
(pass) runMigrations > 002 adds the waitfor_signals.fired_at column [72.30ms]
(pass) runMigrations > workflows.state CHECK rejects an unknown state [68.51ms]
(pass) runMigrations > workflows.state CHECK accepts 'launching' [83.08ms]
(pass) runMigrations > 003 widens workflows.kind to accept 'documentation' but still rejects unknown kinds [76.80ms]
(pass) runMigrations > 003 preserves existing rows and child FK references through the table rebuild [80.85ms]
(pass) openAndMigrate > opens, migrates, and returns a ready database [71.83ms]

packages/dispatcher/test/retention.test.ts:
(pass) runRetentionPass — events cutoff (14d) > deletes events older than 14 days, keeps newer ones [92.82ms]
(pass) runRetentionPass — events cutoff (14d) > an event exactly at the cutoff age is kept (strict `< cutoff`) [87.99ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > archives completed workflows older than 30 days; drops their events, preserves the row [84.27ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > does not archive completed workflows inside the 30-day window [79.84ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > does not archive old non-completed workflows (failed/running/etc.) [77.11ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > is idempotent — a second pass archives nothing new [99.70ms]
(pass) retention_runs recording > records each pass (even a no-op) with ok=true [79.84ms]
(pass) retention_runs recording > recordRetentionRun with a detail marks ok=false [68.99ms]
(pass) retention_runs recording > an empty-string detail still marks ok=false (failure presence, not truthiness) [71.37ms]
(pass) retention_runs recording > getLatestRetentionRun returns the most recent by ran_at [79.94ms]
(pass) collectRetentionStatus > reports row counts (incl. archived) and the last run [88.18ms]
(pass) collectRetentionStatus > lastRun is null before any retention has run [67.05ms]

packages/dispatcher/test/slots.test.ts:
(pass) getSlotState > free-slot: no active work reports full availability across every dimension [1.84ms]
(pass) getSlotState > at-capacity: a full repo reports zero availability and the guard refuses [1.70ms]
(pass) getSlotState > per-adapter cap binds before the repo cap [1.51ms]
(pass) getSlotState > global cap binds across repos even when this repo has room [1.58ms]
(pass) getSlotState > the recommender's own row is never counted against dispatch slots [1.48ms]
(pass) getSlotState > used over max clamps available to 0 (a tightened cap never goes negative) [1.55ms]
(pass) getSlotState > an adapter with no per-adapter cap is gated only by the repo and global dims [1.43ms]
(pass) reserveSlot > decrements the adapter, repo, and global dimensions for the loop's local view [1.64ms]
(pass) reserveSlot > reserving down to capacity flips the guard to refuse [1.49ms]
(pass) reserveSlot > reserving an adapter with no cap still decrements repo + global [1.37ms]

packages/dispatcher/test/auto-dispatch.test.ts:
(pass) autoDispatch > normal pass: enqueues every ready row that has a free slot [0.47ms]
(pass) autoDispatch > does nothing for a repo whose auto-dispatch is disabled [0.06ms]
(pass) autoDispatch > skips a rate-limited adapter but keeps dispatching others [0.06ms]
(pass) autoDispatch > skips a row whose per-adapter slot is exhausted, continues to the next adapter [0.05ms]
(pass) autoDispatch > stops entirely when the repo total is exhausted (slots-exhausted) [0.04ms]
(pass) autoDispatch > stops when the global total is exhausted even if the repo has room [0.04ms]
(pass) autoDispatch > decrements local counters as it enqueues so a shared cap stops mid-pass [0.06ms]
(pass) autoDispatch > a refused enqueue (collision/null) does not consume a local slot [0.12ms]
(pass) autoDispatch > dispatches a file-mode Epic by its slug ref (#200) [0.10ms]
(pass) autoDispatch > extracts a non-kebab slug ref up to the first space (#200) [0.19ms]
(pass) autoDispatch > ignores the empty-state (no ready rows) without enqueuing [0.07ms]
(pass) autoDispatch > no pre-dispatch complexity gate: a large-sub-issue Epic still dispatches (#52) [0.07ms]
(pass) createParseFailureSurfacer (#180) > surfaces a parse failure on the state issue, with the underlying message [0.15ms]
(pass) createParseFailureSurfacer (#180) > dedupes an identical message across a burst — one comment, not N [0.08ms]
(pass) createParseFailureSurfacer (#180) > reset() re-arms surfacing after a healthy read [0.04ms]
(pass) createParseFailureSurfacer (#180) > a different parse message surfaces even without a reset [0.03ms]
(pass) createParseFailureSurfacer (#180) > ignores non-parse errors so transient gh/network failures never spam [0.04ms]
(pass) createParseFailureSurfacer (#180) > a failed comment is not recorded — the next tick retries (no silent suppression) [0.08ms]
(pass) createParseFailureSurfacer (#180) > dedup is per-repo — two repos with the same message each surface once [0.04ms]
(pass) didReadState (#180) — gate re-arming on an actual read > a `disabled` pass did not read — must NOT re-arm surfacing [0.04ms]
(pass) didReadState (#180) — gate re-arming on an actual read > every reason that runs after readState counts as a read [0.01ms]
(pass) didReadState (#180) — gate re-arming on an actual read > disabled tick does not re-arm; a healthy (drained) read does [0.09ms]

packages/dispatcher/test/pr-divergence.test.ts:
(pass) classifyMergeability > DIRTY → CONFLICTED regardless of mergeable [65.57ms]
(pass) classifyMergeability > BEHIND → BEHIND [65.08ms]
(pass) classifyMergeability > CLEAN + MERGEABLE → CLEAN [66.96ms]
(pass) classifyMergeability > CLEAN but not MERGEABLE → UNKNOWN (CI gating, secondary signals) [69.06ms]
(pass) classifyMergeability > BLOCKED / HAS_HOOKS / UNSTABLE / UNKNOWN → UNKNOWN [72.48ms]
(pass) classifyMergeability > a null view (PR doesn't exist) → UNKNOWN [70.01ms]
(pass) classifyMergeability > missing fields → UNKNOWN (legacy fixtures don't tip the classifier) [65.11ms]
(pass) classifyDivergence > classifies BEHIND and persists the row with the supplied clock [73.52ms]
(pass) classifyDivergence > classifies CONFLICTED and overwrites a prior row (upsert keeps the row fresh) [75.36ms]
(pass) classifyDivergence > classifies CLEAN [74.34ms]
(pass) classifyDivergence > classifies UNKNOWN for a PR with no mergeability view (gone / 404) [81.17ms]
(pass) parseEpicFromHeadRef > parses `middle-issue-<N>` to the integer N [66.61ms]
(pass) parseEpicFromHeadRef > a non-managed head ref → null (the helper skips it) [67.88ms]
(pass) parseEpicFromHeadRef > a malformed managed ref → null (defends against an inadvertent rename) [74.70ms]
(pass) worktreePathFor > uses <root>/<repo>/issue-<n> — the same layout createWorktree writes [67.15ms]
(pass) recordDivergenceState > accepts terminal-ish states (DEMOTED, SKIPPED) written by sibling phases [86.26ms]
(pass) recordDivergenceState > the CHECK constraint rejects an out-of-vocabulary state — defends against a reconciler typo [80.47ms]
(pass) recordDivergenceState > the (repo, pr_number) PK lets the same pr_number coexist across repos [77.19ms]
(pass) applyDemoteToWork > flips PR draft, reopens sub-issue, posts dual-surface comment, re-enqueues, state→DEMOTED [76.25ms]
(pass) applyDemoteToWork > per-step idempotency: a second call skips draft-flip + reopen + comments via markers (but still re-enqueues) [77.25ms]
(pass) applyDemoteToWork > partial-retry: prior attempt left the PR drafted but did not reopen / comment / enqueue — second pass completes remediation [75.15ms]
(pass) applyDemoteToWork > partial-retry safety: existing marker on PR skips the duplicate PR comment, still posts on Epic [67.53ms]
(pass) applyDemoteToWork > Epic with no closed sub-issues: still demotes + comments + enqueues; no reopen call [69.91ms]
(pass) applyDemoteToWork > non-managed head ref → no-op (no draft, no comments, no enqueue, no row) [66.02ms]
(pass) applyDemoteToWork > manual recovery: an Epic that already carries the demote marker skips the reopen call (self-review hardening) [70.48ms]
(pass) applyDemoteToWork > a supplied reason (#201 data-loss) replaces the conflict narrative in the escalation comment [67.98ms]
(pass) applyDemoteToWork > PR doesn't exist (gateway returns null) → no-op [64.64ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a PullRequest with " [65.39ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a Branch with the n" [64.83ms]
(pass) ghStderrIsNotFound > recognizes not-found: "HTTP 404: Not Found (https://api.github." [62.32ms]
(pass) ghStderrIsNotFound > recognizes not-found: "graphql: Could not resolve to a Reposito" [63.01ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "error connecting to api.github.com: dial" [62.64ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 401: Bad credentials" [62.24ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 403: API rate limit exceeded" [62.29ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 502: Bad Gateway" [61.23ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "gh: command failed (oauth token expired)" [61.88ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "could not deserialize response" [62.52ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "remote: secret not found, push declined" [63.88ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "Not Found" [62.72ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "" [63.63ms]

packages/core/test/config.test.ts:
(pass) loadConfig — [docs] section > parses a full docs block [2.15ms]
(pass) loadConfig — [docs] section > a tool/path-only override block is valid; bot fields default [0.31ms]
(pass) loadConfig — [docs] section > absent override fields stay undefined so the resolver auto-detects [0.28ms]
(pass) loadConfig — [docs] section > no [docs] section leaves docs undefined [0.20ms]
(pass) loadConfig — [staleness] section > reads spec_path [0.23ms]
(pass) loadConfig — [staleness] section > no [staleness] section leaves staleness undefined [0.19ms]
(pass) loadConfig — [staleness] section > an empty [staleness] block leaves specPath undefined (falls back to the default) [0.22ms]
(pass) loadConfig — [staleness] section > the local cache overrides committed policy spec_path [0.23ms]
(pass) loadConfig — global only > parses the global sections and leaves per-repo sections undefined [0.21ms]
(pass) loadConfig — global only > expands ~ in path values [0.19ms]
(pass) loadConfig — per-repo merge > populates per-repo sections alongside global [0.40ms]
(pass) loadConfig — per-repo merge > per-repo values override global on a colliding key [0.31ms]
(pass) loadConfig — missing files > missing global file falls back to documented defaults without throwing [0.14ms]
(pass) loadConfig — missing files > missing per-repo file leaves per-repo sections undefined [0.22ms]
(pass) loadConfig — missing files > no paths at all yields an all-defaults config [0.17ms]
(pass) loadConfig — committed policy layer > reads policy.toml as the sibling of repoPath, merged with the local cache [0.29ms]
(pass) loadConfig — committed policy layer > a fresh clone with committed policy but no local cache still reads policy [0.26ms]
(pass) loadConfig — committed policy layer > local cache overrides committed policy on a colliding key [0.27ms]
(pass) loadConfig — committed policy layer > policy overrides the global file on a colliding key [0.31ms]
(pass) loadConfig — committed policy layer > an explicit repoPolicyPath overrides the sibling derivation [0.29ms]
(pass) loadConfig — committed policy layer > no repoPath means no policy is derived (global-only callers unaffected) [0.22ms]

packages/core/test/integration-rubric.test.ts:
(pass) parseAcceptanceCriteria > collects list items under the first acceptance heading, stops at next heading [0.05ms]
(pass) parseAcceptanceCriteria > returns [] when there is no acceptance section [0.01ms]
(pass) parseAcceptanceCriteria > only the first acceptance section counts — a later one does not reopen it [0.01ms]
(pass) isIntegrationCriterion > the spec's worked example is an integration criterion [0.01ms]
(pass) isIntegrationCriterion > 'unit tests pass' alone is not an integration criterion [0.01ms]
(pass) isIntegrationCriterion > wiring without a real-path test fails (behavior, not test) [0.02ms]
(pass) isIntegrationCriterion > a real-path test without wiring fails
(pass) isIntegrationCriterion > prose 'get' does not trip the uppercase HTTP-verb signal
(pass) isIntegrationCriterion > served + e2e qualifies
(pass) isIntegrationCriterion > plural 'integration tests' / 'smoke tests' phrasing still qualifies
(pass) detectExemption > reads an inline annotation and a comment form [0.01ms]
(pass) auditIssueBody > passes a body with an integration criterion [0.03ms]
(pass) auditIssueBody > flags a weak body and suggests a concrete rewrite naming the feature [0.03ms]
(pass) auditIssueBody > flags a body with no acceptance section, suggestion says so [0.04ms]
(pass) auditIssueBody > a declared exemption passes and surfaces the reason [0.02ms]

packages/core/test/hook-script.test.ts:
(pass) PR_READY_GATE_SH exit-code contract > HTTP 200 → exit 0 (allow) [2.36ms]
(pass) PR_READY_GATE_SH exit-code contract > curl failure emitting no http code → exit 0 (fails OPEN, not closed) [2.06ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 403 from a reachable dispatcher → exit 2 (blocks) [2.17ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 404 (no gate wired — e.g. a recommender/docs session) → exit 0 (allow, never wedge) [1.81ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 401 (reachable bad-token/missing-session) → exit 2 (surface, don't silently disable the guard) [2.13ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 500 (reachable dispatcher fault) → exit 2 (surface, not a silent allow) [2.06ms]

packages/core/test/select-adapter.test.ts:
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > an agent:<name> label pins that adapter over the default [1.24ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > whitespace around the label and name is tolerated [0.04ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > conflicting agent labels throw [0.09ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > duplicate agent labels for the same name are not a conflict [0.02ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > a label naming an unconfigured adapter throws [0.02ms]
(pass) selectAdapter — rule 2: default adapter > with no agent label, the default adapter is chosen [0.02ms]
(pass) selectAdapter — rule 2: default adapter > a default adapter that isn't configured throws [0.03ms]
(pass) selectAdapter — rule 3: switch away from a rate-limited adapter when portable > a rate-limited default switches to an available adapter for a portable task [0.03ms]
(pass) selectAdapter — rule 3: switch away from a rate-limited adapter when portable > a label pin is never switched away from, even when rate-limited and portable [0.02ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a rate-limited default with a non-portable task is left and marked skip [0.01ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a portable task with no non-rate-limited alternative is left and marked skip [0.01ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a non-rate-limited choice is never marked skip [0.02ms]

packages/core/test/tmux-tui.test.ts:
(pass) capturePane > returns the visible pane contents of a live session [155.35ms]
(pass) capturePane > returns null for an unknown session [1.26ms]
(pass) sendText and sendKeys > sendText writes literal text into the pane [158.32ms]
(pass) sendText and sendKeys > sendKeys with delayBetweenMs sends each key in its own call [222.28ms]
(pass) pollPaneFor > resolves with the predicate's value when the pane matches [312.94ms]
(pass) pollPaneFor > returns null on timeout when the pane never matches [414.05ms]
(pass) pollPaneFor > returns null and bails when the session disappears [1.51ms]
(pass) pollPaneFor > when `tag` is set, writes one stderr line per iteration [4.21ms]

packages/adapters/codex/test/adapter.test.ts:
(pass) codexAdapter identity > name is 'codex' and readyEvent is session.started [0.26ms]
(pass) buildLaunchCommand > argv launches interactive codex (no exec, no prompt) [0.17ms]
(pass) buildLaunchCommand > env sets CODEX_HOME to the worktree-local .codex so the config is loaded [0.13ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.13ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.13ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.12ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.12ms]
(pass) buildPromptText > recommender force-invokes the recommender skill with the @-referenced context [0.11ms]
(pass) buildPromptText > docs force-invokes the documenting-the-repo skill with the @-referenced context [0.10ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.10ms]
(pass) resolveTranscriptPath > returns transcript_path from the SessionStart payload [0.13ms]
(pass) resolveTranscriptPath > falls back to rollout_path when transcript_path is absent [0.10ms]
(pass) resolveTranscriptPath > throws when the payload carries no session-file path [0.12ms]
(pass) readTranscriptState > parses a real-shaped rollout: activity, turn count, last tool use, context tokens [0.31ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.24ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.41ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.32ms]
(pass) classifyStop > asked-question tolerates a malformed blocked.json (sentinel → null) [0.33ms]
(pass) classifyStop > structured rate_limits with rate_limit_reached_type → rate-limited, resetAt from resets_at [0.45ms]
(pass) classifyStop > structured rate_limits at/over 100% used → rate-limited even without reached_type [0.33ms]
(pass) classifyStop > a healthy structured block is authoritative → bare-stop, even with a stray '429' in text [0.34ms]
(pass) classifyStop > text fallback (no structured block): "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.33ms]
(pass) classifyStop > text fallback (no structured block): "Error 429: Too Many Requests" → rate-limited (429 status) [0.27ms]
(pass) classifyStop > text fallback (no structured block): "too many requests — slow down" → rate-limited (too many requests phrase) [0.31ms]
(pass) classifyStop > text fallback (no structured block): "ratelimit exceeded" → rate-limited (ratelimit no-space) [0.27ms]
(pass) classifyStop > a bare "line 4290 of the file" is NOT a rate-limit signal → bare-stop (4290 — a line number) [0.32ms]
(pass) classifyStop > a bare "commit 4291abcdef" is NOT a rate-limit signal → bare-stop (4291 in a hash) [0.28ms]
(pass) classifyStop > a bare "listening on port 14290" is NOT a rate-limit signal → bare-stop (embedded 4290) [0.26ms]
(pass) classifyStop > a bare "processed 42900 rows" is NOT a rate-limit signal → bare-stop (42900) [0.27ms]
(pass) classifyStop > done.json sentinel → done [0.32ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.76ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.46ms]
(pass) classifyStop > nothing notable → bare-stop [0.29ms]
(pass) detectRateLimit > structured block at the limit → detection with the real reset time [0.20ms]
(pass) detectRateLimit > text fallback matches a rate-limit signal when no structured block exists [0.16ms]
(pass) detectRateLimit > returns null when a healthy structured block is present [0.14ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present at all [0.14ms]
(pass) installHooks > writes .codex/config.toml with auto-mode + sandbox_mode (NOT the rejected 'sandbox' key) [2.85ms]
(pass) installHooks > pre-trusts the worktree directory so codex skips the directory-trust dialog [1.15ms]
(pass) installHooks > maps each real Codex event to the normalized taxonomy via the absolute hook path [1.10ms]
(pass) installHooks > registers exactly the real Codex event set (PascalCase, no fictional names) [1.12ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.06ms]
(pass) installHooks > registers the PR-ready gate as a SECOND PreToolUse matcher group scoped to Bash [0.96ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [0.96ms]
(pass) installHooks > symlinks the operator's auth.json into the worktree CODEX_HOME [1.03ms]
(pass) installHooks > does not throw or create a link when the operator has no auth.json [0.94ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.22ms]
(pass) detectNeedsLogin > does not match normal pane content [0.12ms]
(pass) detectHooksTrustPrompt > matches the real 'Hooks need review' dialog text [0.13ms]
(pass) detectHooksTrustPrompt > does not match a normal pane or the directory-trust dialog [0.12ms]
(pass) detectDirTrustPrompt > matches the real first-run directory-trust dialog text [0.13ms]
(pass) detectDirTrustPrompt > does not match a normal pane or the hooks-trust dialog [0.11ms]
(pass) detectReadyForInput > matches the live composer-ready welcome banner (codex 0.133.0) [0.13ms]
(pass) detectReadyForInput > does not match a boot dialog (so a dialog is answered before we treat it as ready) [0.11ms]
(pass) startsSessionOnFirstPrompt > codex sets the prompt-triggered-session flag (it fires no SessionStart until a prompt) [0.10ms]
(pass) enterAutoMode > returns immediately when the target session does not exist [1.98ms]

packages/adapters/claude/test/adapter.test.ts:
(pass) claudeAdapter identity > name is 'claude' and readyEvent is session.started [0.20ms]
(pass) claudeAdapter identity > does NOT set startsSessionOnFirstPrompt — Claude fires SessionStart at boot, so the dispatcher keeps await-first order (#183 regression) [0.12ms]
(pass) buildLaunchCommand > argv launches interactive claude in auto mode via --dangerously-skip-permissions [0.13ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.14ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.11ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.12ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.10ms]
(pass) buildPromptText > recommender force-invokes the recommender skill with the @-referenced context [0.11ms]
(pass) buildPromptText > docs force-invokes the documenting-the-repo skill with the @-referenced context [0.10ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.10ms]
(pass) resolveTranscriptPath > returns transcript_path from the SessionStart payload [0.10ms]
(pass) resolveTranscriptPath > throws when the payload has no transcript_path [0.11ms]
(pass) readTranscriptState > parses activity, turn count, last tool use, and context tokens [0.31ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.24ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.38ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.32ms]
(pass) classifyStop > an unrecognized kind falls back to a plain question (kind omitted) [0.42ms]
(pass) classifyStop > asked-question tolerates a malformed/contentless blocked.json (sentinel → null) [0.36ms]
(pass) classifyStop > usage-limit message in the transcript tail → rate-limited [0.33ms]
(pass) classifyStop > done.json sentinel → done [0.30ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.32ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.38ms]
(pass) classifyStop > nothing notable → bare-stop [0.28ms]
(pass) detectRateLimit > matches a usage-limit message in the transcript tail [0.14ms]
(pass) detectRateLimit > returns null when no usage-limit message is present [0.13ms]
(pass) installHooks > registers the full Claude hook event set in .claude/settings.json [1.02ms]
(pass) installHooks > each entry maps its Claude event to the normalized taxonomy via the absolute hook path [1.09ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [0.93ms]
(pass) installHooks > registers the PR-ready gate as a second Bash-matched PreToolUse hook [0.98ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.17ms]
(pass) detectBypassPrompt > matches representative bypass-mode confirmation strings [0.20ms]
(pass) detectBypassPrompt > does not match normal Claude pane content [0.25ms]
(pass) detectTrustPrompt > matches the first-run folder-trust dialog [0.24ms]
(pass) detectTrustPrompt > does not match the bypass dialog or normal content [0.25ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.24ms]
(pass) detectNeedsLogin > does not match the bypass prompt or normal pane content [0.11ms]
(pass) enterAutoMode > returns immediately when the target session does not exist [1.80ms]

packages/adapters/copilot/test/adapter.test.ts:
(pass) copilotAdapter identity > name is 'copilot' and readyEvent is session.started [0.19ms]
(pass) copilotAdapter identity > sets the prompt-triggered-session flag (fires no sessionStart until a prompt) [0.12ms]
(pass) buildLaunchCommand > argv launches interactive copilot in auto mode (no -p, no prompt) [0.13ms]
(pass) buildLaunchCommand > env sets COPILOT_HOME to the worktree-local .copilot so the config + hooks load [0.12ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.12ms]
(pass) buildLaunchCommand > forwards an exported gh token so token-auth keeps working under the repointed home [0.13ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.20ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.14ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.10ms]
(pass) buildPromptText > recommender / docs force-invoke their skill with the @-referenced context [0.11ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.12ms]
(pass) resolveTranscriptPath > derives <cwd>/.copilot/session-state/<sessionId>/events.jsonl from the payload [0.12ms]
(pass) resolveTranscriptPath > falls back to snake_case session_id defensively [0.10ms]
(pass) resolveTranscriptPath > throws when the payload carries no sessionId [0.13ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "../../../../etc/passwd" (defense-in-depth against path escape) [0.16ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "a/b" (defense-in-depth against path escape) [0.10ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId ".." (defense-in-depth against path escape) [0.09ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "id with spaces" (defense-in-depth against path escape) [0.09ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "id;rm -rf" (defense-in-depth against path escape) [0.09ms]
(pass) readTranscriptState > parses a real-shaped events.jsonl: activity, turn count, last tool use, context tokens [0.30ms]
(pass) readTranscriptState > counts each assistant.turn_end as a turn [0.18ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.19ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.41ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.33ms]
(pass) classifyStop > asked-question tolerates a malformed blocked.json (sentinel → null) [0.32ms]
(pass) classifyStop > rate-limit text "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.31ms]
(pass) classifyStop > rate-limit text "Error 429: Too Many Requests" → rate-limited (429 status) [0.38ms]
(pass) classifyStop > rate-limit text "too many requests — slow down" → rate-limited (too many requests phrase) [0.32ms]
(pass) classifyStop > rate-limit text "ratelimit exceeded" → rate-limited (ratelimit no-space) [0.25ms]
(pass) classifyStop > rate-limit text "weekly quota exceeded for this model" → rate-limited (quota exceeded) [0.27ms]
(pass) classifyStop > rate-limit text "You have reached your usage limit" → rate-limited (usage limit) [0.25ms]
(pass) classifyStop > a bare "line 4290 of the file" is NOT a rate-limit signal → bare-stop (4290 — a line number) [0.31ms]
(pass) classifyStop > a bare "commit 4291abcdef" is NOT a rate-limit signal → bare-stop (4291 in a hash) [0.28ms]
(pass) classifyStop > a bare "listening on port 14290" is NOT a rate-limit signal → bare-stop (embedded 4290) [0.29ms]
(pass) classifyStop > a bare "processed 42900 rows" is NOT a rate-limit signal → bare-stop (42900) [0.38ms]
(pass) classifyStop > done.json sentinel → done [0.50ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.34ms]
(pass) classifyStop > done.json outranks stale rate-limit text in the transcript → done [0.55ms]
(pass) classifyStop > failed.json outranks stale rate-limit text in the transcript → failed [0.43ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.43ms]
(pass) classifyStop > nothing notable → bare-stop [0.29ms]
(pass) detectRateLimit > text rate-limit signal → detection with unknown reset (no structured block on disk) [0.14ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present [0.13ms]
(pass) installHooks > writes .copilot/hooks/middle.json with version 1 and the camelCase event keys [1.13ms]
(pass) installHooks > maps each Copilot event to the normalized taxonomy via the absolute hook path [1.03ms]
(pass) installHooks > registers the PR-ready gate as a SECOND preToolUse hook scoped to the bash tool [1.05ms]
(pass) installHooks > pre-trusts the worktree in config.json so copilot skips the folder-trust dialog [2.60ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.17ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [0.95ms]
(pass) installHooks > writes NO auth file (copilot authenticates via gh, unlike codex) [0.88ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.21ms]
(pass) detectNeedsLogin > does not match normal pane content [0.11ms]
(pass) detectReadyForInput > matches the live composer-ready footer / prompt (copilot 1.0.54) [0.14ms]
(pass) detectReadyForInput > does not match a bare boot screen with no composer [0.12ms]
(pass) detectTrustPrompt > matches a folder-trust dialog (defense-in-depth; pre-empted by trustedFolders) [0.12ms]
(pass) detectTrustPrompt > does not match a normal pane [0.11ms]
(pass) enterAutoMode > throws fast when the target session does not exist (never treated as ready) [1.78ms]

packages/dispatcher/test/epic-store/file-state-gateway.test.ts:
(pass) fileStateGateway > readBody returns the state file contents verbatim [0.39ms]
(pass) fileStateGateway > readBody throws a clear error when the state file is absent [0.17ms]
(pass) fileStateGateway > writeBody creates the parent directory and round-trips [0.31ms]
(pass) fileStateGateway > writeBody is atomic: leaves no `.tmp` sibling after a successful write [0.40ms]
(pass) fileStateGateway > writeBody derives the temp sibling from the filename via `basename` (separator-safe) [0.27ms]
(pass) fileStateGateway > writeBody overwrites an existing file [0.20ms]

packages/dispatcher/test/epic-store/file-poll-gateway.test.ts:
(pass) filePollGateway > listIssueComments derives authorIsBot structurally from the marker kind [0.82ms]
(pass) filePollGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.23ms]
(pass) filePollGateway > findPrForEpic resolves a slug via meta.pr; delegates a numeric ref to gh's finder [0.39ms]
(pass) filePollGateway > findPrForEpic returns null for a slug whose Epic file has no stamped meta.pr [0.22ms]
(pass) filePollGateway > findEpicPrLifecycle resolves a slug via meta.pr; delegates a numeric ref to gh [0.30ms]
(pass) filePollGateway > findEpicPrLifecycle returns null for a slug with no stamped meta.pr [0.25ms]
(pass) filePollGateway > a numeric-named file Epic (e.g. 42.md) resolves via meta.pr, not gh's #42 finder (#200) [0.28ms]
(pass) filePollGateway > prSnapshot / prLifecycle delegate straight to gh by PR number [0.21ms]
(pass) filePollGateway > getRateLimit delegates straight to gh [0.18ms]

packages/dispatcher/test/epic-store/file-epic-gateway.test.ts:
(pass) fileEpicGateway > listOpenEpics scans the dir, derives sub-issue progress, skips closed [0.83ms]
(pass) fileEpicGateway > listIssueComments maps the conversation; answer is attributed to the human [0.49ms]
(pass) fileEpicGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.17ms]
(pass) fileEpicGateway > getCommentAuthor discriminates human (answer) from agent by the file:// fragment [0.18ms]
(pass) fileEpicGateway > getCommentAuthor delegates a github.com URL to gh [0.14ms]
(pass) fileEpicGateway > getIssueLabels reads the Epic meta labels [0.23ms]
(pass) fileEpicGateway > postComment appends a re-parseable dispatch-event block [0.46ms]
(pass) fileEpicGateway > postComment delegates a PR-number ref to gh (no Epic file for it) [0.19ms]
(pass) fileEpicGateway > findEpicPr returns null without a stamped pr, and delegates to gh when present [0.34ms]
(pass) fileEpicGateway > findEpicPr returns null when the Epic file is absent [0.13ms]
(pass) fileEpicGateway > addLabel appends to meta labels and is a no-op if already present [0.60ms]
(pass) fileEpicGateway > a present-but-malformed Epic file surfaces the parser's named error [0.23ms]
(pass) fileEpicGateway > postComment writes atomically — no `.tmp` sibling left behind [0.32ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > re-asking the identical open question is a no-op [0.48ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > a different question (or different kind/context) appends a new entry [0.89ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > round-trip purity survives the skip (renderer remains the sole marker writer) [0.35ms]

packages/dispatcher/test/epic-store/round-trip.test.ts:
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(empty-epic.md)) === empty-epic.md [0.07ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(all-closed.md)) === all-closed.md [0.12ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(codex-adapter.md)) === codex-adapter.md [0.06ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(mid-question.md)) === mid-question.md [0.08ms]

packages/dispatcher/test/epic-store/mode-commands-mirror.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-mirror-L3VcFj/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-L3VcFj/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) dispatch brief — mode-commands mirror (#195) > a file-mode dispatch mirrors file-mode-commands.md into the worktree, byte-identical [238.63ms]
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-mirror-c48tmO/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-c48tmO/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) dispatch brief — mode-commands mirror (#195) > a github-mode dispatch does not mirror the file-mode reference [288.22ms]

packages/dispatcher/test/epic-store/file-dispatch-integration.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fdisp-nyHKah/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fdisp-nyHKah/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) file-mode dispatch — Test A: real workflow drive > a file-mode Epic parks asking a question → row carries the slug, Epic file gains a question block [292.53ms]
(pass) file-mode dispatch — Test B: real buildImplementationDeps selector > postQuestion routes to the Epic file for a file repo, and to gh for a github repo [186.22ms]

packages/dispatcher/test/epic-store/parity.test.ts:
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-sYxFWH/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-sYxFWH/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] Stop received — classification=bare-stop
(pass) implementation parity — github mode > happy-path dispatch reaches completed [272.51ms]
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-DCqRmU/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-DCqRmU/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-6] Stop received — classification=asked-question
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-DCqRmU/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-DCqRmU/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-6] Stop received — classification=bare-stop
(pass) implementation parity — github mode > park → resume-answer → continuation reaches completed [306.93ms]
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-80mizQ/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-80mizQ/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=bare-stop
(pass) implementation parity — file mode > happy-path dispatch reaches completed [221.47ms]
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-YQaGIV/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-YQaGIV/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=asked-question
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-YQaGIV/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-YQaGIV/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=bare-stop
(pass) implementation parity — file mode > park → resume-answer → continuation reaches completed [304.54ms]

packages/dispatcher/test/epic-store/file-watcher-integration.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fw-XdLKme/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-XdLKme/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fw-XdLKme/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-XdLKme/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) file-watcher Q&A loop (#197) > poller cron detects a non-empty answer edit and resumes the parked Epic to completion [425.80ms]

packages/dispatcher/test/epic-store/watcher.test.ts:
(pass) collectChangedSince > includes files with mtime > sinceMs, excludes older + dotfiles/.tmp [0.34ms]
(pass) collectChangedSince > missing dir → empty [0.13ms]
(pass) pollFileSignals > emits an open question that has a non-empty answer [0.20ms]
(pass) pollFileSignals > an unanswered question (placeholder) does NOT trigger [0.24ms]
(pass) pollFileSignals > a resolved question does NOT trigger (only the first non-empty edit fires) [0.17ms]
(pass) pollFileSignals > the mtime gate skips unchanged files [0.14ms]
(pass) resolveQuestion > flips an open question to resolved (the dedup write); idempotent [0.30ms]
(pass) resolveQuestion > a missing file/question is a no-op [0.14ms]
(pass) runFileWatcherTick > fires the resume + resolves the question for an answered-question park [81.49ms]
(pass) runFileWatcherTick > does NOT resume a workflow parked on a non-answered signal (reason guard) [75.32ms]

packages/dispatcher/test/epic-store/selector.test.ts:
(pass) buildGitHubGateways / buildFileGateways > buildGitHubGateways defaults to the real gh-backed trio [0.05ms]
(pass) buildGitHubGateways / buildFileGateways > buildFileGateways returns file-backed implementations (not the gh trio) [0.23ms]
(pass) makeRoutingEpicGateway > routes per-repo: file repo → file backend, github repo → gh backend [70.97ms]
(pass) makeRoutingPollGateway > a file-mode slug never reaches gh's numeric PR-finders; github delegates [69.52ms]
(pass) appendQuestion > appends an open question block that re-parses; ids increment [0.60ms]
(pass) appendQuestion > throws a clear error when the Epic file is absent [0.21ms]

packages/dispatcher/test/epic-store/file-gateways-integration.test.ts:
(pass) file gateways — Phase-1 lifecycle integration > dispatch-event record, human answer on disk, poll surfaces the human reply [0.92ms]
(pass) file gateways — Phase-1 lifecycle integration > state gateway round-trips the recommender state file atomically [0.30ms]

packages/dispatcher/test/epic-store/file-review-resume-integration.test.ts:
(pass) file-mode PR-review resume (real poller path) > a CHANGES_REQUESTED review on the stamped PR resumes the parked file-mode Epic [89.61ms]
(pass) file-mode PR-review resume (real poller path) > no resume while the Epic file has no stamped meta.pr (PR not opened yet) [83.99ms]

packages/dispatcher/test/epic-store/parser.test.ts:
(pass) parseEpicFile — document structure > parses the document marker, title, and minimal meta from an empty Epic [1.17ms]
(pass) parseEpicFile — document structure > throws when the document marker is missing [0.09ms]
(pass) parseEpicFile — document structure > throws when the meta block has no slug key [0.03ms]
(pass) parseEpicFile — meta > parses every recognized meta key from codex-adapter fixture [0.11ms]
(pass) parseEpicFile — meta > parses closed=true [0.08ms]
(pass) parseEpicFile — acceptance criteria > parses unchecked criteria from codex-adapter [0.05ms]
(pass) parseEpicFile — acceptance criteria > parses checked criteria from all-closed [0.05ms]
(pass) parseEpicFile — sub-issues > parses sub-issues with stable IDs + body [0.04ms]
(pass) parseEpicFile — sub-issues > parses checked sub-issues + provenance suffix [0.05ms]
(pass) parseEpicFile — conversation > parses dispatch-event + question entries; empty answer block stays absent [0.12ms]
(pass) parseEpicFile — conversation > treats a non-empty answer block as the resolved reply [0.07ms]
(pass) parseEpicFile — conversation > empty conversation block yields empty conversation array [0.04ms]

packages/dispatcher/test/epic-store/file-auto-dispatch-integration.test.ts:
(pass) file-mode auto-dispatch (real readState path) > reads the state_file and enqueues a file Epic by its slug ref [73.92ms]
(pass) file-mode auto-dispatch (real readState path) > a github-mode repo still routes readState to the gh state issue gateway [67.14ms]

packages/dispatcher/test/gates/verify-config.test.ts:
(pass) parseVerifyConfig — valid > parses gates in declared order and applies the default timeout [0.09ms]
(pass) parseVerifyConfig — valid > carries an optional phases scope [0.04ms]
(pass) parseVerifyConfig — valid > category defaults to unit and accepts integration; integrationGates filters [0.05ms]
(pass) gatesForPhase — per-phase addressing > an unscoped gate runs for every phase [0.02ms]
(pass) gatesForPhase — per-phase addressing > a scoped gate runs only for its listed phases, preserving declared order [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: no gates [0.04ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: missing name
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty name
(pass) parseVerifyConfig — malformed fails loudly > rejects: missing command
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty command [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: duplicate name [0.04ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: non-positive timeout [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: non-int phases [0.05ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: negative phases [0.03ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty phases
(pass) parseVerifyConfig — malformed fails loudly > rejects: unknown key [0.06ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: invalid category [0.05ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: invalid toml [0.08ms]
(pass) parseVerifyConfig — malformed fails loudly > the unknown-key message lists the live key set (incl. `category`) [0.07ms]
(pass) loadVerifyConfig — file IO > loads a valid file from disk [0.35ms]
(pass) loadVerifyConfig — file IO > a missing file fails loudly with the path in the message [0.08ms]
(pass) loadVerifyConfig — file IO > verifyConfigPath resolves the worktree's .middle/verify.toml [0.02ms]

packages/dispatcher/test/gates/plan-comment.test.ts:
(pass) verifyPlanComment > passes when a comment by the agent's account contains the plan body [0.10ms]
(pass) verifyPlanComment > fails with the exact reason when no comment contains the plan body [0.05ms]
(pass) verifyPlanComment > fails when the plan body was posted by a different account [0.03ms]
(pass) verifyPlanComment > tolerates CRLF and trailing-whitespace differences between comment and plan [0.04ms]
(pass) verifyPlanComment > matches regardless of author when no agentLogin filter is supplied [0.03ms]
(pass) verifyPlanComment > an empty plan body never vacuously passes [0.03ms]

packages/dispatcher/test/gates/checkbox-revert.test.ts:
(pass) parseStatusCheckboxes > extracts one entry per Status line carrying a #N reference, stopping at the next heading [0.21ms]
(pass) parseStatusCheckboxes > returns [] when there is no Status section [0.02ms]
(pass) parseStatusCheckboxes > a lookalike heading (## Status notes) does not shadow the real ## Status [0.02ms]
(pass) parseStatusCheckboxes > only a level-2 ## Status heading starts the section (# / ### Status ignored) [0.01ms]
(pass) parseStatusCheckboxes > a ## Status / checkbox inside a fenced code block does not shadow the real section [0.05ms]
(pass) parseStatusCheckboxes > mixed fence delimiters: a ~~~ inside a ``` block does not reopen real parsing [0.02ms]
(pass) parseStatusCheckboxes > only the FIRST ## Status section is parsed; a later one is ignored [0.02ms]
(pass) reconcileCheckboxes > a passing [ ]→[x] transition is left checked, no comment, state recorded [0.29ms]
(pass) reconcileCheckboxes > a failing [ ]→[x] transition is reverted and a comment names the failed gate [0.18ms]
(pass) reconcileCheckboxes > a box already checked on the previous pass is not re-run [0.06ms]
(pass) reconcileCheckboxes > a revert touches only the Status section, not the same #N checkbox elsewhere [0.07ms]
(pass) reconcileCheckboxes > with several transitions, only the failing sub-issue is reverted [0.07ms]

packages/dispatcher/test/gates/pr-ready-handler.test.ts:
(pass) pr-ready gate handler > allows a non-`gh pr ready` command without touching GitHub [0.22ms]
(pass) pr-ready gate handler > allows when the Epic PR's criteria are all evidenced [0.14ms]
(pass) pr-ready gate handler > denies when the Epic PR has unevidenced criteria [0.09ms]
(pass) pr-ready gate handler > denies when no open Epic PR can be found [0.05ms]
(pass) pr-ready gate handler > denies when the session maps to no active workflow [0.04ms]

packages/dispatcher/test/gates/gate-runner.test.ts:
(pass) runGate > a passing gate captures stdout and exit 0 [0.94ms]
(pass) runGate > a failing gate captures the non-zero exit and stderr [0.62ms]
(pass) runGate > a gate that exceeds its timeout is killed and reported as timed out [700.71ms]
(pass) runGate > runs in the given cwd [1.96ms]
(pass) runGates > runs every gate in declared order; aggregate ok when all pass [1.26ms]
(pass) runGates > a failing gate makes the aggregate fail and names the first failure; later gates still run [1.43ms]
(pass) runGates > an empty gate list is a vacuous pass [0.06ms]

packages/dispatcher/test/gates/verify.test.ts:
(pass) verification gates wired into checkbox-revert (end to end) > a failing phase's box is reverted; a passing phase's box stays checked [9.68ms]
(pass) verification gates wired into checkbox-revert (end to end) > evidence is posted for both phases and a revert notice names the failed gate [3.21ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > an evidence-upsert failure yields ok:false (not a throw), preserving a real gate failure [1.29ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > a gate-runner failure (worktree gone) yields ok:false instead of throwing [0.45ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > reconcileCheckboxes still processes every transition + persists state when evidence fails [1.43ms]
(pass) verification gates wired into checkbox-revert (end to end) > re-running after a fix keeps the box checked and updates evidence in place [1.42ms]

packages/dispatcher/test/gates/pr-ready.test.ts:
(pass) parseAcceptanceCriteria > extracts the list items under the acceptance-criteria heading only [0.07ms]
(pass) parseAcceptanceCriteria > returns [] when there is no acceptance-criteria section [0.02ms]
(pass) commandIsPrReady > matches a bare and an argumented `gh pr ready` [0.02ms]
(pass) commandIsPrReady > does not match other gh commands
(pass) extractCommand > reads tool_input.command from a Claude/Codex PreToolUse payload [0.01ms]
(pass) extractCommand > parses Copilot's string-encoded toolArgs (else the gate never fires for copilot) [0.03ms]
(pass) extractCommand > accepts a tool_args object as a defensive snake_case variant
(pass) extractCommand > returns null on malformed toolArgs JSON rather than throwing [0.02ms]
(pass) extractCommand > returns null when there is no command [0.01ms]
(pass) evaluatePrReady > allows when every criterion carries an evidence link or a non-bot deferral [0.09ms]
(pass) evaluatePrReady > denies and names the criterion that has no evidence [0.08ms]
(pass) evaluatePrReady > a `#N` reference counts as an evidence link [0.04ms]
(pass) evaluatePrReady > a stakeholder-deferred criterion (non-bot comment) is allowed [0.05ms]
(pass) evaluatePrReady > a deferral pointing at a bot comment is denied [0.07ms]
(pass) evaluatePrReady > evidence still satisfies a criterion whose deferral is invalid (OR semantics) [0.07ms]
(pass) evaluatePrReady > two bot deferrals and no real evidence is denied (no second-annotation leak) [0.05ms]
(pass) evaluatePrReady > denies when there is no acceptance-criteria section (no bypass by deletion) [0.03ms]
(pass) evaluatePrReady — integration evidence > denies a unit-only PR: every criterion evidenced, none an integration test [0.04ms]
(pass) evaluatePrReady — integration evidence > allows when an integration criterion is evidenced by a named test file [0.04ms]
(pass) evaluatePrReady — integration evidence > a human-authored integration-exempt annotation allows [0.07ms]
(pass) evaluatePrReady — integration evidence > a bot-authored integration-exempt annotation is denied [0.06ms]
(pass) evaluatePrReady — integration evidence > an evidenced integration criterion allows even if a stray bot exemption is present [0.05ms]
(pass) evaluatePrReady — integration evidence > a deferred integration criterion does not count as integration evidence [0.05ms]

packages/dispatcher/test/gates/gate-evidence.test.ts:
(pass) renderEvidence > carries the per-phase marker so re-runs can find it [0.02ms]
(pass) renderEvidence > summarizes each gate's pass/fail in a table [0.04ms]
(pass) renderEvidence > puts full gate output inside collapsed <details> blocks [0.01ms]
(pass) renderEvidence > fences output that itself contains backticks without breaking the block [0.03ms]
(pass) upsertEvidenceComment > posts a fresh comment when none exists for the phase [0.16ms]
(pass) upsertEvidenceComment > re-runs update the same comment in place rather than posting a duplicate [0.18ms]
(pass) upsertEvidenceComment > a different phase's evidence gets its own comment [0.09ms]

packages/dispatcher/test/gates/checkbox-revert-pass.test.ts:
(pass) runCheckboxRevertPass > reverts a failing-gate checkbox after a push: body, comment, persisted state [84.41ms]
(pass) runCheckboxRevertPass > a passing-gate checkbox stays checked; SHA + state persisted [76.56ms]
(pass) runCheckboxRevertPass > head-SHA gate: an unchanged SHA skips a would-be transition entirely [75.54ms]
(pass) runCheckboxRevertPass > an advanced SHA re-processes: the new transition's gate runs and reverts [83.35ms]
(pass) runCheckboxRevertPass > undefined gateway SHA falls through to the reconciler's checkbox-state diff [79.54ms]
(pass) runCheckboxRevertPass > no usable verify.toml → the workflow is skipped (nothing to enforce) [70.50ms]
[checkbox-revert] GitHub budget low (10 < 100); skipping pass — resets 1970-01-01T00:00:00.000Z
(pass) runCheckboxRevertPass > rate-limit ceiling skips the whole pass before any GitHub call [71.55ms]
[checkbox-revert] pass failed for workflow bad (o/r#1): GitHub down
(pass) runCheckboxRevertPass > a per-workflow failure is isolated — other workflows still process [86.21ms]
(pass) runCheckboxRevertPass > a parked (non-running) workflow is not processed [72.33ms]

 1411 pass
 0 fail
 3550 expect() calls
Ran 1411 tests across 127 files. [86.44s]

The cron awaited each repo's recommender run sequentially, so a hung run (gh
blocked on auth, a long state-issue write) stalled every later repo until its
timeout fired. Fire the per-repo runs concurrently instead:

- Phase 1 stamps last_recommender_run for every due repo synchronously (before
  any await), preserving the overlapping-tick double-dispatch guard under
  concurrency; Phase 2 fans them out behind a bounded pool (maxConcurrentRepos,
  default 4), each under a hard per-repo timeout (runTimeoutMs, default 60s).
- A hang/timeout/throw on one repo is isolated: its stamp rolls back (retries
  next tick) and the others complete unaffected.
- New global [recommender] config: max_concurrent_repos, run_timeout_seconds.

Integration test: 3 repos (A 100ms, B hangs 5s, C 200ms) — A+C succeed, B is
marked failed-by-timeout, the pass finishes <2s (B never blocks A/C); plus a
bounded-concurrency test asserting maxInFlight never exceeds the cap.
@thejustinwalsh

thejustinwalsh commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Verification gates — phase #227

All 4 verification gate(s) passed for phase #227.

Gate Result Duration
format ✅ pass 0.3s
lint ✅ pass 0.1s
typecheck ✅ pass 2.0s
test ✅ pass 86.4s
format — ✅ pass (0.3s)
$ bun run format
Finished in 168ms on 337 files using 24 threads.

[stderr]
$ oxfmt

lint — ✅ pass (0.1s)
$ bun run lint
Found 0 warnings and 0 errors.
Finished in 35ms on 303 files with 95 rules using 24 threads.

[stderr]
$ oxlint --fix --deny-warnings

typecheck — ✅ pass (2.0s)
[stderr]
$ tsc --noEmit

test — ✅ pass (86.4s)
$ bun test
bun test v1.3.14 (0d9b296a)

[stderr]

packages/docs/test/resolve.test.ts:
(pass) resolveDocsTarget — detection > detects Starlight from astro.config + @astrojs/starlight [0.35ms]
(pass) resolveDocsTarget — detection > Starlight wins over co-resident TypeDoc [0.05ms]
(pass) resolveDocsTarget — detection > detects Docusaurus from docusaurus.config.js [0.04ms]
(pass) resolveDocsTarget — detection > detects MkDocs and reads a custom docs_dir [0.08ms]
(pass) resolveDocsTarget — detection > detects MkDocs with the default docs_dir [0.06ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from typedoc.json and reads out [0.07ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from a package.json typedoc key [0.05ms]
(pass) resolveDocsTarget — markdown fallback > falls back to markdown in docs/ when nothing is detected [0.06ms]
(pass) resolveDocsTarget — markdown fallback > a bare Astro site (no Starlight signal) does not match Starlight [0.14ms]
(pass) resolveDocsTarget — markdown fallback > resolves to markdown on a nonexistent path [0.18ms]
(pass) resolveDocsTarget — config override > tool override forces the framework, ignoring detection [0.08ms]
(pass) resolveDocsTarget — config override > tool override beats a detected framework [0.01ms]
(pass) resolveDocsTarget — config override > tool + path override sets both framework and root [0.02ms]
(pass) resolveDocsTarget — config override > path override alone overrides a detected target's root [0.05ms]
(pass) resolveDocsTarget — config override > path override alone overrides the fallback root [0.03ms]
(pass) resolveDocsTarget — config override > an unknown tool override throws with the valid names [0.05ms]
(pass) resolveOutputPath — slug normalization > strips a leading slash and an existing .md/.mdx extension [0.05ms]
(pass) DOCS_TARGET_NAMES > lists every resolvable target [0.02ms]

packages/docs/test/util.test.ts:
(pass) makeTarget.resolveOutputPath — path safety > nested slugs route into subfolders (preserved behavior) [0.02ms]
(pass) makeTarget.resolveOutputPath — path safety > leading slashes are stripped, never absolute [0.01ms]
(pass) makeTarget.resolveOutputPath — path safety > an .md/.mdx extension on the slug is not doubled
(pass) makeTarget.resolveOutputPath — path safety > traversal segments cannot escape docsRoot [0.02ms]
(pass) makeTarget.resolveOutputPath — path safety > interior traversal segments are dropped too
(pass) makeTarget.resolveOutputPath — path safety > backslashes are normalized to POSIX separators
(pass) makeTarget.resolveOutputPath — path safety > an empty docsRoot stays repo-relative (no leading slash) [0.02ms]
(pass) readJsonIfExists — contract > a JSON object is returned as a Record [0.07ms]
(pass) readJsonIfExists — contract > a JSON array is rejected (not a Record<string, unknown>) [0.12ms]
(pass) readJsonIfExists — contract > a JSON scalar is rejected [0.04ms]

packages/dashboard/test/guard.test.ts:
(pass) makeGuard > surfaces a rejection as an error keyed by source [0.16ms]
(pass) makeGuard > a non-Error rejection is stringified [0.05ms]
(pass) makeGuard > success clears only its own source's error, never another source's [0.08ms]
(pass) makeGuard > REGRESSION: a nested same-source guard masks the inner failure [0.06ms]
(pass) makeGuard > FIX: awaiting raw work inside one guard surfaces the failure [0.05ms]

packages/dashboard/test/server.test.ts:
(pass) createDashboardRoutes maps /api/* and /events/* to the deps seam [76.99ms]

packages/dashboard/test/runs-deps.test.ts:
(pass) createDbDeps.listRuns > returns only non-implementation kinds, newest-first within kind [80.89ms]
(pass) createDbDeps.listRuns > projects duration, active, transcript, and session fallback [72.95ms]
(pass) createDbDeps.listRuns > outputLink: recommender → state issue, documentation → PR, else null [82.43ms]
(pass) createDbDeps.listRuns > caps at 20 per kind [166.19ms]

packages/dashboard/test/epics-api.test.ts:
(pass) /api/epics > GET /api/epics/:repo returns the card list [0.35ms]
(pass) /api/epics > POST /api/epics/:repo/:n/dispatch forwards adapter + status/body [0.19ms]
(pass) /api/epics > dispatch 404s when no dispatch seam is wired [0.08ms]
(pass) /api/epics > dispatch rejects a missing adapter with 400 [0.07ms]
(pass) /api/epics > POST /api/epics/:repo/refresh forwards [0.06ms]

packages/dashboard/test/queue.test.tsx:
(pass) Queue shows an empty state with no data [3.44ms]
(pass) Queue renders nothing-in-flight row when live is empty [0.99ms]
(pass) Queue renders gauge tile labels and values from totals [0.74ms]
(pass) Queue renders epic as #N for a numeric epic and — for null [0.49ms]
(pass) Queue state cell carries the s-running class [0.29ms]
(pass) Queue renders rate-limit chip with adapter name, status, and chip class [0.26ms]
(pass) Queue sorts waiting-human rows before running rows [0.25ms]

packages/dashboard/test/epic-ref.test.tsx:
(pass) EpicRef > github mode renders plain `#N` text, no anchor (AC4: no behavior change) [0.20ms]
(pass) EpicRef > github mode renders `#N` even if a backfilled epic_ref is also present [0.08ms]
(pass) EpicRef > file mode renders the slug as a file:// link to the Epic file, no GitHub link [0.20ms]
(pass) EpicRef > no-Epic (both null) renders the caller's fallback [0.09ms]
(pass) EpicRef > a blank epicRef (empty / whitespace) falls through to the fallback, not an empty link [0.07ms]
(pass) EpicRef > a slug with surrounding whitespace is trimmed in both label and href [0.05ms]
(pass) EpicRef > a slug with URL-unsafe / traversal chars is encoded into one safe path segment [0.01ms]
(pass) RunnerRow Epic rendering > file-mode runner shows the slug file:// link [0.60ms]
(pass) RunnerRow Epic rendering > github-mode runner is unchanged (`#7`, no link) [0.21ms]
(pass) RunnerRow Epic rendering > no-Epic runner keeps the `#—` fallback [0.20ms]
(pass) Inspector Epic rendering > file-mode panel shows the slug file:// link in the header [0.51ms]
(pass) Inspector Epic rendering > github-mode panel is unchanged (`#7`, no link) [0.27ms]

packages/dashboard/test/sse.test.ts:
(pass) dashboard SSE channels > GET /events/global delivers a broadcast on the global channel [70.90ms]
(pass) dashboard SSE channels > GET /events/repos/:repo delivers only that repo's events [67.00ms]
(pass) dashboard SSE channels > GET /events/sessions/:session delivers session timeline frames [66.05ms]
(pass) dashboard SSE channels > a rate-limit detection pushes a fresh banner on the global channel (the ≤2s path) [72.80ms]
(pass) dashboard SSE channels > a workflow transition pushes a `workflow` nudge on that repo's channel [81.76ms]
(pass) dashboard SSE channels > a file-mode transition pushes the epic_ref slug alongside a null epic [76.99ms]
(pass) dashboard SSE channels > disposing the workflow bridge stops the repo-channel nudges [80.13ms]
(pass) dashboard SSE channels > a malformed percent-encoded channel segment is a 400, not a crash [65.79ms]
(pass) dashboard SSE channels > the /events/* routes 503 when no bus is wired [75.65ms]
(pass) DashboardEventBus channel pruning > drained (zero-subscriber) channels are swept out on the next serve [76.17ms]

packages/dashboard/test/activity.test.tsx:
(pass) Activity > renders Recommender and Documentation sections [0.90ms]
(pass) Activity > shows an output link when present and omits it otherwise [0.29ms]
(pass) Activity > empty state per section when no runs of that kind [0.13ms]
(pass) Activity > renders a state label for each run [0.12ms]
(pass) Activity > state pill tone: completed is ok, compensated/failed are bad [0.32ms]

packages/dashboard/test/epics-deps.test.ts:
(pass) createDbDeps.listEpics > joins cache progress + state-issue decision/recommendation + free slots [71.59ms]
(pass) createDbDeps.listEpics > an in-flight workflow surfaces as the runner and flips inFlight [79.61ms]
(pass) createDbDeps.listEpics > a blocked Epic with no needs-human entry gets a blocked decision callout [75.63ms]
(pass) createDbDeps.listEpics > surfaces a file-mode Epic (slug ref, null number) and resolves its runner by ref (#200) [79.43ms]
(pass) createDbDeps.listEpics > dispatchEpic + refreshEpics delegate to the injected callbacks [62.72ms]

packages/dashboard/test/control-client.test.ts:
(pass) fetchControlMetrics parses the /control/metrics snapshot [0.25ms]
(pass) fetchControlMetrics throws on a non-OK response [0.11ms]

packages/dashboard/test/api.test.ts:
(pass) dashboard JSON API > GET /api/repos returns a JSON array of repo summaries [79.69ms]
(pass) dashboard JSON API > GET /api/repos/:repo returns NEXT UP + IN FLIGHT for a known repo [76.23ms]
(pass) dashboard JSON API > github-mode IN FLIGHT row carries epicRef alongside the numeric epic (#187) [72.30ms]
(pass) dashboard JSON API > file-mode IN FLIGHT row surfaces epic_ref as epicRef with a null epic (#187) [70.21ms]
(pass) dashboard JSON API > GET /api/sessions/:session carries epicRef for a file-mode runner (#187) [72.33ms]
(pass) dashboard JSON API > GET /api/repos/:repo 404s an unknown repo [66.18ms]
(pass) dashboard JSON API > GET /api/banner reports per-adapter rate limits (UNKNOWN unobserved) [65.08ms]
(pass) dashboard JSON API > GET /api/sessions/:session returns the Inspector runner panel with attach commands [77.65ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach control flips controlled_by and spawns a terminal [87.65ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach rejects an invalid mode [73.46ms]
(pass) dashboard JSON API > POST /api/rate-limits/:adapter/clear sets the adapter AVAILABLE [71.65ms]
(pass) dashboard JSON API > GET /api/sessions/:session/events validates the limit param [76.66ms]
(pass) dashboard JSON API > POST /api/repos/:repo/pause validates untilMs [75.56ms]
(pass) dashboard JSON API > a runner with no session_name is reachable by its workflow id [76.35ms]
(pass) dashboard JSON API > a malformed percent-encoded path segment is a 400, not a 500 [68.24ms]
(pass) dashboard JSON API > unknown /api routes 404 as JSON [68.04ms]

packages/dashboard/test/window.test.ts:
(pass) dashboard window launcher > missing URL argument is a usage error (exit 2) [8.62ms]
(pass) dashboard window launcher > an unavailable webview-bun degrades to a logged exit 0 (HTTP still serves) [8.27ms]

packages/dashboard/test/runs-api.test.ts:
(pass) /api/runs > GET /api/runs returns the run list [0.17ms]
(pass) /api/runs > a non-GET method on /api/runs is a 404 miss [0.06ms]

packages/dashboard/test/epics.test.tsx:
(pass) Epics > renders an Epic card with title, progress, and an enabled dispatch button [0.99ms]
(pass) Epics > empty state when there are no Epics [0.10ms]
(pass) Epics > a file-mode Epic renders a file:// slug link and disables in-dashboard dispatch (#200) [0.25ms]
(pass) Epics > disables dispatch when in flight [0.22ms]
(pass) Epics > disables dispatch when the chosen adapter has no free slot [0.20ms]
(pass) Epics > shows a decision callout when present [0.46ms]
(pass) Epics > renders the decision link as an anchor when present [0.32ms]

packages/dashboard/test/app.test.tsx:
(pass) App nav includes a queue tab [0.94ms]
(pass) App nav includes an activity tab [0.40ms]
(pass) api.runs reads runs from a live server [70.02ms]
(pass) App defaults to the Epics view (nav tab + empty state render) [0.47ms]
(pass) api.epics reads Epic cards from a live server [82.98ms]
(pass) applyWorkflowFrame upserts non-terminal and drops terminal workflows [0.19ms]
(pass) dashboard views (static render) > GlobalBanner shows per-adapter rate limits + GitHub quota [0.44ms]
(pass) dashboard views (static render) > NeedsYou lists aggregated items and an empty state [0.35ms]
(pass) dashboard views (static render) > RepoRow expansion shows slot pills, NEXT UP, IN FLIGHT, and an accurate attach command [0.53ms]
(pass) dashboard views (static render) > Inspector renders the per-runner panel, links, affordances, and timeline [0.67ms]
(pass) api-client against a live server > api.repos() + RepoRow render the live repo [80.84ms]
(pass) api-client against a live server > api.attach(control) flips controlled_by; api.release reverts it [91.67ms]
(pass) api-client against a live server > api.runRecommender surfaces a non-2xx as an ApiError [78.91ms]

packages/dashboard/test/settings.test.tsx:
(pass) settings round-trip through the API > GET /api/settings returns global + per-repo config [75.08ms]
(pass) settings round-trip through the API > POST /api/settings/global persists and is reflected back [73.98ms]
(pass) settings round-trip through the API > POST /api/settings/global rejects a non-positive maxConcurrent [74.26ms]
(pass) settings round-trip through the API > pause/resume toggles a repo's auto-dispatch [84.79ms]
(pass) settings round-trip through the API > the rate-limit override button's endpoint sets the adapter AVAILABLE [82.55ms]
(pass) Settings view (static render) > renders global fields, rate-limit override, and per-repo auto toggle [73.91ms]

packages/dashboard/test/spa.test.ts:
Bundled page in 23ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > GET / serves the bundled HTML shell [92.46ms]
Bundled page in 47ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the bundled entry script transpiles the TSX app [115.49ms]
Bundled page in 21ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the JSON API coexists with the SPA fallback on the same server [90.07ms]

packages/state-issue/test/validate.test.ts:
(pass) validate > passes a schema-conforming state [0.18ms]
(pass) validate > fails when a Ready row uses an unconfigured adapter [0.04ms]
(pass) validate > fails when an In-flight item uses an unconfigured adapter [0.02ms]
(pass) validate > accepts a non-numeric file-mode Epic slug as an In-flight ref (rule 4 scopes the numeric check to Ready epics and blocked blockers, not In-flight) [0.01ms]
(pass) validate > fails when generated is not ISO 8601
(pass) validate > fails when an epic reference is malformed [0.01ms]
(pass) validate > fails when a Ready row epic has no title [0.01ms]
(pass) validate > fails when a blocked issue-blocker reference is malformed [0.01ms]
(pass) validate > accepts a non-issue blocker in backticks [0.01ms]
(pass) validate > accepts a cross-repo blocker reference (#225)
(pass) validate > accepts a blocker annotated with a resolved title (#225) [0.04ms]
(pass) validate > accepts a blocker carrying a (stale blocker: <ref>) suffix (#225) [0.02ms]
(pass) validate > fails when a cross-repo blocker reference is malformed
(pass) validate > collects multiple errors [0.02ms]

packages/state-issue/test/fuzz.test.ts:
(pass) parser/renderer round-trip fuzz > renders, parses, and re-renders 10000 random valid states byte-identically [309.46ms]

packages/state-issue/test/schema-path.test.ts:
(pass) STATE_ISSUE_SCHEMA_PATH > is an absolute path ending in the canonical schema filename [0.03ms]
(pass) STATE_ISSUE_SCHEMA_PATH > points at the real schema shipped in the middle install (not a target repo) [0.05ms]

packages/state-issue/test/fixture.test.ts:
(pass) hand-crafted state-issue fixture > parseStateIssue succeeds [0.02ms]
(pass) hand-crafted state-issue fixture > validate returns pass [0.07ms]
(pass) hand-crafted state-issue fixture > round-trips byte-identically [0.03ms]
(pass) hand-crafted state-issue fixture > exercises all seven sections with non-empty content [0.08ms]

packages/state-issue/test/parser.test.ts:
(pass) renderStateIssue > renders an empty state in canonical form [0.03ms]
(pass) renderStateIssue > renders a fully-populated state with all section content [0.04ms]
(pass) parseStateIssue > parses the canonical empty body back to the original state [0.05ms]
(pass) parseStateIssue > parses a fully-populated body back to the original state [0.05ms]
(pass) parseStateIssue > round-trips a file-mode in-flight ref, including a non-kebab slug (#200) [0.07ms]
(pass) parseStateIssue > returns ParseError when the open marker is missing [0.09ms]
(pass) parseStateIssue > returns ParseError when the close marker is missing [0.06ms]
(pass) parseStateIssue > returns ParseError when a section is out of order [0.04ms]
(pass) parseStateIssue > ignores content outside the markers [0.03ms]
(pass) parseStateIssue > ignores dispatcher-tick markers between sections [0.03ms]
(pass) parseStateIssue > returns ParseError when the Ready table omits the documented empty-state row [0.03ms]
(pass) parseStateIssue > an In-flight section with no bullet reads as empty (lenient empty-state) [0.02ms]
(pass) parseStateIssue > returns ParseError when a Ready row rank is below 1 [0.03ms]
(pass) parseStateIssue > returns ParseError when a Ready row sub-issue count is below 1 [0.02ms]
(pass) round-trip > render(parse(render(state))) is byte-identical to render(state) [0.06ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Needs human input accepts "- _none_" (the #84 failure) [0.03ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Blocked accepts "- _none_" [0.01ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > Excluded accepts "- _none_" [0.01ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > In-flight accepts a "- _none_" variant and an empty section [0.02ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > a real item alongside no sentinel still parses strictly (no over-loosening) [0.03ms]
(pass) lenient empty-state sentinels (agent-produced placeholders) > a genuinely malformed item (not a sentinel) still fails [0.02ms]

packages/cli/test/bootstrap-gitignore.test.ts:
(pass) addMiddleIgnore > writes the glob form with policy/verify exceptions into a new file [0.47ms]
(pass) addMiddleIgnore > preserves existing unrelated entries [0.24ms]
(pass) addMiddleIgnore > is idempotent — a second call makes no change [0.19ms]
(pass) addMiddleIgnore > upgrades a legacy bare `.middle/` entry to the glob form [0.20ms]
(pass) removeMiddleIgnore > strips the whole block, leaving other entries [0.32ms]
(pass) removeMiddleIgnore > deletes the file when it empties [0.28ms]
(pass) removeMiddleIgnore > also clears a legacy bare `.middle/` line [0.25ms]
(pass) removeMiddleIgnore > no-op when there's nothing middle-owned to remove [0.17ms]
(pass) removeMiddleIgnore > no-op leaves a file without a trailing newline untouched [0.16ms]
(pass) removeMiddleIgnore > no file at all is a no-op [0.12ms]

packages/cli/test/config.test.ts:
(pass) mm config auto_dispatch > flips an existing toggle in place, preserving comments and other keys [1.36ms]
(pass) mm config auto_dispatch > inserts the key when the [recommender] section lacks it [0.38ms]
(pass) mm config auto_dispatch > appends the section when it does not exist [0.45ms]
(pass) mm config auto_dispatch > matches a header with a trailing comment in place (no duplicate section) [0.32ms]
(pass) mm config auto_dispatch > matches a header with whitespace inside the brackets (no duplicate section) [0.32ms]
(pass) mm config auto_dispatch > rejects an unknown key and an invalid value [0.18ms]
(pass) mm config auto_dispatch > errors when the config file is missing [0.14ms]

packages/cli/test/init-file-store.test.ts:
(pass) mm init --epic-store=file > writes the four scaffold files and makes zero gh calls [9.89ms]
(pass) mm init --epic-store=file > the README template snippet is a parseable v1 Epic body [9.47ms]
(pass) mm init --epic-store=file > calls the setEpicStore callback with file mode + default paths [7.16ms]
(pass) mm init --epic-store=file > a setEpicStore write failure is best-effort — init still succeeds [7.90ms]
(pass) mm init --epic-store=file > --dry-run writes nothing and makes no gh calls [0.35ms]
(pass) mm init — github mode is unchanged > default mode creates the state issue and writes no file-store scaffold [6.78ms]
(pass) mm init — github mode is unchanged > setEpicStore is called with github mode in the default path [11.84ms]

packages/cli/test/pause-resume.test.ts:
(pass) mm pause / mm resume > pause sets paused_until; resume clears it (keyed by the resolved slug) [94.13ms]
(pass) mm pause / mm resume > a slug-resolution failure returns exit 1, not an unhandled rejection [0.54ms]
(pass) mm pause / mm resume > a non-git path is rejected with exit 1 [0.42ms]

packages/cli/test/status.test.ts:
(pass) runStatus > prints a per-repo, per-state summary of recorded workflows [76.35ms]
(pass) runStatus > reports cleanly when the database does not exist yet [0.36ms]
(pass) runStatus > reports cleanly when the database has no workflows [63.40ms]
(pass) runStatus > exits non-zero when the config file is malformed [0.59ms]

packages/cli/test/bootstrap-hook.test.ts:
(pass) bootstrap hook.sh asset > is byte-identical to the canonical HOOK_SH constant [1.33ms]
(pass) bootstrap hook.sh asset > is a POSIX sh script that takes the event name and never blocks the agent [0.06ms]
(pass) bootstrap hook.sh asset > the committed asset is marked executable [0.03ms]

packages/cli/test/file-mode-smoke.test.ts:
(pass) file-mode CLI smoke (#194) > mm dispatch --epic <slug> lands a workflow row with epic_ref=<slug> (file mode selected) [81.94ms]

packages/cli/test/db-scripts.test.ts:
(pass) backup.sh + reset-db.sh round-trip > backup → reset → restore preserves the db and its rows [124.11ms]
(pass) safety guards > backup.sh fails when there is no database [3.12ms]
(pass) safety guards > reset-db.sh is a no-op (exit 0) when there is no database [2.77ms]
(pass) safety guards > reset-db.sh refuses while the dispatcher pidfile is live [76.59ms]
(pass) safety guards > --db points both scripts at a relocated database [107.19ms]
(pass) safety guards > restore creates missing parent dirs for a relocated db and config [125.28ms]
(pass) safety guards > restore refuses while the dispatcher pidfile is live [103.61ms]

packages/cli/test/doctor.test.ts:
(pass) runDoctor — happy path > returns 0 and prints every check when the toolchain is healthy [1472.54ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + existing epics dir → epics_dir pass, no state-issue row [1301.01ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + missing epics dir → epics_dir fail, no state-issue row [1010.97ms]
(pass) runDoctor — mode-aware Epic-store check > github mode (no config row) → state-issue row, no epics_dir row [997.42ms]
(pass) checkAdapterBinaries > null config (unparseable) → single warn, no throw [0.15ms]
(pass) checkAdapterBinaries > no enabled adapters → warn [0.05ms]
(pass) checkAdapterBinaries > reports a row per ENABLED adapter from the passed config — not a reloaded global one [0.10ms]
(pass) checkAdapterBinaries > enabled adapter with a missing binary → warn (never fail) [20.32ms]
(pass) formatAgo > renders sub-minute as seconds [0.06ms]
(pass) formatAgo > renders minutes, hours, and days at the boundaries [0.03ms]
(pass) formatAgo > clamps a future timestamp to 0s (never negative)
(pass) summarizeRetention > never-run → pass, reports counts [0.04ms]
(pass) summarizeRetention > clean last run → pass, reports the run [0.05ms]
(pass) summarizeRetention > failed last run → warn, surfaces FAILED [0.03ms]

packages/cli/test/run-recommender.test.ts:
(pass) runRecommender — local validation > rejects a path that is not a git repository [16.48ms]
(pass) runRecommender — thin client to the daemon > daemon already up: POSTs /trigger/recommender and returns 0 on 202 [7.43ms]
(pass) runRecommender — thin client to the daemon > daemon down: auto-starts it, waits for health, then triggers [6.46ms]
(pass) runRecommender — thin client to the daemon > relays a daemon rejection (non-202) as exit 1 [6.20ms]
(pass) runRecommender — thin client to the daemon > returns 1 when the daemon never becomes ready after an auto-start [57.44ms]
(pass) runRecommender — thin client to the daemon > returns 1 when the dispatcher is unreachable (the POST throws) [6.99ms]

packages/cli/test/state-issue-check.test.ts:
(pass) checkStateIssueRoundTrip > passes for the canonical conforming fixture [0.14ms]
(pass) checkStateIssueRoundTrip > fails when the body does not parse [0.05ms]
(pass) checkStateIssueRoundTrip > fails validate when a Ready row uses an unconfigured adapter [0.09ms]
(pass) checkStateIssue > passes against middle's own source tree [0.07ms]
(pass) checkStateIssue > returns a structured fail (never throws) when the fixture is unreadable [0.09ms]

packages/cli/test/daemon-entry.test.ts:
Bundled page in 28ms: packages/dashboard/src/index.html
(pass) dashboardHostExtras routes + the hook fetch fallback coexist on one port [36.14ms]
(pass) a dispatch POST reaches the host-context dispatch callback [6.28ms]
(pass) dispose clears the process-global rate-limit observer (no broadcast after teardown) [1.92ms]

packages/cli/test/issue-audit.test.ts:
(pass) isFeatureIssue > epics, docs and chore issues are out of scope [0.09ms]
(pass) auditIssues > filters to feature issues and applies the rubric [0.34ms]
(pass) runAuditIssues --issue mode > flags a weak issue, returns 1, and labels it when --label is set [0.50ms]
(pass) runAuditIssues --issue mode > a thrown fetch error is handled: returns 1 and logs, not an unhandled rejection [0.19ms]
(pass) runAuditIssues --issue mode > a label-application failure is surfaced (logged) but does not crash the command [0.17ms]
(pass) runAuditIssues --issue mode > a passing issue returns 0 and is never labelled [0.11ms]
(pass) runAuditIssues backlog mode > returns 1 when any feature issue fails; labels only failures [0.12ms]

packages/cli/test/init-register.test.ts:
(pass) mm init — managed-repo registration > registers the slug + resolved checkout path on a successful init [8.59ms]
(pass) mm init — managed-repo registration > does NOT register under --dry-run (no changes made) [0.32ms]
(pass) mm init — managed-repo registration > a registry write failure is best-effort — init still succeeds [6.45ms]

packages/cli/test/audit-issues-cli.test.ts:
(pass) mm audit-issues --body-file (real CLI) > flags a weak issue and suggests a concrete rewrite (exit 1) [148.37ms]
(pass) mm audit-issues --body-file (real CLI) > passes a well-formed issue carrying an integration criterion (exit 0) [149.37ms]
(pass) mm audit-issues --body-file (real CLI) > --json emits a machine-readable report [144.24ms]
(pass) mm audit-issues --body-file (real CLI) > rejects a non-positive-integer --issue with a clear error (exit 1) [743.35ms]

packages/cli/test/module-index.test.ts:
(pass) parseModuleIndexFrontmatter > accepts a well-formed frontmatter block [0.05ms]
(pass) parseModuleIndexFrontmatter > reads claude-md: true [0.03ms]
(pass) parseModuleIndexFrontmatter > tolerates a leading shebang before the block [0.04ms]
(pass) parseModuleIndexFrontmatter > rejects a file with no leading block comment [0.02ms]
(pass) parseModuleIndexFrontmatter > rejects a block missing @packageDocumentation [0.01ms]
(pass) parseModuleIndexFrontmatter > rejects a block missing the @module tag [0.01ms]
(pass) parseModuleIndexFrontmatter > rejects a missing required section [0.02ms]
(pass) parseModuleIndexFrontmatter > rejects a non-boolean claude-md value
(pass) claudeMdPathForIndex > maps a package's src/index.ts to the package root CLAUDE.md [0.01ms]
(pass) claudeMdPathForIndex > maps a nested module's index.ts to its own dir
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > flags claude-md: true with no CLAUDE.md [0.62ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > flags claude-md: false with a stray CLAUDE.md [0.42ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > passes when flag and presence agree, and skips bootstrap-assets [0.74ms]
(pass) checkModuleIndex — the real middle packages tree > every src/index.ts(x) carries valid, consistent frontmatter [0.58ms]
(pass) checkModuleIndex — the real middle packages tree > finds every package's index front door [0.41ms]

packages/cli/test/bootstrap-init.test.ts:
(pass) mm init — fresh install > stages skills, hooks, config, state issue, and gitignore [8.26ms]
(pass) mm init — fresh install > the created state-issue body parses and validates [6.79ms]
(pass) mm init — idempotent re-init > a matching-version re-init refreshes assets but keeps config and issue [11.87ms]
(pass) mm init — idempotent re-init > re-init does not clobber a team's committed policy edits (AC #103) [11.18ms]
(pass) mm init — idempotent re-init > a fresh clone (committed policy, no local cache) reconciles the issue and keeps policy [6.49ms]
(pass) mm init — idempotent re-init > loadConfig reads init's two files via sibling derivation and merges them [6.58ms]
(pass) mm init — dry run > writes nothing and makes no GitHub calls [0.32ms]
(pass) mm init — validation > rejects a dirty working tree [0.32ms]
(pass) mm init — validation > rejects a repo with no origin remote [0.29ms]
(pass) mm init — validation > fails fast on a malformed existing config instead of re-initializing fresh [0.45ms]
(pass) mm init — existing config without a usable state issue > a matching-version re-init with no issue number mints one and persists it [7.93ms]
(pass) mm init — reconciles the state issue against GitHub > a fresh local install reuses the repo's existing state issue instead of creating one [6.41ms]
(pass) mm init — reconciles the state issue against GitHub > warns and reuses the oldest when GitHub has duplicate state issues [8.00ms]
(pass) mm init — reconciles the state issue against GitHub > creates a state issue only when GitHub has none [6.96ms]
(pass) mm uninit > closes the issue and removes everything init staged [9.77ms]
(pass) mm uninit > closes the state issue even when [repo] metadata is missing (deps fallback) [0.53ms]
(pass) mm uninit > closes the state issue offline by reading [repo] from committed policy (#103) [0.54ms]
(pass) mm uninit > falls back to default_branch 'main' when committed policy has a non-string value (#103) [0.92ms]
(pass) mm uninit > dry run removes nothing [6.37ms]
(pass) mm uninit > strips only middle's hook entries, preserving foreign ones [8.92ms]

packages/cli/test/dispatch.test.ts:
(pass) runDispatch — input validation > rejects a malformed numeric epic (digit-leading but not a whole number) [15.74ms]
(pass) runDispatch — input validation > rejects an epic number below 1 [6.12ms]
(pass) runDispatch — input validation > rejects a path that is not a git repository [0.22ms]
(pass) runDispatch — control client > health already up: dispatches and exits 0 on completed, without spawning a daemon [108.42ms]
(pass) runDispatch — control client > a file-mode slug dispatches with epicRef and skips the gh label fetch [11.60ms]
(pass) runDispatch — control client > subscribes to /control/events BEFORE POSTing /control/dispatch [107.67ms]
(pass) runDispatch — control client > exits 0 when the workflow parks for review (waiting-human) [111.33ms]
(pass) runDispatch — control client > exits 1 when the workflow fails [108.34ms]
(pass) runDispatch — control client > reconnects when the event stream drops mid-flight and follows to completion [111.78ms]
(pass) runDispatch — control client > --adapter overrides the agent label and the default, and is sent to the daemon [11.42ms]
(pass) runDispatch — control client > an agent:<name> label on the Epic selects that adapter [10.65ms]
(pass) runDispatch — control client > no agent label falls back to the default adapter [10.55ms]
(pass) runDispatch — control client > a disabled adapter is rejected (exit 1), even via --adapter, before any dispatch [8.79ms]
(pass) runDispatch — control client > an unconfigured --adapter is rejected (exit 1) before any dispatch [11.44ms]
(pass) runDispatch — control client > friendly failure (exit 1) when the daemon can't be reached or started [531.83ms]

packages/cli/test/state-issue-body.test.ts:
(pass) buildInitialStateIssueBody > parses and validates against the schema (configured adapters) [0.14ms]
(pass) buildInitialStateIssueBody > is empty in every section [0.07ms]
(pass) buildInitialStateIssueBody > round-trips byte-identically (the keystone invariant) [0.04ms]
(pass) buildInitialStateIssueBody > carries the markers and the generated timestamp [0.02ms]
(pass) parseRepoSlug > parses git@github.com:acme/widget.git [0.09ms]
(pass) parseRepoSlug > parses https://github.com/acme/widget.git
(pass) parseRepoSlug > parses https://github.com/acme/widget
(pass) parseRepoSlug > parses ssh://git@github.com/acme/widget.git
(pass) parseRepoSlug > parses https://github.com/acme/widget/
(pass) parseRepoSlug > returns null for an unparseable URL [0.01ms]

packages/cli/test/start-stop.test.ts:
(pass) runStart / runStop lifecycle > start spawns a detached process and records its pid; stop kills it [301.54ms]
(pass) runStart / runStop lifecycle > start refuses when a live dispatcher is already recorded [101.12ms]
(pass) runStart / runStop lifecycle > start clears a stale pid file and launches fresh [0.63ms]
(pass) runStart / runStop lifecycle > stop exits non-zero when no dispatcher is running [0.21ms]
(pass) runStartCommand --window > opens the dashboard window once /health is ready [0.72ms]
(pass) runStartCommand --window > does not open the window when /health never becomes ready (but start still succeeds) [0.51ms]
(pass) runStartCommand --window > a throwing opener (or health probe) never fails the start — window step is best-effort [0.63ms]
(pass) runStartCommand --window > no --window and no windowed config → never opens, never polls health [0.45ms]

packages/cli/test/tsdoc-coverage.test.ts:
(pass) checkTsdocCoverage > counts a documented local export as documented [299.41ms]
(pass) checkTsdocCoverage > flags an undocumented local export [301.31ms]
(pass) checkTsdocCoverage > resolves a re-export to the original declaration's doc comment [269.53ms]
(pass) checkTsdocCoverage > a bare `export {}` module contributes no exports [317.66ms]
(pass) checkTsdocCoverage > analyzes the real middle tree without throwing [471.79ms]

packages/cli/test/init-collision.test.ts:
(pass) mm init — shared-checkout collision guard (#226) > a second init at the same path with a different slug exits non-zero and writes nothing [18.29ms]
(pass) mm init — shared-checkout collision guard (#226) > re-initializing the SAME slug at the same path is allowed (idempotent, no collision) [14.84ms]
(pass) mm init — shared-checkout collision guard (#226) > --dry-run skips the collision guard (it writes nothing anyway) [1.98ms]

packages/cli/test/docs.test.ts:
(pass) runDocs — input validation > rejects a path that is not a git repository [16.81ms]
(pass) runDocs — input validation > rejects an unknown [docs] tool override [6.69ms]
(pass) runDocs — enqueues a documentation run for the repo > resolves the markdown fallback target and dispatches a read-only run [8.05ms]
(pass) runDocs — enqueues a documentation run for the repo > a [docs] tool/path override flows through to the resolved target [9.30ms]
(pass) runDocs — enqueues a documentation run for the repo > returns 1 when the dispatched run does not complete [7.76ms]

packages/cli/test/bun-path.test.ts:
(pass) isDirOnPath > true when present [0.05ms]
(pass) isDirOnPath > false when absent [0.01ms]
(pass) isDirOnPath > tolerates trailing slashes on either side [0.01ms]
(pass) isDirOnPath > false on empty PATH
(pass) resolveShellRc > zsh (platform-independent) [0.04ms]
(pass) resolveShellRc > bash on macOS targets .bash_profile (login shells don't source .bashrc)
(pass) resolveShellRc > bash elsewhere targets .bashrc
(pass) resolveShellRc > unknown shell [0.05ms]
(pass) bunPathSnippet > HOME-relative form when dir is the canonical ~/.bun/bin [0.03ms]
(pass) bunPathSnippet > literal form when dir is non-canonical [0.01ms]
(pass) rcAlreadyConfigured > detects literal bin dir [0.01ms]
(pass) rcAlreadyConfigured > detects BUN_INSTALL form
(pass) rcAlreadyConfigured > false on unrelated rc
(pass) applyPathFix > appends once and is idempotent [0.25ms]
(pass) applyPathFix > creates content when the rc file is absent [0.16ms]

packages/cli/test/skills-sync.test.ts:
(pass) syncSkills > copies every canonical file into the mirror byte-for-byte [1.20ms]
(pass) syncSkills > a second sync is a no-op (inSync, no changes) [1.06ms]
(pass) syncSkills > removes stale files the canonical no longer has [1.02ms]
(pass) syncSkills > detects and removes an orphaned skill DIRECTORY present only in the mirror [1.17ms]
(pass) diffSkills / check mode > check mode reports drift without writing [0.53ms]
(pass) diffSkills / check mode > check mode reports in-sync once synced [0.97ms]
(pass) diffSkills / check mode > check mode catches a single-byte edit in the mirror [0.94ms]
(pass) default repo paths > the shipped canonical and mirror are in sync [0.88ms]
(pass) default repo paths > the shipped skill set includes the three bootstrapped skills [0.54ms]

packages/dispatcher/test/epic-143-demo.test.ts:
(pass) Epic #143 — integration-verified requirements + freshness > 1. the requirements auditor flags a deliberately weak issue [0.07ms]
(pass) Epic #143 — integration-verified requirements + freshness > 2. a unit-only feature cannot reach PR-ready [0.46ms]
[staleness] o/r#50 landed in merged PR #88 → closed
[staleness] o/r: filed reconcile task #900 for Phase 9
(pass) Epic #143 — integration-verified requirements + freshness > 3. reconciliation surfaces a landed-but-open issue and a drifted spec line [0.80ms]

packages/dispatcher/test/watchdog.test.ts:
(pass) watchdog — launch timeout > a launching workflow past the window is failed 'stuck-launching' [79.28ms]
(pass) watchdog — launch timeout > a launching workflow within the window is left alone [75.56ms]
(pass) watchdog — prompt not accepted > a running session that went ready but never started a turn is failed 'prompt-not-accepted' [81.95ms]
(pass) watchdog — prompt not accepted > a running session whose prompt landed (turn.started present) is not failed [76.04ms]
(pass) watchdog — prompt not accepted > a running session still within the launch window is not yet failed [74.70ms]
(pass) watchdog — tmux liveness > a running workflow with a dead session is failed + compensation triggered [77.52ms]
(pass) watchdog — tmux liveness > a running workflow with a live session is not failed for liveness [74.73ms]
[watchdog] status check failed for middle-14, skipping liveness this pass: tmux server not running
(pass) watchdog — tmux liveness > a status() error is inconclusive — liveness is skipped, fresh row not failed [70.84ms]
[watchdog] status check failed for middle-14, skipping liveness this pass: tmux server not running
(pass) watchdog — tmux liveness > a persistent status() error does NOT block rule 3 — a stale row still idle-times-out [80.67ms]
[watchdog] status check failed for middle-bad, skipping liveness this pass: tmux error
(pass) watchdog — tmux liveness > a status() error on one row does not abort reconciliation of others [87.25ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — tmux liveness > a killSession() error still records the failure decision [80.85ms]
(pass) watchdog — activity freshness > idle ≥ threshold marks one idle event but does not kill [76.13ms]
(pass) watchdog — activity freshness > idle ≥ kill-threshold kills the session and fails 'idle-timeout' [77.81ms]
(pass) watchdog — activity freshness > freshness is skipped while controlled_by = 'human' [71.62ms]
(pass) watchdog — activity freshness > a stale heartbeat is rescued by fresh transcript activity (cross-check) [72.13ms]
(pass) watchdog — sentinel re-arm > a blocked.json with no armed signal arms one, idempotently [75.42ms]
(pass) watchdog — sentinel re-arm > no sentinel file → no signal armed [73.20ms]
(pass) watchdog — blocked sentinel self-heal > idle ≥ kill-threshold with a blocked sentinel hands off to the drive, not compensation [85.40ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — blocked sentinel self-heal > a failed kill does not record the handoff — it retries next pass [71.56ms]
(pass) watchdog — blocked sentinel self-heal > the handoff is recorded once, not every idle tick [77.44ms]
(pass) reconcileTranscriptDrift > advances last_heartbeat when the transcript is newer than the recorded beat [75.13ms]
(pass) reconcileTranscriptDrift > leaves the heartbeat alone when the transcript is older [71.54ms]
(pass) notification failsafe — detect + capture + intervene > a notification still within the grace window is left alone [76.98ms]
(pass) notification failsafe — detect + capture + intervene > a notification past the grace window captures the pane, classifies, and nudges [85.89ms]
(pass) notification failsafe — detect + capture + intervene > classifies a plain 'waiting for input' notification as a question (kind=input) [83.95ms]
(pass) notification failsafe — detect + capture + intervene > an agent that resumed after the notification (newer activity) is left alone [78.21ms]
(pass) notification failsafe — detect + capture + intervene > a human-controlled session is never rescued (a human will answer) [75.50ms]
(pass) notification failsafe — detect + capture + intervene > no-op when the tmux surface lacks the failsafe methods [75.02ms]
(pass) notification failsafe — detect + capture + intervene > a capture-only notification (no message payload) still classifies + nudges [86.69ms]
(pass) notification failsafe — fast-fail backstop > still idle past the kill-grace → fast-fails with the captured kind and kills the session [98.40ms]
(pass) notification failsafe — fast-fail backstop > two captures sharing a ts → the latest-by-id kind wins (contract lock) [95.71ms]
(pass) notification failsafe — fast-fail backstop > within the kill-grace → not yet failed (the nudge still has time to take) [87.02ms]
(pass) notification failsafe — fast-fail backstop > a repeat notification with no activity does NOT reset the kill clock — still fast-fails [97.17ms]
(pass) notification failsafe — fast-fail backstop > a fresh notification AFTER genuine activity re-arms the failsafe (re-captures) [97.95ms]

packages/dispatcher/test/recommender-run.test.ts:
[recommender-run] workflow wf_1780555324446_tr1dsks7 enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > runs to completion and records a kind:'recommender' workflow row for the repo [380.98ms]
[recommender-run] workflow wf_1780555324823_uiz8e2fo enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > read-only by default: with no triggerAutoDispatch wired, a clean run dispatches nothing [375.29ms]
[recommender-run] workflow wf_1780555325199_hvvanw01 enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > fires triggerAutoDispatch on a clean run when wired and auto_dispatch is on (trigger #1) [374.44ms]
[recommender-run] workflow wf_1780555325573_m1uttsak enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > forwards epicStore so a file-mode run frames the prompt for the file store (#200) [374.95ms]
[recommender-run] workflow wf_1780555325951_34au3s38 enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > does not fire triggerAutoDispatch when auto_dispatch is off, even if wired [384.68ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [7.39ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > file mode resolves without a state issue — sentinel 0 + epicStore carried (#200) [7.19ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > github mode still requires a configured state issue number [5.99ms]
(pass) resolveRecommenderOptions — schema resolution (issue #107) > resolves schemaPath from the middle install, not from repoPath [7.22ms]

packages/dispatcher/test/state-issue.test.ts:
(pass) applyDispatcherSections > replaces only the three owned sections, keeps the rest [0.04ms]
(pass) updateDispatcherSections > recommender-owned sections come back byte-identical [0.40ms]
(pass) updateDispatcherSections > the owned sections actually changed [0.13ms]
(pass) updateDispatcherSections > a partial patch leaves the unspecified owned sections intact [0.11ms]
(pass) updateDispatcherSections > a dispatcher-tick marker is ignored by the parser and preserves sections [0.22ms]
(pass) updateDispatcherSections > ticks do not accumulate across repeated updates [0.16ms]
(pass) readState > parses a valid body [0.11ms]
(pass) readState > throws on a malformed body [0.09ms]
(pass) insertDispatcherTick > leaves a non-canonical body untouched [0.02ms]

packages/dispatcher/test/stop-wait.test.ts:
(pass) awaitStopOrSessionEnd > resolves via 'stop' when the Stop hook arrives first [6.45ms]
(pass) awaitStopOrSessionEnd > resolves via 'session-ended' when liveness goes false while Stop is pending [10.21ms]
(pass) awaitStopOrSessionEnd > resolves via 'timeout' when the Stop wait rejects and the session stays alive [6.31ms]
(pass) awaitStopOrSessionEnd > without a liveness probe, a rejected Stop wait surfaces as 'timeout' [5.14ms]
(pass) awaitStopOrSessionEnd > liveness-probe errors are ignored — a later Stop still wins [20.15ms]

packages/dispatcher/test/build-deps.test.ts:
(pass) buildImplementationDeps > wires deps from the injected collaborators and returns the gate it built [65.38ms]
(pass) buildImplementationDeps > epicPrReadiness reports a missing PR as { exists: false, isDraft: false } [63.29ms]
(pass) buildImplementationDeps > the factory module imports no engine (no bunqueue construction) [2.07ms]
(pass) buildImplementationDeps > the default postQuestion posts a gh comment framed by pause kind [65.77ms]
(pass) buildImplementationDeps > the default postQuestion is idempotent on a repeated identical question (#205) [69.66ms]
(pass) postQuestionComment (idempotent pause poster, #205) > skips when the latest agent-comment already has the identical body [0.33ms]
(pass) postQuestionComment (idempotent pause poster, #205) > a different body posts a fresh comment (questions are a history) [0.20ms]
(pass) postQuestionComment (idempotent pause poster, #205) > ignores non-agent comments — only the marker-prefixed latest counts [0.22ms]
(pass) formatPauseComment > a complexity pause carries the `complexity pause` label vocabulary [0.13ms]
(pass) formatPauseComment > a plain question reads as an agent question, not a complexity pause [0.13ms]
(pass) formatPauseComment > both kinds start with the hidden agent-comment marker so the poller skips them (#178) [0.13ms]

packages/dispatcher/test/staleness.test.ts:
(pass) detectSpecDrift > flags future-phase lines whose phase has merged [0.06ms]
(pass) detectSpecDrift > does not flag a future phase that has not merged [0.02ms]
(pass) detectSpecDrift > matches the verb-less 'planned for phase N' phrasing [0.03ms]
[staleness] o/r#50 landed in merged PR #88 → closed
[staleness] o/r: filed reconcile task #1001 for Phase 9
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > closes a landed-but-open issue and files a drift task for its phase [0.26ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > does not close an issue no merged PR references, and dedupes an existing reconcile task [0.09ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > maxPerPass caps the TOTAL of closes + filed tasks, not each bucket [0.08ms]
[staleness] o/r#50 landed in merged PR #88 → closed
(pass) reconcileStaleness (integration — real pass, in-memory gateway) > no spec → still reconciles landed issues, no drift [0.05ms]

packages/dispatcher/test/hook-store.test.ts:
(pass) DbHookStore — resolveSessionToken > returns the token of the active workflow owning the session [79.60ms]
(pass) DbHookStore — resolveSessionToken > returns null for an unknown session [67.67ms]
(pass) DbHookStore — resolveSessionToken > ignores terminal workflows that previously held the deterministic session name [89.84ms]
(pass) DbHookStore — record > writes an events row for every hook [85.54ms]
(pass) DbHookStore — record > tool.pre and tool.post advance last_heartbeat [98.37ms]
(pass) DbHookStore — record > a non-tool event records but does not advance last_heartbeat [91.34ms]
(pass) DbHookStore — record > session.started writes session_id + transcript_path onto the workflow [85.82ms]
[hook-store] dropping tool.pre: no active workflow for session middle-GHOST
(pass) DbHookStore — record > an unmatchable session is dropped, not crashed on, and writes nothing [76.62ms]
(pass) DbHookStore — record > oversized payloads are truncated before storage [80.38ms]
[hook-server] received tool.post:middle-14
(pass) HookServer wired to DbHookStore — end to end into SQLite > an authenticated POST flows through the server into the events table + heartbeat [85.55ms]
(pass) serializePayload > returns compact JSON for a small payload [64.87ms]
(pass) serializePayload > clips and marks a payload over 16KB [65.44ms]

packages/dispatcher/test/event-hub.test.ts:
(pass) EventHub > serve emits a `connected` frame first, with SSE content-type [0.52ms]
(pass) EventHub > serve replays caller-supplied init events after `connected` [0.16ms]
(pass) EventHub > a broadcast reaches a live subscriber [0.13ms]
(pass) EventHub > a heartbeat keeps the stream alive (injectable interval) [22.05ms]
(pass) EventHub > an aborted client is unsubscribed cleanly [11.52ms]
(pass) EventHub > a slow consumer that overflows its buffer is dropped without throwing [0.36ms]

packages/dispatcher/test/notification-classify.test.ts:
(pass) classifyNotification — permission blocks > message "Claude needs your permission to use Bash" → permission [0.02ms]
(pass) classifyNotification — permission blocks > message "Claude needs permission to run a command" → permission
(pass) classifyNotification — permission blocks > message "This action requires your approval" → permission
(pass) classifyNotification — permission blocks > message "Claude wants to use the Edit tool" → permission
(pass) classifyNotification — permission blocks > message "Allow Claude to run `chmod +x`?" → permission
(pass) classifyNotification — permission blocks > pane "Do you want to proceed?" → permission even with a generic message [0.01ms]
(pass) classifyNotification — permission blocks > pane "Do you want to allow this?" → permission even with a generic message
(pass) classifyNotification — permission blocks > pane "❯ 1. Yes" → permission even with a generic message
(pass) classifyNotification — permission blocks > pane "❯ 2. Allow" → permission even with a generic message
(pass) classifyNotification — permission blocks > permission outranks an input-shaped message when the pane shows a dialog [0.01ms]
(pass) classifyNotification — input (genuine question) > message "Claude is waiting for your input" → input [0.01ms]
(pass) classifyNotification — input (genuine question) > message "Waiting for input" → input
(pass) classifyNotification — input (genuine question) > message "Claude needs your input to continue" → input
(pass) classifyNotification — input (genuine question) > message "Awaiting your input" → input
(pass) classifyNotification — idle/unknown > unattributable message "" → idle-unknown
(pass) classifyNotification — idle/unknown > unattributable message "Some unrelated notification" → idle-unknown
(pass) classifyNotification — idle/unknown > unattributable message "Task finished" → idle-unknown
(pass) classifyNotification — idle/unknown > a long whitespace-laden 'allow …' message classifies fast (no catastrophic backtracking) [0.09ms]
(pass) classifyNotification — idle/unknown > still matches a legitimate 'allow … to' permission request [0.01ms]
(pass) classifyNotification — idle/unknown > tolerates missing message/pane (undefined-safe)

packages/dispatcher/test/multi-repo-blockers.test.ts:
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > an open cross-repo blocker keeps the Epic blocked, annotated with Repo B's title [252.74ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > closing the cross-repo blocker moves the Epic to Ready within one tick [325.97ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > an unresolvable (404) blocker stays blocked with a (stale blocker: <ref>) suffix [253.18ms]
(pass) multi-repo cross-repo blocker resolution (#225) — through the workflow > the resolved state body round-trips byte-identically [249.45ms]

packages/dispatcher/test/poller-gateway.test.ts:
(pass) deriveCiStatus > no checks configured → none (nothing to gate on) [0.10ms]
(pass) deriveCiStatus > all check runs succeeded (incl. neutral/skipped) → passing [0.04ms]
(pass) deriveCiStatus > any failed/errored/cancelled/timed-out check → failing [0.04ms]
(pass) deriveCiStatus > an unfinished check run (not COMPLETED) → pending [0.01ms]
(pass) deriveCiStatus > a failure outranks a still-running check → failing
(pass) deriveCiStatus > legacy StatusContext entries (state) are read too [0.02ms]
(pass) deriveCiStatus > EXPECTED is pending, not passing — a green gate requires an actual SUCCESS
(pass) ghPollGateway.prSnapshot failure isolation > a transient reviews-fetch failure degrades to null, not a thrown pass [1.95ms]
(pass) ghPollGateway.prSnapshot failure isolation > a `pr view` failure also degrades to null (the symmetric branch) [0.77ms]
(pass) ghPollGateway.prSnapshot failure isolation > both fetches succeed → a populated snapshot [1.22ms]

packages/dispatcher/test/backlog-audit.test.ts:
[backlog-audit] o/r#2 fails the integration rubric → needs-design
(pass) runBacklogAudit > flags rubric-failing feature issues; passes the good one; skips epics [0.40ms]
(pass) runBacklogAudit > does not re-label an issue already marked needs-design [0.06ms]
[backlog-audit] o/r#10 fails the integration rubric → needs-design
[backlog-audit] o/r#11 fails the integration rubric → needs-design
(pass) runBacklogAudit > respects the per-pass cap [0.12ms]
[backlog-audit] failed to label o/r#1 (continuing): boom
[backlog-audit] o/r#2 fails the integration rubric → needs-design
(pass) runBacklogAudit > an addLabel failure is isolated — the sweep continues [0.16ms]
[backlog-audit] o/active#1 fails the integration rubric → needs-design
(pass) runAuditCronPass > sweeps managed repos, skips paused ones [2.18ms]

packages/dispatcher/test/db-migrations.test.ts:
(pass) migration 007 — repo_config epic-store columns > adds epic_store TEXT NOT NULL DEFAULT 'github' [65.40ms]
(pass) migration 007 — repo_config epic-store columns > adds epics_dir TEXT (nullable — only set in file mode) [61.64ms]
(pass) migration 007 — repo_config epic-store columns > adds state_file TEXT (nullable — only set in file mode) [66.52ms]
(pass) migration 007 — repo_config epic-store columns > workflows table gains a nullable epic_ref TEXT column [63.71ms]
(pass) migration 007 — repo_config epic-store columns > backfill: existing implementation rows get epic_ref = stringified epic_number [69.44ms]
(pass) migration 007 — repo_config epic-store columns > a freshly-inserted row defaults epic_store to 'github' [65.31ms]

packages/dispatcher/test/epics-cache.test.ts:
(pass) epics-cache > refreshEpics upserts open Epics and readEpics returns them newest-first [66.18ms]
(pass) epics-cache > an Epic that vanishes from the open set is marked closed and dropped from readEpics [71.11ms]
(pass) epics-cache > a closed Epic that reappears is reopened and visible again [75.97ms]
(pass) epics-cache > caches a file-mode Epic (slug ref, null number) and surfaces it in readEpics (#200) [67.95ms]
(pass) epics-cache > mixed github + file Epics: github (by number desc) first, file (null number) after [67.53ms]
(pass) epics-cache > a file Epic that vanishes is marked closed by its slug ref [73.51ms]
(pass) epics-cache > refresh is repo-scoped — another repo's rows are untouched [72.47ms]

packages/dispatcher/test/metrics.test.ts:
(pass) collectMetrics > empty db → zeroed snapshot [71.01ms]
(pass) collectMetrics > groups workflows by (repo, kind, state) and rolls up totals [95.53ms]
(pass) collectMetrics > a completed implementation frees its slot but stays counted in totals [72.02ms]
(pass) collectMetrics > surfaces rate-limit standing per adapter [66.16ms]
(pass) renderPrometheus > emits gauges with HELP/TYPE and a trailing newline [75.21ms]
(pass) renderPrometheus > an AVAILABLE adapter renders rate_limited 0 [71.65ms]
(pass) renderPrometheus > escapes special characters in label values [74.24ms]

packages/dispatcher/test/implementation-workflow.test.ts:
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Sk4xbX/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Sk4xbX/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=failed
(pass) implementation workflow — terminal stops fall through the waitFor > a 'failed' classifyStop ends 'failed', destroys the worktree, leaks no session [272.84ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-wZkyXv/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-wZkyXv/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — terminal stops fall through the waitFor > a 'bare-stop' ends 'completed' without parking [266.05ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-qKGDRU/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-qKGDRU/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=rate-limited
(pass) implementation workflow — terminal stops fall through the waitFor > a rate-limited classifyStop ends 'rate-limited' and records rate_limit_state [278.36ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-moJfYO/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-moJfYO/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] prompt-first launch: dismissing boot dialogs before prompt
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=s
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > prompt-first adapter sends the prompt BEFORE awaiting SessionStart (codex; no deadlock) [278.65ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-vQdRF7/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-vQdRF7/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=s
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > boot-first adapter awaits SessionStart BEFORE sending the prompt (Claude path, unchanged) [282.43ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-JRTupB/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-JRTupB/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 250ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: timed out waiting for session.started
(pass) implementation workflow — launch ordering honors startsSessionOnFirstPrompt (#183) > await-first ordering deadlocks a prompt-triggered CLI — why the flag exists [528.08ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-PK0tjb/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-PK0tjb/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — prepare-worktree survives a step retry (#108) > a transient createWorktree failure retries to success — the re-INSERT is a no-op, not a masking UNIQUE [950.92ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-wxZCjg/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-wxZCjg/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > a hung agent whose session dies parks for resume; worktree preserved [283.77ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Hl1VQy/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Hl1VQy/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > parkForResume keeps a pre-armed blocked signal (no duplicate) [283.65ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-fEkejp/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-fEkejp/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: session ended before Stop hook
(pass) implementation workflow — blocked sentinel self-heal > a hung agent with NO sentinel still fails (compensates, worktree pruned) [283.73ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-gTDvjB/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-gTDvjB/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — blocked sentinel self-heal > parkForResume removes the consumed blocked.json sentinel (#205) [284.64ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-6U1keX/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-6U1keX/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
(pass) implementation workflow — blocked sentinel self-heal > a session that dies mid-nudge with a blocked sentinel parks, not compensates [289.02ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-NCvVNj/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-NCvVNj/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-NCvVNj/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-NCvVNj/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[recommender-run] engine.close drain timed out after 10s — proceeding
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-NCvVNj/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-NCvVNj/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] session-ended with blocked.json present — parking for resume
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — question-spam integration (#205) > three consecutive dispatch ticks on a stale sentinel grow the Epic by ≤1 comment [406.24ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-yjq44D/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-yjq44D/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > a complexity-kind pause routes to waiting-human and surfaces with kind 'complexity' [257.15ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-IG8Coe/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-IG8Coe/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > a plain question pause surfaces with kind 'question' (the default) [256.35ms]
[recommender-run] engine.close drain timed out after 10s — proceeding
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-2xl1ei/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-2xl1ei/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > the dispatch brief carries the repo's complexity_ceiling as the agent's fork budget [261.67ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-6d4R5y/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-6d4R5y/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > an in-ceiling decision never surfaces a complexity pause [310.82ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ETDjZ2/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ETDjZ2/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[recommender-run] engine.close drain timed out after 10s — proceeding
(pass) implementation workflow — complexity pause (#52) > an approved Epic's brief authorizes proceeding past a complexity overrun (#53) [258.95ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] brief-context resolution failed, using defaults (ceiling=3, approved=false): gh rate limited
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-td46bi/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-td46bi/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
(pass) implementation workflow — complexity pause (#52) > a flaky brief-context read falls back to safe defaults, never failing the dispatch [258.05ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-q84I64/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-q84I64/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-99] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-99] installing hooks in /tmp/middle-wf-q84I64/worktrees/thejustinwalsh/middle/issue-99
[workflow:middle-thejustinwalsh-middle-99] launching tmux session: true (cwd=/tmp/middle-wf-q84I64/worktrees/thejustinwalsh/middle/issue-99)
[workflow:middle-thejustinwalsh-middle-99] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-99] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-99] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-99] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-99] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-99] Stop received — classification=asked-question
(pass) implementation workflow — dispatch source (#53) > records source 'manual' for a manual dispatch and 'auto' by default [244.91ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-wSBDo3/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-wSBDo3/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=asked-question
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-wSBDo3/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-wSBDo3/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (answer): "@.middle/prompt.md (answer)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — asked-question park → answer → resume (e2e) > parks on asked-question, a human reply resumes a fresh continuation with the answer injected [329.25ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-sihQjW/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-sihQjW/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-sihQjW/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-sihQjW/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a CHANGES_REQUESTED pass resumes a continuation with the address-review brief; APPROVED ends the loop [328.70ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-uoUIqe/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-uoUIqe/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-uoUIqe/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-uoUIqe/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a CI_FAILED verdict resumes a continuation with the fix-CI brief (not the address-review one) [309.03ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-GZCCS6/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-GZCCS6/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — done park → review-changes → resume (e2e) > a resolved review reverts a previously RATE_LIMITED adapter to AVAILABLE [281.00ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-pvMMEk/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-pvMMEk/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-pvMMEk/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-pvMMEk/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-pvMMEk/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-pvMMEk/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — review-round cap > after the configured cap of CHANGES_REQUESTED passes without APPROVED, it parks in waiting-human and stops auto-resuming [356.92ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-HqyYfQ
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-HqyYfQ)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] plan-comment guard: Plan-comment guard: no plan comment found on Epic #6
(pass) implementation workflow — plan-comment completion gate > a 'done' drive with no plan comment ends 'failed' (guard fires) [255.83ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-o3xFIU
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-o3xFIU)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — plan-comment completion gate > a 'done' with a matching plan comment passes the guard and parks for review [270.80ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-at3kAe/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-at3kAe/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — plan-comment completion gate > without a planCommentReader wired, a 'done' parks unguarded (back-compat) [280.73ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-yaSvmd/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-yaSvmd/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/2
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 2/2
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 2 nudges — parking for a human
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a bare-stop with no ready Epic PR nudges, then parks in waiting-human [275.98ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-K5J1Dq/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-K5J1Dq/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] positive done-signal: ready Epic PR — completing
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a ready, non-draft Epic PR is the positive done-signal — done (no nudge), parks for review [276.28ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-EmCc3s/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-EmCc3s/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
[workflow:middle-thejustinwalsh-middle-6] bare-stop, no ready PR — nudge 1/1
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 1 nudges — parking for a human
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > a draft Epic PR is not a positive done-signal — it still nudges [254.68ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-MDhya5/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-MDhya5/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=bare-stop
(pass) implementation workflow — positive done-signal (bare-stop nudge loop) > without an epicPrReadiness seam, a bare-stop keeps the legacy completion (back-compat) [263.08ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-LY23ms/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-LY23ms/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] drive failed: launch timeout
(pass) implementation workflow — compensation > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [270.39ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-JmIVEe/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-JmIVEe/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: all gates pass — done stands
(pass) implementation workflow — verify-on-stop gate > a `done` whose verify fails then passes nudges in-session, then parks for review [259.07ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-OPz0Kn/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-OPz0Kn/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/1
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: still failing after 1 rounds — parking for a human
(pass) implementation workflow — verify-on-stop gate > a `done` whose verify never passes parks for a human and keeps the worktree [254.20ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-e3gTxr/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-e3gTxr/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] verify-on-stop: gates failed — nudge 1/3
[workflow:middle-thejustinwalsh-middle-6] no done-signal after 0 nudges — parking for a human
(pass) implementation workflow — verify-on-stop gate > a verify re-stop classified `bare-stop` can't bypass the done-signal [255.02ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-nz0UD9/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-nz0UD9/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — verify-on-stop gate > no runVerifyGates seam → a `done` parks for review unchanged (verify is opt-in) [257.92ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-OyOaDM/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-OyOaDM/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-OyOaDM/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-OyOaDM/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (resume): "@.middle/prompt.md (resume)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — durable recovery across daemon restart (#116) > a workflow parked on .waitFor(RESUME_EVENT) survives a restart; a review verdict resumes it [896.19ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-wvQ7d2/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-wvQ7d2/worktrees/thejustinwalsh/middle/issue-6)
[workflow:middle-thejustinwalsh-middle-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-thejustinwalsh-middle-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] SessionStart received — session_id=stub-session
[workflow:middle-thejustinwalsh-middle-6] sending prompt (initial): "@.middle/prompt.md (initial)"
[workflow:middle-thejustinwalsh-middle-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-thejustinwalsh-middle-6] Stop received — classification=done
(pass) implementation workflow — durable recovery across daemon restart (#116) > an orphaned parked signal (store lost the execution) is reconciled, not left for the poller [686.84ms]

packages/dispatcher/test/pr-divergence-integration.test.ts:
(pass) tryRebaseOntoMain — fixture repo > clean fast-forward: feature has no commits past old main; main advanced → rebase FFs [141.75ms]
(pass) tryRebaseOntoMain — fixture repo > non-FF, no conflict: feature edits A, main edits B, no shared paths → rebase replays cleanly [148.85ms]
(pass) tryRebaseOntoMain — fixture repo > conflict: feature + main both edit shared.txt → rebase aborts, paths reported, worktree clean [182.83ms]
(pass) tryRebaseOntoMain — fixture repo > data-loss guard (#201): a rebase that drops ALL of the PR's commits → restore worktree, droppedAllCommits, branch not emptied [199.15ms]
(pass) tryRebaseOntoMain — fixture repo > gitOps.revListCount: counts a resolvable range and falls back to 0 on an unresolvable one (the guard's conservative semantics) [119.68ms]
(pass) tryRebaseOntoMain — fixture repo > a non-managed head ref (not middle-issue-*) → ok:false with empty paths (skip signal) [104.96ms]
(pass) tryRebaseOntoMain — fixture repo > a missing PR (gateway returns null) → ok:false with empty paths (skip signal) [103.32ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict rebase failure (missing upstream) THROWS — not shaped as a path-less conflict [111.79ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict merge failure (missing ref) THROWS — symmetric to the rebase hardening [109.70ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > rebase would loop but merge -X ours lands cleanly (same line, feature wins) [191.10ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > residual conflict -X ours can't auto-resolve (rename/rename) → abort, paths reported [185.86ms]
(pass) applySuccess — fixture repo > pushes the rebased branch, posts one PR comment, and records CLEAN — twice = idempotent [195.85ms]
(pass) applySuccess — fixture repo > a different mainCommitSha allows a fresh announcement (the marker is sha-keyed) [167.21ms]
(pass) applySuccess — fixture repo > null mainCommitSha skips the comment but still pushes and records CLEAN (self-review hardening) [183.90ms]
(pass) applySuccess — fixture repo > keystone data-loss guard (#201): refuses to push when local HEAD is emptied but the remote branch has commits [173.24ms]
(pass) applySuccess — fixture repo > a non-managed head ref is a no-op (no push, no comment, no row) [102.74ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > BEHIND PR rebases cleanly on the next tick, applies success, and a re-tick is idempotent [178.06ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR rebase-fails → merge fallback lands → applySuccess('merged-new-work-as-base') [234.75ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR both attempts fail (rename/rename) → applyDemoteToWork fires [232.37ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > data-loss regression (#201): rebase that would empty the branch → escalation fires; branch NOT reset to main, PR NOT closed [208.49ms]
[pr-divergence] GitHub budget low (10 < 100); skipping pass — resets 2026-06-04T06:43:26.455Z
(pass) reconcileOpenPRs — end-to-end against the fixture repo > rate-limit floor short-circuits the pass; no listing happens [124.76ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CLEAN PR → walked but unchanged; nothing posted, no state advance [131.77ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > two open managed PRs in one pass — both walked, mix of CLEAN + BEHIND→rebased [177.89ms]
[pr-divergence] o/r PR #300 reconciliation failed: transient classify boom
(pass) reconcileOpenPRs — end-to-end against the fixture repo > per-PR throw increments `failed` and the pass continues on subsequent PRs (self-review hardening) [110.27ms]
[pr-divergence] list open managed PRs for o/r failed: transient gh outage
(pass) reconcileOpenPRs — end-to-end against the fixture repo > listOpenManagedPrs throws → pass returns 0s and logs, no orchestration [105.24ms]

packages/dispatcher/test/documentation-workflow.test.ts:
(pass) documentation workflow — shell: step order + dedicated slot > declares the six steps in order [173.67ms]
(pass) documentation workflow — shell: step order + dedicated slot > runs the steps in order at runtime and completes [272.58ms]
(pass) documentation workflow — shell: step order + dedicated slot > records its row with kind 'documentation' — its own dedicated slot, off maxConcurrent [264.80ms]
(pass) documentation workflow — shell: step order + dedicated slot > claims the 'docs' worktree unit, distinct from the recommender's [268.40ms]
(pass) documentation workflow — shell: step order + dedicated slot > spawn-docs-agent has the spec's 5-minute hard cap [173.28ms]
(pass) documentation workflow — shell: step order + dedicated slot > prepare-docs-worktree registers a compensation handler [170.87ms]
(pass) documentation workflow — shell: step order + dedicated slot > check-rate-limit does not retry [174.06ms]
(pass) documentation workflow — shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' [237.59ms]
[documentation:middle-docs-thejustinwalsh-middle-84683439] spawn failed: launch timeout
(pass) documentation workflow — shell: step order + dedicated slot > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [280.21ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=false: persist seam is never invoked [272.15ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true but persistDocs UNWIRED: still persists nothing (read-only first) [269.23ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true and persistDocs wired: persist runs after the agent, before cleanup [264.68ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports the resolved target, audit mode, and config; invokes the skill via @-reference [268.24ms]
(pass) documentation workflow — assembleDocumentationPrompt > includes the llms.txt audit line only when the target supports it [176.20ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports write=true to the agent when configured [176.02ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=true selects write mode: discover-or-author, agent does not commit [172.86ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=false stays in audit mode (read-only), never write mode [173.90ms]
(pass) documentation workflow — assembleDocumentationPrompt > write mode keeps the llms.txt instruction only when the target supports it [171.95ms]
(pass) documentation workflow — sessionNameFor collision-resistance > is deterministic for a given repo [176.27ms]
(pass) documentation workflow — sessionNameFor collision-resistance > produces a tmux-safe session name under the docs namespace [172.01ms]
(pass) documentation workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [173.52ms]

packages/dispatcher/test/recommender-cron-parallel.test.ts:
[recommender-cron] acme/b run timed out after 500ms — abandoned (retries next tick)
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > a hung repo times out without blocking the others; A+C succeed, B fails [503.10ms]
[recommender-cron] bad/b run failed: boom
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > a throwing run is isolated the same way (stamp rolled back, others succeed) [23.71ms]
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > concurrency is bounded by maxConcurrentRepos [95.12ms]
(pass) runRecommenderCronPass — per-repo parallelism + timeout (#227) > the pass still works (and is sequential-equivalent) with a single due repo [12.11ms]

packages/dispatcher/test/host-context.test.ts:
(pass) DaemonHostContext exposes dispatch + refreshEpics callbacks [0.03ms]

packages/dispatcher/test/control-routes.test.ts:
(pass) HookServer control routes > GET /health reports liveness, port, and version [2.02ms]
(pass) HookServer control routes > the server idle-timeout exceeds the SSE heartbeat (else /control/events streams drop) [0.03ms]
(pass) HookServer control routes > POST /control/dispatch starts the workflow and returns its id [1.90ms]
(pass) HookServer control routes > POST /control/dispatch rejects invalid bodies with 400 and starts nothing [2.59ms]
(pass) HookServer control routes > POST /control/dispatch surfaces the disabled-vs-unknown distinction in the 400 body [1.62ms]
(pass) HookServer control routes > POST /control/dispatch refuses with 429 when no slot is available (manual respects limits) [1.81ms]
(pass) HookServer control routes > POST /control/dispatch proceeds when a slot is available [1.29ms]
[hook-server] afterDispatch failed for o/r: scheduler boom
(pass) HookServer control routes > POST /control/dispatch survives a throwing afterDispatch (best-effort, still 200) [2.00ms]
(pass) HookServer control routes > POST /control/dispatch rejects a colliding Epic with 409 [2.82ms]
(pass) HookServer control routes > POST /control/dispatch maps a shared-checkout collision to 400 (#226) [2.00ms]
(pass) HookServer control routes > two concurrent dispatches of the same Epic: exactly one 200, one 409 [6.61ms]
(pass) HookServer control routes > GET /control/events opens an SSE stream with a connected frame [2.47ms]
(pass) HookServer control routes > GET /control/events replays the injected init events [1.88ms]
(pass) HookServer control routes > GET / 404s in the bare server (the status page is gone; the SPA mounts via extraRoutes) [1.87ms]
(pass) HookServer control routes > GET /metrics renders Prometheus text from the metrics seam [1.37ms]
(pass) HookServer control routes > GET /control/metrics returns the raw snapshot as JSON [2.07ms]
(pass) HookServer control routes > metric routes 404 without a metrics seam [1.53ms]
(pass) HookServer control routes > POST /control/resume fires the parked Epic's resume and returns its id [2.40ms]
(pass) HookServer control routes > POST /control/resume 404s when no parked workflow owns the ref [1.72ms]
(pass) HookServer control routes > POST /control/resume 400s on a missing epicRef or answer [2.35ms]
(pass) HookServer control routes > control routes 404 in gate-only mode (no control plane wired) [1.96ms]

packages/dispatcher/test/tmux.test.ts:
(pass) tmux session lifecycle > launch → has-session → send-text → capture-pane → kill [266.04ms]
(pass) tmux session lifecycle > newSession injects env via -e KEY=val [255.97ms]
(pass) tmux session lifecycle > hasSession is false for an unknown session [1.28ms]
(pass) tmux session lifecycle > status reports not-alive for an unknown session [1.24ms]
(pass) tmux session lifecycle > killSession on an already-gone session is a no-op, not a throw [2.48ms]
(pass) tmux session lifecycle > newSession rejects a duplicate session name with a TmuxError [5.35ms]
(pass) tmux session lifecycle > getTmuxVersion parses the installed tmux's version [0.94ms]
(pass) parseTmuxVersion > parses release versions [0.05ms]
(pass) parseTmuxVersion > parses pre-release builds (next-X.Y, X.Ya) [0.02ms]
(pass) parseTmuxVersion > returns null on garbage input [0.01ms]
(pass) tmuxVersionAtLeast > compares major then minor against the threshold [0.03ms]

packages/dispatcher/test/workflow-record.test.ts:
(pass) getWorkflow epic_ref (#187) > reads back epic_ref straight from the column (slug, number-string, or null) [89.12ms]
(pass) dispatch source (#53) > records and reads back source 'manual' / 'auto'; null when unset [75.61ms]
(pass) workflow meta_json accessors > readWorkflowMeta returns {} for a missing row, a null meta, and malformed JSON [76.70ms]
(pass) workflow meta_json accessors > patchWorkflowMeta merges, preserving keys it does not set [79.84ms]
(pass) workflow meta_json accessors > patchWorkflowMeta does not bump updated_at — meta is scratch, not an activity signal [74.26ms]
(pass) workflow meta_json accessors > checkbox-reconcile state round-trips; defaults when unset [70.47ms]
(pass) workflow meta_json accessors > getCheckboxReconcileState sanitizes malformed nested meta back to the contract [97.72ms]
(pass) listRunningImplementationWorkflows > returns only running implementation rows that own both an epic and a worktree [131.39ms]
(pass) createWorkflowRecord > inserts a pending implementation row carrying epic_number [66.90ms]
(pass) createWorkflowRecord > a second create with the same id is a no-op (idempotent on retry), not a UNIQUE error [72.08ms]
(pass) createWorkflowRecord > a non-PK constraint violation (bad kind) still throws — not swallowed [61.38ms]
(pass) countActiveImplementationSlots > counts non-terminal implementation rows, grouped by adapter [75.95ms]
(pass) countActiveImplementationSlots > excludes terminal implementation rows [74.86ms]
(pass) countActiveImplementationSlots > excludes the recommender's own row — its dedicated slot is not a dispatch slot [73.01ms]
(pass) updateWorkflow > transitions state and bumps updated_at [79.52ms]
(pass) updateWorkflow > patches session fields without disturbing others [73.63ms]
(pass) updateWorkflow > a no-op patch leaves the row intact [71.23ms]
(pass) getWorkflow > returns null for an unknown id [60.28ms]
(pass) hasNonTerminalEpicWorkflow > true while an implementation Epic workflow is non-terminal, false once terminal [147.10ms]
(pass) hasNonTerminalEpicWorkflow > scopes by repo and epic; a recommender row never collides [73.94ms]
(pass) findParkedWorkflowByRef > finds the waiting-human workflow for a ref (slug or number); null otherwise [76.91ms]
(pass) listActiveImplementationWorkflows (#180) > returns lastHeartbeat (null when none observed, the touched epoch otherwise) [78.22ms]
(pass) listNonTerminalWorkflows > returns id/repo/epic/state for non-terminal implementation rows only [87.35ms]
(pass) workflow observers > notifies the observer of each patch, and stops after dispose [81.84ms]
[workflow-record] update observer threw: observer boom
(pass) workflow observers > a throwing observer does not break the DB write [71.80ms]
(pass) workflow observers > addWorkflowObserver fans out to every observer; disposers independent [84.36ms]
(pass) workflow observers > the finalize path notifies observers on a real transition only [76.72ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > flips a still-pending row to failed and reports the transition [70.51ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a row already past pending (e.g. a later step's compensated failure) [73.68ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a launching row — the launch step already advanced it [74.56ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on an unknown id [64.12ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > does NOT touch a pending recommender row — it legitimately sits at pending through build-prompt, where compensation owns the terminal state [75.80ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > does NOT touch a pending documentation row (same reason as recommender) [73.15ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > notifies observers only on a real transition [75.98ms]

packages/dispatcher/test/recovery.test.ts:
(pass) reconcileOrphanedSignals > an armed signal with no recoverable execution is finalized failed, consumed, and surfaced [88.49ms]
(pass) reconcileOrphanedSignals > a recoverable parked execution is left untouched (not an orphan) [79.89ms]
(pass) reconcileOrphanedSignals > only the orphaned rows are reconciled when alive and orphaned parks coexist [100.04ms]
(pass) reconcileOrphanedSignals > respects a custom finalState and tolerates a missing surface callback [87.63ms]
[recover] surfacing orphaned signal 89f7125e-c1e1-4db6-a8f8-e40eeea3999e (epic-9-answered) failed: comment failed
(pass) reconcileOrphanedSignals > a surface callback that throws never aborts the reconcile (still finalized + consumed) [86.27ms]
(pass) reconcileOrphanedSignals > an orphaned signal with a null epicNumber still reconciles [83.05ms]
(pass) reconcileOrphanedSignals > a non-parked (terminal) workflow's stale signal is ignored — only waiting-human rows are pollable [76.02ms]
(pass) reconcileOrphanedSignals > finalState is typed to terminal states only (compile-time guard) [60.60ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BUNQUEUE_DATA_PATH) when it is set [61.26ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BQ_DATA_PATH) when it is set [62.25ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming DATA_PATH) when it is set [61.11ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming SQLITE_PATH) when it is set [61.69ms]
(pass) createDurableEngine (transient-queue env guard) > an empty-string env var still trips the guard (bunqueue coalesces with ??) [67.18ms]
(pass) createDurableEngine (transient-queue env guard) > names every offending var when several are set at once [61.43ms]
(pass) recoverEngine (durable engine across restart) > re-arms a parked waiting execution so a later signal resumes it [405.52ms]
(pass) recoverEngine (durable engine across restart) > drops a mid-drive (running) execution instead of re-driving it [363.12ms]

packages/dispatcher/test/hook-server.test.ts:
[hook-server] received session.started:middle-6
(pass) HookServer — SessionStart > awaitSessionStart resolves with the posted payload [2.82ms]
[hook-server] received session.started:middle-7
(pass) HookServer — SessionStart > a payload that arrives before anyone awaits is stashed and delivered [1.77ms]
[hook-server] received session.started:middle-9
[hook-server] received session.started:middle-9
(pass) HookServer — SessionStart > duplicate pre-await arrivals keep the FIRST payload, not the last [1.94ms]
[hook-server] received session.started:middle-DIFFERENT
(pass) HookServer — SessionStart > waiters are keyed by session — one session's event does not satisfy another [301.65ms]
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > awaitStop resolves on an agent.stopped POST [2.48ms]
[hook-server] received agent.subagent-stopped:middle-6
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > a subagent stop does NOT resolve awaitStop — only the main agent's Stop does [300.84ms]
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > a re-registered awaitStop is not evicted by an abandoned waiter's stale timeout [63.89ms]
[hook-server] received tool.pre:middle-42
(pass) HookServer — HMAC auth + event validation (with store) > a valid POST (correct token, known event) is accepted and recorded [3.53ms]
[hook-server] rejected tool.pre:middle-42 — bad or unknown token
(pass) HookServer — HMAC auth + event validation (with store) > a bad-HMAC POST is rejected 401 and never recorded [3.25ms]
[hook-server] rejected tool.pre:middle-DOES-NOT-EXIST — bad or unknown token
(pass) HookServer — HMAC auth + event validation (with store) > a POST for an unknown session is rejected 401 (no token resolvable) [2.85ms]
[hook-server] rejected unknown event "not.a.real.event"
(pass) HookServer — HMAC auth + event validation (with store) > an unknown event name is rejected 400 before auth or recording [2.86ms]
[hook-server] received session.started:middle-42
(pass) HookServer — HMAC auth + event validation (with store) > session.started with a valid token resolves the SessionGate awaiter [2.84ms]
(pass) HookServer — lifecycle > awaitSessionStart rejects on timeout [52.58ms]
(pass) HookServer — lifecycle > non-POST and unknown paths return 404 [2.00ms]
(pass) HookServer — lifecycle > stop() rejects outstanding waiters [1.41ms]
(pass) HookServer — recommender trigger endpoint > 404s when no trigger is wired (gate-only mode) [1.93ms]
(pass) HookServer — recommender trigger endpoint > wired trigger receives the posted repo and returns its status/body verbatim [2.77ms]
(pass) HookServer — recommender trigger endpoint > tolerates a garbled body — the trigger validates its own inputs [3.28ms]
(pass) HookServer — recommender trigger endpoint > coerces non-string repoSlug/repoPath to undefined before forwarding [3.03ms]
(pass) HookServer — recommender trigger endpoint > a non-object JSON body (null, primitive, array) is treated as empty, not a 500 [3.24ms]
(pass) HookServer — recommender trigger endpoint > passes a string field through while dropping a non-string sibling [3.86ms]
(pass) HookServer — merged routes > extraRoutes are served, and the fetch fallback still answers /health [2.95ms]
(pass) HookServer — merged routes > GET / no longer returns the status page (404 with no SPA route) [2.88ms]

packages/dispatcher/test/docs-persist.test.ts:
(pass) commitDocs > stages and commits authored docs; returns the sha + sorted file list [32.53ms]
(pass) commitDocs > returns null on a clean worktree — no empty commit [15.86ms]
(pass) commitDocs > excludes middle's .middle/ scratch even when the repo does not gitignore it [18.66ms]
(pass) commitDocs > honors a custom commit message [20.41ms]
(pass) makeGhPersistDocs > commits, then invokes the push seam with the commit it produced [21.10ms]
(pass) makeGhPersistDocs > clean worktree: the push seam is never invoked (no empty PR) [14.65ms]
(pass) pushDocsBranch > first run creates the branch on origin at the authored commit [36.66ms]
(pass) pushDocsBranch > re-run force-pushes a divergent commit (rebuilt branch is non-fast-forward) [65.93ms]
(pass) pushDocsBranch > surfaces a push failure rather than swallowing it (no origin configured) [20.77ms]
(pass) docsPrBody > lists the committed files, the commit sha, and the draft notice [10.30ms]

packages/dispatcher/test/documentation-run.test.ts:
[documentation-run] workflow wf_1780555358305_7xkj0nvg enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > runs to completion and records a kind:'documentation' row for the repo [379.91ms]
[documentation-run] workflow wf_1780555358686_3sndr5g5 enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > write=true but a clean worktree: the wired seam opens no PR (no empty commit) [387.58ms]
[documentation-run] workflow wf_1780555359074_1tzvf9m1 enqueued
(pass) dispatchDocumentation — integration: authors markdown into docs/ and persists it > no docs surface + write=true: the agent authors docs/, the run commits + pushes it [381.15ms]
(pass) resolveDocumentationOptions > accepts a configured non-default adapter (e.g. codex) [11.48ms]
(pass) resolveDocumentationOptions > rejects an adapter the registry doesn't know [10.82ms]
(pass) resolveDocumentationOptions > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [9.53ms]
(pass) resolveDocumentationOptions > resolves the markdown fallback target for a plain repo [11.22ms]
(pass) resolveDocumentationOptions > honors a [docs] tool/path override [11.75ms]
(pass) resolveDocumentationOptions > surfaces an unknown tool override as an error rather than falling back [9.54ms]

packages/dispatcher/test/recommender-cron.test.ts:
(pass) runRecommenderCronPass > fires a due, enabled, unpaused repo and stamps last_recommender_run [2.17ms]
(pass) runRecommenderCronPass > does not re-fire a repo whose interval hasn't elapsed [1.59ms]
(pass) runRecommenderCronPass > fires once the interval has elapsed [1.53ms]
(pass) runRecommenderCronPass > skips a paused repo [1.49ms]
(pass) runRecommenderCronPass > skips a repo whose recommender is disabled or unconfigured [1.66ms]
(pass) runRecommenderCronPass > skips a repo with a non-positive interval (never auto-runs) [1.48ms]
[recommender-cron] bad/repo run failed: recommender run boom
[recommender-cron] bad/repo run failed: recommender run boom
(pass) runRecommenderCronPass > a failed launch rolls the stamp back (retries next tick) and is isolated [1.75ms]
(pass) runRecommenderCronPass > ignores unmanaged rows (no checkout path) [1.72ms]

packages/dispatcher/test/poller.test.ts:
(pass) reasonFromSignalName > maps the durable signal names to resume reasons [62.62ms]
(pass) classifyNewHumanReply > returns the newest non-bot reply posted after the wait armed [63.34ms]
(pass) classifyNewHumanReply > returns null when only bot/stale comments exist [63.45ms]
(pass) classifyNewHumanReply > skips the dispatcher's own marked pause comment (posted as a non-bot human identity) [66.81ms]
(pass) classifyNewHumanReply > a genuine human reply that quote-replies the pause comment still resumes [63.85ms]
(pass) classifyReviewOutcome > a fresh CHANGES_REQUESTED review → changes-requested [63.85ms]
(pass) classifyReviewOutcome > a fresh APPROVED review → resolved [62.32ms]
(pass) classifyReviewOutcome > a fresh 0-actionable re-review → resolved even while decision stays CHANGES_REQUESTED [68.24ms]
(pass) classifyReviewOutcome > the `changes-requested` label alone (no fresh review) → changes-requested [63.67ms]
(pass) classifyReviewOutcome > only stale reviews and no actionable label → null (nothing changed) [61.37ms]
(pass) classifyReviewOutcome > a stale standing CHANGES_REQUESTED decision (no fresh review, no label) → null [61.61ms]
(pass) classifyReviewOutcome — CI gate > failing CI with no review feedback → resume to fix CI (CI_FAILED) [62.40ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review while CI is still pending is held (null) — don't end on un-built CI [60.90ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review with passing CI resolves [60.19ms]
(pass) classifyReviewOutcome — CI gate > explicit review feedback wins over red CI (address the review, which greens CI) [66.33ms]
(pass) classifyReviewOutcome — CI gate > absent CI (`none`) is non-blocking — the pre-CI review loop is unchanged [62.95ms]
(pass) classifyReviewOutcome — CI gate > failing CI but no PR change and no review → still CI_FAILED (red build is actionable) [61.40ms]
(pass) runPoller — answered-question > a new human reply fires epic-<n>-answered exactly once (idempotent across passes) [83.59ms]
(pass) runPoller — answered-question > a bot-only reply does not fire [78.69ms]
(pass) runPoller — answered-question > the dispatcher's own pause comment does not self-resume (#178) [78.19ms]
(pass) runPoller — review-changes > CHANGES_REQUESTED fires review-resolved with outcome 'changes-requested' [81.62ms]
(pass) runPoller — review-changes > APPROVED fires review-resolved as resolved [82.12ms]
(pass) runPoller — review-changes > a 0-actionable re-review fires review-resolved as resolved [86.71ms]
(pass) runPoller — review-changes > no PR yet → no fire [80.03ms]
[poller] poll failed for workflow 74401092-bc15-4e00-837d-b2f87274e996 (epic-200-answered): API rate limit exceeded
(pass) runPoller — resilience > a gateway error for one workflow is isolated; others still fire [98.50ms]
[poller] GitHub budget low (50 < 100); skipping pass — resets 1970-01-01T00:17:40.000Z
(pass) runPoller — GitHub rate-limit guards > skips the whole pass when remaining budget is below the buffer [78.90ms]
(pass) runPoller — GitHub rate-limit guards > a healthy budget proceeds (the guard isn't always-on) [83.39ms]
(pass) runPoller — GitHub rate-limit guards > caps the workflows polled per pass (burst protection) [119.20ms]

packages/dispatcher/test/github-epics.test.ts:
(pass) parseEpicsList > maps sub_issues_summary into Epic rows [0.78ms]
(pass) parseEpicsList > tolerates blank lines and ignores rows missing a summary [0.03ms]
(pass) parseEpicsList > parses with labels: [] when labels key is wholly absent [0.03ms]

packages/dispatcher/test/reconcile.test.ts:
[reconcile] thejustinwalsh/middle#50 PR MERGED → completed (workflow e5849640-327c-49fd-8a72-38afad01a223)
(pass) reconcileMergedParks > a merged PR finalizes the parked workflow to `completed` and tears down its worktree [82.98ms]
[reconcile] thejustinwalsh/middle#51 PR CLOSED → cancelled (workflow c0896612-6b05-4377-87dc-fbc20033771f)
(pass) reconcileMergedParks > a closed-unmerged PR finalizes to `cancelled` [79.83ms]
(pass) reconcileMergedParks > an open PR (a live review park) is left alone [70.47ms]
(pass) reconcileMergedParks > no PR for the Epic (a pending question) is left alone [71.44ms]
[reconcile] thejustinwalsh/middle#54 PR MERGED → completed (workflow e90fc1da-889c-4f78-a979-76bfa55eb518)
[reconcile] worktree cleanup failed for e90fc1da-889c-4f78-a979-76bfa55eb518 (continuing): git worktree remove failed
(pass) reconcileMergedParks > finalizes the row even when worktree teardown throws (best-effort) [75.47ms]
(pass) reconcileMergedParks > only walks `waiting-human` rows — running/terminal rows are untouched [87.27ms]
[reconcile] GitHub budget low (10 < 100); skipping pass — resets 1970-01-01T00:00:00.000Z
(pass) reconcileMergedParks > skips the whole pass when the GitHub budget is below the buffer [68.86ms]
[reconcile] thejustinwalsh/middle#70 PR MERGED → completed (workflow 62762486-959f-420a-b532-f4a724331b9d)
[reconcile] thejustinwalsh/middle#71 PR CLOSED → cancelled (workflow d68735af-53b1-44e0-a5a1-ccd39c9bcd8f)
[reconcile] thejustinwalsh/middle#72 PR MERGED → completed (workflow b6e0a6da-be45-47fa-a0ca-b7cae923d03a)
(pass) reconcileMergedParks > fires onMergedTransition at most once per repo per pass (Epic #168 wiring) [104.58ms]
[reconcile] thejustinwalsh/middle#75 PR MERGED → completed (workflow 70769108-e3ae-40b0-bb7b-f89b8362507d)
[reconcile] onMergedTransition for thejustinwalsh/middle failed (continuing): downstream sweep boom
[reconcile] thejustinwalsh/middle#76 PR MERGED → completed (workflow d535fc74-de16-412a-b957-9b94cfe69687)
(pass) reconcileMergedParks > a thrown onMergedTransition is isolated — the merged-parks pass still finishes [89.12ms]
[reconcile] thejustinwalsh/middle#60 PR MERGED → completed (workflow 6d9fad04-b58a-4c73-a568-4e6411d3f6dd)
[reconcile] thejustinwalsh/middle#61 PR MERGED → completed (workflow 3bac2f25-e2df-4e28-ad13-684a02010673)
(pass) reconcileMergedParks > honors the per-pass burst cap [97.19ms]
(pass) reconcileMergedParks > does not tear down the worktree when it loses the race to a concurrent resume [76.15ms]
(pass) finalizeParkedWorkflow > transitions a still-parked row and reports the change [74.07ms]
(pass) finalizeParkedWorkflow > no-ops (returns false) a row that already left waiting-human [80.19ms]

packages/dispatcher/test/recommender-workflow.test.ts:
(pass) recommender workflow — #43 shell: step order + dedicated slot > declares the eight spec steps in order [172.12ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > runs the steps in spec order at runtime and completes [267.74ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > records its row with kind 'recommender' — its own dedicated slot, off maxConcurrent [264.65ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > spawn-recommender-agent's step backstop is sized for the per-repo ceiling [172.82ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > prepare-shallow-worktree registers a compensation handler [175.35ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > check-rate-limit does not retry — it creates the row then may throw, and a retry would re-INSERT [173.12ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' (not a UNIQUE error) [233.78ms]
[recommender:middle-rec-thejustinwalsh-middle-84683439] spawn failed: launch timeout
(pass) recommender workflow — #43 shell: step order + dedicated slot > a launch failure compensates: worktree rolled back, session freed, state 'compensated' [262.81ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > assembles all eight Phase-1 inputs, with dispatcher-owned context verbatim [174.74ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > file mode reframes the prompt for the file-backed store (#200) [176.99ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > writes the assembled prompt to .middle/prompt.md and launches it via the @-reference [267.37ms]
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a valid produced body verifies ok and the workflow proceeds to trigger-auto-dispatch [270.84ms]
[recommender] reapply skipped — agent body for #99 does not parse: missing open marker
[recommender] state issue #99 does not parse: missing open marker
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a malformed produced body does NOT proceed to auto-dispatch and surfaces the problem [272.91ms]
[recommender] state issue #99 failed validation: Ready row uses unconfigured adapter: "ghost"
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a body that parses but fails validation is also gated and surfaced [269.32ms]
[recommender] reapply skipped — agent body for #99 does not parse: missing open marker
[recommender] state issue #99 does not parse: missing open marker
[recommender] surfaceProblem failed: gh comment failed
(pass) recommender workflow — #45 verify-state-issue-parses: gate auto-dispatch > a failed surfaceProblem callback does not abort cleanup (best-effort surfacing) [270.65ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > heartbeatRel formats epoch deltas; null → 'unknown' [173.85ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > dispatcherSectionsFromContext builds canonical sections (heartbeat, null-issue dropped, null-session→pending) [174.90ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > self-heal: agent emits empty In-flight; dispatcher overwrites with the canonical 5-field line [266.24ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > no-op: when the agent body already matches the dispatcher's sections, reapply skips the write [213.16ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > a throwing reapply write compensates (worktree rolled back, no dispatch) [2050.54ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
[recommender] reapply skipped — agent body for #99 does not parse: malformed "In-flight" item: "- **#60** · claude · running · [tmux: middle-thejustinwalsh-middle-60]"
[recommender] state issue #99 does not parse: malformed "In-flight" item: "- **#60** · claude · running · [tmux: middle-thejustinwalsh-middle-60]"
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > exact bug shape: agent body with a 4-field In-flight line is left to verify, which surfaces it [268.21ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > derives rate_limits, in_flight, and slots from db + config [198.87ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > excludes the recommender's own row from in_flight and slots [186.61ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > scopes per-repo slots/in_flight to the repo, but global_used spans all repos [192.79ms]
(pass) recommender workflow — sessionNameFor collision-resistance > is deterministic for a given repo [171.57ms]
(pass) recommender workflow — sessionNameFor collision-resistance > produces a tmux-safe session name (no separators survive) [171.08ms]
(pass) recommender workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [173.07ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > runs on the engine via per-repo resolveRunSettings and creates the recommender row [266.29ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > a clear wiring error when neither resolveRunSettings nor static settings are provided [2043.73ms]

packages/dispatcher/test/staleness-cron.test.ts:
[staleness] o/active#50 landed in merged PR #88 → closed
[staleness] o/active: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass > reads the repo's spec from its checkout, closes + flags; skips paused [3.06ms]
(pass) runStalenessCronPass > a non-ENOENT spec read error surfaces (not silently treated as missing spec) [2.29ms]
[staleness] o/custom#50 landed in merged PR #88 → closed
[staleness] o/custom: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — per-repo spec path > a repo's [staleness] spec_path points the drift check at a non-default location [2.42ms]
[staleness] o/defaulted#50 landed in merged PR #88 → closed
[staleness] o/defaulted: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — per-repo spec path > a repo with no configured spec_path falls back to the default path [2.23ms]
[staleness] o/nospec#50 landed in merged PR #88 → closed
(pass) runStalenessCronPass — per-repo spec path > a repo with no spec file still reconciles landed issues (no drift) [1.82ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a `..` traversal spec_path is rejected — the pass never reads outside the checkout [2.27ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a deeper `../../` traversal is rejected too [2.28ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > an absolute spec_path is rejected (the field is repo-relative by contract) [2.19ms]
[staleness] o/dotdotname#50 landed in merged PR #88 → closed
[staleness] o/dotdotname: filed reconcile task #999 for Phase 9
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a filename whose segment merely starts with `..` is allowed (not a traversal) [2.14ms]

packages/dispatcher/test/rate-limits.test.ts:
(pass) rate_limit_state > getRateLimitState is null until observed [66.85ms]
(pass) rate_limit_state > setRateLimited records status, reset_at, and source [68.58ms]
(pass) rate_limit_state > setRateLimited upserts an existing adapter row [71.68ms]
(pass) rate_limit_state > markAvailable clears the reset time [72.84ms]
(pass) rate_limit_state > markAvailableOnSuccess flips RATE_LIMITED → AVAILABLE and reports it [73.88ms]
(pass) rate_limit_state > markAvailableOnSuccess is a no-op when not rate-limited [68.17ms]
(pass) rate-limit observer fan-out > addRateLimitObserver fans out to every observer; disposers are independent [71.35ms]
[rate-limits] observer threw: boom
(pass) rate-limit observer fan-out > a throwing observer does not stop the others or the write path [67.00ms]
(pass) parseResetAt > parses an ISO timestamp to unix ms [65.50ms]
(pass) parseResetAt > returns null for unrecognized text [62.69ms]

packages/dispatcher/test/poller-cron.test.ts:
(pass) POLLER_INTERVAL_MS matches the dispatcher CLAUDE.md cadence contract (60s) [1.46ms]

packages/dispatcher/test/blocker-resolution.test.ts:
(pass) parseBlockerRef > same-repo #<n> [0.09ms]
(pass) parseBlockerRef > cross-repo <owner>/<repo>#<n> [0.03ms]
(pass) parseBlockerRef > strips a trailing title annotation when extracting the ref [0.02ms]
(pass) parseBlockerRef > backticked non-issue blocker is non-resolvable
(pass) parseBlockerRef > free text without a #<n> is non-issue
(pass) resolveBlockers > a closed same-repo blocker moves the item to Ready to dispatch [0.29ms]
(pass) resolveBlockers > an open blocker stays Blocked, annotated with the resolved title [0.09ms]
(pass) resolveBlockers > an unresolvable (404) blocker stays Blocked with a (stale blocker: <ref>) suffix [0.07ms]
(pass) resolveBlockers > a backticked non-issue blocker is left untouched [0.06ms]
(pass) resolveBlockers > an open blocker with an empty title falls back to the bare ref (never `#42 ()`) [0.07ms]
(pass) resolveBlockers > a long open-blocker title is truncated to 60 chars in the annotation [0.05ms]
(pass) resolveBlockers > re-resolving is idempotent — a re-annotated open blocker does not accumulate [0.04ms]
(pass) resolveBlockers > re-resolving a now-closed previously-stale blocker unblocks it [0.05ms]
(pass) resolveBlockers > appended Ready rows are re-ranked after existing rows [0.12ms]
(pass) resolveBlockers > falls back to resolveIssue for the title when selfEpic has no entry [0.07ms]
(pass) resolveBlockers > the produced state still round-trips through render/parse [0.13ms]
(pass) resolveBlockers > no resolvable blockers → state is returned structurally unchanged [0.06ms]
(pass) resolveBlockers > a long blocker title is truncated to 60 chars with an ellipsis in the Ready epic [0.08ms]

packages/dispatcher/test/hook-server-gates.test.ts:
(pass) HookServer — /gates/pr-ready > returns 200 when the gate allows [2.54ms]
[hook-server] pr-ready gate DENY for middle-27: criteria X and Y lack evidence
(pass) HookServer — /gates/pr-ready > returns 403 with the reason in the body when the gate denies [1.45ms]
(pass) HookServer — /gates/pr-ready > forwards the session name and payload to the gate handler [2.05ms]
(pass) HookServer — /gates/pr-ready > 404s the gate route when no gate handler is wired [1.50ms]

packages/dispatcher/test/repo-config.test.ts:
(pass) repo pause/resume > an unpaused repo (no row) reads as not paused [2.92ms]
(pass) repo pause/resume > mm pause (indefinite) suspends the repo [1.49ms]
(pass) repo pause/resume > a paused_until in the future reads as paused; in the past auto-expires [1.41ms]
(pass) repo pause/resume > mm resume clears the pause [1.47ms]
(pass) repo pause/resume > pausing is idempotent and re-pausing updates the timestamp [1.41ms]
(pass) repo pause/resume > resume on a never-paused repo is a harmless no-op [1.34ms]
(pass) managed-repo registry (#135) > an unregistered repo has no path and isn't listed [1.48ms]
(pass) managed-repo registry (#135) > registerManagedRepo records the checkout path and lists it [1.58ms]
(pass) managed-repo registry (#135) > registering is idempotent and updates the path in place (one row) [1.53ms]
(pass) managed-repo registry (#135) > registering preserves an existing pause (doesn't clobber paused_until) [1.63ms]
(pass) managed-repo registry (#135) > listManagedRepos excludes rows with no checkout path (e.g. a pause-only row) [1.60ms]
(pass) managed-repo registry (#135) > setLastRecommenderRun writes a value and clears it with null (cron rollback) [1.47ms]
(pass) managed-repo registry (#135) > markRecommenderRun stamps and reads back last_recommender_run [1.45ms]
(pass) shared-checkout collision guard (#226) > (a) registering acme/a at /tmp/X succeeds [1.41ms]
(pass) shared-checkout collision guard (#226) > (b) re-registering the SAME repo at the same path is idempotent and succeeds [1.54ms]
(pass) shared-checkout collision guard (#226) > (c) registering a DIFFERENT repo at the same path rejects, naming both repos + the path [1.52ms]
(pass) shared-checkout collision guard (#226) > the rejected repo is NOT written (the collision guard runs before the insert) [1.55ms]
(pass) shared-checkout collision guard (#226) > the same repo can move to a new path (no self-collision) [1.50ms]
(pass) shared-checkout collision guard (#226) > assertNoRepoPathCollision is a standalone guard (used by mm init before scaffolding) [1.44ms]
(pass) shared-checkout collision guard (#226) > trailing-slash / dot-segment spellings of the same path still collide (normalized) [1.50ms]

packages/dispatcher/test/worktree.test.ts:
(pass) createWorktree → listWorktrees → destroyWorktree > create places the worktree under <root>/<repo>/issue-<n> on a fresh branch [16.17ms]
(pass) createWorktree → listWorktrees → destroyWorktree > the recommender unit is named 'recommender' [14.36ms]
(pass) createWorktree → listWorktrees → destroyWorktree > list enumerates active worktrees under the root [23.53ms]
(pass) createWorktree → listWorktrees → destroyWorktree > destroy removes the worktree directory and its branch [24.74ms]
(pass) idempotency > creating an already-existing worktree returns the handle without throwing [15.93ms]
(pass) idempotency > destroying an already-removed worktree is a no-op, not a throw [22.92ms]
(pass) branch reuse (issue #179) > reuses an existing branch — does not pass -b, so it doesn't error [20.63ms]
(pass) branch reuse (issue #179) > reuse checks out the existing branch's own tip, not a fresh branch from HEAD [21.51ms]
(pass) branch reuse (issue #179) > still creates a fresh branch when none exists (first dispatch unchanged) [16.38ms]
(pass) branch reuse (issue #179) > dispatch → prune (branch survives) → re-dispatch all succeed [24.42ms]
(pass) failure surfacing > create against a non-git directory throws WorktreeError [7.90ms]

packages/dispatcher/test/adapter-conformance.test.ts:
(pass) the registry knows all three adapters [0.26ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("toString") throws unknown-adapter [0.19ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("toString") is false [0.12ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("constructor") throws unknown-adapter [0.13ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("constructor") is false [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("hasOwnProperty") throws unknown-adapter [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("hasOwnProperty") is false [0.08ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("__proto__") throws unknown-adapter [0.09ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("__proto__") is false [0.08ms]
(pass) AgentAdapter contract — claude > resolveTranscriptPath yields a path from this adapter's own ready payload [0.15ms]
(pass) AgentAdapter contract — claude > identity: name matches its registry key and readyEvent is a normalized event [0.11ms]
(pass) AgentAdapter contract — claude > buildLaunchCommand yields a non-empty argv and the session env [0.21ms]
(pass) AgentAdapter contract — claude > buildPromptText: initial is the skill slash-command on the Epic [0.14ms]
(pass) AgentAdapter contract — claude > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.10ms]
(pass) AgentAdapter contract — claude > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.31ms]
(pass) AgentAdapter contract — claude > classifyStop: blocked.json → asked-question [0.42ms]
(pass) AgentAdapter contract — claude > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.43ms]
(pass) AgentAdapter contract — claude > detectRateLimit is implemented and returns null on a clean transcript [0.20ms]
(pass) AgentAdapter contract — codex > resolveTranscriptPath yields a path from this adapter's own ready payload [0.15ms]
(pass) AgentAdapter contract — codex > identity: name matches its registry key and readyEvent is a normalized event [0.10ms]
(pass) AgentAdapter contract — codex > buildLaunchCommand yields a non-empty argv and the session env [0.13ms]
(pass) AgentAdapter contract — codex > buildPromptText: initial is the skill slash-command on the Epic [0.16ms]
(pass) AgentAdapter contract — codex > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.13ms]
(pass) AgentAdapter contract — codex > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.38ms]
(pass) AgentAdapter contract — codex > classifyStop: blocked.json → asked-question [0.41ms]
(pass) AgentAdapter contract — codex > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.49ms]
(pass) AgentAdapter contract — codex > detectRateLimit is implemented and returns null on a clean transcript [0.18ms]
(pass) AgentAdapter contract — copilot > resolveTranscriptPath yields a path from this adapter's own ready payload [0.20ms]
(pass) AgentAdapter contract — copilot > identity: name matches its registry key and readyEvent is a normalized event [0.16ms]
(pass) AgentAdapter contract — copilot > buildLaunchCommand yields a non-empty argv and the session env [0.21ms]
(pass) AgentAdapter contract — copilot > buildPromptText: initial is the skill slash-command on the Epic [0.14ms]
(pass) AgentAdapter contract — copilot > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.09ms]
(pass) AgentAdapter contract — copilot > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.44ms]
(pass) AgentAdapter contract — copilot > classifyStop: blocked.json → asked-question [0.44ms]
(pass) AgentAdapter contract — copilot > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.41ms]
(pass) AgentAdapter contract — copilot > detectRateLimit is implemented and returns null on a clean transcript [0.15ms]

packages/dispatcher/test/main.test.ts:
(pass) dispatcher main > starts the hook server, announces readiness, and exits 0 on SIGTERM [1552.62ms]
(pass) dispatcher main > hosts a dispatch on its own engine and broadcasts a workflow SSE event [1241.32ms]
(pass) dispatcher main > a terminal prepare-worktree failure marks the row failed, so the next dispatch isn't 409-blocked (issue #179) [3792.97ms]
(pass) dispatcher main > daemon rejects a disabled adapter on /control/dispatch (configured+enabled+implemented gate) [1239.63ms]
(pass) dispatcher main > two concurrent dispatches of the same Epic: exactly one starts, the other 409s [1211.24ms]

packages/dispatcher/test/db.test.ts:
(pass) openDb > opens a file database in WAL mode [13.05ms]
(pass) runMigrations > a fresh db starts at schema version 0 [12.15ms]
(pass) runMigrations > applies every migration and reports the latest version [61.57ms]
(pass) runMigrations > 001_initial creates every documented table [61.32ms]
(pass) runMigrations > 001_initial creates every documented index [63.08ms]
(pass) runMigrations > is idempotent — running twice leaves version at the latest and does not throw [64.30ms]
(pass) runMigrations > 002 adds the waitfor_signals.fired_at column [61.93ms]
(pass) runMigrations > workflows.state CHECK rejects an unknown state [71.23ms]
(pass) runMigrations > workflows.state CHECK accepts 'launching' [65.11ms]
(pass) runMigrations > 003 widens workflows.kind to accept 'documentation' but still rejects unknown kinds [68.49ms]
(pass) runMigrations > 003 preserves existing rows and child FK references through the table rebuild [70.59ms]
(pass) openAndMigrate > opens, migrates, and returns a ready database [65.39ms]

packages/dispatcher/test/retention.test.ts:
(pass) runRetentionPass — events cutoff (14d) > deletes events older than 14 days, keeps newer ones [87.88ms]
(pass) runRetentionPass — events cutoff (14d) > an event exactly at the cutoff age is kept (strict `< cutoff`) [77.91ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > archives completed workflows older than 30 days; drops their events, preserves the row [82.18ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > does not archive completed workflows inside the 30-day window [73.94ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > does not archive old non-completed workflows (failed/running/etc.) [74.59ms]
(pass) runRetentionPass — workflow archival (30d, completed only) > is idempotent — a second pass archives nothing new [79.67ms]
(pass) retention_runs recording > records each pass (even a no-op) with ok=true [67.22ms]
(pass) retention_runs recording > recordRetentionRun with a detail marks ok=false [70.37ms]
(pass) retention_runs recording > an empty-string detail still marks ok=false (failure presence, not truthiness) [65.47ms]
(pass) retention_runs recording > getLatestRetentionRun returns the most recent by ran_at [72.09ms]
(pass) collectRetentionStatus > reports row counts (incl. archived) and the last run [85.26ms]
(pass) collectRetentionStatus > lastRun is null before any retention has run [63.08ms]

packages/dispatcher/test/slots.test.ts:
(pass) getSlotState > free-slot: no active work reports full availability across every dimension [1.86ms]
(pass) getSlotState > at-capacity: a full repo reports zero availability and the guard refuses [1.65ms]
(pass) getSlotState > per-adapter cap binds before the repo cap [1.49ms]
(pass) getSlotState > global cap binds across repos even when this repo has room [1.65ms]
(pass) getSlotState > the recommender's own row is never counted against dispatch slots [1.57ms]
(pass) getSlotState > used over max clamps available to 0 (a tightened cap never goes negative) [1.55ms]
(pass) getSlotState > an adapter with no per-adapter cap is gated only by the repo and global dims [1.52ms]
(pass) reserveSlot > decrements the adapter, repo, and global dimensions for the loop's local view [2.32ms]
(pass) reserveSlot > reserving down to capacity flips the guard to refuse [1.46ms]
(pass) reserveSlot > reserving an adapter with no cap still decrements repo + global [1.43ms]

packages/dispatcher/test/auto-dispatch.test.ts:
(pass) autoDispatch > normal pass: enqueues every ready row that has a free slot [0.41ms]
(pass) autoDispatch > does nothing for a repo whose auto-dispatch is disabled [0.06ms]
(pass) autoDispatch > skips a rate-limited adapter but keeps dispatching others [0.06ms]
(pass) autoDispatch > skips a row whose per-adapter slot is exhausted, continues to the next adapter [0.06ms]
(pass) autoDispatch > stops entirely when the repo total is exhausted (slots-exhausted) [0.04ms]
(pass) autoDispatch > stops when the global total is exhausted even if the repo has room [0.03ms]
(pass) autoDispatch > decrements local counters as it enqueues so a shared cap stops mid-pass [0.06ms]
(pass) autoDispatch > a refused enqueue (collision/null) does not consume a local slot [0.11ms]
(pass) autoDispatch > dispatches a file-mode Epic by its slug ref (#200) [0.10ms]
(pass) autoDispatch > extracts a non-kebab slug ref up to the first space (#200) [0.17ms]
(pass) autoDispatch > ignores the empty-state (no ready rows) without enqueuing [0.11ms]
(pass) autoDispatch > no pre-dispatch complexity gate: a large-sub-issue Epic still dispatches (#52) [0.10ms]
(pass) createParseFailureSurfacer (#180) > surfaces a parse failure on the state issue, with the underlying message [0.20ms]
(pass) createParseFailureSurfacer (#180) > dedupes an identical message across a burst — one comment, not N [0.10ms]
(pass) createParseFailureSurfacer (#180) > reset() re-arms surfacing after a healthy read [0.07ms]
(pass) createParseFailureSurfacer (#180) > a different parse message surfaces even without a reset [0.06ms]
(pass) createParseFailureSurfacer (#180) > ignores non-parse errors so transient gh/network failures never spam [0.03ms]
(pass) createParseFailureSurfacer (#180) > a failed comment is not recorded — the next tick retries (no silent suppression) [0.09ms]
(pass) createParseFailureSurfacer (#180) > dedup is per-repo — two repos with the same message each surface once [0.04ms]
(pass) didReadState (#180) — gate re-arming on an actual read > a `disabled` pass did not read — must NOT re-arm surfacing [0.03ms]
(pass) didReadState (#180) — gate re-arming on an actual read > every reason that runs after readState counts as a read [0.01ms]
(pass) didReadState (#180) — gate re-arming on an actual read > disabled tick does not re-arm; a healthy (drained) read does [0.09ms]

packages/dispatcher/test/pr-divergence.test.ts:
(pass) classifyMergeability > DIRTY → CONFLICTED regardless of mergeable [62.22ms]
(pass) classifyMergeability > BEHIND → BEHIND [62.27ms]
(pass) classifyMergeability > CLEAN + MERGEABLE → CLEAN [62.62ms]
(pass) classifyMergeability > CLEAN but not MERGEABLE → UNKNOWN (CI gating, secondary signals) [64.20ms]
(pass) classifyMergeability > BLOCKED / HAS_HOOKS / UNSTABLE / UNKNOWN → UNKNOWN [63.09ms]
(pass) classifyMergeability > a null view (PR doesn't exist) → UNKNOWN [62.52ms]
(pass) classifyMergeability > missing fields → UNKNOWN (legacy fixtures don't tip the classifier) [59.19ms]
(pass) classifyDivergence > classifies BEHIND and persists the row with the supplied clock [66.17ms]
(pass) classifyDivergence > classifies CONFLICTED and overwrites a prior row (upsert keeps the row fresh) [77.94ms]
(pass) classifyDivergence > classifies CLEAN [72.23ms]
(pass) classifyDivergence > classifies UNKNOWN for a PR with no mergeability view (gone / 404) [66.45ms]
(pass) parseEpicFromHeadRef > parses `middle-issue-<N>` to the integer N [64.66ms]
(pass) parseEpicFromHeadRef > a non-managed head ref → null (the helper skips it) [63.88ms]
(pass) parseEpicFromHeadRef > a malformed managed ref → null (defends against an inadvertent rename) [64.54ms]
(pass) worktreePathFor > uses <root>/<repo>/issue-<n> — the same layout createWorktree writes [68.07ms]
(pass) recordDivergenceState > accepts terminal-ish states (DEMOTED, SKIPPED) written by sibling phases [73.49ms]
(pass) recordDivergenceState > the CHECK constraint rejects an out-of-vocabulary state — defends against a reconciler typo [63.27ms]
(pass) recordDivergenceState > the (repo, pr_number) PK lets the same pr_number coexist across repos [76.47ms]
(pass) applyDemoteToWork > flips PR draft, reopens sub-issue, posts dual-surface comment, re-enqueues, state→DEMOTED [66.92ms]
(pass) applyDemoteToWork > per-step idempotency: a second call skips draft-flip + reopen + comments via markers (but still re-enqueues) [73.18ms]
(pass) applyDemoteToWork > partial-retry: prior attempt left the PR drafted but did not reopen / comment / enqueue — second pass completes remediation [65.73ms]
(pass) applyDemoteToWork > partial-retry safety: existing marker on PR skips the duplicate PR comment, still posts on Epic [66.80ms]
(pass) applyDemoteToWork > Epic with no closed sub-issues: still demotes + comments + enqueues; no reopen call [76.29ms]
(pass) applyDemoteToWork > non-managed head ref → no-op (no draft, no comments, no enqueue, no row) [70.33ms]
(pass) applyDemoteToWork > manual recovery: an Epic that already carries the demote marker skips the reopen call (self-review hardening) [68.09ms]
(pass) applyDemoteToWork > a supplied reason (#201 data-loss) replaces the conflict narrative in the escalation comment [73.44ms]
(pass) applyDemoteToWork > PR doesn't exist (gateway returns null) → no-op [72.80ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a PullRequest with " [72.58ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a Branch with the n" [73.28ms]
(pass) ghStderrIsNotFound > recognizes not-found: "HTTP 404: Not Found (https://api.github." [63.23ms]
(pass) ghStderrIsNotFound > recognizes not-found: "graphql: Could not resolve to a Reposito" [68.49ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "error connecting to api.github.com: dial" [70.74ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 401: Bad credentials" [77.50ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 403: API rate limit exceeded" [65.86ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 502: Bad Gateway" [65.59ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "gh: command failed (oauth token expired)" [65.01ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "could not deserialize response" [71.69ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "remote: secret not found, push declined" [64.95ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "Not Found" [71.31ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "" [64.76ms]

packages/core/test/config.test.ts:
(pass) loadConfig — [docs] section > parses a full docs block [2.09ms]
(pass) loadConfig — [docs] section > a tool/path-only override block is valid; bot fields default [0.34ms]
(pass) loadConfig — [docs] section > absent override fields stay undefined so the resolver auto-detects [0.26ms]
(pass) loadConfig — [docs] section > no [docs] section leaves docs undefined [0.18ms]
(pass) loadConfig — [staleness] section > reads spec_path [0.22ms]
(pass) loadConfig — [staleness] section > no [staleness] section leaves staleness undefined [0.17ms]
(pass) loadConfig — [staleness] section > an empty [staleness] block leaves specPath undefined (falls back to the default) [0.21ms]
(pass) loadConfig — [staleness] section > the local cache overrides committed policy spec_path [0.24ms]
(pass) loadConfig — global only > parses the global sections and leaves per-repo sections undefined [0.22ms]
(pass) loadConfig — global only > expands ~ in path values [0.20ms]
(pass) loadConfig — per-repo merge > populates per-repo sections alongside global [0.39ms]
(pass) loadConfig — per-repo merge > per-repo values override global on a colliding key [0.30ms]
(pass) loadConfig — missing files > missing global file falls back to documented defaults without throwing [0.14ms]
(pass) loadConfig — missing files > missing per-repo file leaves per-repo sections undefined [0.19ms]
(pass) loadConfig — missing files > no paths at all yields an all-defaults config [0.15ms]
(pass) loadConfig — committed policy layer > reads policy.toml as the sibling of repoPath, merged with the local cache [0.28ms]
(pass) loadConfig — committed policy layer > a fresh clone with committed policy but no local cache still reads policy [0.24ms]
(pass) loadConfig — committed policy layer > local cache overrides committed policy on a colliding key [0.28ms]
(pass) loadConfig — committed policy layer > policy overrides the global file on a colliding key [0.29ms]
(pass) loadConfig — committed policy layer > an explicit repoPolicyPath overrides the sibling derivation [0.31ms]
(pass) loadConfig — committed policy layer > no repoPath means no policy is derived (global-only callers unaffected) [0.23ms]

packages/core/test/integration-rubric.test.ts:
(pass) parseAcceptanceCriteria > collects list items under the first acceptance heading, stops at next heading [0.05ms]
(pass) parseAcceptanceCriteria > returns [] when there is no acceptance section [0.02ms]
(pass) parseAcceptanceCriteria > only the first acceptance section counts — a later one does not reopen it [0.01ms]
(pass) isIntegrationCriterion > the spec's worked example is an integration criterion [0.01ms]
(pass) isIntegrationCriterion > 'unit tests pass' alone is not an integration criterion [0.01ms]
(pass) isIntegrationCriterion > wiring without a real-path test fails (behavior, not test) [0.02ms]
(pass) isIntegrationCriterion > a real-path test without wiring fails
(pass) isIntegrationCriterion > prose 'get' does not trip the uppercase HTTP-verb signal
(pass) isIntegrationCriterion > served + e2e qualifies
(pass) isIntegrationCriterion > plural 'integration tests' / 'smoke tests' phrasing still qualifies [0.01ms]
(pass) detectExemption > reads an inline annotation and a comment form [0.02ms]
(pass) auditIssueBody > passes a body with an integration criterion [0.03ms]
(pass) auditIssueBody > flags a weak body and suggests a concrete rewrite naming the feature [0.03ms]
(pass) auditIssueBody > flags a body with no acceptance section, suggestion says so [0.01ms]
(pass) auditIssueBody > a declared exemption passes and surfaces the reason [0.01ms]

packages/core/test/hook-script.test.ts:
(pass) PR_READY_GATE_SH exit-code contract > HTTP 200 → exit 0 (allow) [2.38ms]
(pass) PR_READY_GATE_SH exit-code contract > curl failure emitting no http code → exit 0 (fails OPEN, not closed) [1.93ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 403 from a reachable dispatcher → exit 2 (blocks) [2.16ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 404 (no gate wired — e.g. a recommender/docs session) → exit 0 (allow, never wedge) [1.95ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 401 (reachable bad-token/missing-session) → exit 2 (surface, don't silently disable the guard) [2.18ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 500 (reachable dispatcher fault) → exit 2 (surface, not a silent allow) [3.36ms]

packages/core/test/select-adapter.test.ts:
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > an agent:<name> label pins that adapter over the default [0.14ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > whitespace around the label and name is tolerated [0.02ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > conflicting agent labels throw [0.07ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > duplicate agent labels for the same name are not a conflict [0.02ms]
(pass) selectAdapter — rule 1: explicit agent:<name> label overrides > a label naming an unconfigured adapter throws [0.02ms]
(pass) selectAdapter — rule 2: default adapter > with no agent label, the default adapter is chosen [0.01ms]
(pass) selectAdapter — rule 2: default adapter > a default adapter that isn't configured throws [0.03ms]
(pass) selectAdapter — rule 3: switch away from a rate-limited adapter when portable > a rate-limited default switches to an available adapter for a portable task [0.03ms]
(pass) selectAdapter — rule 3: switch away from a rate-limited adapter when portable > a label pin is never switched away from, even when rate-limited and portable [0.02ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a rate-limited default with a non-portable task is left and marked skip [0.01ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a portable task with no non-rate-limited alternative is left and marked skip [0.01ms]
(pass) selectAdapter — rule 4: otherwise leave it for auto-dispatch to skip > a non-rate-limited choice is never marked skip [0.01ms]

packages/core/test/tmux-tui.test.ts:
(pass) capturePane > returns the visible pane contents of a live session [155.40ms]
(pass) capturePane > returns null for an unknown session [1.42ms]
(pass) sendText and sendKeys > sendText writes literal text into the pane [158.30ms]
(pass) sendText and sendKeys > sendKeys with delayBetweenMs sends each key in its own call [223.47ms]
(pass) pollPaneFor > resolves with the predicate's value when the pane matches [312.94ms]
(pass) pollPaneFor > returns null on timeout when the pane never matches [414.71ms]
(pass) pollPaneFor > returns null and bails when the session disappears [1.65ms]
(pass) pollPaneFor > when `tag` is set, writes one stderr line per iteration [4.30ms]

packages/adapters/codex/test/adapter.test.ts:
(pass) codexAdapter identity > name is 'codex' and readyEvent is session.started [0.22ms]
(pass) buildLaunchCommand > argv launches interactive codex (no exec, no prompt) [0.15ms]
(pass) buildLaunchCommand > env sets CODEX_HOME to the worktree-local .codex so the config is loaded [0.12ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.13ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.12ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.12ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.10ms]
(pass) buildPromptText > recommender force-invokes the recommender skill with the @-referenced context [0.09ms]
(pass) buildPromptText > docs force-invokes the documenting-the-repo skill with the @-referenced context [0.09ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.11ms]
(pass) resolveTranscriptPath > returns transcript_path from the SessionStart payload [0.11ms]
(pass) resolveTranscriptPath > falls back to rollout_path when transcript_path is absent [0.11ms]
(pass) resolveTranscriptPath > throws when the payload carries no session-file path [0.11ms]
(pass) readTranscriptState > parses a real-shaped rollout: activity, turn count, last tool use, context tokens [0.29ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.26ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.38ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.34ms]
(pass) classifyStop > asked-question tolerates a malformed blocked.json (sentinel → null) [0.33ms]
(pass) classifyStop > structured rate_limits with rate_limit_reached_type → rate-limited, resetAt from resets_at [0.43ms]
(pass) classifyStop > structured rate_limits at/over 100% used → rate-limited even without reached_type [0.35ms]
(pass) classifyStop > a healthy structured block is authoritative → bare-stop, even with a stray '429' in text [0.33ms]
(pass) classifyStop > text fallback (no structured block): "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.34ms]
(pass) classifyStop > text fallback (no structured block): "Error 429: Too Many Requests" → rate-limited (429 status) [0.28ms]
(pass) classifyStop > text fallback (no structured block): "too many requests — slow down" → rate-limited (too many requests phrase) [0.29ms]
(pass) classifyStop > text fallback (no structured block): "ratelimit exceeded" → rate-limited (ratelimit no-space) [0.27ms]
(pass) classifyStop > a bare "line 4290 of the file" is NOT a rate-limit signal → bare-stop (4290 — a line number) [0.32ms]
(pass) classifyStop > a bare "commit 4291abcdef" is NOT a rate-limit signal → bare-stop (4291 in a hash) [0.28ms]
(pass) classifyStop > a bare "listening on port 14290" is NOT a rate-limit signal → bare-stop (embedded 4290) [0.26ms]
(pass) classifyStop > a bare "processed 42900 rows" is NOT a rate-limit signal → bare-stop (42900) [0.26ms]
(pass) classifyStop > done.json sentinel → done [0.55ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.59ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.41ms]
(pass) classifyStop > nothing notable → bare-stop [0.38ms]
(pass) detectRateLimit > structured block at the limit → detection with the real reset time [0.53ms]
(pass) detectRateLimit > text fallback matches a rate-limit signal when no structured block exists [0.26ms]
(pass) detectRateLimit > returns null when a healthy structured block is present [0.16ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present at all [0.28ms]
(pass) installHooks > writes .codex/config.toml with auto-mode + sandbox_mode (NOT the rejected 'sandbox' key) [3.07ms]
(pass) installHooks > pre-trusts the worktree directory so codex skips the directory-trust dialog [1.10ms]
(pass) installHooks > maps each real Codex event to the normalized taxonomy via the absolute hook path [1.13ms]
(pass) installHooks > registers exactly the real Codex event set (PascalCase, no fictional names) [1.11ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.12ms]
(pass) installHooks > registers the PR-ready gate as a SECOND PreToolUse matcher group scoped to Bash [1.04ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.07ms]
(pass) installHooks > symlinks the operator's auth.json into the worktree CODEX_HOME [1.05ms]
(pass) installHooks > does not throw or create a link when the operator has no auth.json [0.97ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.22ms]
(pass) detectNeedsLogin > does not match normal pane content [0.13ms]
(pass) detectHooksTrustPrompt > matches the real 'Hooks need review' dialog text [0.14ms]
(pass) detectHooksTrustPrompt > does not match a normal pane or the directory-trust dialog [0.13ms]
(pass) detectDirTrustPrompt > matches the real first-run directory-trust dialog text [0.13ms]
(pass) detectDirTrustPrompt > does not match a normal pane or the hooks-trust dialog [0.12ms]
(pass) detectReadyForInput > matches the live composer-ready welcome banner (codex 0.133.0) [0.13ms]
(pass) detectReadyForInput > does not match a boot dialog (so a dialog is answered before we treat it as ready) [0.10ms]
(pass) startsSessionOnFirstPrompt > codex sets the prompt-triggered-session flag (it fires no SessionStart until a prompt) [0.09ms]
(pass) enterAutoMode > returns immediately when the target session does not exist [2.09ms]

packages/adapters/claude/test/adapter.test.ts:
(pass) claudeAdapter identity > name is 'claude' and readyEvent is session.started [0.21ms]
(pass) claudeAdapter identity > does NOT set startsSessionOnFirstPrompt — Claude fires SessionStart at boot, so the dispatcher keeps await-first order (#183 regression) [0.12ms]
(pass) buildLaunchCommand > argv launches interactive claude in auto mode via --dangerously-skip-permissions [0.12ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.12ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.11ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.11ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.12ms]
(pass) buildPromptText > recommender force-invokes the recommender skill with the @-referenced context [0.11ms]
(pass) buildPromptText > docs force-invokes the documenting-the-repo skill with the @-referenced context [0.10ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.11ms]
(pass) resolveTranscriptPath > returns transcript_path from the SessionStart payload [0.12ms]
(pass) resolveTranscriptPath > throws when the payload has no transcript_path [0.11ms]
(pass) readTranscriptState > parses activity, turn count, last tool use, and context tokens [0.31ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.37ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.39ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.34ms]
(pass) classifyStop > an unrecognized kind falls back to a plain question (kind omitted) [0.31ms]
(pass) classifyStop > asked-question tolerates a malformed/contentless blocked.json (sentinel → null) [0.35ms]
(pass) classifyStop > usage-limit message in the transcript tail → rate-limited [0.33ms]
(pass) classifyStop > done.json sentinel → done [0.31ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.31ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.40ms]
(pass) classifyStop > nothing notable → bare-stop [0.28ms]
(pass) detectRateLimit > matches a usage-limit message in the transcript tail [0.15ms]
(pass) detectRateLimit > returns null when no usage-limit message is present [0.14ms]
(pass) installHooks > registers the full Claude hook event set in .claude/settings.json [1.05ms]
(pass) installHooks > each entry maps its Claude event to the normalized taxonomy via the absolute hook path [1.02ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [0.95ms]
(pass) installHooks > registers the PR-ready gate as a second Bash-matched PreToolUse hook [0.93ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.18ms]
(pass) detectBypassPrompt > matches representative bypass-mode confirmation strings [0.17ms]
(pass) detectBypassPrompt > does not match normal Claude pane content [0.10ms]
(pass) detectTrustPrompt > matches the first-run folder-trust dialog [0.14ms]
(pass) detectTrustPrompt > does not match the bypass dialog or normal content [0.14ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.21ms]
(pass) detectNeedsLogin > does not match the bypass prompt or normal pane content [0.12ms]
(pass) enterAutoMode > returns immediately when the target session does not exist [2.02ms]

packages/adapters/copilot/test/adapter.test.ts:
(pass) copilotAdapter identity > name is 'copilot' and readyEvent is session.started [0.23ms]
(pass) copilotAdapter identity > sets the prompt-triggered-session flag (fires no sessionStart until a prompt) [0.12ms]
(pass) buildLaunchCommand > argv launches interactive copilot in auto mode (no -p, no prompt) [0.13ms]
(pass) buildLaunchCommand > env sets COPILOT_HOME to the worktree-local .copilot so the config + hooks load [0.12ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.12ms]
(pass) buildLaunchCommand > forwards an exported gh token so token-auth keeps working under the repointed home [0.16ms]
(pass) buildPromptText > initial force-invokes the skill via slash command on the epic [0.13ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.11ms]
(pass) buildPromptText > answer frames the @-reference as a human reply [0.10ms]
(pass) buildPromptText > recommender / docs force-invoke their skill with the @-referenced context [0.11ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.11ms]
(pass) resolveTranscriptPath > derives <cwd>/.copilot/session-state/<sessionId>/events.jsonl from the payload [0.13ms]
(pass) resolveTranscriptPath > falls back to snake_case session_id defensively [0.10ms]
(pass) resolveTranscriptPath > throws when the payload carries no sessionId [0.14ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "../../../../etc/passwd" (defense-in-depth against path escape) [0.13ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "a/b" (defense-in-depth against path escape) [0.10ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId ".." (defense-in-depth against path escape) [0.08ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "id with spaces" (defense-in-depth against path escape) [0.09ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "id;rm -rf" (defense-in-depth against path escape) [0.08ms]
(pass) readTranscriptState > parses a real-shaped events.jsonl: activity, turn count, last tool use, context tokens [0.29ms]
(pass) readTranscriptState > counts each assistant.turn_end as a turn [0.20ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.18ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.35ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.47ms]
(pass) classifyStop > asked-question tolerates a malformed blocked.json (sentinel → null) [0.32ms]
(pass) classifyStop > rate-limit text "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.32ms]
(pass) classifyStop > rate-limit text "Error 429: Too Many Requests" → rate-limited (429 status) [0.27ms]
(pass) classifyStop > rate-limit text "too many requests — slow down" → rate-limited (too many requests phrase) [0.30ms]
(pass) classifyStop > rate-limit text "ratelimit exceeded" → rate-limited (ratelimit no-space) [0.26ms]
(pass) classifyStop > rate-limit text "weekly quota exceeded for this model" → rate-limited (quota exceeded) [0.26ms]
(pass) classifyStop > rate-limit text "You have reached your usage limit" → rate-limited (usage limit) [0.26ms]
(pass) classifyStop > a bare "line 4290 of the file" is NOT a rate-limit signal → bare-stop (4290 — a line number) [0.30ms]
(pass) classifyStop > a bare "commit 4291abcdef" is NOT a rate-limit signal → bare-stop (4291 in a hash) [0.26ms]
(pass) classifyStop > a bare "listening on port 14290" is NOT a rate-limit signal → bare-stop (embedded 4290) [0.30ms]
(pass) classifyStop > a bare "processed 42900 rows" is NOT a rate-limit signal → bare-stop (42900) [0.25ms]
(pass) classifyStop > done.json sentinel → done [0.49ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.60ms]
(pass) classifyStop > done.json outranks stale rate-limit text in the transcript → done [0.35ms]
(pass) classifyStop > failed.json outranks stale rate-limit text in the transcript → failed [0.34ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.40ms]
(pass) classifyStop > nothing notable → bare-stop [0.28ms]
(pass) detectRateLimit > text rate-limit signal → detection with unknown reset (no structured block on disk) [0.17ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present [0.22ms]
(pass) installHooks > writes .copilot/hooks/middle.json with version 1 and the camelCase event keys [1.14ms]
(pass) installHooks > maps each Copilot event to the normalized taxonomy via the absolute hook path [1.05ms]
(pass) installHooks > registers the PR-ready gate as a SECOND preToolUse hook scoped to the bash tool [0.97ms]
(pass) installHooks > pre-trusts the worktree in config.json so copilot skips the folder-trust dialog [2.35ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.12ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [0.89ms]
(pass) installHooks > writes NO auth file (copilot authenticates via gh, unlike codex) [0.85ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.21ms]
(pass) detectNeedsLogin > does not match normal pane content [0.11ms]
(pass) detectReadyForInput > matches the live composer-ready footer / prompt (copilot 1.0.54) [0.17ms]
(pass) detectReadyForInput > does not match a bare boot screen with no composer [0.12ms]
(pass) detectTrustPrompt > matches a folder-trust dialog (defense-in-depth; pre-empted by trustedFolders) [0.12ms]
(pass) detectTrustPrompt > does not match a normal pane [0.10ms]
(pass) enterAutoMode > throws fast when the target session does not exist (never treated as ready) [1.83ms]

packages/dispatcher/test/epic-store/file-state-gateway.test.ts:
(pass) fileStateGateway > readBody returns the state file contents verbatim [0.56ms]
(pass) fileStateGateway > readBody throws a clear error when the state file is absent [0.19ms]
(pass) fileStateGateway > writeBody creates the parent directory and round-trips [0.30ms]
(pass) fileStateGateway > writeBody is atomic: leaves no `.tmp` sibling after a successful write [0.33ms]
(pass) fileStateGateway > writeBody derives the temp sibling from the filename via `basename` (separator-safe) [0.37ms]
(pass) fileStateGateway > writeBody overwrites an existing file [0.21ms]

packages/dispatcher/test/epic-store/file-poll-gateway.test.ts:
(pass) filePollGateway > listIssueComments derives authorIsBot structurally from the marker kind [0.83ms]
(pass) filePollGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.16ms]
(pass) filePollGateway > findPrForEpic resolves a slug via meta.pr; delegates a numeric ref to gh's finder [0.41ms]
(pass) filePollGateway > findPrForEpic returns null for a slug whose Epic file has no stamped meta.pr [0.24ms]
(pass) filePollGateway > findEpicPrLifecycle resolves a slug via meta.pr; delegates a numeric ref to gh [0.31ms]
(pass) filePollGateway > findEpicPrLifecycle returns null for a slug with no stamped meta.pr [0.23ms]
(pass) filePollGateway > a numeric-named file Epic (e.g. 42.md) resolves via meta.pr, not gh's #42 finder (#200) [0.26ms]
(pass) filePollGateway > prSnapshot / prLifecycle delegate straight to gh by PR number [0.17ms]
(pass) filePollGateway > getRateLimit delegates straight to gh [0.15ms]

packages/dispatcher/test/epic-store/file-epic-gateway.test.ts:
(pass) fileEpicGateway > listOpenEpics scans the dir, derives sub-issue progress, skips closed [0.69ms]
(pass) fileEpicGateway > listIssueComments maps the conversation; answer is attributed to the human [0.49ms]
(pass) fileEpicGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.16ms]
(pass) fileEpicGateway > getCommentAuthor discriminates human (answer) from agent by the file:// fragment [0.17ms]
(pass) fileEpicGateway > getCommentAuthor delegates a github.com URL to gh [0.14ms]
(pass) fileEpicGateway > getIssueLabels reads the Epic meta labels [0.21ms]
(pass) fileEpicGateway > postComment appends a re-parseable dispatch-event block [0.51ms]
(pass) fileEpicGateway > postComment delegates a PR-number ref to gh (no Epic file for it) [0.17ms]
(pass) fileEpicGateway > findEpicPr returns null without a stamped pr, and delegates to gh when present [0.31ms]
(pass) fileEpicGateway > findEpicPr returns null when the Epic file is absent [0.14ms]
(pass) fileEpicGateway > addLabel appends to meta labels and is a no-op if already present [0.54ms]
(pass) fileEpicGateway > a present-but-malformed Epic file surfaces the parser's named error [0.21ms]
(pass) fileEpicGateway > postComment writes atomically — no `.tmp` sibling left behind [0.30ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > re-asking the identical open question is a no-op [0.48ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > a different question (or different kind/context) appends a new entry [0.90ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > round-trip purity survives the skip (renderer remains the sole marker writer) [0.34ms]

packages/dispatcher/test/epic-store/round-trip.test.ts:
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(empty-epic.md)) === empty-epic.md [0.07ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(all-closed.md)) === all-closed.md [0.13ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(codex-adapter.md)) === codex-adapter.md [0.06ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(mid-question.md)) === mid-question.md [0.06ms]

packages/dispatcher/test/epic-store/mode-commands-mirror.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-mirror-IUXKSb/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-IUXKSb/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) dispatch brief — mode-commands mirror (#195) > a file-mode dispatch mirrors file-mode-commands.md into the worktree, byte-identical [241.01ms]
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-mirror-K6T0mD/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-K6T0mD/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) dispatch brief — mode-commands mirror (#195) > a github-mode dispatch does not mirror the file-mode reference [285.38ms]

packages/dispatcher/test/epic-store/file-dispatch-integration.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fdisp-9xLO0F/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fdisp-9xLO0F/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) file-mode dispatch — Test A: real workflow drive > a file-mode Epic parks asking a question → row carries the slug, Epic file gains a question block [297.10ms]
(pass) file-mode dispatch — Test B: real buildImplementationDeps selector > postQuestion routes to the Epic file for a file repo, and to gh for a github repo [185.96ms]

packages/dispatcher/test/epic-store/parity.test.ts:
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-LATueo/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-LATueo/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] Stop received — classification=bare-stop
(pass) implementation parity — github mode > happy-path dispatch reaches completed [276.94ms]
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-zTCfDi/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-zTCfDi/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-6] Stop received — classification=asked-question
[workflow:middle-o-parity-repo-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-6] installing hooks in /tmp/middle-parity-zTCfDi/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-zTCfDi/worktrees/o/parity-repo/issue-6)
[workflow:middle-o-parity-repo-6] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-6] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-6] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-6] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-6] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-6] Stop received — classification=bare-stop
(pass) implementation parity — github mode > park → resume-answer → continuation reaches completed [318.46ms]
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-2pra39/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-2pra39/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=bare-stop
(pass) implementation parity — file mode > happy-path dispatch reaches completed [219.02ms]
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-U0XVJT/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-U0XVJT/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=asked-question
[workflow:middle-o-parity-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-parity-repo-rollout-epic-store] installing hooks in /tmp/middle-parity-U0XVJT/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-U0XVJT/worktrees/o/parity-repo/issue-rollout-epic-store)
[workflow:middle-o-parity-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-parity-repo-rollout-epic-store] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-parity-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-parity-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-parity-repo-rollout-epic-store] Stop received — classification=bare-stop
(pass) implementation parity — file mode > park → resume-answer → continuation reaches completed [303.73ms]

packages/dispatcher/test/epic-store/file-watcher-integration.test.ts:
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fw-ZHNsHc/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-ZHNsHc/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (initial): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
[workflow:middle-o-file-repo-rollout-epic-store] ensuring .middle/prompt.md exists in worktree
[workflow:middle-o-file-repo-rollout-epic-store] installing hooks in /tmp/middle-fw-ZHNsHc/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-ZHNsHc/worktrees/o/file-repo/issue-rollout-epic-store)
[workflow:middle-o-file-repo-rollout-epic-store] starting bypass-prompt dismisser (parallel to SessionStart wait)
[workflow:middle-o-file-repo-rollout-epic-store] waiting for SessionStart hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] SessionStart received — session_id=stub
[workflow:middle-o-file-repo-rollout-epic-store] sending prompt (answer): "@.middle/prompt.md"
[workflow:middle-o-file-repo-rollout-epic-store] waiting for Stop hook (timeout 2000ms)
[workflow:middle-o-file-repo-rollout-epic-store] session-ended with blocked.json present — parking for resume
[workflow:middle-o-file-repo-rollout-epic-store] Stop received — classification=asked-question
(pass) file-watcher Q&A loop (#197) > poller cron detects a non-empty answer edit and resumes the parked Epic to completion [421.45ms]

packages/dispatcher/test/epic-store/watcher.test.ts:
(pass) collectChangedSince > includes files with mtime > sinceMs, excludes older + dotfiles/.tmp [0.33ms]
(pass) collectChangedSince > missing dir → empty [0.13ms]
(pass) pollFileSignals > emits an open question that has a non-empty answer [0.19ms]
(pass) pollFileSignals > an unanswered question (placeholder) does NOT trigger [0.23ms]
(pass) pollFileSignals > a resolved question does NOT trigger (only the first non-empty edit fires) [0.18ms]
(pass) pollFileSignals > the mtime gate skips unchanged files [0.15ms]
(pass) resolveQuestion > flips an open question to resolved (the dedup write); idempotent [0.35ms]
(pass) resolveQuestion > a missing file/question is a no-op [0.15ms]
(pass) runFileWatcherTick > fires the resume + resolves the question for an answered-question park [83.42ms]
(pass) runFileWatcherTick > does NOT resume a workflow parked on a non-answered signal (reason guard) [73.31ms]

packages/dispatcher/test/epic-store/selector.test.ts:
(pass) buildGitHubGateways / buildFileGateways > buildGitHubGateways defaults to the real gh-backed trio [0.06ms]
(pass) buildGitHubGateways / buildFileGateways > buildFileGateways returns file-backed implementations (not the gh trio) [0.21ms]
(pass) makeRoutingEpicGateway > routes per-repo: file repo → file backend, github repo → gh backend [71.98ms]
(pass) makeRoutingPollGateway > a file-mode slug never reaches gh's numeric PR-finders; github delegates [72.63ms]
(pass) appendQuestion > appends an open question block that re-parses; ids increment [0.67ms]
(pass) appendQuestion > throws a clear error when the Epic file is absent [0.22ms]

packages/dispatcher/test/epic-store/file-gateways-integration.test.ts:
(pass) file gateways — Phase-1 lifecycle integration > dispatch-event record, human answer on disk, poll surfaces the human reply [0.99ms]
(pass) file gateways — Phase-1 lifecycle integration > state gateway round-trips the recommender state file atomically [0.33ms]

packages/dispatcher/test/epic-store/file-review-resume-integration.test.ts:
(pass) file-mode PR-review resume (real poller path) > a CHANGES_REQUESTED review on the stamped PR resumes the parked file-mode Epic [84.84ms]
(pass) file-mode PR-review resume (real poller path) > no resume while the Epic file has no stamped meta.pr (PR not opened yet) [83.98ms]

packages/dispatcher/test/epic-store/parser.test.ts:
(pass) parseEpicFile — document structure > parses the document marker, title, and minimal meta from an empty Epic [1.41ms]
(pass) parseEpicFile — document structure > throws when the document marker is missing [0.09ms]
(pass) parseEpicFile — document structure > throws when the meta block has no slug key [0.03ms]
(pass) parseEpicFile — meta > parses every recognized meta key from codex-adapter fixture [0.10ms]
(pass) parseEpicFile — meta > parses closed=true [0.09ms]
(pass) parseEpicFile — acceptance criteria > parses unchecked criteria from codex-adapter [0.06ms]
(pass) parseEpicFile — acceptance criteria > parses checked criteria from all-closed [0.05ms]
(pass) parseEpicFile — sub-issues > parses sub-issues with stable IDs + body [0.05ms]
(pass) parseEpicFile — sub-issues > parses checked sub-issues + provenance suffix [0.06ms]
(pass) parseEpicFile — conversation > parses dispatch-event + question entries; empty answer block stays absent [0.11ms]
(pass) parseEpicFile — conversation > treats a non-empty answer block as the resolved reply [0.06ms]
(pass) parseEpicFile — conversation > empty conversation block yields empty conversation array [0.03ms]

packages/dispatcher/test/epic-store/file-auto-dispatch-integration.test.ts:
(pass) file-mode auto-dispatch (real readState path) > reads the state_file and enqueues a file Epic by its slug ref [68.80ms]
(pass) file-mode auto-dispatch (real readState path) > a github-mode repo still routes readState to the gh state issue gateway [65.48ms]

packages/dispatcher/test/gates/verify-config.test.ts:
(pass) parseVerifyConfig — valid > parses gates in declared order and applies the default timeout [0.08ms]
(pass) parseVerifyConfig — valid > carries an optional phases scope [0.05ms]
(pass) parseVerifyConfig — valid > category defaults to unit and accepts integration; integrationGates filters [0.06ms]
(pass) gatesForPhase — per-phase addressing > an unscoped gate runs for every phase [0.03ms]
(pass) gatesForPhase — per-phase addressing > a scoped gate runs only for its listed phases, preserving declared order [0.02ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: no gates [0.04ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: missing name [0.02ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty name [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: missing command
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty command [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: duplicate name [0.04ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: non-positive timeout [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: non-int phases [0.06ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: negative phases [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty phases
(pass) parseVerifyConfig — malformed fails loudly > rejects: unknown key [0.05ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: invalid category [0.05ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: invalid toml [0.07ms]
(pass) parseVerifyConfig — malformed fails loudly > the unknown-key message lists the live key set (incl. `category`) [0.07ms]
(pass) loadVerifyConfig — file IO > loads a valid file from disk [0.32ms]
(pass) loadVerifyConfig — file IO > a missing file fails loudly with the path in the message [0.08ms]
(pass) loadVerifyConfig — file IO > verifyConfigPath resolves the worktree's .middle/verify.toml [0.02ms]

packages/dispatcher/test/gates/plan-comment.test.ts:
(pass) verifyPlanComment > passes when a comment by the agent's account contains the plan body [0.10ms]
(pass) verifyPlanComment > fails with the exact reason when no comment contains the plan body [0.05ms]
(pass) verifyPlanComment > fails when the plan body was posted by a different account [0.03ms]
(pass) verifyPlanComment > tolerates CRLF and trailing-whitespace differences between comment and plan [0.04ms]
(pass) verifyPlanComment > matches regardless of author when no agentLogin filter is supplied [0.03ms]
(pass) verifyPlanComment > an empty plan body never vacuously passes [0.03ms]

packages/dispatcher/test/gates/checkbox-revert.test.ts:
(pass) parseStatusCheckboxes > extracts one entry per Status line carrying a #N reference, stopping at the next heading [0.21ms]
(pass) parseStatusCheckboxes > returns [] when there is no Status section [0.02ms]
(pass) parseStatusCheckboxes > a lookalike heading (## Status notes) does not shadow the real ## Status [0.02ms]
(pass) parseStatusCheckboxes > only a level-2 ## Status heading starts the section (# / ### Status ignored) [0.01ms]
(pass) parseStatusCheckboxes > a ## Status / checkbox inside a fenced code block does not shadow the real section [0.06ms]
(pass) parseStatusCheckboxes > mixed fence delimiters: a ~~~ inside a ``` block does not reopen real parsing [0.06ms]
(pass) parseStatusCheckboxes > only the FIRST ## Status section is parsed; a later one is ignored [0.02ms]
(pass) reconcileCheckboxes > a passing [ ]→[x] transition is left checked, no comment, state recorded [0.32ms]
(pass) reconcileCheckboxes > a failing [ ]→[x] transition is reverted and a comment names the failed gate [0.21ms]
(pass) reconcileCheckboxes > a box already checked on the previous pass is not re-run [0.07ms]
(pass) reconcileCheckboxes > a revert touches only the Status section, not the same #N checkbox elsewhere [0.06ms]
(pass) reconcileCheckboxes > with several transitions, only the failing sub-issue is reverted [0.07ms]

packages/dispatcher/test/gates/pr-ready-handler.test.ts:
(pass) pr-ready gate handler > allows a non-`gh pr ready` command without touching GitHub [0.20ms]
(pass) pr-ready gate handler > allows when the Epic PR's criteria are all evidenced [0.13ms]
(pass) pr-ready gate handler > denies when the Epic PR has unevidenced criteria [0.07ms]
(pass) pr-ready gate handler > denies when no open Epic PR can be found [0.05ms]
(pass) pr-ready gate handler > denies when the session maps to no active workflow [0.04ms]

packages/dispatcher/test/gates/gate-runner.test.ts:
(pass) runGate > a passing gate captures stdout and exit 0 [0.94ms]
(pass) runGate > a failing gate captures the non-zero exit and stderr [0.61ms]
(pass) runGate > a gate that exceeds its timeout is killed and reported as timed out [700.70ms]
(pass) runGate > runs in the given cwd [1.92ms]
(pass) runGates > runs every gate in declared order; aggregate ok when all pass [1.26ms]
(pass) runGates > a failing gate makes the aggregate fail and names the first failure; later gates still run [1.52ms]
(pass) runGates > an empty gate list is a vacuous pass [0.06ms]

packages/dispatcher/test/gates/verify.test.ts:
(pass) verification gates wired into checkbox-revert (end to end) > a failing phase's box is reverted; a passing phase's box stays checked [1.97ms]
(pass) verification gates wired into checkbox-revert (end to end) > evidence is posted for both phases and a revert notice names the failed gate [1.49ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > an evidence-upsert failure yields ok:false (not a throw), preserving a real gate failure [1.33ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > a gate-runner failure (worktree gone) yields ok:false instead of throwing [0.42ms]
(pass) verification gates wired into checkbox-revert (end to end) > seam never throws into the reconcile loop > reconcileCheckboxes still processes every transition + persists state when evidence fails [1.43ms]
(pass) verification gates wired into checkbox-revert (end to end) > re-running after a fix keeps the box checked and updates evidence in place [1.50ms]

packages/dispatcher/test/gates/pr-ready.test.ts:
(pass) parseAcceptanceCriteria > extracts the list items under the acceptance-criteria heading only [0.07ms]
(pass) parseAcceptanceCriteria > returns [] when there is no acceptance-criteria section [0.02ms]
(pass) commandIsPrReady > matches a bare and an argumented `gh pr ready` [0.02ms]
(pass) commandIsPrReady > does not match other gh commands
(pass) extractCommand > reads tool_input.command from a Claude/Codex PreToolUse payload [0.01ms]
(pass) extractCommand > parses Copilot's string-encoded toolArgs (else the gate never fires for copilot) [0.04ms]
(pass) extractCommand > accepts a tool_args object as a defensive snake_case variant
(pass) extractCommand > returns null on malformed toolArgs JSON rather than throwing [0.03ms]
(pass) extractCommand > returns null when there is no command [0.02ms]
(pass) evaluatePrReady > allows when every criterion carries an evidence link or a non-bot deferral [0.09ms]
(pass) evaluatePrReady > denies and names the criterion that has no evidence [0.06ms]
(pass) evaluatePrReady > a `#N` reference counts as an evidence link [0.04ms]
(pass) evaluatePrReady > a stakeholder-deferred criterion (non-bot comment) is allowed [0.04ms]
(pass) evaluatePrReady > a deferral pointing at a bot comment is denied [0.06ms]
(pass) evaluatePrReady > evidence still satisfies a criterion whose deferral is invalid (OR semantics) [0.06ms]
(pass) evaluatePrReady > two bot deferrals and no real evidence is denied (no second-annotation leak) [0.05ms]
(pass) evaluatePrReady > denies when there is no acceptance-criteria section (no bypass by deletion) [0.03ms]
(pass) evaluatePrReady — integration evidence > denies a unit-only PR: every criterion evidenced, none an integration test [0.04ms]
(pass) evaluatePrReady — integration evidence > allows when an integration criterion is evidenced by a named test file [0.04ms]
(pass) evaluatePrReady — integration evidence > a human-authored integration-exempt annotation allows [0.04ms]
(pass) evaluatePrReady — integration evidence > a bot-authored integration-exempt annotation is denied [0.04ms]
(pass) evaluatePrReady — integration evidence > an evidenced integration criterion allows even if a stray bot exemption is present [0.07ms]
(pass) evaluatePrReady — integration evidence > a deferred integration criterion does not count as integration evidence [0.06ms]

packages/dispatcher/test/gates/gate-evidence.test.ts:
(pass) renderEvidence > carries the per-phase marker so re-runs can find it [0.02ms]
(pass) renderEvidence > summarizes each gate's pass/fail in a table [0.05ms]
(pass) renderEvidence > puts full gate output inside collapsed <details> blocks [0.02ms]
(pass) renderEvidence > fences output that itself contains backticks without breaking the block [0.04ms]
(pass) upsertEvidenceComment > posts a fresh comment when none exists for the phase [0.17ms]
(pass) upsertEvidenceComment > re-runs update the same comment in place rather than posting a duplicate [0.20ms]
(pass) upsertEvidenceComment > a different phase's evidence gets its own comment [0.10ms]

packages/dispatcher/test/gates/checkbox-revert-pass.test.ts:
(pass) runCheckboxRevertPass > reverts a failing-gate checkbox after a push: body, comment, persisted state [84.98ms]
(pass) runCheckboxRevertPass > a passing-gate checkbox stays checked; SHA + state persisted [78.25ms]
(pass) runCheckboxRevertPass > head-SHA gate: an unchanged SHA skips a would-be transition entirely [77.15ms]
(pass) runCheckboxRevertPass > an advanced SHA re-processes: the new transition's gate runs and reverts [82.82ms]
(pass) runCheckboxRevertPass > undefined gateway SHA falls through to the reconciler's checkbox-state diff [76.62ms]
(pass) runCheckboxRevertPass > no usable verify.toml → the workflow is skipped (nothing to enforce) [71.58ms]
[checkbox-revert] GitHub budget low (10 < 100); skipping pass — resets 1970-01-01T00:00:00.000Z
(pass) runCheckboxRevertPass > rate-limit ceiling skips the whole pass before any GitHub call [69.19ms]
[checkbox-revert] pass failed for workflow bad (o/r#1): GitHub down
(pass) runCheckboxRevertPass > a per-workflow failure is isolated — other workflows still process [86.67ms]
(pass) runCheckboxRevertPass > a parked (non-running) workflow is not processed [70.87ms]

 1411 pass
 0 fail
 3550 expect() calls
Ran 1411 tests across 127 files. [86.41s]

…tion

Self-review (internal code-review pass) findings:
- resolveBlockers: an open blocker with an empty/whitespace title produced
  '#42 ()', which the verify step's validate() then rejected — fall back to the
  bare ref when the sanitized title is empty; also truncate the annotation title
  to 60 chars (consistent with the Epic-cell rule).
- repo-config: normalize checkout paths (resolve) on store + collision-compare so
  trailing-slash / dot-segment spellings of one directory don't slip the #226
  guard. Symlinks remain out of scope (documented).

Each with a regression test.

@thejustinwalsh thejustinwalsh left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decision-log highlights, distilled inline (rationale lives in planning/issues/211/decisions.md).

* Returns the original `state` object unchanged when nothing reclassified (so a
* no-resolvable-blockers pass is a cheap no-op the caller can skip writing).
*/
export async function resolveBlockers(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#225 design. Resolution is a deterministic post-agent step, not the agent's job — the audit's finding was that no code consumes blockedItem.blocker. A closed blocker → Ready to dispatch (best-effort row the next full recommender run re-ranks); open → stays blocked, annotated with the title; unresolvable → (stale blocker: <ref>). The "needs-human if its own criteria are unmet" branch of the AC is intentionally NOT done here — judging an Epic's own acceptance-criteria readiness requires reading its body and is the recommender agent's job, not deterministic code. Pure module so it unit-tests with an injected resolver.

* AFTER the dispatcher-owned reapply so it reads the latest body, and BEFORE
* verify so the reclassified body is what gets validated.
*/
async function resolveBlockersStep(ctx: StepContext<RecommenderInput>): Promise<void> {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#225 placement. Runs after reapply-dispatcher-sections (so it reads the latest body) and before verify-state-issue-parses (so the reclassified body is what gets validated). Best-effort + idempotent, mirroring the reapply step: skip on parse error, skip the listOpenEpics/gh round-trips entirely when no blocked item carries a resolvable issue reference, skip the write on a no-op.

// optionally annotated with a resolved title or `(stale blocker: …)`), or a
// backticked / free-text non-issue blocker (exempt). Only validate the shape
// of one that's *trying* to be an issue reference.
if (REF_LIKE_BLOCKER_RE.test(item.blocker) && !BLOCKER_REF_RE.test(item.blocker)) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#225 schema change. This previously rejected any #-prefixed blocker that wasn't exactly #\d+. Relaxed to accept an optional <owner>/<repo> cross-repo prefix and an optional trailing (<title>) / (stale blocker: <ref>) annotation — otherwise the resolution pass's own output would fail the verify step it feeds. schemas/state-issue.v1.md (the source of truth) is updated in the same change.

* {@link registerManagedRepo} before it writes, and by `mm init` *before* it
* scaffolds (so a rejected init writes nothing).
*/
export function assertNoRepoPathCollision(db: Database, repo: string, checkoutPath: string): void {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#226 guard. One helper, two callers: registerManagedRepo calls it before its INSERT (so the daemon's rememberRepoPath rejects too) and mm init calls it via an injected hook before scaffolding (so a rejected init writes nothing). The repo != ? filter keeps a same-slug re-register idempotent — the daemon re-registers a repo's path on every dispatch and that must never self-collide. Paths are resolve()-normalized so /foo and /foo/ collide; symlinks are out of scope (documented).

const timeoutMs = deps.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
const limit = deps.maxConcurrentRepos ?? DEFAULT_MAX_CONCURRENT_REPOS;

// Phase 1 — due-check + stamp, SYNCHRONOUSLY before any run fires. Stamping all

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#227 ordering. Two phases: stamp last_recommender_run for every due repo synchronously (no intervening await), then fan out the runs concurrently. Stamping all due repos before any await is what preserves the existing "overlapping tick can't double-dispatch" invariant under concurrency. The per-repo withTimeout is what actually delivers the fix: a hung gh/state-write on one repo is abandoned (its promise orphaned but no longer blocking) so the others still run; its stamp rolls back so it retries next tick.

@thejustinwalsh thejustinwalsh marked this pull request as ready for review June 4, 2026 06:19
@thejustinwalsh thejustinwalsh added the ready-for-review All phases done and verified — PR ready for final human review and merge label Jun 4, 2026
@thejustinwalsh

Copy link
Copy Markdown
Owner Author

Reviewer's brief — Epic #211 (PR #229)

Three independent multi-repo coordination fixes, one per closed sub-issue. Posted on both the Epic and the PR.

How to run it

bun install
bun run typecheck          # clean
bun run lint               # clean
bun test                   # 1408 pass

# The three integration tests that prove the fixes:
bun test packages/dispatcher/test/multi-repo-blockers.test.ts        # #225
bun test packages/cli/test/init-collision.test.ts                    # #226
bun test packages/dispatcher/test/recommender-cron-parallel.test.ts  # #227

What to verify (per fix)

  • feat(dispatcher): BlockedItem.blocker runtime resolution (cross-repo unblock) #225 — cross-repo blocker resolution. packages/dispatcher/src/blocker-resolution.ts (pure) + the resolve-blockers step in workflows/recommender.ts. Confirm the three branches: a closed blocker moves the item to ## Ready to dispatch; an open one stays ## Blocked annotated <ref> (<title>); an unresolvable one gets (stale blocker: <ref>). The integration test drives the real workflow through a bunqueue engine and asserts on the live state body (not a unit call). Note the validator/schema were relaxed (packages/state-issue/src/validate.ts, schemas/state-issue.v1.md) to accept the new blocker grammar — check that relaxation didn't widen too far (a malformed #abc / owner/repo#abc must still fail; covered in validate.test.ts).
  • fix(dispatcher): repo_config collision guard — reject mm init on shared checkout_path #226 — shared-checkout collision guard. assertNoRepoPathCollision in repo-config.ts is the single guard; registerManagedRepo calls it before its write and mm init calls it (injected hook) before scaffolding. Verify: same-slug re-register stays idempotent (the daemon re-registers a path every dispatch — must not self-collide), the dispatch route returns 400 (hook-server.ts), and a rejected mm init writes no .middle/<slug>.toml. Paths are resolve()-normalized (trailing-slash collides); symlinks are explicitly out of scope.
  • feat(dispatcher): parallelize the recommender cron per-repo (hung repo doesn't block others) #227 — parallel recommender cron. runRecommenderCronPass in recommender-cron.ts. The load-bearing ordering: it stamps last_recommender_run for every due repo synchronously before any await, then fans out behind a bounded pool with a per-repo withTimeout. Confirm the stamp-before-fanout preserves the double-dispatch guard and that a timed-out run rolls its stamp back without blocking the others.

How to review it

Three commits, one per sub-issue (plus a review-hardening commit 6ae8348). The decision rationale is in inline review comments on this PR and in planning/issues/211/decisions.md. I ran a clean-eyes adversarial pass before marking ready — it caught an empty-title #42 () bug and two adjacent hardenings, all fixed with regression tests and re-reviewed clean.

Fragile bits worth extra eyes

  • bunqueue single-engine rule: the feat(dispatcher): BlockedItem.blocker runtime resolution (cross-repo unblock) #225 integration test registers the workflow on one shared engine and ticks via engine.start — a fresh Engine per tick hangs (embedded engines share a process-singleton queue manager; see packages/dispatcher/CLAUDE.md).
  • verify-after-resolve coupling: the resolve-blockers step runs before verify-state-issue-parses, so any blocker string it emits must satisfy validate() — that's why an empty resolved title falls back to the bare ref rather than #42 ().
  • raw-vs-normalized split (benign): the daemon's in-memory repoPaths map holds the raw path while the db stores the normalized one. Both point at the same directory; every consumer hands the path straight to git/join, so it's cosmetic — flagged so it isn't mistaken for a bug.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dispatcher/src/epic-store/file-epic-gateway.ts`:
- Around line 144-145: The current branch calls gh.getIssueState when
epicFileExists(epicsDir, ref) is false which causes failures for non-numeric
slug refs; change the logic in file-epic-gateway so that when
epicFileExists(epicsDir, ref) is false you first detect whether ref is a numeric
issue id (e.g. /^\d+$/) and only call gh.getIssueState for numeric refs,
otherwise return null to mark the slug as unresolvable; update the conditional
around epicFileExists/readEpicFile to implement this numeric check and return
null for non-numeric missing refs.

In `@packages/dispatcher/src/github.ts`:
- Around line 380-382: getIssueState() currently treats any parsed.state value
other than "OPEN" as "closed"; update it to explicitly handle known uppercase gh
values: return "open" for "OPEN", "closed" for "CLOSED", and "merged" for
"MERGED" (PR refs), and return null for any other/unexpected parsed.state to
mark the status as unknown/stale; keep returning parsed.title unchanged. Locate
the logic using parsed.state and parsed.title in getIssueState() and replace the
ternary that maps everything else to "closed" with an explicit switch/if that
returns "open"/"closed"/"merged"/null as described.

In `@packages/dispatcher/src/recommender-cron.ts`:
- Around line 120-121: Guard against non-positive runTimeoutMs by ensuring
timeoutMs uses the default when deps.runTimeoutMs is 0 or negative: replace the
current assignment for timeoutMs (which reads deps.runTimeoutMs ??
DEFAULT_RUN_TIMEOUT_MS) with a check that picks deps.runTimeoutMs only if it's >
0, otherwise use DEFAULT_RUN_TIMEOUT_MS (e.g. timeoutMs = deps.runTimeoutMs &&
deps.runTimeoutMs > 0 ? deps.runTimeoutMs : DEFAULT_RUN_TIMEOUT_MS). This uses
the existing symbols timeoutMs, deps.runTimeoutMs, and DEFAULT_RUN_TIMEOUT_MS so
runs won't immediately time out when a non-positive value is provided.

In `@packages/dispatcher/src/repo-config.ts`:
- Around line 115-121: The current read-then-write flow in
assertNoRepoPathCollision and registerManagedRepo can race; add a DB-level
uniqueness guarantee by creating a partial unique index on
repo_config.checkout_path for non-null values (so nulls remain allowed) in the
migration that added checkout_path, then modify registerManagedRepo to handle
unique-constraint violations from the INSERT ... ON CONFLICT(...) DO UPDATE and
translate them into a RepoPathCollisionError: on constraint error, normalize the
attempted checkoutPath, query repo_config for the existing repo with that
normalized checkout_path, and throw new RepoPathCollisionError(existingRepo,
attemptedRepo, normalizedPath); keep assertNoRepoPathCollision for eager checks
but rely on the DB constraint + error handling as the atomic guard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 849cb601-6fd2-4171-80d9-2fe9a714900c

📥 Commits

Reviewing files that changed from the base of the PR and between 31ccad3 and 6ae8348.

📒 Files selected for processing (29)
  • packages/cli/src/bootstrap/init.ts
  • packages/cli/src/bootstrap/types.ts
  • packages/cli/src/commands/init.ts
  • packages/cli/src/index.ts
  • packages/cli/test/init-collision.test.ts
  • packages/core/src/config.ts
  • packages/core/test/config.test.ts
  • packages/dispatcher/src/blocker-resolution.ts
  • packages/dispatcher/src/epic-store/file-epic-gateway.ts
  • packages/dispatcher/src/epic-store/index.ts
  • packages/dispatcher/src/github.ts
  • packages/dispatcher/src/hook-server.ts
  • packages/dispatcher/src/main.ts
  • packages/dispatcher/src/recommender-cron.ts
  • packages/dispatcher/src/recommender-run.ts
  • packages/dispatcher/src/repo-config.ts
  • packages/dispatcher/src/workflows/recommender.ts
  • packages/dispatcher/test/blocker-resolution.test.ts
  • packages/dispatcher/test/control-routes.test.ts
  • packages/dispatcher/test/gates/checkbox-revert-pass.test.ts
  • packages/dispatcher/test/multi-repo-blockers.test.ts
  • packages/dispatcher/test/recommender-cron-parallel.test.ts
  • packages/dispatcher/test/recommender-workflow.test.ts
  • packages/dispatcher/test/repo-config.test.ts
  • packages/state-issue/src/validate.ts
  • packages/state-issue/test/validate.test.ts
  • planning/issues/211/decisions.md
  • planning/issues/211/plan.md
  • schemas/state-issue.v1.md

Comment thread packages/dispatcher/src/epic-store/file-epic-gateway.ts Outdated
Comment thread packages/dispatcher/src/github.ts Outdated
Comment thread packages/dispatcher/src/recommender-cron.ts Outdated
Comment thread packages/dispatcher/src/repo-config.ts
…ates/inputs

Address review round 1 on PR #229 (multi-repo coordination). Each finding
resolved class-wide within its blast radius, with a test per fix:

- getIssueState (github mode): map only known gh states — OPEN→open,
  CLOSED/MERGED→closed — and treat any other value as unresolvable (null /
  stale blocker) instead of default-unblocking. A MERGED PR ref is a satisfied
  blocker (closed); a future/unexpected state no longer silently unblocks.
  Extracted as the pure, tested mapGhIssueState.
- getIssueState (file mode): a non-numeric slug with no Epic file returns null
  rather than forwarding to gh, where refToIssueNumber would throw on the
  non-numeric ref. Only numeric refs fall through; matches the documented
  'no file → stale' contract.
- recommender cron: guard non-positive / non-finite runTimeoutMs and
  maxConcurrentRepos (new isPositiveNumber helper). A 0/NaN timeout would
  expire every run immediately (all stamps roll back, nothing completes); a
  NaN concurrency would zero the worker pool.
- repo_config checkout-path collision: add migration 011, a partial UNIQUE
  index on checkout_path (non-null), as the atomic backstop the eager read-
  check can't provide across processes. registerManagedRepo translates the
  constraint violation into RepoPathCollisionError. The migration de-dupes
  pre-existing duplicate paths first so CREATE UNIQUE INDEX can't abort
  startup on an older db.
A long assistant.message line in classifyStop's rate-limit test wasn't
format-clean; oxfmt reflows it. No behavior change.
The db-scripts backup/restore round-trip asserts the restored db's schema
version; migration 011 (checkout_path unique index) bumped it 10→11.
@thejustinwalsh thejustinwalsh merged commit 128056e into main Jun 4, 2026
1 check passed
thejustinwalsh added a commit that referenced this pull request Jun 4, 2026
…ates/inputs

Address review round 1 on PR #229 (multi-repo coordination). Each finding
resolved class-wide within its blast radius, with a test per fix:

- getIssueState (github mode): map only known gh states — OPEN→open,
  CLOSED/MERGED→closed — and treat any other value as unresolvable (null /
  stale blocker) instead of default-unblocking. A MERGED PR ref is a satisfied
  blocker (closed); a future/unexpected state no longer silently unblocks.
  Extracted as the pure, tested mapGhIssueState.
- getIssueState (file mode): a non-numeric slug with no Epic file returns null
  rather than forwarding to gh, where refToIssueNumber would throw on the
  non-numeric ref. Only numeric refs fall through; matches the documented
  'no file → stale' contract.
- recommender cron: guard non-positive / non-finite runTimeoutMs and
  maxConcurrentRepos (new isPositiveNumber helper). A 0/NaN timeout would
  expire every run immediately (all stamps roll back, nothing completes); a
  NaN concurrency would zero the worker pool.
- repo_config checkout-path collision: add migration 011, a partial UNIQUE
  index on checkout_path (non-null), as the atomic backstop the eager read-
  check can't provide across processes. registerManagedRepo translates the
  constraint violation into RepoPathCollisionError. The migration de-dupes
  pre-existing duplicate paths first so CREATE UNIQUE INDEX can't abort
  startup on an older db.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review All phases done and verified — PR ready for final human review and merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(dispatcher): multi-repo coordination — close the real holes

1 participant