Skip to content

docs: operator docs hardening (file mode flip, vocabulary, daemon-as-service)#228

Merged
thejustinwalsh merged 7 commits into
mainfrom
middle-issue-209
Jun 4, 2026
Merged

docs: operator docs hardening (file mode flip, vocabulary, daemon-as-service)#228
thejustinwalsh merged 7 commits into
mainfrom
middle-issue-209

Conversation

@thejustinwalsh

@thejustinwalsh thejustinwalsh commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #209

Ships the three operator-facing docs the docs audit flagged, each backed by a real-path integration test so the docs can't silently drift from the code: enabling file mode on an existing repo, the full label vocabulary, and running middle as a system service. One Epic → one branch → one PR; the three open sub-issues were the phases.

What changed

  • docs/operator.md — new "Enable file mode on an existing repo" how-to (flip via mm init --epic-store=file, directory layout, a round-tripping worked-example Epic file, operator-visible differences); mm start --foreground + service cross-links in "Start and stop"; command-table updates.
  • docs/vocabulary.md (new) — every middle label (meaning / who applies / middle's response / when to use), the single source of truth.
  • docs/daemon-as-a-service.md (new) — complete systemd (user + system) units and a launchd plist, with install / verify / log-tail commands.
  • README.md — daemon-as-a-service as the next step after mm doctor; "Going deeper" links for the two new docs.
  • packages/cli/src/commands/doctor.ts — file-mode check extended to three rows (epics_dir, state_file, Epic-file round-trip); new mm doctor --vocabulary-check docs↔code drift guard.
  • packages/cli/src/commands/start.ts + index.tsmm start --foreground (in-process daemon, no pid file, clean SIGTERM).
  • Three Epic-aware skills (creating-/recommending-/implementing-github-issues) — definition-shaped label prose replaced with a cross-link to docs/vocabulary.md (red-flag entries kept); bootstrap mirror re-synced.
  • Tests: packages/cli/test/doctor.test.ts (file-mode + vocabulary), packages/cli/test/start-foreground.test.ts (new).

Why these changes

The recommender's classification is LLM-driven, so #217's "the check exits 0 only when docs and code agree" can't be a deterministic replay of the recommender — it's realized as a docs↔code drift guard asserting every label the code keys on is documented. The existing-repo file-mode flip is documented as mm init --epic-store=file (not a hand TOML edit) because mm init writes both the toml and the daemon-db row the dispatcher routes on; a toml-only edit is a silent half-switch. Full reasoning in planning/issues/209/decisions.md.

Acceptance criteria

Verification

Full suite green: bun test1381 pass, 0 fail; bun run typecheck clean; bun run lint/format clean.

Fresh-operator walkthrough (every documented command run against the real CLI):

  • mm start --help / mm doctor --help / mm init --help show the documented --foreground, --vocabulary-check, --epic-store flags. ✅
  • mm init <repo> --epic-store=file --dry-run prints exactly the documented file-mode scaffold: would scaffold .middle/<owner>-<name>.toml ([epic_store] mode=file), planning/epics/ (README.md + .keep), .middle/state.md. ✅
  • The [epic_store] TOML the docs show is byte-identical to what renderEpicStoreToml() writes (mode = "file", epics_dir = "planning/epics", state_file = ".middle/state.md"). ✅
  • mm doctor --vocabulary-check✓ docs and code agree — 6 code-keyed label(s) documented, all 13 labels listed. ✅
  • mm start --foreground boots the real daemon (integration test): /health answers, no ~/.middle/dispatcher.pid written, SIGTERM → exit 0. ✅

Per-phase test evidence:

Stumbling points

  • The repo worktree had no node_modules (no bun install had run), so @middle/* deep imports failed until bun install.
  • oxfmt reformats a pre-existing drift in packages/adapters/copilot/test/adapter.test.ts on every run; reverted each time to keep this PR scoped (not mine to fold in).
  • The sub-issue text pointed the docs(vocabulary): single page consolidating every middle label + its semantic #217 test at packages/dispatcher/test/workflows/recommender.test.ts, which doesn't exist (the recommender test is recommender-workflow.test.ts); the drift guard lives in mm doctor, so its test is in the CLI suite — see decisions.md.

Suggested CLAUDE.md updates

None — the conventions held. (The EpicStoreSettings "writes both" docstring in core/config.ts was the key that prevented documenting a misleading toml-only flip; it's already well-placed.)

Follow-up issues

None — scope delivered in full.

Out of scope

Windows service templates and Docker/containerized deployment (sibling-noted out of scope in #218); changing recommender/dispatch behavior (docs + a drift guard only).

Summary by CodeRabbit

  • New Features

    • Added --foreground to mm start to run the dispatcher in-process
    • Added --vocabulary-check to mm doctor to validate label docs vs code
    • Added docs for running the dispatcher as a managed service (systemd/launchd)
  • Documentation

    • Added a comprehensive label vocabulary reference
    • Expanded operator guide with file-mode repo workflow and new CLI options
    • Updated README links and clarified skill docs on label ownership
  • Tests

    • Added tests for foreground start behavior and vocabulary-doc checks

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds operator-facing docs for running middle as a service, a canonical label vocabulary plus a docs↔code vocabulary drift check, expanded file-mode operator docs and doctor checks, CLI support for in-process foreground start, and corresponding tests and cross-links.

Changes

Operator Docs Hardening

Layer / File(s) Summary
Service daemon documentation
docs/daemon-as-a-service.md
New guidance for running mm start --foreground under systemd user/system unit and macOS launchd, listing outcomes and exclusions.
Foreground start CLI and runtime
packages/cli/src/commands/start.ts, packages/cli/src/index.ts, packages/cli/test/start-foreground.test.ts
Adds --foreground support, pid-file preflight helper, in-process runner (dynamic import of runDaemon), injectable test seam, and unit/integration tests validating no PID file and clean SIGTERM shutdown.
Label vocabulary documentation
docs/vocabulary.md
Adds a canonical label vocabulary page defining label groups (epic, dispatch gates, agent routing, internals, grouping metadata) and cross-links.
Doctor vocabulary drift guard
packages/cli/src/commands/doctor.ts, packages/cli/src/index.ts, packages/cli/test/doctor.test.ts
Implements runVocabularyCheck and --vocabulary-check CLI flag; parses docs/vocabulary.md (ignoring fenced code blocks) and asserts code-keyed and required labels are documented; tests cover positive and negative cases and an integration run of mm doctor --vocabulary-check.
File-mode enablement guide
docs/operator.md
Adds a step-by-step "Enable file mode" walkthrough (mm init --epic-store=file), worked Epic example, and command reference update to document --foreground.
Doctor file-mode checks and tests
packages/cli/src/commands/doctor.ts, packages/cli/test/doctor.test.ts
checkEpicStore now returns multiple checks for file-mode (epics_dir, state_file, epic-files) and enforces a byte-identical parse→render round-trip for Epic-marked files; tests validate correct and failing file-mode layouts.
Skill docs aligned to vocabulary
packages/cli/src/bootstrap-assets/skills/*/SKILL.md, packages/skills/*/SKILL.md
Updates Skill documentation to reference docs/vocabulary.md as source of truth and clarifies which labels skills apply vs. operator-applied labels.
README, tests, and planning docs
README.md, packages/cli/test/doctor.test.ts, planning/issues/209/*
README links updated to include daemon-as-a-service; test helpers/imports extended to keep fixtures aligned with docs; planning/decisions documents added for issue #209.

Sequence diagram (foreground start flow):

sequenceDiagram
  participant CLI as mm start --foreground
  participant StartCmd as runStartCommand
  participant ForegroundRunner as runForegroundDaemon
  participant Daemon as runDaemon (in-process)
  participant ServiceMgr as systemd/launchd
  CLI->>StartCmd: foreground=true
  StartCmd->>ForegroundRunner: runForegroundDaemon()
  ForegroundRunner->>Daemon: dynamic import + runDaemon()
  Daemon->>Daemon: dispatcher runs (no fork, no pidfile)
  ServiceMgr->>Daemon: SIGTERM
  Daemon->>StartCmd: exit 0 on clean shutdown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #218: Implements daemon-as-a-service docs and the mm start --foreground flow with integration tests.
  • #217: Implements the label vocabulary and mm doctor --vocabulary-check drift guard with SKILL.md alignments.
  • #216: Implements the file-mode operator walkthrough and expanded doctor file-mode checks.

Possibly related PRs

Suggested labels

ready-for-review

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: operator documentation hardening focusing on file mode flip, vocabulary, and daemon-as-service functionality.
Linked Issues check ✅ Passed The PR comprehensively delivers all objectives from issue #209: file-mode flip documentation with mm init --epic-store=file workflow, canonical label vocabulary in docs/vocabulary.md, daemon-as-service documentation with systemd/launchd examples, mm start --foreground implementation, and integration tests validating the documented behavior.
Out of Scope Changes check ✅ Passed All changes are within scope of issue #209 closure: operator-facing documentation, CLI command additions (--foreground, --vocabulary-check), integration tests, and supporting skill documentation updates; Windows services and Docker deployment are explicitly noted as out-of-scope.
Docstring Coverage ✅ Passed Docstring coverage is 84.21% which is sufficient. The required threshold is 80.00%.

✏️ 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.

@thejustinwalsh

Copy link
Copy Markdown
Owner Author

Verification gates — phase #216

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

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

[stderr]
$ oxfmt

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

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

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

test — ✅ pass (86.9s)
$ 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.06ms]
(pass) resolveDocsTarget — detection > detects Docusaurus from docusaurus.config.js [0.04ms]
(pass) resolveDocsTarget — detection > detects MkDocs and reads a custom docs_dir [0.21ms]
(pass) resolveDocsTarget — detection > detects MkDocs with the default docs_dir [0.07ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from typedoc.json and reads out [0.09ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from a package.json typedoc key [0.06ms]
(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.09ms]
(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.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.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.04ms]
(pass) resolveDocsTarget — config override > an unknown tool override throws with the valid names [0.64ms]
(pass) resolveOutputPath — slug normalization > strips a leading slash and an existing .md/.mdx extension [0.06ms]
(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.02ms]
(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.23ms]
(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.08ms]
(pass) makeGuard > REGRESSION: a nested same-source guard masks the inner failure [0.05ms]
(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 [68.79ms]

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

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.21ms]
(pass) /api/epics > dispatch 404s when no dispatch seam is wired [0.06ms]
(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.25ms]
(pass) Queue renders nothing-in-flight row when live is empty [0.83ms]
(pass) Queue renders gauge tile labels and values from totals [0.59ms]
(pass) Queue renders epic as #N for a numeric epic and — for null [0.51ms]
(pass) Queue state cell carries the s-running class [0.30ms]
(pass) Queue renders rate-limit chip with adapter name, status, and chip class [0.28ms]
(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.19ms]
(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.08ms]
(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.59ms]
(pass) RunnerRow Epic rendering > github-mode runner is unchanged (`#7`, no link) [0.29ms]
(pass) RunnerRow Epic rendering > no-Epic runner keeps the `#—` fallback [0.16ms]
(pass) Inspector Epic rendering > file-mode panel shows the slug file:// link in the header [0.45ms]
(pass) Inspector Epic rendering > github-mode panel is unchanged (`#7`, no link) [0.26ms]

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

packages/dashboard/test/activity.test.tsx:
(pass) Activity > renders Recommender and Documentation sections [0.78ms]
(pass) Activity > shows an output link when present and omits it otherwise [0.31ms]
(pass) Activity > empty state per section when no runs of that kind [0.13ms]
(pass) Activity > renders a state label for each run [0.13ms]
(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 [74.38ms]
(pass) createDbDeps.listEpics > an in-flight workflow surfaces as the runner and flips inFlight [81.78ms]
(pass) createDbDeps.listEpics > a blocked Epic with no needs-human entry gets a blocked decision callout [72.09ms]
(pass) createDbDeps.listEpics > surfaces a file-mode Epic (slug ref, null number) and resolves its runner by ref (#200) [80.70ms]
(pass) createDbDeps.listEpics > dispatchEpic + refreshEpics delegate to the injected callbacks [69.36ms]

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.13ms]

packages/dashboard/test/api.test.ts:
(pass) dashboard JSON API > GET /api/repos returns a JSON array of repo summaries [89.27ms]
(pass) dashboard JSON API > GET /api/repos/:repo returns NEXT UP + IN FLIGHT for a known repo [78.85ms]
(pass) dashboard JSON API > github-mode IN FLIGHT row carries epicRef alongside the numeric epic (#187) [76.04ms]
(pass) dashboard JSON API > file-mode IN FLIGHT row surfaces epic_ref as epicRef with a null epic (#187) [74.85ms]
(pass) dashboard JSON API > GET /api/sessions/:session carries epicRef for a file-mode runner (#187) [75.27ms]
(pass) dashboard JSON API > GET /api/repos/:repo 404s an unknown repo [70.18ms]
(pass) dashboard JSON API > GET /api/banner reports per-adapter rate limits (UNKNOWN unobserved) [65.59ms]
(pass) dashboard JSON API > GET /api/sessions/:session returns the Inspector runner panel with attach commands [78.76ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach control flips controlled_by and spawns a terminal [84.76ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach rejects an invalid mode [76.18ms]
(pass) dashboard JSON API > POST /api/rate-limits/:adapter/clear sets the adapter AVAILABLE [71.41ms]
(pass) dashboard JSON API > GET /api/sessions/:session/events validates the limit param [76.51ms]
(pass) dashboard JSON API > POST /api/repos/:repo/pause validates untilMs [77.38ms]
(pass) dashboard JSON API > a runner with no session_name is reachable by its workflow id [75.55ms]
(pass) dashboard JSON API > a malformed percent-encoded path segment is a 400, not a 500 [72.11ms]
(pass) dashboard JSON API > unknown /api routes 404 as JSON [69.63ms]

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

packages/dashboard/test/runs-api.test.ts:
(pass) /api/runs > GET /api/runs returns the run list [0.20ms]
(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.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.24ms]
(pass) Epics > disables dispatch when the chosen adapter has no free slot [0.18ms]
(pass) Epics > shows a decision callout when present [0.30ms]
(pass) Epics > renders the decision link as an anchor when present [0.28ms]

packages/dashboard/test/app.test.tsx:
(pass) App nav includes a queue tab [0.95ms]
(pass) App nav includes an activity tab [0.30ms]
(pass) api.runs reads runs from a live server [92.58ms]
(pass) App defaults to the Epics view (nav tab + empty state render) [0.52ms]
(pass) api.epics reads Epic cards from a live server [85.62ms]
(pass) applyWorkflowFrame upserts non-terminal and drops terminal workflows [0.18ms]
(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.48ms]
(pass) dashboard views (static render) > Inspector renders the per-runner panel, links, affordances, and timeline [0.58ms]
(pass) api-client against a live server > api.repos() + RepoRow render the live repo [79.79ms]
(pass) api-client against a live server > api.attach(control) flips controlled_by; api.release reverts it [87.99ms]
(pass) api-client against a live server > api.runRecommender surfaces a non-2xx as an ApiError [79.58ms]

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

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 [90.37ms]
Bundled page in 46ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the bundled entry script transpiles the TSX app [114.64ms]
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 [89.53ms]

packages/state-issue/test/validate.test.ts:
(pass) validate > passes a schema-conforming state [0.15ms]
(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 > 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 [288.41ms]

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.01ms]
(pass) hand-crafted state-issue fixture > validate returns pass [0.06ms]
(pass) hand-crafted state-issue fixture > round-trips byte-identically [0.02ms]
(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.03ms]
(pass) renderStateIssue > renders a fully-populated state with all section content [0.08ms]
(pass) parseStateIssue > parses the canonical empty body back to the original state [0.07ms]
(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.05ms]
(pass) parseStateIssue > returns ParseError when the open marker is missing [0.09ms]
(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.06ms]
(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_" [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.44ms]
(pass) addMiddleIgnore > preserves existing unrelated entries [0.29ms]
(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.23ms]
(pass) removeMiddleIgnore > strips the whole block, leaving other entries [0.35ms]
(pass) removeMiddleIgnore > deletes the file when it empties [0.33ms]
(pass) removeMiddleIgnore > also clears a legacy bare `.middle/` line [0.27ms]
(pass) removeMiddleIgnore > no-op when there's nothing middle-owned to remove [0.19ms]
(pass) removeMiddleIgnore > no-op leaves a file without a trailing newline untouched [0.15ms]
(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.16ms]
(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.39ms]
(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.33ms]
(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.03ms]
(pass) mm init --epic-store=file > the README template snippet is a parseable v1 Epic body [7.63ms]
(pass) mm init --epic-store=file > calls the setEpicStore callback with file mode + default paths [7.73ms]
(pass) mm init --epic-store=file > a setEpicStore write failure is best-effort — init still succeeds [6.99ms]
(pass) mm init --epic-store=file > --dry-run writes nothing and makes no gh calls [0.30ms]
(pass) mm init — github mode is unchanged > default mode creates the state issue and writes no file-store scaffold [8.28ms]
(pass) mm init — github mode is unchanged > setEpicStore is called with github mode in the default path [6.64ms]

packages/cli/test/pause-resume.test.ts:
(pass) mm pause / mm resume > pause sets paused_until; resume clears it (keyed by the resolved slug) [92.95ms]
(pass) mm pause / mm resume > a slug-resolution failure returns exit 1, not an unhandled rejection [0.46ms]
(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 [77.54ms]
(pass) runStatus > reports cleanly when the database does not exist yet [0.34ms]
(pass) runStatus > reports cleanly when the database has no workflows [62.54ms]
(pass) runStatus > exits non-zero when the config file is malformed [0.56ms]

packages/cli/test/bootstrap-hook.test.ts:
(pass) bootstrap hook.sh asset > is byte-identical to the canonical HOOK_SH constant [0.75ms]
(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.85ms]

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

packages/cli/test/doctor.test.ts:
(pass) runDoctor — happy path > returns 0 and prints every check when the toolchain is healthy [1168.32ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + existing epics dir → epics_dir pass, no state-issue row [1057.15ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + missing epics dir → epics_dir fail, no state-issue row [1077.02ms]
(pass) runDoctor — mode-aware Epic-store check > github mode (no config row) → state-issue row, no epics_dir row [997.27ms]
(pass) runDoctor — mode-aware Epic-store check > doctor honors the documented file-mode config [984.37ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + malformed Epic file → epic-files fail [1015.92ms]
(pass) checkAdapterBinaries > null config (unparseable) → single warn, no throw [0.10ms]
(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) [27.04ms]
(pass) formatAgo > renders sub-minute as seconds [0.07ms]
(pass) formatAgo > renders minutes, hours, and days at the boundaries [0.02ms]
(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.02ms]

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

packages/cli/test/state-issue-check.test.ts:
(pass) checkStateIssueRoundTrip > passes for the canonical conforming fixture [0.17ms]
(pass) checkStateIssueRoundTrip > fails when the body does not parse [0.05ms]
(pass) checkStateIssueRoundTrip > fails validate when a Ready row uses an unconfigured adapter [0.08ms]
(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.08ms]

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

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.36ms]
(pass) runAuditIssues --issue mode > flags a weak issue, returns 1, and labels it when --label is set [0.54ms]
(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.13ms]
(pass) runAuditIssues backlog mode > returns 1 when any feature issue fails; labels only failures [0.11ms]

packages/cli/test/init-register.test.ts:
(pass) mm init — managed-repo registration > registers the slug + resolved checkout path on a successful init [9.47ms]
(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.05ms]

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) [155.65ms]
(pass) mm audit-issues --body-file (real CLI) > passes a well-formed issue carrying an integration criterion (exit 0) [151.17ms]
(pass) mm audit-issues --body-file (real CLI) > --json emits a machine-readable report [145.68ms]
(pass) mm audit-issues --body-file (real CLI) > rejects a non-positive-integer --issue with a clear error (exit 1) [743.69ms]

packages/cli/test/module-index.test.ts:
(pass) parseModuleIndexFrontmatter > accepts a well-formed frontmatter block [0.06ms]
(pass) parseModuleIndexFrontmatter > reads claude-md: true [0.03ms]
(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 [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.49ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > passes when flag and presence agree, and skips bootstrap-assets [0.85ms]
(pass) checkModuleIndex — the real middle packages tree > every src/index.ts(x) carries valid, consistent frontmatter [0.60ms]
(pass) checkModuleIndex — the real middle packages tree > finds every package's index front door [10.03ms]

packages/cli/test/bootstrap-init.test.ts:
(pass) mm init — fresh install > stages skills, hooks, config, state issue, and gitignore [8.91ms]
(pass) mm init — fresh install > the created state-issue body parses and validates [6.78ms]
(pass) mm init — idempotent re-init > a matching-version re-init refreshes assets but keeps config and issue [11.97ms]
(pass) mm init — idempotent re-init > re-init does not clobber a team's committed policy edits (AC #103) [12.05ms]
(pass) mm init — idempotent re-init > a fresh clone (committed policy, no local cache) reconciles the issue and keeps policy [6.66ms]
(pass) mm init — idempotent re-init > loadConfig reads init's two files via sibling derivation and merges them [8.24ms]
(pass) mm init — dry run > writes nothing and makes no GitHub calls [0.33ms]
(pass) mm init — validation > rejects a dirty working tree [0.30ms]
(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 [6.63ms]
(pass) mm init — reconciles the state issue against GitHub > a fresh local install reuses the repo's existing state issue instead of creating one [8.44ms]
(pass) mm init — reconciles the state issue against GitHub > warns and reuses the oldest when GitHub has duplicate state issues [6.16ms]
(pass) mm init — reconciles the state issue against GitHub > creates a state issue only when GitHub has none [7.28ms]
(pass) mm uninit > closes the issue and removes everything init staged [7.97ms]
(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.60ms]
(pass) mm uninit > falls back to default_branch 'main' when committed policy has a non-string value (#103) [0.67ms]
(pass) mm uninit > dry run removes nothing [8.57ms]
(pass) mm uninit > strips only middle's hook entries, preserving foreign ones [7.46ms]

packages/cli/test/dispatch.test.ts:
(pass) runDispatch — input validation > rejects a malformed numeric epic (digit-leading but not a whole number) [7.90ms]
(pass) runDispatch — input validation > rejects an epic number below 1 [6.69ms]
(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 [132.05ms]
(pass) runDispatch — control client > a file-mode slug dispatches with epicRef and skips the gh label fetch [12.91ms]
(pass) runDispatch — control client > subscribes to /control/events BEFORE POSTing /control/dispatch [105.83ms]
(pass) runDispatch — control client > exits 0 when the workflow parks for review (waiting-human) [134.28ms]
(pass) runDispatch — control client > exits 1 when the workflow fails [113.29ms]
(pass) runDispatch — control client > reconnects when the event stream drops mid-flight and follows to completion [129.93ms]
(pass) runDispatch — control client > --adapter overrides the agent label and the default, and is sent to the daemon [11.40ms]
(pass) runDispatch — control client > an agent:<name> label on the Epic selects that adapter [11.75ms]
(pass) runDispatch — control client > no agent label falls back to the default adapter [11.22ms]
(pass) runDispatch — control client > a disabled adapter is rejected (exit 1), even via --adapter, before any dispatch [8.83ms]
(pass) runDispatch — control client > an unconfigured --adapter is rejected (exit 1) before any dispatch [10.46ms]
(pass) runDispatch — control client > friendly failure (exit 1) when the daemon can't be reached or started [505.31ms]

packages/cli/test/state-issue-body.test.ts:
(pass) buildInitialStateIssueBody > parses and validates against the schema (configured adapters) [0.16ms]
(pass) buildInitialStateIssueBody > is empty in every section [0.07ms]
(pass) buildInitialStateIssueBody > round-trips byte-identically (the keystone invariant) [0.05ms]
(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.02ms]

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

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

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

packages/cli/test/bun-path.test.ts:
(pass) isDirOnPath > true when present [0.04ms]
(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) [0.01ms]
(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.03ms]
(pass) rcAlreadyConfigured > detects literal bin dir [0.02ms]
(pass) rcAlreadyConfigured > detects BUN_INSTALL form
(pass) rcAlreadyConfigured > false on unrelated rc
(pass) applyPathFix > appends once and is idempotent [0.32ms]
(pass) applyPathFix > creates content when the rc file is absent [0.18ms]

packages/cli/test/skills-sync.test.ts:
(pass) syncSkills > copies every canonical file into the mirror byte-for-byte [1.15ms]
(pass) syncSkills > a second sync is a no-op (inSync, no changes) [0.92ms]
(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.20ms]
(pass) diffSkills / check mode > check mode reports drift without writing [0.60ms]
(pass) diffSkills / check mode > check mode reports in-sync once synced [1.06ms]
(pass) diffSkills / check mode > check mode catches a single-byte edit in the mirror [1.11ms]
(pass) default repo paths > the shipped canonical and mirror are in sync [0.90ms]
(pass) default repo paths > the shipped skill set includes the three bootstrapped skills [0.51ms]

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.50ms]
[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.81ms]

packages/dispatcher/test/watchdog.test.ts:
(pass) watchdog — launch timeout > a launching workflow past the window is failed 'stuck-launching' [82.64ms]
(pass) watchdog — launch timeout > a launching workflow within the window is left alone [72.85ms]
(pass) watchdog — prompt not accepted > a running session that went ready but never started a turn is failed 'prompt-not-accepted' [88.30ms]
(pass) watchdog — prompt not accepted > a running session whose prompt landed (turn.started present) is not failed [81.82ms]
(pass) watchdog — prompt not accepted > a running session still within the launch window is not yet failed [78.96ms]
(pass) watchdog — tmux liveness > a running workflow with a dead session is failed + compensation triggered [81.81ms]
(pass) watchdog — tmux liveness > a running workflow with a live session is not failed for liveness [75.43ms]
[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 [74.10ms]
[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 [85.59ms]
[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 [92.81ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — tmux liveness > a killSession() error still records the failure decision [82.30ms]
(pass) watchdog — activity freshness > idle ≥ threshold marks one idle event but does not kill [76.25ms]
(pass) watchdog — activity freshness > idle ≥ kill-threshold kills the session and fails 'idle-timeout' [88.50ms]
(pass) watchdog — activity freshness > freshness is skipped while controlled_by = 'human' [71.15ms]
(pass) watchdog — activity freshness > a stale heartbeat is rescued by fresh transcript activity (cross-check) [73.49ms]
(pass) watchdog — sentinel re-arm > a blocked.json with no armed signal arms one, idempotently [76.30ms]
(pass) watchdog — sentinel re-arm > no sentinel file → no signal armed [71.18ms]
(pass) watchdog — blocked sentinel self-heal > idle ≥ kill-threshold with a blocked sentinel hands off to the drive, not compensation [79.12ms]
[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 [72.79ms]
(pass) watchdog — blocked sentinel self-heal > the handoff is recorded once, not every idle tick [82.58ms]
(pass) reconcileTranscriptDrift > advances last_heartbeat when the transcript is newer than the recorded beat [79.53ms]
(pass) reconcileTranscriptDrift > leaves the heartbeat alone when the transcript is older [73.49ms]
(pass) notification failsafe — detect + capture + intervene > a notification still within the grace window is left alone [79.10ms]
(pass) notification failsafe — detect + capture + intervene > a notification past the grace window captures the pane, classifies, and nudges [87.77ms]
(pass) notification failsafe — detect + capture + intervene > classifies a plain 'waiting for input' notification as a question (kind=input) [86.20ms]
(pass) notification failsafe — detect + capture + intervene > an agent that resumed after the notification (newer activity) is left alone [79.80ms]
(pass) notification failsafe — detect + capture + intervene > a human-controlled session is never rescued (a human will answer) [78.00ms]
(pass) notification failsafe — detect + capture + intervene > no-op when the tmux surface lacks the failsafe methods [78.68ms]
(pass) notification failsafe — detect + capture + intervene > a capture-only notification (no message payload) still classifies + nudges [87.14ms]
(pass) notification failsafe — fast-fail backstop > still idle past the kill-grace → fast-fails with the captured kind and kills the session [96.12ms]
(pass) notification failsafe — fast-fail backstop > two captures sharing a ts → the latest-by-id kind wins (contract lock) [102.42ms]
(pass) notification failsafe — fast-fail backstop > within the kill-grace → not yet failed (the nudge still has time to take) [87.21ms]
(pass) notification failsafe — fast-fail backstop > a repeat notification with no activity does NOT reset the kill clock — still fast-fails [100.43ms]
(pass) notification failsafe — fast-fail backstop > a fresh notification AFTER genuine activity re-arms the failsafe (re-captures) [102.17ms]

packages/dispatcher/test/recommender-run.test.ts:
[recommender-run] workflow wf_1780551449894_ez5vdyfb enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > runs to completion and records a kind:'recommender' workflow row for the repo [384.40ms]
[recommender-run] workflow wf_1780551450280_rwjgu6z7 enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > read-only by default: with no triggerAutoDispatch wired, a clean run dispatches nothing [381.81ms]
[recommender-run] workflow wf_1780551450658_5lp9gm3y enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > fires triggerAutoDispatch on a clean run when wired and auto_dispatch is on (trigger #1) [381.21ms]
[recommender-run] workflow wf_1780551451040_686abg8j enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > forwards epicStore so a file-mode run frames the prompt for the file store (#200) [377.48ms]
[recommender-run] workflow wf_1780551451417_o087jpai enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > does not fire triggerAutoDispatch when auto_dispatch is off, even if wired [376.90ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [7.74ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > file mode resolves without a state issue — sentinel 0 + epicStore carried (#200) [7.90ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > github mode still requires a configured state issue number [6.34ms]
(pass) resolveRecommenderOptions — schema resolution (issue #107) > resolves schemaPath from the middle install, not from repoPath [7.73ms]

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.43ms]
(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.23ms]
(pass) updateDispatcherSections > ticks do not accumulate across repeated updates [0.18ms]
(pass) readState > parses a valid body [0.12ms]
(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 [5.29ms]
(pass) awaitStopOrSessionEnd > resolves via 'session-ended' when liveness goes false while Stop is pending [11.48ms]
(pass) awaitStopOrSessionEnd > resolves via 'timeout' when the Stop wait rejects and the session stays alive [5.23ms]
(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 [21.58ms]

packages/dispatcher/test/build-deps.test.ts:
(pass) buildImplementationDeps > wires deps from the injected collaborators and returns the gate it built [66.36ms]
(pass) buildImplementationDeps > epicPrReadiness reports a missing PR as { exists: false, isDraft: false } [63.45ms]
(pass) buildImplementationDeps > the factory module imports no engine (no bunqueue construction) [1.94ms]
(pass) buildImplementationDeps > the default postQuestion posts a gh comment framed by pause kind [66.11ms]
(pass) buildImplementationDeps > the default postQuestion is idempotent on a repeated identical question (#205) [63.98ms]
(pass) postQuestionComment (idempotent pause poster, #205) > skips when the latest agent-comment already has the identical body [0.31ms]
(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.26ms]
(pass) formatPauseComment > a complexity pause carries the `complexity pause` label vocabulary [0.14ms]
(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.15ms]

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.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.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.06ms]

packages/dispatcher/test/hook-store.test.ts:
(pass) DbHookStore — resolveSessionToken > returns the token of the active workflow owning the session [72.19ms]
(pass) DbHookStore — resolveSessionToken > returns null for an unknown session [68.02ms]
(pass) DbHookStore — resolveSessionToken > ignores terminal workflows that previously held the deterministic session name [83.82ms]
(pass) DbHookStore — record > writes an events row for every hook [83.87ms]
(pass) DbHookStore — record > tool.pre and tool.post advance last_heartbeat [88.55ms]
(pass) DbHookStore — record > a non-tool event records but does not advance last_heartbeat [85.71ms]
(pass) DbHookStore — record > session.started writes session_id + transcript_path onto the workflow [81.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 [73.10ms]
(pass) DbHookStore — record > oversized payloads are truncated before storage [84.69ms]
[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 [93.24ms]
(pass) serializePayload > returns compact JSON for a small payload [63.86ms]
(pass) serializePayload > clips and marks a payload over 16KB [68.99ms]

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.15ms]
(pass) EventHub > a broadcast reaches a live subscriber [0.12ms]
(pass) EventHub > a heartbeat keeps the stream alive (injectable interval) [21.80ms]
(pass) EventHub > an aborted client is unsubscribed cleanly [11.75ms]
(pass) EventHub > a slow consumer that overflows its buffer is dropped without throwing [0.44ms]

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
(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/poller-gateway.test.ts:
(pass) deriveCiStatus > no checks configured → none (nothing to gate on) [0.08ms]
(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.01ms]
(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.91ms]
(pass) ghPollGateway.prSnapshot failure isolation > a `pr view` failure also degrades to null (the symmetric branch) [0.90ms]
(pass) ghPollGateway.prSnapshot failure isolation > both fetches succeed → a populated snapshot [1.34ms]

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.38ms]
(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.13ms]
[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.15ms]
[backlog-audit] o/active#1 fails the integration rubric → needs-design
(pass) runAuditCronPass > sweeps managed repos, skips paused ones [2.07ms]

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

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

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

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-uh2H4n/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-uh2H4n/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.91ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ywcrn6/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ywcrn6/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.15ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-quG719/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-quG719/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 [272.81ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-I3tDEr/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-I3tDEr/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) [281.95ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Qrfw8F/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Qrfw8F/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) [267.65ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ANOLD1/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ANOLD1/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 [518.30ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-NytNUw/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-NytNUw/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 [908.80ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-fSlGP4/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-fSlGP4/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 [289.77ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-0buMFU/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-0buMFU/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) [285.47ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-IZj6mO/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-IZj6mO/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) [286.03ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-JD1b1L/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-JD1b1L/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) [285.28ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-KNv9r1/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-KNv9r1/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 [292.26ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Vc4Zi7/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Vc4Zi7/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-Vc4Zi7/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Vc4Zi7/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-Vc4Zi7/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Vc4Zi7/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 [415.60ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-fAadI5/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-fAadI5/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' [267.13ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-qXVwDK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-qXVwDK/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) [262.58ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-wjNItQ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-wjNItQ/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 [274.21ms]
[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-S7PQ1T/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-S7PQ1T/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 [323.37ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-S3vqFK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-S3vqFK/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) [256.75ms]
[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-yUus2b/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-yUus2b/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 [265.99ms]
[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-mtvSr8/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-mtvSr8/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-mtvSr8/worktrees/thejustinwalsh/middle/issue-99
[workflow:middle-thejustinwalsh-middle-99] launching tmux session: true (cwd=/tmp/middle-wf-mtvSr8/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.05ms]
[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-ddbat4/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ddbat4/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-ddbat4/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ddbat4/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 [335.70ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-AzNbYK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-AzNbYK/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
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-AzNbYK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-AzNbYK/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 [333.74ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-u72NCQ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-u72NCQ/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-u72NCQ/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-u72NCQ/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) [321.09ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ct4kwp/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ct4kwp/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 [284.03ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-gL9U1H/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-gL9U1H/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-gL9U1H/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-gL9U1H/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-gL9U1H/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-gL9U1H/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.50ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-VXUx4k
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-VXUx4k)
[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) [251.64ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-bJwvT6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-bJwvT6)
[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 [252.32ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-4zy4OK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-4zy4OK/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) [262.18ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-zyVGvh/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-zyVGvh/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 [259.37ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-1mUvWc/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-1mUvWc/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 [263.85ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-NSOpZC/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-NSOpZC/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 [257.57ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-xeVfXs/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-xeVfXs/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) [271.33ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-M2gOFo/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-M2gOFo/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' [275.07ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-1v77Qc/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-1v77Qc/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.81ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-reisZK/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-reisZK/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 [266.32ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-nwwXCx/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-nwwXCx/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 [268.47ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-BZRKhl/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-BZRKhl/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) [264.08ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-q8dXbI/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-q8dXbI/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-q8dXbI/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-q8dXbI/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 [935.20ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-I71OrC/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-I71OrC/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 [708.91ms]

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 [145.40ms]
(pass) tryRebaseOntoMain — fixture repo > non-FF, no conflict: feature edits A, main edits B, no shared paths → rebase replays cleanly [154.68ms]
(pass) tryRebaseOntoMain — fixture repo > conflict: feature + main both edit shared.txt → rebase aborts, paths reported, worktree clean [189.56ms]
(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.93ms]
(pass) tryRebaseOntoMain — fixture repo > gitOps.revListCount: counts a resolvable range and falls back to 0 on an unresolvable one (the guard's conservative semantics) [118.28ms]
(pass) tryRebaseOntoMain — fixture repo > a non-managed head ref (not middle-issue-*) → ok:false with empty paths (skip signal) [105.10ms]
(pass) tryRebaseOntoMain — fixture repo > a missing PR (gateway returns null) → ok:false with empty paths (skip signal) [105.79ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict rebase failure (missing upstream) THROWS — not shaped as a path-less conflict [110.71ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict merge failure (missing ref) THROWS — symmetric to the rebase hardening [112.26ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > rebase would loop but merge -X ours lands cleanly (same line, feature wins) [198.91ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > residual conflict -X ours can't auto-resolve (rename/rename) → abort, paths reported [176.47ms]
(pass) applySuccess — fixture repo > pushes the rebased branch, posts one PR comment, and records CLEAN — twice = idempotent [198.27ms]
(pass) applySuccess — fixture repo > a different mainCommitSha allows a fresh announcement (the marker is sha-keyed) [170.03ms]
(pass) applySuccess — fixture repo > null mainCommitSha skips the comment but still pushes and records CLEAN (self-review hardening) [178.38ms]
(pass) applySuccess — fixture repo > keystone data-loss guard (#201): refuses to push when local HEAD is emptied but the remote branch has commits [172.85ms]
(pass) applySuccess — fixture repo > a non-managed head ref is a no-op (no push, no comment, no row) [120.21ms]
(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.34ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR rebase-fails → merge fallback lands → applySuccess('merged-new-work-as-base') [246.08ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR both attempts fail (rename/rename) → applyDemoteToWork fires [210.48ms]
(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 [209.26ms]
[pr-divergence] GitHub budget low (10 < 100); skipping pass — resets 2026-06-04T05:38:50.976Z
(pass) reconcileOpenPRs — end-to-end against the fixture repo > rate-limit floor short-circuits the pass; no listing happens [104.47ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CLEAN PR → walked but unchanged; nothing posted, no state advance [109.28ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > two open managed PRs in one pass — both walked, mix of CLEAN + BEHIND→rebased [180.78ms]
[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) [116.59ms]
[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 [118.10ms]

packages/dispatcher/test/documentation-workflow.test.ts:
(pass) documentation workflow — shell: step order + dedicated slot > declares the six steps in order [174.92ms]
(pass) documentation workflow — shell: step order + dedicated slot > runs the steps in order at runtime and completes [274.24ms]
(pass) documentation workflow — shell: step order + dedicated slot > records its row with kind 'documentation' — its own dedicated slot, off maxConcurrent [269.63ms]
(pass) documentation workflow — shell: step order + dedicated slot > claims the 'docs' worktree unit, distinct from the recommender's [268.61ms]
(pass) documentation workflow — shell: step order + dedicated slot > spawn-docs-agent has the spec's 5-minute hard cap [178.58ms]
(pass) documentation workflow — shell: step order + dedicated slot > prepare-docs-worktree registers a compensation handler [171.72ms]
(pass) documentation workflow — shell: step order + dedicated slot > check-rate-limit does not retry [171.72ms]
(pass) documentation workflow — shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' [237.72ms]
[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' [282.80ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=false: persist seam is never invoked [279.32ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true but persistDocs UNWIRED: still persists nothing (read-only first) [280.80ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true and persistDocs wired: persist runs after the agent, before cleanup [276.29ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports the resolved target, audit mode, and config; invokes the skill via @-reference [275.38ms]
(pass) documentation workflow — assembleDocumentationPrompt > includes the llms.txt audit line only when the target supports it [173.65ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports write=true to the agent when configured [183.87ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=true selects write mode: discover-or-author, agent does not commit [179.12ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=false stays in audit mode (read-only), never write mode [177.33ms]
(pass) documentation workflow — assembleDocumentationPrompt > write mode keeps the llms.txt instruction only when the target supports it [176.99ms]
(pass) documentation workflow — sessionNameFor collision-resistance > is deterministic for a given repo [183.18ms]
(pass) documentation workflow — sessionNameFor collision-resistance > produces a tmux-safe session name under the docs namespace [175.88ms]
(pass) documentation workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [182.87ms]

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

packages/dispatcher/test/control-routes.test.ts:
(pass) HookServer control routes > GET /health reports liveness, port, and version [2.72ms]
(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.65ms]
(pass) HookServer control routes > POST /control/dispatch rejects invalid bodies with 400 and starts nothing [2.31ms]
(pass) HookServer control routes > POST /control/dispatch surfaces the disabled-vs-unknown distinction in the 400 body [1.47ms]
(pass) HookServer control routes > POST /control/dispatch refuses with 429 when no slot is available (manual respects limits) [1.84ms]
(pass) HookServer control routes > POST /control/dispatch proceeds when a slot is available [1.28ms]
[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.92ms]
(pass) HookServer control routes > POST /control/dispatch rejects a colliding Epic with 409 [2.68ms]
(pass) HookServer control routes > two concurrent dispatches of the same Epic: exactly one 200, one 409 [6.82ms]
(pass) HookServer control routes > GET /control/events opens an SSE stream with a connected frame [2.18ms]
(pass) HookServer control routes > GET /control/events replays the injected init events [2.34ms]
(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 [2.73ms]
(pass) HookServer control routes > GET /control/metrics returns the raw snapshot as JSON [2.09ms]
(pass) HookServer control routes > metric routes 404 without a metrics seam [1.21ms]
(pass) HookServer control routes > POST /control/resume fires the parked Epic's resume and returns its id [2.33ms]
(pass) HookServer control routes > POST /control/resume 404s when no parked workflow owns the ref [1.73ms]
(pass) HookServer control routes > POST /control/resume 400s on a missing epicRef or answer [1.82ms]
(pass) HookServer control routes > control routes 404 in gate-only mode (no control plane wired) [1.66ms]

packages/dispatcher/test/tmux.test.ts:
(pass) tmux session lifecycle > launch → has-session → send-text → capture-pane → kill [266.08ms]
(pass) tmux session lifecycle > newSession injects env via -e KEY=val [257.11ms]
(pass) tmux session lifecycle > hasSession is false for an unknown session [1.36ms]
(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.35ms]
(pass) tmux session lifecycle > newSession rejects a duplicate session name with a TmuxError [5.19ms]
(pass) tmux session lifecycle > getTmuxVersion parses the installed tmux's version [0.91ms]
(pass) parseTmuxVersion > parses release versions [0.04ms]
(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) [97.91ms]
(pass) dispatch source (#53) > records and reads back source 'manual' / 'auto'; null when unset [83.78ms]
(pass) workflow meta_json accessors > readWorkflowMeta returns {} for a missing row, a null meta, and malformed JSON [84.02ms]
(pass) workflow meta_json accessors > patchWorkflowMeta merges, preserving keys it does not set [83.13ms]
(pass) workflow meta_json accessors > patchWorkflowMeta does not bump updated_at — meta is scratch, not an activity signal [83.27ms]
(pass) workflow meta_json accessors > checkbox-reconcile state round-trips; defaults when unset [80.81ms]
(pass) workflow meta_json accessors > getCheckboxReconcileState sanitizes malformed nested meta back to the contract [95.64ms]
(pass) listRunningImplementationWorkflows > returns only running implementation rows that own both an epic and a worktree [126.58ms]
(pass) createWorkflowRecord > inserts a pending implementation row carrying epic_number [69.37ms]
(pass) createWorkflowRecord > a second create with the same id is a no-op (idempotent on retry), not a UNIQUE error [75.77ms]
(pass) createWorkflowRecord > a non-PK constraint violation (bad kind) still throws — not swallowed [62.51ms]
(pass) countActiveImplementationSlots > counts non-terminal implementation rows, grouped by adapter [76.02ms]
(pass) countActiveImplementationSlots > excludes terminal implementation rows [77.44ms]
(pass) countActiveImplementationSlots > excludes the recommender's own row — its dedicated slot is not a dispatch slot [74.30ms]
(pass) updateWorkflow > transitions state and bumps updated_at [81.52ms]
(pass) updateWorkflow > patches session fields without disturbing others [84.50ms]
(pass) updateWorkflow > a no-op patch leaves the row intact [72.91ms]
(pass) getWorkflow > returns null for an unknown id [63.74ms]
(pass) hasNonTerminalEpicWorkflow > true while an implementation Epic workflow is non-terminal, false once terminal [75.16ms]
(pass) hasNonTerminalEpicWorkflow > scopes by repo and epic; a recommender row never collides [73.75ms]
(pass) findParkedWorkflowByRef > finds the waiting-human workflow for a ref (slug or number); null otherwise [80.92ms]
(pass) listActiveImplementationWorkflows (#180) > returns lastHeartbeat (null when none observed, the touched epoch otherwise) [92.03ms]
(pass) listNonTerminalWorkflows > returns id/repo/epic/state for non-terminal implementation rows only [83.67ms]
(pass) workflow observers > notifies the observer of each patch, and stops after dispose [82.08ms]
[workflow-record] update observer threw: observer boom
(pass) workflow observers > a throwing observer does not break the DB write [74.05ms]
(pass) workflow observers > addWorkflowObserver fans out to every observer; disposers independent [97.13ms]
(pass) workflow observers > the finalize path notifies observers on a real transition only [104.21ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > flips a still-pending row to failed and reports the transition [82.50ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a row already past pending (e.g. a later step's compensated failure) [77.11ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a launching row — the launch step already advanced it [77.59ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on an unknown id [66.15ms]
(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 [68.41ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > does NOT touch a pending documentation row (same reason as recommender) [70.78ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > notifies observers only on a real transition [71.50ms]

packages/dispatcher/test/recovery.test.ts:
(pass) reconcileOrphanedSignals > an armed signal with no recoverable execution is finalized failed, consumed, and surfaced [85.26ms]
(pass) reconcileOrphanedSignals > a recoverable parked execution is left untouched (not an orphan) [77.83ms]
(pass) reconcileOrphanedSignals > only the orphaned rows are reconciled when alive and orphaned parks coexist [105.36ms]
(pass) reconcileOrphanedSignals > respects a custom finalState and tolerates a missing surface callback [92.07ms]
[recover] surfacing orphaned signal a71757eb-0a92-4253-867f-a4b14e2d262a (epic-9-answered) failed: comment failed
(pass) reconcileOrphanedSignals > a surface callback that throws never aborts the reconcile (still finalized + consumed) [90.34ms]
(pass) reconcileOrphanedSignals > an orphaned signal with a null epicNumber still reconciles [89.11ms]
(pass) reconcileOrphanedSignals > a non-parked (terminal) workflow's stale signal is ignored — only waiting-human rows are pollable [85.79ms]
(pass) reconcileOrphanedSignals > finalState is typed to terminal states only (compile-time guard) [61.86ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BUNQUEUE_DATA_PATH) when it is set [67.14ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BQ_DATA_PATH) when it is set [83.01ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming DATA_PATH) when it is set [101.80ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming SQLITE_PATH) when it is set [74.00ms]
(pass) createDurableEngine (transient-queue env guard) > an empty-string env var still trips the guard (bunqueue coalesces with ??) [70.08ms]
(pass) createDurableEngine (transient-queue env guard) > names every offending var when several are set at once [67.77ms]
(pass) recoverEngine (durable engine across restart) > re-arms a parked waiting execution so a later signal resumes it [489.35ms]
(pass) recoverEngine (durable engine across restart) > drops a mid-drive (running) execution instead of re-driving it [369.69ms]

packages/dispatcher/test/hook-server.test.ts:
[hook-server] received session.started:middle-6
(pass) HookServer — SessionStart > awaitSessionStart resolves with the posted payload [2.60ms]
[hook-server] received session.started:middle-7
(pass) HookServer — SessionStart > a payload that arrives before anyone awaits is stashed and delivered [1.70ms]
[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.03ms]
[hook-server] received session.started:middle-DIFFERENT
(pass) HookServer — SessionStart > waiters are keyed by session — one session's event does not satisfy another [301.66ms]
[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.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 [64.60ms]
[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.67ms]
[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.40ms]
[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.39ms]
[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.81ms]
[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.21ms]
(pass) HookServer — lifecycle > awaitSessionStart rejects on timeout [53.05ms]
(pass) HookServer — lifecycle > non-POST and unknown paths return 404 [2.13ms]
(pass) HookServer — lifecycle > stop() rejects outstanding waiters [1.71ms]
(pass) HookServer — recommender trigger endpoint > 404s when no trigger is wired (gate-only mode) [1.97ms]
(pass) HookServer — recommender trigger endpoint > wired trigger receives the posted repo and returns its status/body verbatim [2.99ms]
(pass) HookServer — recommender trigger endpoint > tolerates a garbled body — the trigger validates its own inputs [3.32ms]
(pass) HookServer — recommender trigger endpoint > coerces non-string repoSlug/repoPath to undefined before forwarding [3.23ms]
(pass) HookServer — recommender trigger endpoint > a non-object JSON body (null, primitive, array) is treated as empty, not a 500 [5.35ms]
(pass) HookServer — recommender trigger endpoint > passes a string field through while dropping a non-string sibling [3.68ms]
(pass) HookServer — merged routes > extraRoutes are served, and the fetch fallback still answers /health [3.32ms]
(pass) HookServer — merged routes > GET / no longer returns the status page (404 with no SPA route) [3.39ms]

packages/dispatcher/test/docs-persist.test.ts:
(pass) commitDocs > stages and commits authored docs; returns the sha + sorted file list [32.68ms]
(pass) commitDocs > returns null on a clean worktree — no empty commit [16.55ms]
(pass) commitDocs > excludes middle's .middle/ scratch even when the repo does not gitignore it [21.94ms]
(pass) commitDocs > honors a custom commit message [26.82ms]
(pass) makeGhPersistDocs > commits, then invokes the push seam with the commit it produced [21.65ms]
(pass) makeGhPersistDocs > clean worktree: the push seam is never invoked (no empty PR) [15.74ms]
(pass) pushDocsBranch > first run creates the branch on origin at the authored commit [40.97ms]
(pass) pushDocsBranch > re-run force-pushes a divergent commit (rebuilt branch is non-fast-forward) [62.38ms]
(pass) pushDocsBranch > surfaces a push failure rather than swallowing it (no origin configured) [23.41ms]
(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_1780551482569_n680thsp enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > runs to completion and records a kind:'documentation' row for the repo [383.73ms]
[documentation-run] workflow wf_1780551482951_4n0fbfx3 enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > write=true but a clean worktree: the wired seam opens no PR (no empty commit) [380.02ms]
[documentation-run] workflow wf_1780551483332_3ddxan5n 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 [382.66ms]
(pass) resolveDocumentationOptions > accepts a configured non-default adapter (e.g. codex) [11.87ms]
(pass) resolveDocumentationOptions > rejects an adapter the registry doesn't know [11.94ms]
(pass) resolveDocumentationOptions > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [10.37ms]
(pass) resolveDocumentationOptions > resolves the markdown fallback target for a plain repo [11.52ms]
(pass) resolveDocumentationOptions > honors a [docs] tool/path override [11.61ms]
(pass) resolveDocumentationOptions > surfaces an unknown tool override as an error rather than falling back [11.76ms]

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.75ms]
(pass) runRecommenderCronPass > fires once the interval has elapsed [1.50ms]
(pass) runRecommenderCronPass > skips a paused repo [1.45ms]
(pass) runRecommenderCronPass > skips a repo whose recommender is disabled or unconfigured [1.58ms]
(pass) runRecommenderCronPass > skips a repo with a non-positive interval (never auto-runs) [1.47ms]
[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.49ms]

packages/dispatcher/test/poller.test.ts:
(pass) reasonFromSignalName > maps the durable signal names to resume reasons [64.43ms]
(pass) classifyNewHumanReply > returns the newest non-bot reply posted after the wait armed [64.54ms]
(pass) classifyNewHumanReply > returns null when only bot/stale comments exist [66.54ms]
(pass) classifyNewHumanReply > skips the dispatcher's own marked pause comment (posted as a non-bot human identity) [64.67ms]
(pass) classifyNewHumanReply > a genuine human reply that quote-replies the pause comment still resumes [65.37ms]
(pass) classifyReviewOutcome > a fresh CHANGES_REQUESTED review → changes-requested [65.70ms]
(pass) classifyReviewOutcome > a fresh APPROVED review → resolved [64.67ms]
(pass) classifyReviewOutcome > a fresh 0-actionable re-review → resolved even while decision stays CHANGES_REQUESTED [64.29ms]
(pass) classifyReviewOutcome > the `changes-requested` label alone (no fresh review) → changes-requested [68.62ms]
(pass) classifyReviewOutcome > only stale reviews and no actionable label → null (nothing changed) [65.15ms]
(pass) classifyReviewOutcome > a stale standing CHANGES_REQUESTED decision (no fresh review, no label) → null [67.87ms]
(pass) classifyReviewOutcome — CI gate > failing CI with no review feedback → resume to fix CI (CI_FAILED) [64.08ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review while CI is still pending is held (null) — don't end on un-built CI [65.89ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review with passing CI resolves [67.76ms]
(pass) classifyReviewOutcome — CI gate > explicit review feedback wins over red CI (address the review, which greens CI) [64.40ms]
(pass) classifyReviewOutcome — CI gate > absent CI (`none`) is non-blocking — the pre-CI review loop is unchanged [68.92ms]
(pass) classifyReviewOutcome — CI gate > failing CI but no PR change and no review → still CI_FAILED (red build is actionable) [68.23ms]
(pass) runPoller — answered-question > a new human reply fires epic-<n>-answered exactly once (idempotent across passes) [88.44ms]
(pass) runPoller — answered-question > a bot-only reply does not fire [82.59ms]
(pass) runPoller — answered-question > the dispatcher's own pause comment does not self-resume (#178) [82.19ms]
(pass) runPoller — review-changes > CHANGES_REQUESTED fires review-resolved with outcome 'changes-requested' [87.88ms]
(pass) runPoller — review-changes > APPROVED fires review-resolved as resolved [86.57ms]
(pass) runPoller — review-changes > a 0-actionable re-review fires review-resolved as resolved [87.33ms]
(pass) runPoller — review-changes > no PR yet → no fire [81.02ms]
[poller] poll failed for workflow 7c6147ec-4f0b-49fc-96f0-698fb5b72167 (epic-200-answered): API rate limit exceeded
(pass) runPoller — resilience > a gateway error for one workflow is isolated; others still fire [107.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 [79.38ms]
(pass) runPoller — GitHub rate-limit guards > a healthy budget proceeds (the guard isn't always-on) [91.57ms]
(pass) runPoller — GitHub rate-limit guards > caps the workflows polled per pass (burst protection) [124.07ms]

packages/dispatcher/test/github-epics.test.ts:
(pass) parseEpicsList > maps sub_issues_summary into Epic rows [0.89ms]
(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 751029a2-9293-4ce9-a207-e4ee089e7fc4)
(pass) reconcileMergedParks > a merged PR finalizes the parked workflow to `completed` and tears down its worktree [78.89ms]
[reconcile] thejustinwalsh/middle#51 PR CLOSED → cancelled (workflow 17084430-bc0b-48b4-8676-1cfb3373fb59)
(pass) reconcileMergedParks > a closed-unmerged PR finalizes to `cancelled` [77.86ms]
(pass) reconcileMergedParks > an open PR (a live review park) is left alone [77.23ms]
(pass) reconcileMergedParks > no PR for the Epic (a pending question) is left alone [74.03ms]
[reconcile] thejustinwalsh/middle#54 PR MERGED → completed (workflow 7275314b-f757-4184-9384-c433c299051b)
[reconcile] worktree cleanup failed for 7275314b-f757-4184-9384-c433c299051b (continuing): git worktree remove failed
(pass) reconcileMergedParks > finalizes the row even when worktree teardown throws (best-effort) [83.22ms]
(pass) reconcileMergedParks > only walks `waiting-human` rows — running/terminal rows are untouched [90.36ms]
[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 [76.88ms]
[reconcile] thejustinwalsh/middle#70 PR MERGED → completed (workflow f95458c9-5f64-4430-a1f1-974124dec355)
[reconcile] thejustinwalsh/middle#71 PR CLOSED → cancelled (workflow d5587512-2eab-44a3-9d78-6f5cd104fc6a)
[reconcile] thejustinwalsh/middle#72 PR MERGED → completed (workflow e2d3fbdc-7563-4add-b65d-822cc7911b6c)
(pass) reconcileMergedParks > fires onMergedTransition at most once per repo per pass (Epic #168 wiring) [101.92ms]
[reconcile] thejustinwalsh/middle#75 PR MERGED → completed (workflow 47e6215b-1943-439f-ad81-e3b522834819)
[reconcile] onMergedTransition for thejustinwalsh/middle failed (continuing): downstream sweep boom
[reconcile] thejustinwalsh/middle#76 PR MERGED → completed (workflow e53725b1-1038-4bf9-b222-beab2a359016)
(pass) reconcileMergedParks > a thrown onMergedTransition is isolated — the merged-parks pass still finishes [100.45ms]
[reconcile] thejustinwalsh/middle#60 PR MERGED → completed (workflow 74b0bb33-b0b0-4b49-8a08-14824dc9140c)
[reconcile] thejustinwalsh/middle#61 PR MERGED → completed (workflow 589588e2-59f3-484f-bde8-c1fd74d5a388)
(pass) reconcileMergedParks > honors the per-pass burst cap [104.50ms]
(pass) reconcileMergedParks > does not tear down the worktree when it loses the race to a concurrent resume [95.06ms]
(pass) finalizeParkedWorkflow > transitions a still-parked row and reports the change [87.79ms]
(pass) finalizeParkedWorkflow > no-ops (returns false) a row that already left waiting-human [82.14ms]

packages/dispatcher/test/recommender-workflow.test.ts:
(pass) recommender workflow — #43 shell: step order + dedicated slot > declares the seven spec steps in order [221.87ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > runs the steps in spec order at runtime and completes [302.91ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > records its row with kind 'recommender' — its own dedicated slot, off maxConcurrent [294.81ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > spawn-recommender-agent's step backstop is sized for the per-repo ceiling [190.61ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > prepare-shallow-worktree registers a compensation handler [186.78ms]
(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 [178.06ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' (not a UNIQUE error) [237.89ms]
[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' [273.57ms]
(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) [174.67ms]
(pass) recommender workflow — #44 build-prompt: every required input, verbatim > writes the assembled prompt to .middle/prompt.md and launches it via the @-reference [273.76ms]
(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 [268.18ms]
[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 [221.62ms]
[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 [266.08ms]
[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) [275.13ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > heartbeatRel formats epoch deltas; null → 'unknown' [178.91ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > dispatcherSectionsFromContext builds canonical sections (heartbeat, null-issue dropped, null-session→pending) [175.10ms]
(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.80ms]
(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 [268.22ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
[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) [2243.61ms]
[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 [273.62ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > derives rate_limits, in_flight, and slots from db + config [205.53ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > excludes the recommender's own row from in_flight and slots [186.25ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > scopes per-repo slots/in_flight to the repo, but global_used spans all repos [193.60ms]
(pass) recommender workflow — sessionNameFor collision-resistance > is deterministic for a given repo [177.04ms]
(pass) recommender workflow — sessionNameFor collision-resistance > produces a tmux-safe session name (no separators survive) [174.63ms]
(pass) recommender workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [180.16ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > runs on the engine via per-repo resolveRunSettings and creates the recommender row [267.78ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > a clear wiring error when neither resolveRunSettings nor static settings are provided [2069.86ms]

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 [2.97ms]
(pass) runStalenessCronPass > a non-ENOENT spec read error surfaces (not silently treated as missing spec) [2.24ms]
[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.37ms]
[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 [3.11ms]
[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.78ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a `..` traversal spec_path is rejected — the pass never reads outside the checkout [2.22ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a deeper `../../` traversal is rejected too [2.11ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > an absolute spec_path is rejected (the field is repo-relative by contract) [2.06ms]
[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.21ms]

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

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

packages/dispatcher/test/hook-server-gates.test.ts:
(pass) HookServer — /gates/pr-ready > returns 200 when the gate allows [2.33ms]
[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.49ms]
(pass) HookServer — /gates/pr-ready > forwards the session name and payload to the gate handler [1.72ms]
(pass) HookServer — /gates/pr-ready > 404s the gate route when no gate handler is wired [1.37ms]

packages/dispatcher/test/repo-config.test.ts:
(pass) repo pause/resume > an unpaused repo (no row) reads as not paused [1.89ms]
(pass) repo pause/resume > mm pause (indefinite) suspends the repo [1.95ms]
(pass) repo pause/resume > a paused_until in the future reads as paused; in the past auto-expires [1.50ms]
(pass) repo pause/resume > mm resume clears the pause [1.51ms]
(pass) repo pause/resume > pausing is idempotent and re-pausing updates the timestamp [1.40ms]
(pass) repo pause/resume > resume on a never-paused repo is a harmless no-op [1.37ms]
(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.39ms]
(pass) managed-repo registry (#135) > registering is idempotent and updates the path in place (one row) [1.39ms]
(pass) managed-repo registry (#135) > registering preserves an existing pause (doesn't clobber paused_until) [1.42ms]
(pass) managed-repo registry (#135) > listManagedRepos excludes rows with no checkout path (e.g. a pause-only row) [1.47ms]
(pass) managed-repo registry (#135) > setLastRecommenderRun writes a value and clears it with null (cron rollback) [1.43ms]
(pass) managed-repo registry (#135) > markRecommenderRun stamps and reads back last_recommender_run [1.41ms]

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

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.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.09ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("constructor") is false [0.11ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("hasOwnProperty") throws unknown-adapter [0.13ms]
(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.08ms]
(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.20ms]
(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.20ms]
(pass) AgentAdapter contract — claude > classifyStop: blocked.json → asked-question [0.44ms]
(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.17ms]
(pass) AgentAdapter contract — codex > resolveTranscriptPath yields a path from this adapter's own ready payload [0.13ms]
(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.09ms]
(pass) AgentAdapter contract — codex > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.44ms]
(pass) AgentAdapter contract — codex > classifyStop: blocked.json → asked-question [0.39ms]
(pass) AgentAdapter contract — codex > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.60ms]
(pass) AgentAdapter contract — codex > detectRateLimit is implemented and returns null on a clean transcript [0.17ms]
(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.13ms]
(pass) AgentAdapter contract — copilot > buildLaunchCommand yields a non-empty argv and the session env [0.35ms]
(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.18ms]
(pass) AgentAdapter contract — copilot > classifyStop: blocked.json → asked-question [0.40ms]
(pass) AgentAdapter contract — copilot > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.42ms]
(pass) AgentAdapter contract — copilot > detectRateLimit is implemented and returns null on a clean transcript [0.17ms]

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

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

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

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.61ms]
(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.57ms]
(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.54ms]
(pass) getSlotState > an adapter with no per-adapter cap is gated only by the repo and global dims [1.40ms]
(pass) reserveSlot > decrements the adapter, repo, and global dimensions for the loop's local view [1.45ms]
(pass) reserveSlot > reserving down to capacity flips the guard to refuse [1.83ms]
(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.44ms]
(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.07ms]
(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.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.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.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.09ms]
(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.08ms]
(pass) createParseFailureSurfacer (#180) > reset() re-arms surfacing after a healthy read [0.08ms]
(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
(pass) didReadState (#180) — gate re-arming on an actual read > disabled tick does not re-arm; a healthy (drained) read does [0.07ms]

packages/dispatcher/test/pr-divergence.test.ts:
(pass) classifyMergeability > DIRTY → CONFLICTED regardless of mergeable [62.49ms]
(pass) classifyMergeability > BEHIND → BEHIND [67.54ms]
(pass) classifyMergeability > CLEAN + MERGEABLE → CLEAN [63.35ms]
(pass) classifyMergeability > CLEAN but not MERGEABLE → UNKNOWN (CI gating, secondary signals) [62.76ms]
(pass) classifyMergeability > BLOCKED / HAS_HOOKS / UNSTABLE / UNKNOWN → UNKNOWN [67.96ms]
(pass) classifyMergeability > a null view (PR doesn't exist) → UNKNOWN [65.01ms]
(pass) classifyMergeability > missing fields → UNKNOWN (legacy fixtures don't tip the classifier) [68.14ms]
(pass) classifyDivergence > classifies BEHIND and persists the row with the supplied clock [70.58ms]
(pass) classifyDivergence > classifies CONFLICTED and overwrites a prior row (upsert keeps the row fresh) [76.38ms]
(pass) classifyDivergence > classifies CLEAN [70.52ms]
(pass) classifyDivergence > classifies UNKNOWN for a PR with no mergeability view (gone / 404) [68.90ms]
(pass) parseEpicFromHeadRef > parses `middle-issue-<N>` to the integer N [63.01ms]
(pass) parseEpicFromHeadRef > a non-managed head ref → null (the helper skips it) [65.74ms]
(pass) parseEpicFromHeadRef > a malformed managed ref → null (defends against an inadvertent rename) [64.37ms]
(pass) worktreePathFor > uses <root>/<repo>/issue-<n> — the same layout createWorktree writes [65.04ms]
(pass) recordDivergenceState > accepts terminal-ish states (DEMOTED, SKIPPED) written by sibling phases [72.10ms]
(pass) recordDivergenceState > the CHECK constraint rejects an out-of-vocabulary state — defends against a reconciler typo [68.41ms]
(pass) recordDivergenceState > the (repo, pr_number) PK lets the same pr_number coexist across repos [73.01ms]
(pass) applyDemoteToWork > flips PR draft, reopens sub-issue, posts dual-surface comment, re-enqueues, state→DEMOTED [71.35ms]
(pass) applyDemoteToWork > per-step idempotency: a second call skips draft-flip + reopen + comments via markers (but still re-enqueues) [71.42ms]
(pass) applyDemoteToWork > partial-retry: prior attempt left the PR drafted but did not reopen / comment / enqueue — second pass completes remediation [67.77ms]
(pass) applyDemoteToWork > partial-retry safety: existing marker on PR skips the duplicate PR comment, still posts on Epic [68.37ms]
(pass) applyDemoteToWork > Epic with no closed sub-issues: still demotes + comments + enqueues; no reopen call [69.58ms]
(pass) applyDemoteToWork > non-managed head ref → no-op (no draft, no comments, no enqueue, no row) [64.06ms]
(pass) applyDemoteToWork > manual recovery: an Epic that already carries the demote marker skips the reopen call (self-review hardening) [71.69ms]
(pass) applyDemoteToWork > a supplied reason (#201 data-loss) replaces the conflict narrative in the escalation comment [75.45ms]
(pass) applyDemoteToWork > PR doesn't exist (gateway returns null) → no-op [67.81ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a PullRequest with " [63.45ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a Branch with the n" [70.38ms]
(pass) ghStderrIsNotFound > recognizes not-found: "HTTP 404: Not Found (https://api.github." [65.82ms]
(pass) ghStderrIsNotFound > recognizes not-found: "graphql: Could not resolve to a Reposito" [66.42ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "error connecting to api.github.com: dial" [66.06ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 401: Bad credentials" [63.81ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 403: API rate limit exceeded" [69.87ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 502: Bad Gateway" [70.84ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "gh: command failed (oauth token expired)" [67.29ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "could not deserialize response" [67.31ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "remote: secret not found, push declined" [65.82ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "Not Found" [66.63ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "" [69.95ms]

packages/core/test/config.test.ts:
(pass) loadConfig — [docs] section > parses a full docs block [2.13ms]
(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.26ms]
(pass) loadConfig — [docs] section > no [docs] section leaves docs undefined [0.18ms]
(pass) loadConfig — [staleness] section > reads spec_path [0.21ms]
(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.30ms]
(pass) loadConfig — global only > parses the global sections and leaves per-repo sections undefined [0.19ms]
(pass) loadConfig — global only > expands ~ in path values [0.21ms]
(pass) loadConfig — per-repo merge > populates per-repo sections alongside global [0.38ms]
(pass) loadConfig — per-repo merge > per-repo values override global on a colliding key [0.34ms]
(pass) loadConfig — missing files > missing global file falls back to documented defaults without throwing [0.15ms]
(pass) loadConfig — missing files > missing per-repo file leaves per-repo sections undefined [0.24ms]
(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.26ms]
(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.28ms]
(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.29ms]
(pass) loadConfig — committed policy layer > no repoPath means no policy is derived (global-only callers unaffected) [0.21ms]

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.02ms]
(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.04ms]
(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.02ms]

packages/core/test/hook-script.test.ts:
(pass) PR_READY_GATE_SH exit-code contract > HTTP 200 → exit 0 (allow) [2.50ms]
(pass) PR_READY_GATE_SH exit-code contract > curl failure emitting no http code → exit 0 (fails OPEN, not closed) [2.00ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 403 from a reachable dispatcher → exit 2 (blocks) [2.27ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 404 (no gate wired — e.g. a recommender/docs session) → exit 0 (allow, never wedge) [2.04ms]
(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.26ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 500 (reachable dispatcher fault) → exit 2 (surface, not a silent allow) [2.50ms]

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.08ms]
(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 [156.54ms]
(pass) capturePane > returns null for an unknown session [1.25ms]
(pass) sendText and sendKeys > sendText writes literal text into the pane [158.56ms]
(pass) sendText and sendKeys > sendKeys with delayBetweenMs sends each key in its own call [224.79ms]
(pass) pollPaneFor > resolves with the predicate's value when the pane matches [313.40ms]
(pass) pollPaneFor > returns null on timeout when the pane never matches [415.37ms]
(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.46ms]

packages/adapters/codex/test/adapter.test.ts:
(pass) codexAdapter identity > name is 'codex' and readyEvent is session.started [0.24ms]
(pass) buildLaunchCommand > argv launches interactive codex (no exec, no prompt) [0.21ms]
(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.19ms]
(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.43ms]
(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.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.12ms]
(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.13ms]
(pass) readTranscriptState > parses a real-shaped rollout: activity, turn count, last tool use, context tokens [0.30ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.21ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.45ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.36ms]
(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.35ms]
(pass) classifyStop > a healthy structured block is authoritative → bare-stop, even with a stray '429' in text [0.32ms]
(pass) classifyStop > text fallback (no structured block): "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.37ms]
(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.33ms]
(pass) classifyStop > a bare "commit 4291abcdef" is NOT a rate-limit signal → bare-stop (4291 in a hash) [0.41ms]
(pass) classifyStop > a bare "listening on port 14290" is NOT a rate-limit signal → bare-stop (embedded 4290) [0.34ms]
(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.33ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.47ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.42ms]
(pass) classifyStop > nothing notable → bare-stop [0.30ms]
(pass) detectRateLimit > structured block at the limit → detection with the real reset time [0.19ms]
(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.18ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present at all [0.16ms]
(pass) installHooks > writes .codex/config.toml with auto-mode + sandbox_mode (NOT the rejected 'sandbox' key) [3.27ms]
(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.14ms]
(pass) installHooks > registers exactly the real Codex event set (PascalCase, no fictional names) [1.14ms]
(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.06ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.00ms]
(pass) installHooks > symlinks the operator's auth.json into the worktree CODEX_HOME [1.10ms]
(pass) installHooks > does not throw or create a link when the operator has no auth.json [0.90ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.21ms]
(pass) detectNeedsLogin > does not match normal pane content [0.11ms]
(pass) detectHooksTrustPrompt > matches the real 'Hooks need review' dialog text [0.16ms]
(pass) detectHooksTrustPrompt > does not match a normal pane or the directory-trust dialog [0.11ms]
(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.10ms]
(pass) detectReadyForInput > matches the live composer-ready welcome banner (codex 0.133.0) [0.11ms]
(pass) detectReadyForInput > does not match a boot dialog (so a dialog is answered before we treat it as ready) [0.13ms]
(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.88ms]

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.14ms]
(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.10ms]
(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.11ms]
(pass) resolveTranscriptPath > throws when the payload has no transcript_path [0.12ms]
(pass) readTranscriptState > parses activity, turn count, last tool use, and context tokens [0.33ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.25ms]
(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.37ms]
(pass) classifyStop > an unrecognized kind falls back to a plain question (kind omitted) [0.45ms]
(pass) classifyStop > asked-question tolerates a malformed/contentless blocked.json (sentinel → null) [0.40ms]
(pass) classifyStop > usage-limit message in the transcript tail → rate-limited [0.36ms]
(pass) classifyStop > done.json sentinel → done [0.32ms]
(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.41ms]
(pass) classifyStop > nothing notable → bare-stop [0.30ms]
(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.13ms]
(pass) installHooks > registers the full Claude hook event set in .claude/settings.json [1.00ms]
(pass) installHooks > each entry maps its Claude event to the normalized taxonomy via the absolute hook path [0.96ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.01ms]
(pass) installHooks > registers the PR-ready gate as a second Bash-matched PreToolUse hook [0.90ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.03ms]
(pass) detectBypassPrompt > matches representative bypass-mode confirmation strings [0.18ms]
(pass) detectBypassPrompt > does not match normal Claude pane content [0.13ms]
(pass) detectTrustPrompt > matches the first-run folder-trust dialog [0.13ms]
(pass) detectTrustPrompt > does not match the bypass dialog or normal content [0.13ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.18ms]
(pass) detectNeedsLogin > does not match the bypass prompt or normal pane content [0.10ms]
(pass) enterAutoMode > returns immediately when the target session does not exist [1.89ms]

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.11ms]
(pass) buildLaunchCommand > env carries the session vars and merges envOverrides [0.11ms]
(pass) buildLaunchCommand > forwards an exported gh token so token-auth keeps working under the repointed home [0.12ms]
(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 / 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.11ms]
(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.14ms]
(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.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.09ms]
(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.17ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.19ms]
(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.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.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.28ms]
(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.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.27ms]
(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.30ms]
(pass) classifyStop > done.json sentinel → done [0.34ms]
(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.30ms]
(pass) classifyStop > failed.json outranks stale rate-limit text in the transcript → failed [0.55ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.45ms]
(pass) classifyStop > nothing notable → bare-stop [0.35ms]
(pass) detectRateLimit > text rate-limit signal → detection with unknown reset (no structured block on disk) [0.19ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present [0.15ms]
(pass) installHooks > writes .copilot/hooks/middle.json with version 1 and the camelCase event keys [1.31ms]
(pass) installHooks > maps each Copilot event to the normalized taxonomy via the absolute hook path [1.10ms]
(pass) installHooks > registers the PR-ready gate as a SECOND preToolUse hook scoped to the bash tool [1.02ms]
(pass) installHooks > pre-trusts the worktree in config.json so copilot skips the folder-trust dialog [2.76ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.15ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [0.92ms]
(pass) installHooks > writes NO auth file (copilot authenticates via gh, unlike codex) [0.93ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.20ms]
(pass) detectNeedsLogin > does not match normal pane content [0.10ms]
(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.14ms]
(pass) detectTrustPrompt > matches a folder-trust dialog (defense-in-depth; pre-empted by trustedFolders) [0.14ms]
(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.83ms]

packages/dispatcher/test/epic-store/file-state-gateway.test.ts:
(pass) fileStateGateway > readBody returns the state file contents verbatim [0.49ms]
(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.41ms]
(pass) fileStateGateway > writeBody derives the temp sibling from the filename via `basename` (separator-safe) [0.31ms]
(pass) fileStateGateway > writeBody overwrites an existing file [0.24ms]

packages/dispatcher/test/epic-store/file-poll-gateway.test.ts:
(pass) filePollGateway > listIssueComments derives authorIsBot structurally from the marker kind [0.84ms]
(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.41ms]
(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.31ms]
(pass) filePollGateway > findEpicPrLifecycle returns null for a slug with no stamped meta.pr [0.22ms]
(pass) filePollGateway > a numeric-named file Epic (e.g. 42.md) resolves via meta.pr, not gh's #42 finder (#200) [0.22ms]
(pass) filePollGateway > prSnapshot / prLifecycle delegate straight to gh by PR number [0.18ms]
(pass) filePollGateway > getRateLimit delegates straight to gh [0.14ms]

packages/dispatcher/test/epic-store/file-epic-gateway.test.ts:
(pass) fileEpicGateway > listOpenEpics scans the dir, derives sub-issue progress, skips closed [0.73ms]
(pass) fileEpicGateway > listIssueComments maps the conversation; answer is attributed to the human [0.50ms]
(pass) fileEpicGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.21ms]
(pass) fileEpicGateway > getCommentAuthor discriminates human (answer) from agent by the file:// fragment [0.19ms]
(pass) fileEpicGateway > getCommentAuthor delegates a github.com URL to gh [0.15ms]
(pass) fileEpicGateway > getIssueLabels reads the Epic meta labels [0.22ms]
(pass) fileEpicGateway > postComment appends a re-parseable dispatch-event block [0.60ms]
(pass) fileEpicGateway > postComment delegates a PR-number ref to gh (no Epic file for it) [0.18ms]
(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.12ms]
(pass) fileEpicGateway > addLabel appends to meta labels and is a no-op if already present [0.42ms]
(pass) fileEpicGateway > a present-but-malformed Epic file surfaces the parser's named error [0.20ms]
(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.73ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > a different question (or different kind/context) appends a new entry [1.43ms]
(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.08ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(all-closed.md)) === all-closed.md [0.10ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(codex-adapter.md)) === codex-adapter.md [0.05ms]
(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-vxCmxR/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-vxCmxR/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 [240.00ms]
[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-Rg3SWT/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-Rg3SWT/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 [292.49ms]

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-TNBhFJ/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fdisp-TNBhFJ/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 [301.78ms]
(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.52ms]

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-CxiQCc/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-CxiQCc/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 [281.61ms]
[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-bn4t3B/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-bn4t3B/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-bn4t3B/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-bn4t3B/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 [312.65ms]
[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-zfAKOU/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-zfAKOU/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 [225.13ms]
[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-Bva0Wb/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-Bva0Wb/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-Bva0Wb/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-Bva0Wb/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 [314.35ms]

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-FNP2ZB/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-FNP2ZB/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-FNP2ZB/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-FNP2ZB/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 [423.88ms]

packages/dispatcher/test/epic-store/watcher.test.ts:
(pass) collectChangedSince > includes files with mtime > sinceMs, excludes older + dotfiles/.tmp [0.46ms]
(pass) collectChangedSince > missing dir → empty [0.14ms]
(pass) pollFileSignals > emits an open question that has a non-empty answer [0.20ms]
(pass) pollFileSignals > an unanswered question (placeholder) does NOT trigger [0.19ms]
(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.32ms]
(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 [84.38ms]
(pass) runFileWatcherTick > does NOT resume a workflow parked on a non-answered signal (reason guard) [76.44ms]

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

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.31ms]

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.29ms]
(pass) file-mode PR-review resume (real poller path) > no resume while the Epic file has no stamped meta.pr (PR not opened yet) [84.68ms]

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.24ms]
(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.08ms]
(pass) parseEpicFile — acceptance criteria > parses unchecked criteria from codex-adapter [0.06ms]
(pass) parseEpicFile — acceptance criteria > parses checked criteria from all-closed [0.06ms]
(pass) parseEpicFile — sub-issues > parses sub-issues with stable IDs + body [0.06ms]
(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.10ms]
(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 [68.50ms]
(pass) file-mode auto-dispatch (real readState path) > a github-mode repo still routes readState to the gh state issue gateway [68.56ms]

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.07ms]
(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 [0.01ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: missing command
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty command
(pass) parseVerifyConfig — malformed fails loudly > rejects: duplicate name [0.03ms]
(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.02ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty phases [0.01ms]
(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.44ms]
(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.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) [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.03ms]
(pass) parseStatusCheckboxes > only the FIRST ## Status section is parsed; a later one is ignored [0.01ms]
(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.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.09ms]

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.25ms]
(pass) pr-ready gate handler > allows when the Epic PR's criteria are all evidenced [0.15ms]
(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.06ms]
(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 [1.06ms]
(pass) runGate > a failing gate captures the non-zero exit and stderr [0.59ms]
(pass) runGate > a gate that exceeds its timeout is killed and reported as timed out [701.93ms]
(pass) runGate > runs in the given cwd [1.92ms]
(pass) runGates > runs every gate in declared order; aggregate ok when all pass [1.23ms]
(pass) runGates > a failing gate makes the aggregate fail and names the first failure; later gates still run [1.50ms]
(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 [2.20ms]
(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.60ms]
(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.42ms]
(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.50ms]
(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.82ms]
(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.69ms]

packages/dispatcher/test/gates/pr-ready.test.ts:
(pass) parseAcceptanceCriteria > extracts the list items under the acceptance-criteria heading only [0.06ms]
(pass) parseAcceptanceCriteria > returns [] when there is no acceptance-criteria section [0.01ms]
(pass) commandIsPrReady > matches a bare and an argumented `gh pr ready` [0.01ms]
(pass) commandIsPrReady > does not match other gh commands
(pass) extractCommand > reads tool_input.command from a Claude/Codex PreToolUse payload [0.02ms]
(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.11ms]
(pass) evaluatePrReady > denies and names the criterion that has no evidence [0.09ms]
(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.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.05ms]
(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.08ms]
(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.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.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 [84.49ms]
(pass) runCheckboxRevertPass > a passing-gate checkbox stays checked; SHA + state persisted [78.96ms]
(pass) runCheckboxRevertPass > head-SHA gate: an unchanged SHA skips a would-be transition entirely [79.53ms]
(pass) runCheckboxRevertPass > an advanced SHA re-processes: the new transition's gate runs and reverts [84.72ms]
(pass) runCheckboxRevertPass > undefined gateway SHA falls through to the reconciler's checkbox-state diff [80.53ms]
(pass) runCheckboxRevertPass > no usable verify.toml → the workflow is skipped (nothing to enforce) [72.93ms]
[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 [73.27ms]
[checkbox-revert] pass failed for workflow bad (o/r#1): GitHub down
(pass) runCheckboxRevertPass > a per-workflow failure is isolated — other workflows still process [91.33ms]
(pass) runCheckboxRevertPass > a parked (non-running) workflow is not processed [73.72ms]

 1372 pass
 0 fail
 3448 expect() calls
Ran 1372 tests across 123 files. [86.91s]

@thejustinwalsh

Copy link
Copy Markdown
Owner Author

Verification gates — phase #217

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

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

[stderr]
$ oxfmt

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

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

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

test — ✅ pass (94.6s)
$ 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.42ms]
(pass) resolveDocsTarget — detection > Starlight wins over co-resident TypeDoc [0.09ms]
(pass) resolveDocsTarget — detection > detects Docusaurus from docusaurus.config.js [0.05ms]
(pass) resolveDocsTarget — detection > detects MkDocs and reads a custom docs_dir [0.09ms]
(pass) resolveDocsTarget — detection > detects MkDocs with the default docs_dir [0.12ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from typedoc.json and reads out [0.17ms]
(pass) resolveDocsTarget — detection > detects TypeDoc from a package.json typedoc key [0.06ms]
(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.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.04ms]
(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.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 [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.04ms]
(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.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 [68.84ms]

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

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.26ms]
(pass) /api/epics > dispatch 404s when no dispatch seam is wired [0.08ms]
(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.58ms]
(pass) Queue renders nothing-in-flight row when live is empty [0.68ms]
(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.61ms]
(pass) Queue state cell carries the s-running class [0.32ms]
(pass) Queue renders rate-limit chip with adapter name, status, and chip class [0.28ms]
(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.17ms]
(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.14ms]
(pass) EpicRef > no-Epic (both null) renders the caller's fallback [0.10ms]
(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.06ms]
(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.58ms]
(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.45ms]
(pass) Inspector Epic rendering > github-mode panel is unchanged (`#7`, no link) [0.26ms]

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

packages/dashboard/test/activity.test.tsx:
(pass) Activity > renders Recommender and Documentation sections [0.80ms]
(pass) Activity > shows an output link when present and omits it otherwise [0.31ms]
(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.34ms]

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

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

packages/dashboard/test/api.test.ts:
(pass) dashboard JSON API > GET /api/repos returns a JSON array of repo summaries [81.96ms]
(pass) dashboard JSON API > GET /api/repos/:repo returns NEXT UP + IN FLIGHT for a known repo [137.41ms]
(pass) dashboard JSON API > github-mode IN FLIGHT row carries epicRef alongside the numeric epic (#187) [74.87ms]
(pass) dashboard JSON API > file-mode IN FLIGHT row surfaces epic_ref as epicRef with a null epic (#187) [116.02ms]
(pass) dashboard JSON API > GET /api/sessions/:session carries epicRef for a file-mode runner (#187) [90.23ms]
(pass) dashboard JSON API > GET /api/repos/:repo 404s an unknown repo [81.36ms]
(pass) dashboard JSON API > GET /api/banner reports per-adapter rate limits (UNKNOWN unobserved) [105.10ms]
(pass) dashboard JSON API > GET /api/sessions/:session returns the Inspector runner panel with attach commands [83.53ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach control flips controlled_by and spawns a terminal [86.85ms]
(pass) dashboard JSON API > POST /api/sessions/:session/attach rejects an invalid mode [75.79ms]
(pass) dashboard JSON API > POST /api/rate-limits/:adapter/clear sets the adapter AVAILABLE [72.64ms]
(pass) dashboard JSON API > GET /api/sessions/:session/events validates the limit param [87.68ms]
(pass) dashboard JSON API > POST /api/repos/:repo/pause validates untilMs [77.47ms]
(pass) dashboard JSON API > a runner with no session_name is reachable by its workflow id [77.14ms]
(pass) dashboard JSON API > a malformed percent-encoded path segment is a 400, not a 500 [72.25ms]
(pass) dashboard JSON API > unknown /api routes 404 as JSON [95.76ms]

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

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

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

packages/dashboard/test/app.test.tsx:
(pass) App nav includes a queue tab [0.94ms]
(pass) App nav includes an activity tab [0.33ms]
(pass) api.runs reads runs from a live server [131.69ms]
(pass) App defaults to the Epics view (nav tab + empty state render) [0.46ms]
(pass) api.epics reads Epic cards from a live server [155.64ms]
(pass) applyWorkflowFrame upserts non-terminal and drops terminal workflows [0.16ms]
(pass) dashboard views (static render) > GlobalBanner shows per-adapter rate limits + GitHub quota [0.36ms]
(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.47ms]
(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 [154.16ms]
(pass) api-client against a live server > api.attach(control) flips controlled_by; api.release reverts it [166.93ms]
(pass) api-client against a live server > api.runRecommender surfaces a non-2xx as an ApiError [156.22ms]

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

packages/dashboard/test/spa.test.ts:
Bundled page in 21ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > GET / serves the bundled HTML shell [144.50ms]
Bundled page in 34ms: packages/dashboard/src/index.html
(pass) dashboard SPA + server > the bundled entry script transpiles the TSX app [167.83ms]
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 [149.80ms]

packages/state-issue/test/validate.test.ts:
(pass) validate > passes a schema-conforming state [0.16ms]
(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.03ms]
(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.02ms]
(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
(pass) validate > fails when a blocked issue-blocker reference is malformed [0.01ms]
(pass) validate > accepts a non-issue blocker in backticks
(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 [299.77ms]

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

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.06ms]
(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.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.05ms]
(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.10ms]
(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.03ms]
(pass) parseStateIssue > returns ParseError when the Ready table omits the documented empty-state row [0.02ms]
(pass) parseStateIssue > an In-flight section with no bullet reads as empty (lenient empty-state) [0.05ms]
(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.02ms]
(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.45ms]
(pass) addMiddleIgnore > preserves existing unrelated entries [0.25ms]
(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.29ms]
(pass) removeMiddleIgnore > deletes the file when it empties [0.36ms]
(pass) removeMiddleIgnore > also clears a legacy bare `.middle/` line [0.26ms]
(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.26ms]
(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.39ms]
(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.27ms]
(pass) mm config auto_dispatch > rejects an unknown key and an invalid value [0.20ms]
(pass) mm config auto_dispatch > errors when the config file is missing [0.45ms]

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

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

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

packages/cli/test/bootstrap-hook.test.ts:
(pass) bootstrap hook.sh asset > is byte-identical to the canonical HOOK_SH constant [0.91ms]
(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) [145.50ms]

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

packages/cli/test/doctor.test.ts:
(pass) runDoctor — happy path > returns 0 and prints every check when the toolchain is healthy [1193.91ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + existing epics dir → epics_dir pass, no state-issue row [1321.59ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + missing epics dir → epics_dir fail, no state-issue row [1090.31ms]
(pass) runDoctor — mode-aware Epic-store check > github mode (no config row) → state-issue row, no epics_dir row [1434.29ms]
(pass) runDoctor — mode-aware Epic-store check > doctor honors the documented file-mode config [1655.02ms]
(pass) runDoctor — mode-aware Epic-store check > file mode + malformed Epic file → epic-files fail [1085.13ms]
(pass) checkAdapterBinaries > null config (unparseable) → single warn, no throw [0.10ms]
(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) [24.59ms]
(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.03ms]
(pass) summarizeRetention > clean last run → pass, reports the run [0.06ms]
(pass) summarizeRetention > failed last run → warn, surfaces FAILED [0.02ms]
(pass) runVocabularyCheck — docs↔code label drift guard (#217) > complete doc → exit 0, lists every documented label [0.61ms]
(pass) runVocabularyCheck — docs↔code label drift guard (#217) > missing a code-keyed label → exit 1, names the code disagreement [0.23ms]
(pass) runVocabularyCheck — docs↔code label drift guard (#217) > missing a code-keyed internals label → exit 1 [0.40ms]
(pass) runVocabularyCheck — docs↔code label drift guard (#217) > missing a required-but-not-code-keyed label → exit 1 (deleted section caught) [0.25ms]
(pass) runVocabularyCheck — docs↔code label drift guard (#217) > missing doc file → exit 1 [0.17ms]
(pass) runVocabularyCheck — docs↔code label drift guard (#217) > `mm doctor --vocabulary-check` boots the CLI and exits 0 against the shipped doc [152.63ms]

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

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.10ms]
(pass) checkStateIssue > passes against middle's own source tree [0.06ms]
(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 45ms: packages/dashboard/src/index.html
(pass) dashboardHostExtras routes + the hook fetch fallback coexist on one port [53.23ms]
(pass) a dispatch POST reaches the host-context dispatch callback [6.14ms]
(pass) dispose clears the process-global rate-limit observer (no broadcast after teardown) [2.04ms]

packages/cli/test/issue-audit.test.ts:
(pass) isFeatureIssue > epics, docs and chore issues are out of scope [0.10ms]
(pass) auditIssues > filters to feature issues and applies the rubric [0.35ms]
(pass) runAuditIssues --issue mode > flags a weak issue, returns 1, and labels it when --label is set [0.48ms]
(pass) runAuditIssues --issue mode > a thrown fetch error is handled: returns 1 and logs, not an unhandled rejection [0.20ms]
(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.10ms]
(pass) runAuditIssues backlog mode > returns 1 when any feature issue fails; labels only failures [0.11ms]

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

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) [153.64ms]
(pass) mm audit-issues --body-file (real CLI) > passes a well-formed issue carrying an integration criterion (exit 0) [144.69ms]
(pass) mm audit-issues --body-file (real CLI) > --json emits a machine-readable report [150.93ms]
(pass) mm audit-issues --body-file (real CLI) > rejects a non-positive-integer --issue with a clear error (exit 1) [743.90ms]

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.01ms]
(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.76ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > flags claude-md: false with a stray CLAUDE.md [0.60ms]
(pass) checkModuleIndex — flag↔CLAUDE.md consistency > passes when flag and presence agree, and skips bootstrap-assets [1.13ms]
(pass) checkModuleIndex — the real middle packages tree > every src/index.ts(x) carries valid, consistent frontmatter [0.78ms]
(pass) checkModuleIndex — the real middle packages tree > finds every package's index front door [0.64ms]

packages/cli/test/bootstrap-init.test.ts:
(pass) mm init — fresh install > stages skills, hooks, config, state issue, and gitignore [14.22ms]
(pass) mm init — fresh install > the created state-issue body parses and validates [8.80ms]
(pass) mm init — idempotent re-init > a matching-version re-init refreshes assets but keeps config and issue [10.53ms]
(pass) mm init — idempotent re-init > re-init does not clobber a team's committed policy edits (AC #103) [12.52ms]
(pass) mm init — idempotent re-init > a fresh clone (committed policy, no local cache) reconciles the issue and keeps policy [8.90ms]
(pass) mm init — idempotent re-init > loadConfig reads init's two files via sibling derivation and merges them [7.56ms]
(pass) mm init — dry run > writes nothing and makes no GitHub calls [0.37ms]
(pass) mm init — validation > rejects a dirty working tree [0.46ms]
(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.47ms]
(pass) mm init — existing config without a usable state issue > a matching-version re-init with no issue number mints one and persists it [8.87ms]
(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.00ms]
(pass) mm init — reconciles the state issue against GitHub > warns and reuses the oldest when GitHub has duplicate state issues [9.06ms]
(pass) mm init — reconciles the state issue against GitHub > creates a state issue only when GitHub has none [6.62ms]
(pass) mm uninit > closes the issue and removes everything init staged [11.28ms]
(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.56ms]
(pass) mm uninit > falls back to default_branch 'main' when committed policy has a non-string value (#103) [0.58ms]
(pass) mm uninit > dry run removes nothing [8.44ms]
(pass) mm uninit > strips only middle's hook entries, preserving foreign ones [8.20ms]

packages/cli/test/dispatch.test.ts:
(pass) runDispatch — input validation > rejects a malformed numeric epic (digit-leading but not a whole number) [6.98ms]
(pass) runDispatch — input validation > rejects an epic number below 1 [6.57ms]
(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 [113.65ms]
(pass) runDispatch — control client > a file-mode slug dispatches with epicRef and skips the gh label fetch [11.26ms]
(pass) runDispatch — control client > subscribes to /control/events BEFORE POSTing /control/dispatch [107.33ms]
(pass) runDispatch — control client > exits 0 when the workflow parks for review (waiting-human) [115.26ms]
(pass) runDispatch — control client > exits 1 when the workflow fails [128.98ms]
(pass) runDispatch — control client > reconnects when the event stream drops mid-flight and follows to completion [110.28ms]
(pass) runDispatch — control client > --adapter overrides the agent label and the default, and is sent to the daemon [11.23ms]
(pass) runDispatch — control client > an agent:<name> label on the Epic selects that adapter [10.83ms]
(pass) runDispatch — control client > no agent label falls back to the default adapter [10.78ms]
(pass) runDispatch — control client > a disabled adapter is rejected (exit 1), even via --adapter, before any dispatch [10.44ms]
(pass) runDispatch — control client > an unconfigured --adapter is rejected (exit 1) before any dispatch [10.30ms]
(pass) runDispatch — control client > friendly failure (exit 1) when the daemon can't be reached or started [511.63ms]

packages/cli/test/state-issue-body.test.ts:
(pass) buildInitialStateIssueBody > parses and validates against the schema (configured adapters) [0.15ms]
(pass) buildInitialStateIssueBody > is empty in every section [0.06ms]
(pass) buildInitialStateIssueBody > round-trips byte-identically (the keystone invariant) [0.05ms]
(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.49ms]
(pass) runStart / runStop lifecycle > start refuses when a live dispatcher is already recorded [101.08ms]
(pass) runStart / runStop lifecycle > start clears a stale pid file and launches fresh [0.64ms]
(pass) runStart / runStop lifecycle > stop exits non-zero when no dispatcher is running [0.23ms]
(pass) runStartCommand --window > opens the dashboard window once /health is ready [0.80ms]
(pass) runStartCommand --window > does not open the window when /health never becomes ready (but start still succeeds) [0.54ms]
(pass) runStartCommand --window > a throwing opener (or health probe) never fails the start — window step is best-effort [0.50ms]
(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 [315.11ms]
(pass) checkTsdocCoverage > flags an undocumented local export [307.60ms]
(pass) checkTsdocCoverage > resolves a re-export to the original declaration's doc comment [280.94ms]
(pass) checkTsdocCoverage > a bare `export {}` module contributes no exports [295.87ms]
(pass) checkTsdocCoverage > analyzes the real middle tree without throwing [471.35ms]

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

packages/cli/test/bun-path.test.ts:
(pass) isDirOnPath > true when present [0.04ms]
(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.05ms]
(pass) resolveShellRc > bash on macOS targets .bash_profile (login shells don't source .bashrc)
(pass) resolveShellRc > bash elsewhere targets .bashrc
(pass) resolveShellRc > unknown shell
(pass) bunPathSnippet > HOME-relative form when dir is the canonical ~/.bun/bin [0.03ms]
(pass) bunPathSnippet > literal form when dir is non-canonical [0.03ms]
(pass) rcAlreadyConfigured > detects literal bin dir [0.02ms]
(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.20ms]

packages/cli/test/skills-sync.test.ts:
(pass) syncSkills > copies every canonical file into the mirror byte-for-byte [1.15ms]
(pass) syncSkills > a second sync is a no-op (inSync, no changes) [0.99ms]
(pass) syncSkills > removes stale files the canonical no longer has [1.03ms]
(pass) syncSkills > detects and removes an orphaned skill DIRECTORY present only in the mirror [1.31ms]
(pass) diffSkills / check mode > check mode reports drift without writing [0.61ms]
(pass) diffSkills / check mode > check mode reports in-sync once synced [1.13ms]
(pass) diffSkills / check mode > check mode catches a single-byte edit in the mirror [1.00ms]
(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.53ms]

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.48ms]
[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.74ms]

packages/dispatcher/test/watchdog.test.ts:
(pass) watchdog — launch timeout > a launching workflow past the window is failed 'stuck-launching' [81.55ms]
(pass) watchdog — launch timeout > a launching workflow within the window is left alone [71.68ms]
(pass) watchdog — prompt not accepted > a running session that went ready but never started a turn is failed 'prompt-not-accepted' [84.11ms]
(pass) watchdog — prompt not accepted > a running session whose prompt landed (turn.started present) is not failed [79.69ms]
(pass) watchdog — prompt not accepted > a running session still within the launch window is not yet failed [74.33ms]
(pass) watchdog — tmux liveness > a running workflow with a dead session is failed + compensation triggered [79.75ms]
(pass) watchdog — tmux liveness > a running workflow with a live session is not failed for liveness [71.10ms]
[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 [71.24ms]
[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 [78.74ms]
[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.09ms]
[watchdog] killSession failed for middle-14: kill failed
(pass) watchdog — tmux liveness > a killSession() error still records the failure decision [78.61ms]
(pass) watchdog — activity freshness > idle ≥ threshold marks one idle event but does not kill [73.39ms]
(pass) watchdog — activity freshness > idle ≥ kill-threshold kills the session and fails 'idle-timeout' [91.15ms]
(pass) watchdog — activity freshness > freshness is skipped while controlled_by = 'human' [71.56ms]
(pass) watchdog — activity freshness > a stale heartbeat is rescued by fresh transcript activity (cross-check) [72.15ms]
(pass) watchdog — sentinel re-arm > a blocked.json with no armed signal arms one, idempotently [76.38ms]
(pass) watchdog — sentinel re-arm > no sentinel file → no signal armed [71.00ms]
(pass) watchdog — blocked sentinel self-heal > idle ≥ kill-threshold with a blocked sentinel hands off to the drive, not compensation [82.72ms]
[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.55ms]
(pass) watchdog — blocked sentinel self-heal > the handoff is recorded once, not every idle tick [79.68ms]
(pass) reconcileTranscriptDrift > advances last_heartbeat when the transcript is newer than the recorded beat [78.13ms]
(pass) reconcileTranscriptDrift > leaves the heartbeat alone when the transcript is older [74.06ms]
(pass) notification failsafe — detect + capture + intervene > a notification still within the grace window is left alone [78.04ms]
(pass) notification failsafe — detect + capture + intervene > a notification past the grace window captures the pane, classifies, and nudges [85.46ms]
(pass) notification failsafe — detect + capture + intervene > classifies a plain 'waiting for input' notification as a question (kind=input) [93.86ms]
(pass) notification failsafe — detect + capture + intervene > an agent that resumed after the notification (newer activity) is left alone [123.47ms]
(pass) notification failsafe — detect + capture + intervene > a human-controlled session is never rescued (a human will answer) [99.46ms]
(pass) notification failsafe — detect + capture + intervene > no-op when the tmux surface lacks the failsafe methods [95.77ms]
(pass) notification failsafe — detect + capture + intervene > a capture-only notification (no message payload) still classifies + nudges [114.06ms]
(pass) notification failsafe — fast-fail backstop > still idle past the kill-grace → fast-fails with the captured kind and kills the session [142.88ms]
(pass) notification failsafe — fast-fail backstop > two captures sharing a ts → the latest-by-id kind wins (contract lock) [122.39ms]
(pass) notification failsafe — fast-fail backstop > within the kill-grace → not yet failed (the nudge still has time to take) [124.11ms]
(pass) notification failsafe — fast-fail backstop > a repeat notification with no activity does NOT reset the kill clock — still fast-fails [124.46ms]
(pass) notification failsafe — fast-fail backstop > a fresh notification AFTER genuine activity re-arms the failsafe (re-captures) [140.36ms]

packages/dispatcher/test/recommender-run.test.ts:
[recommender-run] workflow wf_1780551933543_no26smm6 enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > runs to completion and records a kind:'recommender' workflow row for the repo [404.72ms]
[recommender-run] workflow wf_1780551933969_zmsme5ul enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > read-only by default: with no triggerAutoDispatch wired, a clean run dispatches nothing [421.59ms]
[recommender-run] workflow wf_1780551934354_z4x2lrsz enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > fires triggerAutoDispatch on a clean run when wired and auto_dispatch is on (trigger #1) [385.32ms]
[recommender-run] workflow wf_1780551934736_ct2fppxb enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > forwards epicStore so a file-mode run frames the prompt for the file store (#200) [383.09ms]
[recommender-run] workflow wf_1780551935112_h3bk5dj0 enqueued
(pass) dispatchRecommender — enqueues a recommender workflow (read-only) > does not fire triggerAutoDispatch when auto_dispatch is off, even if wired [376.55ms]
(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.54ms]
(pass) resolveRecommenderOptions — adapter enabled-gate > github mode still requires a configured state issue number [6.19ms]
(pass) resolveRecommenderOptions — schema resolution (issue #107) > resolves schemaPath from the middle install, not from repoPath [7.58ms]

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.53ms]
(pass) updateDispatcherSections > the owned sections actually changed [0.15ms]
(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.23ms]
(pass) updateDispatcherSections > ticks do not accumulate across repeated updates [0.24ms]
(pass) readState > parses a valid body [0.12ms]
(pass) readState > throws on a malformed body [0.08ms]
(pass) insertDispatcherTick > leaves a non-canonical body untouched [0.01ms]

packages/dispatcher/test/stop-wait.test.ts:
(pass) awaitStopOrSessionEnd > resolves via 'stop' when the Stop hook arrives first [5.25ms]
(pass) awaitStopOrSessionEnd > resolves via 'session-ended' when liveness goes false while Stop is pending [11.18ms]
(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' [5.13ms]
(pass) awaitStopOrSessionEnd > liveness-probe errors are ignored — a later Stop still wins [21.18ms]

packages/dispatcher/test/build-deps.test.ts:
(pass) buildImplementationDeps > wires deps from the injected collaborators and returns the gate it built [66.37ms]
(pass) buildImplementationDeps > epicPrReadiness reports a missing PR as { exists: false, isDraft: false } [65.04ms]
(pass) buildImplementationDeps > the factory module imports no engine (no bunqueue construction) [2.96ms]
(pass) buildImplementationDeps > the default postQuestion posts a gh comment framed by pause kind [65.61ms]
(pass) buildImplementationDeps > the default postQuestion is idempotent on a repeated identical question (#205) [66.09ms]
(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.21ms]
(pass) formatPauseComment > a complexity pause carries the `complexity pause` label vocabulary [0.14ms]
(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.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.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.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.05ms]

packages/dispatcher/test/hook-store.test.ts:
(pass) DbHookStore — resolveSessionToken > returns the token of the active workflow owning the session [76.68ms]
(pass) DbHookStore — resolveSessionToken > returns null for an unknown session [64.11ms]
(pass) DbHookStore — resolveSessionToken > ignores terminal workflows that previously held the deterministic session name [84.55ms]
(pass) DbHookStore — record > writes an events row for every hook [79.71ms]
(pass) DbHookStore — record > tool.pre and tool.post advance last_heartbeat [88.52ms]
(pass) DbHookStore — record > a non-tool event records but does not advance last_heartbeat [85.25ms]
(pass) DbHookStore — record > session.started writes session_id + transcript_path onto the workflow [79.62ms]
[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.22ms]
(pass) DbHookStore — record > oversized payloads are truncated before storage [82.93ms]
[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 [83.46ms]
(pass) serializePayload > returns compact JSON for a small payload [94.32ms]
(pass) serializePayload > clips and marks a payload over 16KB [124.05ms]

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

packages/dispatcher/test/notification-classify.test.ts:
(pass) classifyNotification — permission blocks > message "Claude needs your permission to use Bash" → permission [0.03ms]
(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
(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/poller-gateway.test.ts:
(pass) deriveCiStatus > no checks configured → none (nothing to gate on) [0.09ms]
(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.02ms]
(pass) ghPollGateway.prSnapshot failure isolation > a `pr view` failure also degrades to null (the symmetric branch) [0.88ms]
(pass) ghPollGateway.prSnapshot failure isolation > both fetches succeed → a populated snapshot [1.49ms]

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.38ms]
(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.18ms]
[backlog-audit] o/active#1 fails the integration rubric → needs-design
(pass) runAuditCronPass > sweeps managed repos, skips paused ones [2.04ms]

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

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

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

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-16Pycs/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-16Pycs/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.26ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-TE8oue/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-TE8oue/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 [268.53ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-LQ97Do/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-LQ97Do/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 [300.02ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-2l19Ks/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-2l19Ks/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) [267.57ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-OPwU13/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-OPwU13/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) [288.96ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-0nkSRj/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-0nkSRj/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 [512.45ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-InnOIb/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-InnOIb/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 [881.90ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-stjxqr/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-stjxqr/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 [287.38ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Nquesv/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Nquesv/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) [294.86ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-XETsIp/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-XETsIp/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) [294.00ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Zn3ES3/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Zn3ES3/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) [288.20ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-eMJo6s/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-eMJo6s/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 [286.67ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-A6Kj3h/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-A6Kj3h/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-A6Kj3h/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-A6Kj3h/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-A6Kj3h/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-A6Kj3h/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 [417.54ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-cHkE68/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-cHkE68/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' [207.98ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ca2D8P/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ca2D8P/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) [260.23ms]
[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-q0sAIi/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-q0sAIi/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 [318.44ms]
[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-QgYSEr/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-QgYSEr/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 [329.09ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-uqGXQO/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-uqGXQO/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) [265.98ms]
[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-kCY0TO/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-kCY0TO/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 [280.50ms]
[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-KWs2Bt/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-KWs2Bt/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-KWs2Bt/worktrees/thejustinwalsh/middle/issue-99
[workflow:middle-thejustinwalsh-middle-99] launching tmux session: true (cwd=/tmp/middle-wf-KWs2Bt/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 [331.26ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ZGZa5m/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ZGZa5m/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-ZGZa5m/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ZGZa5m/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 [331.08ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-mkg87Q/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-mkg87Q/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-mkg87Q/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-mkg87Q/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 [333.25ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-b5Fy08/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-b5Fy08/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-b5Fy08/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-b5Fy08/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) [333.71ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-cgNMJV/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-cgNMJV/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 [352.71ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-FnkRln/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-FnkRln/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-FnkRln/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-FnkRln/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-FnkRln/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-FnkRln/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 [481.18ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-bK7t98
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-bK7t98)
[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) [294.56ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wt-stub-Z8pTPW
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wt-stub-Z8pTPW)
[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 [314.66ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-OSaWBz/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-OSaWBz/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) [329.13ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-C1lLdr/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-C1lLdr/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 [328.47ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-ALaYqz/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-ALaYqz/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 [330.06ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-UZZWU6/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-UZZWU6/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 [330.64ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-YlH5yz/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-YlH5yz/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) [332.64ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-hl0veO/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-hl0veO/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' [324.24ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-Z6Yug7/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-Z6Yug7/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 [338.35ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-E6jcvS/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-E6jcvS/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 [330.65ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-V2GNRF/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-V2GNRF/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 [327.43ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-gv3Yg8/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-gv3Yg8/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) [336.11ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-haXLcA/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-haXLcA/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-haXLcA/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-haXLcA/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 [1167.39ms]
[workflow:middle-thejustinwalsh-middle-6] ensuring .middle/prompt.md exists in worktree
[workflow:middle-thejustinwalsh-middle-6] installing hooks in /tmp/middle-wf-kXejig/worktrees/thejustinwalsh/middle/issue-6
[workflow:middle-thejustinwalsh-middle-6] launching tmux session: true (cwd=/tmp/middle-wf-kXejig/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 [689.64ms]

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 [143.62ms]
(pass) tryRebaseOntoMain — fixture repo > non-FF, no conflict: feature edits A, main edits B, no shared paths → rebase replays cleanly [157.97ms]
(pass) tryRebaseOntoMain — fixture repo > conflict: feature + main both edit shared.txt → rebase aborts, paths reported, worktree clean [183.61ms]
(pass) tryRebaseOntoMain — fixture repo > data-loss guard (#201): a rebase that drops ALL of the PR's commits → restore worktree, droppedAllCommits, branch not emptied [247.70ms]
(pass) tryRebaseOntoMain — fixture repo > gitOps.revListCount: counts a resolvable range and falls back to 0 on an unresolvable one (the guard's conservative semantics) [140.14ms]
(pass) tryRebaseOntoMain — fixture repo > a non-managed head ref (not middle-issue-*) → ok:false with empty paths (skip signal) [116.16ms]
(pass) tryRebaseOntoMain — fixture repo > a missing PR (gateway returns null) → ok:false with empty paths (skip signal) [104.60ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict rebase failure (missing upstream) THROWS — not shaped as a path-less conflict [175.50ms]
(pass) tryRebaseOntoMain — fixture repo > non-conflict merge failure (missing ref) THROWS — symmetric to the rebase hardening [133.55ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > rebase would loop but merge -X ours lands cleanly (same line, feature wins) [201.05ms]
(pass) tryMergeMainNewWorkAsBase — fixture repo > residual conflict -X ours can't auto-resolve (rename/rename) → abort, paths reported [186.66ms]
(pass) applySuccess — fixture repo > pushes the rebased branch, posts one PR comment, and records CLEAN — twice = idempotent [221.91ms]
(pass) applySuccess — fixture repo > a different mainCommitSha allows a fresh announcement (the marker is sha-keyed) [195.35ms]
(pass) applySuccess — fixture repo > null mainCommitSha skips the comment but still pushes and records CLEAN (self-review hardening) [182.68ms]
(pass) applySuccess — fixture repo > keystone data-loss guard (#201): refuses to push when local HEAD is emptied but the remote branch has commits [192.86ms]
(pass) applySuccess — fixture repo > a non-managed head ref is a no-op (no push, no comment, no row) [151.45ms]
(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 [220.58ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR rebase-fails → merge fallback lands → applySuccess('merged-new-work-as-base') [292.55ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CONFLICTED PR both attempts fail (rename/rename) → applyDemoteToWork fires [215.98ms]
(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 [267.92ms]
[pr-divergence] GitHub budget low (10 < 100); skipping pass — resets 2026-06-04T05:46:57.120Z
(pass) reconcileOpenPRs — end-to-end against the fixture repo > rate-limit floor short-circuits the pass; no listing happens [163.78ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > CLEAN PR → walked but unchanged; nothing posted, no state advance [169.71ms]
(pass) reconcileOpenPRs — end-to-end against the fixture repo > two open managed PRs in one pass — both walked, mix of CLEAN + BEHIND→rebased [247.11ms]
[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) [178.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 [105.18ms]

packages/dispatcher/test/documentation-workflow.test.ts:
(pass) documentation workflow — shell: step order + dedicated slot > declares the six steps in order [174.21ms]
(pass) documentation workflow — shell: step order + dedicated slot > runs the steps in order at runtime and completes [270.57ms]
(pass) documentation workflow — shell: step order + dedicated slot > records its row with kind 'documentation' — its own dedicated slot, off maxConcurrent [270.01ms]
(pass) documentation workflow — shell: step order + dedicated slot > claims the 'docs' worktree unit, distinct from the recommender's [329.02ms]
(pass) documentation workflow — shell: step order + dedicated slot > spawn-docs-agent has the spec's 5-minute hard cap [235.99ms]
(pass) documentation workflow — shell: step order + dedicated slot > prepare-docs-worktree registers a compensation handler [249.10ms]
(pass) documentation workflow — shell: step order + dedicated slot > check-rate-limit does not retry [235.35ms]
(pass) documentation workflow — shell: step order + dedicated slot > a rate-limited adapter fails the run with state 'rate-limited' [235.32ms]
[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' [271.48ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=false: persist seam is never invoked [271.32ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true but persistDocs UNWIRED: still persists nothing (read-only first) [273.27ms]
(pass) documentation workflow — read-only/dry-run first: persist-docs gating > write=true and persistDocs wired: persist runs after the agent, before cleanup [273.90ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports the resolved target, audit mode, and config; invokes the skill via @-reference [271.73ms]
(pass) documentation workflow — assembleDocumentationPrompt > includes the llms.txt audit line only when the target supports it [175.92ms]
(pass) documentation workflow — assembleDocumentationPrompt > reports write=true to the agent when configured [171.80ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=true selects write mode: discover-or-author, agent does not commit [172.41ms]
(pass) documentation workflow — assembleDocumentationPrompt > write=false stays in audit mode (read-only), never write mode [171.87ms]
(pass) documentation workflow — assembleDocumentationPrompt > write mode keeps the llms.txt instruction only when the target supports it [175.25ms]
(pass) documentation workflow — sessionNameFor collision-resistance > is deterministic for a given repo [176.74ms]
(pass) documentation workflow — sessionNameFor collision-resistance > produces a tmux-safe session name under the docs namespace [171.55ms]
(pass) documentation workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [177.75ms]

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

packages/dispatcher/test/control-routes.test.ts:
(pass) HookServer control routes > GET /health reports liveness, port, and version [2.16ms]
(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.91ms]
(pass) HookServer control routes > POST /control/dispatch rejects invalid bodies with 400 and starts nothing [2.42ms]
(pass) HookServer control routes > POST /control/dispatch surfaces the disabled-vs-unknown distinction in the 400 body [1.48ms]
(pass) HookServer control routes > POST /control/dispatch refuses with 429 when no slot is available (manual respects limits) [2.00ms]
(pass) HookServer control routes > POST /control/dispatch proceeds when a slot is available [1.92ms]
[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.22ms]
(pass) HookServer control routes > POST /control/dispatch rejects a colliding Epic with 409 [1.69ms]
(pass) HookServer control routes > two concurrent dispatches of the same Epic: exactly one 200, one 409 [6.76ms]
(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.80ms]
(pass) HookServer control routes > GET / 404s in the bare server (the status page is gone; the SPA mounts via extraRoutes) [3.05ms]
(pass) HookServer control routes > GET /metrics renders Prometheus text from the metrics seam [1.82ms]
(pass) HookServer control routes > GET /control/metrics returns the raw snapshot as JSON [1.78ms]
(pass) HookServer control routes > metric routes 404 without a metrics seam [1.84ms]
(pass) HookServer control routes > POST /control/resume fires the parked Epic's resume and returns its id [1.62ms]
(pass) HookServer control routes > POST /control/resume 404s when no parked workflow owns the ref [2.06ms]
(pass) HookServer control routes > POST /control/resume 400s on a missing epicRef or answer [1.73ms]
(pass) HookServer control routes > control routes 404 in gate-only mode (no control plane wired) [2.21ms]

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

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) [91.29ms]
(pass) dispatch source (#53) > records and reads back source 'manual' / 'auto'; null when unset [74.98ms]
(pass) workflow meta_json accessors > readWorkflowMeta returns {} for a missing row, a null meta, and malformed JSON [74.75ms]
(pass) workflow meta_json accessors > patchWorkflowMeta merges, preserving keys it does not set [74.86ms]
(pass) workflow meta_json accessors > patchWorkflowMeta does not bump updated_at — meta is scratch, not an activity signal [72.17ms]
(pass) workflow meta_json accessors > checkbox-reconcile state round-trips; defaults when unset [70.68ms]
(pass) workflow meta_json accessors > getCheckboxReconcileState sanitizes malformed nested meta back to the contract [88.04ms]
(pass) listRunningImplementationWorkflows > returns only running implementation rows that own both an epic and a worktree [121.87ms]
(pass) createWorkflowRecord > inserts a pending implementation row carrying epic_number [67.46ms]
(pass) createWorkflowRecord > a second create with the same id is a no-op (idempotent on retry), not a UNIQUE error [74.59ms]
(pass) createWorkflowRecord > a non-PK constraint violation (bad kind) still throws — not swallowed [63.22ms]
(pass) countActiveImplementationSlots > counts non-terminal implementation rows, grouped by adapter [75.18ms]
(pass) countActiveImplementationSlots > excludes terminal implementation rows [75.20ms]
(pass) countActiveImplementationSlots > excludes the recommender's own row — its dedicated slot is not a dispatch slot [71.04ms]
(pass) updateWorkflow > transitions state and bumps updated_at [76.04ms]
(pass) updateWorkflow > patches session fields without disturbing others [76.45ms]
(pass) updateWorkflow > a no-op patch leaves the row intact [70.27ms]
(pass) getWorkflow > returns null for an unknown id [63.32ms]
(pass) hasNonTerminalEpicWorkflow > true while an implementation Epic workflow is non-terminal, false once terminal [72.55ms]
(pass) hasNonTerminalEpicWorkflow > scopes by repo and epic; a recommender row never collides [80.23ms]
(pass) findParkedWorkflowByRef > finds the waiting-human workflow for a ref (slug or number); null otherwise [77.92ms]
(pass) listActiveImplementationWorkflows (#180) > returns lastHeartbeat (null when none observed, the touched epoch otherwise) [76.45ms]
(pass) listNonTerminalWorkflows > returns id/repo/epic/state for non-terminal implementation rows only [87.95ms]
(pass) workflow observers > notifies the observer of each patch, and stops after dispose [81.46ms]
[workflow-record] update observer threw: observer boom
(pass) workflow observers > a throwing observer does not break the DB write [72.72ms]
(pass) workflow observers > addWorkflowObserver fans out to every observer; disposers independent [74.22ms]
(pass) workflow observers > the finalize path notifies observers on a real transition only [80.77ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > flips a still-pending row to failed and reports the transition [91.61ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a row already past pending (e.g. a later step's compensated failure) [105.45ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on a launching row — the launch step already advanced it [108.86ms]
(pass) promotePendingToFailed — orphaned prepare-worktree (issue #179) > no-ops on an unknown id [74.26ms]
(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 [68.55ms]
(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.54ms]

packages/dispatcher/test/recovery.test.ts:
(pass) reconcileOrphanedSignals > an armed signal with no recoverable execution is finalized failed, consumed, and surfaced [82.84ms]
(pass) reconcileOrphanedSignals > a recoverable parked execution is left untouched (not an orphan) [79.27ms]
(pass) reconcileOrphanedSignals > only the orphaned rows are reconciled when alive and orphaned parks coexist [96.48ms]
(pass) reconcileOrphanedSignals > respects a custom finalState and tolerates a missing surface callback [83.75ms]
[recover] surfacing orphaned signal 25f9277d-530e-4c57-a4af-76893493c176 (epic-9-answered) failed: comment failed
(pass) reconcileOrphanedSignals > a surface callback that throws never aborts the reconcile (still finalized + consumed) [85.83ms]
(pass) reconcileOrphanedSignals > an orphaned signal with a null epicNumber still reconciles [87.58ms]
(pass) reconcileOrphanedSignals > a non-parked (terminal) workflow's stale signal is ignored — only waiting-human rows are pollable [77.85ms]
(pass) reconcileOrphanedSignals > finalState is typed to terminal states only (compile-time guard) [65.03ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BUNQUEUE_DATA_PATH) when it is set [62.27ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming BQ_DATA_PATH) when it is set [66.11ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming DATA_PATH) when it is set [64.25ms]
(pass) createDurableEngine (transient-queue env guard) > throws (naming SQLITE_PATH) when it is set [62.01ms]
(pass) createDurableEngine (transient-queue env guard) > an empty-string env var still trips the guard (bunqueue coalesces with ??) [64.51ms]
(pass) createDurableEngine (transient-queue env guard) > names every offending var when several are set at once [63.34ms]
(pass) recoverEngine (durable engine across restart) > re-arms a parked waiting execution so a later signal resumes it [406.32ms]
(pass) recoverEngine (durable engine across restart) > drops a mid-drive (running) execution instead of re-driving it [358.41ms]

packages/dispatcher/test/hook-server.test.ts:
[hook-server] received session.started:middle-6
(pass) HookServer — SessionStart > awaitSessionStart resolves with the posted payload [2.33ms]
[hook-server] received session.started:middle-7
(pass) HookServer — SessionStart > a payload that arrives before anyone awaits is stashed and delivered [1.70ms]
[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.60ms]
[hook-server] received agent.stopped:middle-6
(pass) HookServer — Stop > awaitStop resolves on an agent.stopped POST [2.30ms]
[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.76ms]
[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.94ms]
[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.97ms]
[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 [2.96ms]
[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.23ms]
[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.05ms]
[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.27ms]
(pass) HookServer — lifecycle > awaitSessionStart rejects on timeout [53.00ms]
(pass) HookServer — lifecycle > non-POST and unknown paths return 404 [2.04ms]
(pass) HookServer — lifecycle > stop() rejects outstanding waiters [1.21ms]
(pass) HookServer — recommender trigger endpoint > 404s when no trigger is wired (gate-only mode) [1.95ms]
(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.11ms]
(pass) HookServer — recommender trigger endpoint > coerces non-string repoSlug/repoPath to undefined before forwarding [3.31ms]
(pass) HookServer — recommender trigger endpoint > a non-object JSON body (null, primitive, array) is treated as empty, not a 500 [5.14ms]
(pass) HookServer — recommender trigger endpoint > passes a string field through while dropping a non-string sibling [3.51ms]
(pass) HookServer — merged routes > extraRoutes are served, and the fetch fallback still answers /health [3.25ms]
(pass) HookServer — merged routes > GET / no longer returns the status page (404 with no SPA route) [3.11ms]

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

packages/dispatcher/test/documentation-run.test.ts:
[documentation-run] workflow wf_1780551968769_7jjkmh6o enqueued
(pass) dispatchDocumentation — enqueues a documentation workflow (read-only) > runs to completion and records a kind:'documentation' row for the repo [391.98ms]
[documentation-run] workflow wf_1780551969159_fq4mhuq1 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.17ms]
[documentation-run] workflow wf_1780551969538_75bjbyv0 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.33ms]
(pass) resolveDocumentationOptions > accepts a configured non-default adapter (e.g. codex) [12.21ms]
(pass) resolveDocumentationOptions > rejects an adapter the registry doesn't know [11.09ms]
(pass) resolveDocumentationOptions > rejects an implemented-but-disabled adapter — mirrors the daemon's dispatch gate [9.97ms]
(pass) resolveDocumentationOptions > resolves the markdown fallback target for a plain repo [10.92ms]
(pass) resolveDocumentationOptions > honors a [docs] tool/path override [11.72ms]
(pass) resolveDocumentationOptions > surfaces an unknown tool override as an error rather than falling back [9.94ms]

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.60ms]
(pass) runRecommenderCronPass > fires once the interval has elapsed [1.49ms]
(pass) runRecommenderCronPass > skips a paused repo [1.48ms]
(pass) runRecommenderCronPass > skips a repo whose recommender is disabled or unconfigured [1.73ms]
(pass) runRecommenderCronPass > skips a repo with a non-positive interval (never auto-runs) [1.64ms]
[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.95ms]
(pass) runRecommenderCronPass > ignores unmanaged rows (no checkout path) [1.76ms]

packages/dispatcher/test/poller.test.ts:
(pass) reasonFromSignalName > maps the durable signal names to resume reasons [69.66ms]
(pass) classifyNewHumanReply > returns the newest non-bot reply posted after the wait armed [66.47ms]
(pass) classifyNewHumanReply > returns null when only bot/stale comments exist [62.16ms]
(pass) classifyNewHumanReply > skips the dispatcher's own marked pause comment (posted as a non-bot human identity) [63.23ms]
(pass) classifyNewHumanReply > a genuine human reply that quote-replies the pause comment still resumes [63.76ms]
(pass) classifyReviewOutcome > a fresh CHANGES_REQUESTED review → changes-requested [64.06ms]
(pass) classifyReviewOutcome > a fresh APPROVED review → resolved [62.75ms]
(pass) classifyReviewOutcome > a fresh 0-actionable re-review → resolved even while decision stays CHANGES_REQUESTED [66.75ms]
(pass) classifyReviewOutcome > the `changes-requested` label alone (no fresh review) → changes-requested [63.68ms]
(pass) classifyReviewOutcome > only stale reviews and no actionable label → null (nothing changed) [65.08ms]
(pass) classifyReviewOutcome > a stale standing CHANGES_REQUESTED decision (no fresh review, no label) → null [63.07ms]
(pass) classifyReviewOutcome — CI gate > failing CI with no review feedback → resume to fix CI (CI_FAILED) [65.92ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review while CI is still pending is held (null) — don't end on un-built CI [65.30ms]
(pass) classifyReviewOutcome — CI gate > an APPROVED review with passing CI resolves [63.04ms]
(pass) classifyReviewOutcome — CI gate > explicit review feedback wins over red CI (address the review, which greens CI) [62.62ms]
(pass) classifyReviewOutcome — CI gate > absent CI (`none`) is non-blocking — the pre-CI review loop is unchanged [64.39ms]
(pass) classifyReviewOutcome — CI gate > failing CI but no PR change and no review → still CI_FAILED (red build is actionable) [64.83ms]
(pass) runPoller — answered-question > a new human reply fires epic-<n>-answered exactly once (idempotent across passes) [88.27ms]
(pass) runPoller — answered-question > a bot-only reply does not fire [80.89ms]
(pass) runPoller — answered-question > the dispatcher's own pause comment does not self-resume (#178) [81.27ms]
(pass) runPoller — review-changes > CHANGES_REQUESTED fires review-resolved with outcome 'changes-requested' [83.39ms]
(pass) runPoller — review-changes > APPROVED fires review-resolved as resolved [85.55ms]
(pass) runPoller — review-changes > a 0-actionable re-review fires review-resolved as resolved [83.25ms]
(pass) runPoller — review-changes > no PR yet → no fire [82.32ms]
[poller] poll failed for workflow d8bd7222-1da7-4229-bb6f-fe172f7b821b (epic-200-answered): API rate limit exceeded
(pass) runPoller — resilience > a gateway error for one workflow is isolated; others still fire [133.25ms]
[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.71ms]
(pass) runPoller — GitHub rate-limit guards > a healthy budget proceeds (the guard isn't always-on) [83.42ms]
(pass) runPoller — GitHub rate-limit guards > caps the workflows polled per pass (burst protection) [120.70ms]

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.04ms]

packages/dispatcher/test/reconcile.test.ts:
[reconcile] thejustinwalsh/middle#50 PR MERGED → completed (workflow 35ed66f8-b04f-4699-aeb5-75e89212fe3d)
(pass) reconcileMergedParks > a merged PR finalizes the parked workflow to `completed` and tears down its worktree [82.31ms]
[reconcile] thejustinwalsh/middle#51 PR CLOSED → cancelled (workflow 946bb483-ec50-4354-b844-4601747f26ca)
(pass) reconcileMergedParks > a closed-unmerged PR finalizes to `cancelled` [75.36ms]
(pass) reconcileMergedParks > an open PR (a live review park) is left alone [77.46ms]
(pass) reconcileMergedParks > no PR for the Epic (a pending question) is left alone [70.43ms]
[reconcile] thejustinwalsh/middle#54 PR MERGED → completed (workflow ad0c0606-638c-4f43-a41e-01fe9d6f4a0a)
[reconcile] worktree cleanup failed for ad0c0606-638c-4f43-a41e-01fe9d6f4a0a (continuing): git worktree remove failed
(pass) reconcileMergedParks > finalizes the row even when worktree teardown throws (best-effort) [75.34ms]
(pass) reconcileMergedParks > only walks `waiting-human` rows — running/terminal rows are untouched [86.63ms]
[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.66ms]
[reconcile] thejustinwalsh/middle#70 PR MERGED → completed (workflow 6bc98045-91a5-491f-9802-637585667940)
[reconcile] thejustinwalsh/middle#71 PR CLOSED → cancelled (workflow 0e740498-5659-4468-9e3a-eee829099e21)
[reconcile] thejustinwalsh/middle#72 PR MERGED → completed (workflow ad125dd3-43a2-4019-916c-17500b66a17e)
(pass) reconcileMergedParks > fires onMergedTransition at most once per repo per pass (Epic #168 wiring) [98.72ms]
[reconcile] thejustinwalsh/middle#75 PR MERGED → completed (workflow 9ef43760-e48b-4f8c-b0cd-d5cb9bc0baa8)
[reconcile] onMergedTransition for thejustinwalsh/middle failed (continuing): downstream sweep boom
[reconcile] thejustinwalsh/middle#76 PR MERGED → completed (workflow 22152a1e-4cc8-442a-b6a0-5d14cc34f0f1)
(pass) reconcileMergedParks > a thrown onMergedTransition is isolated — the merged-parks pass still finishes [95.37ms]
[reconcile] thejustinwalsh/middle#60 PR MERGED → completed (workflow f8492bc6-c235-4d71-8ed2-44fd9ddb9fcb)
[reconcile] thejustinwalsh/middle#61 PR MERGED → completed (workflow 97463366-9228-4005-8ed0-f5ba110fd16a)
(pass) reconcileMergedParks > honors the per-pass burst cap [96.02ms]
(pass) reconcileMergedParks > does not tear down the worktree when it loses the race to a concurrent resume [75.40ms]
(pass) finalizeParkedWorkflow > transitions a still-parked row and reports the change [74.71ms]
(pass) finalizeParkedWorkflow > no-ops (returns false) a row that already left waiting-human [77.28ms]

packages/dispatcher/test/recommender-workflow.test.ts:
(pass) recommender workflow — #43 shell: step order + dedicated slot > declares the seven spec steps in order [173.56ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > runs the steps in spec order at runtime and completes [269.80ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > records its row with kind 'recommender' — its own dedicated slot, off maxConcurrent [266.67ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > spawn-recommender-agent's step backstop is sized for the per-repo ceiling [175.35ms]
(pass) recommender workflow — #43 shell: step order + dedicated slot > prepare-shallow-worktree registers a compensation handler [176.82ms]
(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.77ms]
(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.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' [271.90ms]
(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) [174.19ms]
(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.56ms]
(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 [266.47ms]
[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 [274.00ms]
[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 [266.97ms]
[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) [276.77ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > heartbeatRel formats epoch deltas; null → 'unknown' [177.69ms]
(pass) recommender workflow — #180 dispatcher is the sole In-flight writer > dispatcherSectionsFromContext builds canonical sections (heartbeat, null-issue dropped, null-session→pending) [177.40ms]
(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 [225.87ms]
(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 [268.44ms]
[documentation-run] engine.close drain timed out after 10s — proceeding
[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) [2354.76ms]
[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 [278.65ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > derives rate_limits, in_flight, and slots from db + config [210.90ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > excludes the recommender's own row from in_flight and slots [188.28ms]
(pass) recommender workflow — #44 buildRecommenderContext: from dispatcher state > scopes per-repo slots/in_flight to the repo, but global_used spans all repos [202.33ms]
(pass) recommender workflow — sessionNameFor collision-resistance > is deterministic for a given repo [171.65ms]
(pass) recommender workflow — sessionNameFor collision-resistance > produces a tmux-safe session name (no separators survive) [173.76ms]
(pass) recommender workflow — sessionNameFor collision-resistance > distinct repos that share a lossy slug do not collide [172.13ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > runs on the engine via per-repo resolveRunSettings and creates the recommender row [217.40ms]
(pass) recommender workflow — daemon path (resolveRunSettings, #135 fix) > a clear wiring error when neither resolveRunSettings nor static settings are provided [2319.56ms]

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.09ms]
(pass) runStalenessCronPass > a non-ENOENT spec read error surfaces (not silently treated as missing spec) [2.22ms]
[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.15ms]
[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.88ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a `..` traversal spec_path is rejected — the pass never reads outside the checkout [2.53ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > a deeper `../../` traversal is rejected too [2.21ms]
(pass) runStalenessCronPass — spec_path is constrained to the checkout > an absolute spec_path is rejected (the field is repo-relative by contract) [2.24ms]
[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.08ms]

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

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

packages/dispatcher/test/hook-server-gates.test.ts:
(pass) HookServer — /gates/pr-ready > returns 200 when the gate allows [2.51ms]
[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.99ms]
(pass) HookServer — /gates/pr-ready > forwards the session name and payload to the gate handler [2.07ms]
(pass) HookServer — /gates/pr-ready > 404s the gate route when no gate handler is wired [1.45ms]

packages/dispatcher/test/repo-config.test.ts:
(pass) repo pause/resume > an unpaused repo (no row) reads as not paused [2.00ms]
(pass) repo pause/resume > mm pause (indefinite) suspends the repo [1.44ms]
(pass) repo pause/resume > a paused_until in the future reads as paused; in the past auto-expires [1.64ms]
(pass) repo pause/resume > mm resume clears the pause [1.41ms]
(pass) repo pause/resume > pausing is idempotent and re-pausing updates the timestamp [1.54ms]
(pass) repo pause/resume > resume on a never-paused repo is a harmless no-op [1.42ms]
(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.50ms]
(pass) managed-repo registry (#135) > registering is idempotent and updates the path in place (one row) [1.44ms]
(pass) managed-repo registry (#135) > registering preserves an existing pause (doesn't clobber paused_until) [1.40ms]
(pass) managed-repo registry (#135) > listManagedRepos excludes rows with no checkout path (e.g. a pause-only row) [1.54ms]
(pass) managed-repo registry (#135) > setLastRecommenderRun writes a value and clears it with null (cron rollback) [1.55ms]
(pass) managed-repo registry (#135) > markRecommenderRun stamps and reads back last_recommender_run [1.60ms]

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

packages/dispatcher/test/adapter-conformance.test.ts:
(pass) the registry knows all three adapters [0.24ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("toString") throws unknown-adapter [0.25ms]
(pass) registry lookup is exact-key (no prototype walk) > isKnownAdapter("toString") is false [0.14ms]
(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.10ms]
(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.10ms]
(pass) registry lookup is exact-key (no prototype walk) > getAdapter("__proto__") throws unknown-adapter [0.08ms]
(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.10ms]
(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.13ms]
(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.37ms]
(pass) AgentAdapter contract — claude > classifyStop: blocked.json → asked-question [0.44ms]
(pass) AgentAdapter contract — claude > classifyStop: done.json → done; failed.json → failed; neither → bare-stop [0.50ms]
(pass) AgentAdapter contract — claude > detectRateLimit is implemented and returns null on a clean transcript [0.29ms]
(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.10ms]
(pass) AgentAdapter contract — codex > buildLaunchCommand yields a non-empty argv and the session env [0.12ms]
(pass) AgentAdapter contract — codex > buildPromptText: initial is the skill slash-command on the Epic [0.09ms]
(pass) AgentAdapter contract — codex > buildPromptText: recommender / docs force-invoke their skill with the @-ref [0.09ms]
(pass) AgentAdapter contract — codex > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.65ms]
(pass) AgentAdapter contract — codex > classifyStop: blocked.json → asked-question [0.39ms]
(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.15ms]
(pass) AgentAdapter contract — copilot > resolveTranscriptPath yields a path from this adapter's own ready payload [0.15ms]
(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.19ms]
(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.08ms]
(pass) AgentAdapter contract — copilot > installHooks writes the shared hook.sh + pr-ready-gate.sh into the worktree [1.17ms]
(pass) AgentAdapter contract — copilot > classifyStop: blocked.json → asked-question [0.42ms]
(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 [1935.83ms]
(pass) dispatcher main > hosts a dispatch on its own engine and broadcasts a workflow SSE event [1309.02ms]
(pass) dispatcher main > a terminal prepare-worktree failure marks the row failed, so the next dispatch isn't 409-blocked (issue #179) [3415.60ms]
(pass) dispatcher main > daemon rejects a disabled adapter on /control/dispatch (configured+enabled+implemented gate) [1227.37ms]
(pass) dispatcher main > two concurrent dispatches of the same Epic: exactly one starts, the other 409s [1234.16ms]

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

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

packages/dispatcher/test/slots.test.ts:
(pass) getSlotState > free-slot: no active work reports full availability across every dimension [3.39ms]
(pass) getSlotState > at-capacity: a full repo reports zero availability and the guard refuses [1.68ms]
(pass) getSlotState > per-adapter cap binds before the repo cap [1.64ms]
(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.52ms]
(pass) getSlotState > used over max clamps available to 0 (a tightened cap never goes negative) [1.57ms]
(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.59ms]
(pass) reserveSlot > reserving down to capacity flips the guard to refuse [1.47ms]
(pass) reserveSlot > reserving an adapter with no cap still decrements repo + global [1.40ms]

packages/dispatcher/test/auto-dispatch.test.ts:
(pass) autoDispatch > normal pass: enqueues every ready row that has a free slot [0.43ms]
(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.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.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.12ms]
(pass) autoDispatch > no pre-dispatch complexity gate: a large-sub-issue Epic still dispatches (#52) [0.11ms]
(pass) createParseFailureSurfacer (#180) > surfaces a parse failure on the state issue, with the underlying message [0.19ms]
(pass) createParseFailureSurfacer (#180) > dedupes an identical message across a burst — one comment, not N [0.09ms]
(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
(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 [63.90ms]
(pass) classifyMergeability > BEHIND → BEHIND [63.13ms]
(pass) classifyMergeability > CLEAN + MERGEABLE → CLEAN [70.27ms]
(pass) classifyMergeability > CLEAN but not MERGEABLE → UNKNOWN (CI gating, secondary signals) [66.00ms]
(pass) classifyMergeability > BLOCKED / HAS_HOOKS / UNSTABLE / UNKNOWN → UNKNOWN [69.01ms]
(pass) classifyMergeability > a null view (PR doesn't exist) → UNKNOWN [63.72ms]
(pass) classifyMergeability > missing fields → UNKNOWN (legacy fixtures don't tip the classifier) [70.95ms]
(pass) classifyDivergence > classifies BEHIND and persists the row with the supplied clock [74.36ms]
(pass) classifyDivergence > classifies CONFLICTED and overwrites a prior row (upsert keeps the row fresh) [83.33ms]
(pass) classifyDivergence > classifies CLEAN [74.82ms]
(pass) classifyDivergence > classifies UNKNOWN for a PR with no mergeability view (gone / 404) [68.76ms]
(pass) parseEpicFromHeadRef > parses `middle-issue-<N>` to the integer N [68.53ms]
(pass) parseEpicFromHeadRef > a non-managed head ref → null (the helper skips it) [67.99ms]
(pass) parseEpicFromHeadRef > a malformed managed ref → null (defends against an inadvertent rename) [62.65ms]
(pass) worktreePathFor > uses <root>/<repo>/issue-<n> — the same layout createWorktree writes [65.94ms]
(pass) recordDivergenceState > accepts terminal-ish states (DEMOTED, SKIPPED) written by sibling phases [75.80ms]
(pass) recordDivergenceState > the CHECK constraint rejects an out-of-vocabulary state — defends against a reconciler typo [69.39ms]
(pass) recordDivergenceState > the (repo, pr_number) PK lets the same pr_number coexist across repos [75.39ms]
(pass) applyDemoteToWork > flips PR draft, reopens sub-issue, posts dual-surface comment, re-enqueues, state→DEMOTED [75.55ms]
(pass) applyDemoteToWork > per-step idempotency: a second call skips draft-flip + reopen + comments via markers (but still re-enqueues) [72.14ms]
(pass) applyDemoteToWork > partial-retry: prior attempt left the PR drafted but did not reopen / comment / enqueue — second pass completes remediation [71.19ms]
(pass) applyDemoteToWork > partial-retry safety: existing marker on PR skips the duplicate PR comment, still posts on Epic [68.12ms]
(pass) applyDemoteToWork > Epic with no closed sub-issues: still demotes + comments + enqueues; no reopen call [72.01ms]
(pass) applyDemoteToWork > non-managed head ref → no-op (no draft, no comments, no enqueue, no row) [63.88ms]
(pass) applyDemoteToWork > manual recovery: an Epic that already carries the demote marker skips the reopen call (self-review hardening) [69.97ms]
(pass) applyDemoteToWork > a supplied reason (#201 data-loss) replaces the conflict narrative in the escalation comment [69.00ms]
(pass) applyDemoteToWork > PR doesn't exist (gateway returns null) → no-op [71.86ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a PullRequest with " [70.59ms]
(pass) ghStderrIsNotFound > recognizes not-found: "Could not resolve to a Branch with the n" [63.58ms]
(pass) ghStderrIsNotFound > recognizes not-found: "HTTP 404: Not Found (https://api.github." [69.55ms]
(pass) ghStderrIsNotFound > recognizes not-found: "graphql: Could not resolve to a Reposito" [68.32ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "error connecting to api.github.com: dial" [68.48ms]
(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.30ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "HTTP 502: Bad Gateway" [65.34ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "gh: command failed (oauth token expired)" [64.83ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "could not deserialize response" [66.29ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "remote: secret not found, push declined" [63.02ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "Not Found" [65.98ms]
(pass) ghStderrIsNotFound > treats non-404 failure as throw-worthy: "" [62.91ms]

packages/core/test/config.test.ts:
(pass) loadConfig — [docs] section > parses a full docs block [0.62ms]
(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.27ms]
(pass) loadConfig — [docs] section > no [docs] section leaves docs undefined [0.25ms]
(pass) loadConfig — [staleness] section > reads spec_path [0.24ms]
(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.20ms]
(pass) loadConfig — [staleness] section > the local cache overrides committed policy spec_path [0.25ms]
(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.20ms]
(pass) loadConfig — per-repo merge > populates per-repo sections alongside global [0.44ms]
(pass) loadConfig — per-repo merge > per-repo values override global on a colliding key [0.34ms]
(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.21ms]
(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.27ms]
(pass) loadConfig — committed policy layer > local cache overrides committed policy on a colliding key [0.26ms]
(pass) loadConfig — committed policy layer > policy overrides the global file on a colliding key [0.39ms]
(pass) loadConfig — committed policy layer > an explicit repoPolicyPath overrides the sibling derivation [0.33ms]
(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
(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.02ms]
(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.50ms]
(pass) PR_READY_GATE_SH exit-code contract > curl failure emitting no http code → exit 0 (fails OPEN, not closed) [1.94ms]
(pass) PR_READY_GATE_SH exit-code contract > HTTP 403 from a reachable dispatcher → exit 2 (blocks) [2.23ms]
(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.91ms]
(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.07ms]

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.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.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.04ms]
(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

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

packages/adapters/codex/test/adapter.test.ts:
(pass) codexAdapter identity > name is 'codex' and readyEvent is session.started [0.23ms]
(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.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.11ms]
(pass) buildPromptText > resume frames the @-reference as a continuation [0.12ms]
(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.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.10ms]
(pass) resolveTranscriptPath > returns transcript_path from the SessionStart payload [0.11ms]
(pass) resolveTranscriptPath > falls back to rollout_path when transcript_path is absent [0.17ms]
(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.30ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.22ms]
(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.32ms]
(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.36ms]
(pass) classifyStop > text fallback (no structured block): "You've hit a rate limit, try later." → rate-limited (rate limit phrase) [0.37ms]
(pass) classifyStop > text fallback (no structured block): "Error 429: Too Many Requests" → rate-limited (429 status) [0.26ms]
(pass) classifyStop > text fallback (no structured block): "too many requests — slow down" → rate-limited (too many requests phrase) [0.32ms]
(pass) classifyStop > text fallback (no structured block): "ratelimit exceeded" → rate-limited (ratelimit no-space) [0.30ms]
(pass) classifyStop > a bare "line 4290 of the file" is NOT a rate-limit signal → bare-stop (4290 — a line number) [0.36ms]
(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.28ms]
(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.34ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.48ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.40ms]
(pass) classifyStop > nothing notable → bare-stop [0.30ms]
(pass) detectRateLimit > structured block at the limit → detection with the real reset time [0.19ms]
(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.21ms]
(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.23ms]
(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.26ms]
(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.14ms]
(pass) installHooks > registers the PR-ready gate as a SECOND PreToolUse matcher group scoped to Bash [1.03ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.16ms]
(pass) installHooks > symlinks the operator's auth.json into the worktree CODEX_HOME [0.98ms]
(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.23ms]
(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.11ms]
(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.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.85ms]

packages/adapters/claude/test/adapter.test.ts:
(pass) claudeAdapter identity > name is 'claude' and readyEvent is session.started [0.18ms]
(pass) claudeAdapter identity > does NOT set startsSessionOnFirstPrompt — Claude fires SessionStart at boot, so the dispatcher keeps await-first order (#183 regression) [0.17ms]
(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.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.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.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.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.27ms]
(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.34ms]
(pass) classifyStop > an unrecognized kind falls back to a plain question (kind omitted) [0.34ms]
(pass) classifyStop > asked-question tolerates a malformed/contentless blocked.json (sentinel → null) [0.33ms]
(pass) classifyStop > usage-limit message in the transcript tail → rate-limited [0.51ms]
(pass) classifyStop > done.json sentinel → done [0.54ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.39ms]
(pass) classifyStop > sentinels are found even when payload.cwd is a worktree subdirectory [0.41ms]
(pass) classifyStop > nothing notable → bare-stop [0.32ms]
(pass) detectRateLimit > matches a usage-limit message in the transcript tail [0.20ms]
(pass) detectRateLimit > returns null when no usage-limit message is present [0.17ms]
(pass) installHooks > registers the full Claude hook event set in .claude/settings.json [1.08ms]
(pass) installHooks > each entry maps its Claude event to the normalized taxonomy via the absolute hook path [0.99ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [0.89ms]
(pass) installHooks > registers the PR-ready gate as a second Bash-matched PreToolUse hook [1.05ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [1.02ms]
(pass) detectBypassPrompt > matches representative bypass-mode confirmation strings [0.18ms]
(pass) detectBypassPrompt > does not match normal Claude pane content [0.13ms]
(pass) detectTrustPrompt > matches the first-run folder-trust dialog [0.15ms]
(pass) detectTrustPrompt > does not match the bypass dialog or normal content [0.12ms]
(pass) detectNeedsLogin > matches representative not-authenticated messages [0.18ms]
(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.93ms]

packages/adapters/copilot/test/adapter.test.ts:
(pass) copilotAdapter identity > name is 'copilot' and readyEvent is session.started [0.24ms]
(pass) copilotAdapter identity > sets the prompt-triggered-session flag (fires no sessionStart until a prompt) [0.11ms]
(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.11ms]
(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.15ms]
(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.12ms]
(pass) buildPromptText > type contract: dispatched-issue kinds require an epicRef; recommender forbids one [0.13ms]
(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.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.14ms]
(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.08ms]
(pass) resolveTranscriptPath > rejects a non-identifier sessionId "id with spaces" (defense-in-depth against path escape) [0.10ms]
(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.20ms]
(pass) readTranscriptState > tolerates a corrupt line without throwing [0.20ms]
(pass) classifyStop > sentinelPresent → asked-question, surfacing the blocked.json path + question/context [0.44ms]
(pass) classifyStop > a blocked.json with kind 'complexity' surfaces the complexity pause kind [0.37ms]
(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.33ms]
(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.33ms]
(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.35ms]
(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.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.39ms]
(pass) classifyStop > failed.json sentinel → failed, carrying its reason [0.42ms]
(pass) classifyStop > done.json outranks stale rate-limit text in the transcript → done [0.46ms]
(pass) classifyStop > failed.json outranks stale rate-limit text in the transcript → failed [0.38ms]
(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.15ms]
(pass) detectRateLimit > returns null when no rate-limit signal is present [0.16ms]
(pass) installHooks > writes .copilot/hooks/middle.json with version 1 and the camelCase event keys [1.17ms]
(pass) installHooks > maps each Copilot event to the normalized taxonomy via the absolute hook path [1.57ms]
(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.58ms]
(pass) installHooks > writes an executable hook.sh into the worktree at the configured path [1.16ms]
(pass) installHooks > writes an executable pr-ready-gate.sh that POSTs to /gates/pr-ready [0.97ms]
(pass) installHooks > writes NO auth file (copilot authenticates via gh, unlike codex) [0.96ms]
(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.13ms]
(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.84ms]

packages/dispatcher/test/epic-store/file-state-gateway.test.ts:
(pass) fileStateGateway > readBody returns the state file contents verbatim [0.42ms]
(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.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.39ms]
(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.17ms]
(pass) filePollGateway > findPrForEpic resolves a slug via meta.pr; delegates a numeric ref to gh's finder [0.33ms]
(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.27ms]
(pass) filePollGateway > prSnapshot / prLifecycle delegate straight to gh by PR number [0.20ms]
(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.64ms]
(pass) fileEpicGateway > listIssueComments maps the conversation; answer is attributed to the human [0.51ms]
(pass) fileEpicGateway > listIssueComments delegates to gh for a non-Epic (PR-number) ref [0.20ms]
(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.13ms]
(pass) fileEpicGateway > getIssueLabels reads the Epic meta labels [0.23ms]
(pass) fileEpicGateway > postComment appends a re-parseable dispatch-event block [0.50ms]
(pass) fileEpicGateway > postComment delegates a PR-number ref to gh (no Epic file for it) [0.18ms]
(pass) fileEpicGateway > findEpicPr returns null without a stamped pr, and delegates to gh when present [0.33ms]
(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.41ms]
(pass) fileEpicGateway > a present-but-malformed Epic file surfaces the parser's named error [0.20ms]
(pass) fileEpicGateway > postComment writes atomically — no `.tmp` sibling left behind [0.34ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > re-asking the identical open question is a no-op [0.51ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > a different question (or different kind/context) appends a new entry [0.91ms]
(pass) appendQuestion — idempotent on a repeated park (#205) > round-trip purity survives the skip (renderer remains the sole marker writer) [0.33ms]

packages/dispatcher/test/epic-store/round-trip.test.ts:
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(empty-epic.md)) === empty-epic.md [0.08ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(all-closed.md)) === all-closed.md [0.10ms]
(pass) Epic file round-trip > renderEpicFile(parseEpicFile(codex-adapter.md)) === codex-adapter.md [0.05ms]
(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-bjIzTk/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-bjIzTk/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 [240.04ms]
[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-A5qUaY/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-mirror-A5qUaY/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 [291.00ms]

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-fZ140D/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fdisp-fZ140D/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 [302.62ms]
(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 [187.44ms]

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-ixoUCg/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-ixoUCg/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 [283.13ms]
[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-o27YGT/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-o27YGT/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-o27YGT/worktrees/o/parity-repo/issue-6
[workflow:middle-o-parity-repo-6] launching tmux session: true (cwd=/tmp/middle-parity-o27YGT/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 [322.43ms]
[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-Tsq5Vc/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-Tsq5Vc/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 [224.18ms]
[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-98ySOo/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-98ySOo/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-98ySOo/worktrees/o/parity-repo/issue-rollout-epic-store
[workflow:middle-o-parity-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-parity-98ySOo/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 [317.52ms]

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-eblVSC/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-eblVSC/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-eblVSC/worktrees/o/file-repo/issue-rollout-epic-store
[workflow:middle-o-file-repo-rollout-epic-store] launching tmux session: true (cwd=/tmp/middle-fw-eblVSC/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 [427.87ms]

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.14ms]
(pass) pollFileSignals > emits an open question that has a non-empty answer [0.26ms]
(pass) pollFileSignals > an unanswered question (placeholder) does NOT trigger [0.22ms]
(pass) pollFileSignals > a resolved question does NOT trigger (only the first non-empty edit fires) [0.16ms]
(pass) pollFileSignals > the mtime gate skips unchanged files [0.13ms]
(pass) resolveQuestion > flips an open question to resolved (the dedup write); idempotent [0.32ms]
(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 [98.69ms]
(pass) runFileWatcherTick > does NOT resume a workflow parked on a non-answered signal (reason guard) [85.94ms]

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.22ms]
(pass) makeRoutingEpicGateway > routes per-repo: file repo → file backend, github repo → gh backend [82.78ms]
(pass) makeRoutingPollGateway > a file-mode slug never reaches gh's numeric PR-finders; github delegates [78.92ms]
(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.23ms]

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.32ms]

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 [95.08ms]
(pass) file-mode PR-review resume (real poller path) > no resume while the Epic file has no stamped meta.pr (PR not opened yet) [92.96ms]

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.28ms]
(pass) parseEpicFile — document structure > throws when the document marker is missing [0.07ms]
(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.06ms]
(pass) parseEpicFile — acceptance criteria > parses checked criteria from all-closed [0.07ms]
(pass) parseEpicFile — sub-issues > parses sub-issues with stable IDs + body [0.07ms]
(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.08ms]
(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 [79.30ms]
(pass) file-mode auto-dispatch (real readState path) > a github-mode repo still routes readState to the gh state issue gateway [81.36ms]

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.04ms]
(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.02ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: non-int phases [0.05ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: negative phases [0.02ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: empty phases
(pass) parseVerifyConfig — malformed fails loudly > rejects: unknown key [0.08ms]
(pass) parseVerifyConfig — malformed fails loudly > rejects: invalid category [0.06ms]
(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.34ms]
(pass) loadVerifyConfig — file IO > a missing file fails loudly with the path in the message [0.07ms]
(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.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.19ms]
(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.04ms]
(pass) parseStatusCheckboxes > only the FIRST ## Status section is parsed; a later one is ignored [0.01ms]
(pass) reconcileCheckboxes > a passing [ ]→[x] transition is left checked, no comment, state recorded [0.33ms]
(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.06ms]
(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.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.19ms]
(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.03ms]

packages/dispatcher/test/gates/gate-runner.test.ts:
(pass) runGate > a passing gate captures stdout and exit 0 [1.44ms]
(pass) runGate > a failing gate captures the non-zero exit and stderr [0.77ms]
(pass) runGate > a gate that exceeds its timeout is killed and reported as timed out [700.69ms]
(pass) runGate > runs in the given cwd [1.94ms]
(pass) runGates > runs every gate in declared order; aggregate ok when all pass [1.30ms]
(pass) runGates > a failing gate makes the aggregate fail and names the first failure; later gates still run [1.51ms]
(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.96ms]
(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.45ms]
(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.32ms]
(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.48ms]
(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.65ms]
(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.47ms]

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.01ms]
(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.13ms]
(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.05ms]
(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.09ms]
(pass) evaluatePrReady — integration evidence > an evidenced integration criterion allows even if a stray bot exemption is present [0.06ms]
(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.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.04ms]
(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.16ms]
(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 [93.61ms]
(pass) runCheckboxRevertPass > a passing-gate checkbox stays checked; SHA + state persisted [81.55ms]
(pass) runCheckboxRevertPass > head-SHA gate: an unchanged SHA skips a would-be transition entirely [85.94ms]
(pass) runCheckboxRevertPass > an advanced SHA re-processes: the new transition's gate runs and reverts [92.72ms]
(pass) runCheckboxRevertPass > undefined gateway SHA falls through to the reconciler's checkbox-state diff [85.52ms]
(pass) runCheckboxRevertPass > no usable verify.toml → the workflow is skipped (nothing to enforce) [79.33ms]
[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 [77.64ms]
[checkbox-revert] pass failed for workflow bad (o/r#1): GitHub down
(pass) runCheckboxRevertPass > a per-workflow failure is isolated — other workflows still process [97.59ms]
(pass) runCheckboxRevertPass > a parked (non-running) workflow is not processed [77.68ms]

 1378 pass
 0 fail
 3474 expect() calls
Ran 1378 tests across 123 files. [94.56s]

Comment thread docs/operator.md

```bash
mm stop # if the dispatcher is running
mm init <repo-path> --epic-store=file # idempotent: re-init flips the mode

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 (#216): documenting the existing-repo flip as mm init --epic-store=file, not a hand TOML edit. mm init writes both the .middle/<slug>.toml and the daemon-db repo_config.epic_store row the dispatcher's per-repo gateway router reads (readEpicStoreConfig); nothing re-derives the db row from the toml at boot or dispatch. A toml-only edit would flip the recommender's config-only read but not dispatch routing — a silent half-switch. mm init is idempotent and preserves committed policy. See planning/issues/209/decisions.md.

* whole file-mode design rests on, surfaced where an operator can act on it.
*/
function checkEpicStore(store: EpicStoreConfig, opts: DoctorOptions): Check {
function checkEpicStore(store: EpicStoreConfig, opts: DoctorOptions): Check[] {

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 (#216): checkEpicStore returns Check[] so file mode emits three granular rows (epics_dir / state_file / Epic-file round-trip) instead of one ambiguous line — matching the sub-issue's "run the file-mode checks". Only marker-led files are round-tripped, so the scaffold's own README.md (not an Epic) isn't falsely failed.

* Exits 0 only when both hold. (Extra documented labels are allowed — the doc may
* cover operator conventions the code doesn't key on.) Returns the process exit code.
*/
export function runVocabularyCheck(opts: Pick<DoctorOptions, "vocabularyDocPath"> = {}): number {

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 (#217): the recommender's classification is LLM-driven (no deterministic classifier to replay), so "docs and code agree" is realized as a coverage drift guard: every label the code deterministically keys on (NEEDS_DESIGN_LABEL, STATE_LABEL, middle-owned NON_FEATURE_LABELS) must be documented, plus the canonical vocabulary is complete. Rename a constant without updating the doc → this fails. The sub-issue explicitly authorized "an extended mm doctor flag". Full rationale in decisions.md.

* owns start/stop/restart, so middle must not fork or leave a pid file the manager
* doesn't know about. Blocks until the daemon shuts down (SIGTERM → exit 0).
*/
async function runForegroundDaemon(opts: StartOptions): Promise<number> {

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 (#218): --foreground bypasses the fork/pid-file path and runs runDaemon in-process so a service manager (systemd Restart=on-failure / launchd KeepAlive) owns the lifecycle. The daemon entry is pulled in via a dynamic import so the default background path never loads the dashboard/daemon modules; the runForeground seam keeps unit tests fast while the integration test boots the real binary. runDaemon already installs SIGTERM→drain→exit(0).

@thejustinwalsh

Copy link
Copy Markdown
Owner Author

Reviewer's brief — Epic #209 (operator docs hardening)

Three operator docs + their integration tests. All three sub-issues (#216, #217, #218) are closed; the branch is MERGEABLE/CLEAN.

How to run it

bun install
bun test            # 1383 pass, 0 fail
bun run typecheck   # clean
bun run lint        # clean

# The three new operator-facing surfaces, against the real CLI:
bun packages/cli/src/index.ts doctor --vocabulary-check     # ✓ docs and code agree — 6 code-keyed labels documented
bun packages/cli/src/index.ts start --help                  # shows --foreground
bun packages/cli/src/index.ts init --help                   # shows --epic-store

What to verify (and what "correct" looks like)

  • docs/operator.md → "Enable file mode on an existing repo": the flip is mm init --epic-store=file (idempotent), not a hand TOML edit — because init writes both the toml and the daemon-db row the dispatcher routes on. The worked-example Epic file round-trips byte-identically (the doctor test lifts it straight from the doc as its fixture). Differences section: slug refs, recommender ranks files, PRs still GitHub.
  • docs/vocabulary.md: every middle label, one section each (meaning / who applies / middle's response / when to use). The three Epic-aware skills now cross-link here instead of restating label meanings (red-flag entries kept — action-shaped). The cross-links use an absolute GitHub URL because skills are stamped into target repos.
  • docs/daemon-as-a-service.md: complete systemd (user + system) units and a launchd plist, with install/verify/log commands. mm start --foreground runs in-process, writes no pid file, exits 0 on SIGTERM.

How to review

  • Integration tests are the evidence (each boots a real path, not a unit stub):
    • packages/cli/test/doctor.test.tsdoctor honors the documented file-mode config (boots mm doctor on the doc's worked example) and the vocabulary-check tests (one boots the real mm binary via Bun.spawn).
    • packages/cli/test/start-foreground.test.ts → boots the real mm start --foreground daemon, confirms /health, asserts no pid file, SIGTERM → exit 0.
  • mm doctor --vocabulary-check is a docs↔code coverage drift guard, deliberately not a replay of the LLM-driven recommender — rationale in planning/issues/209/decisions.md and the inline PR review comment on doctor.ts.

Fragile bits that want extra eyes

  • parseDocumentedLabels (doctor.ts) is a line-oriented matcher over docs/vocabulary.md headings; it now skips fenced code blocks (a self-review fix) so an example heading can't inflate the documented set. If the doc's heading style changes (e.g. labels move from ### \x`` to plain text), the guard would under-count.
  • checkEpicFilesRoundTrip reads every marker-led *.md under epics_dir; the read is inside the try so a directory named *.md (EISDIR) surfaces as a fail, not a crash (self-review fix).
  • The foreground integration test binds a real daemon on a fixed high port (41877); fine for serial bun test, worth noting if CI ever parallelizes per-file.

No follow-ups filed — scope delivered in full. Out of scope (sibling-noted): Windows service templates, Docker deployment.

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

@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: 5

🤖 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 `@docs/daemon-as-a-service.md`:
- Around line 81-110: The plist example contains literal "<you>" in XML text
nodes (seen in the ProgramArguments entry "/Users/<you>/.bun/bin/mm", the PATH
string, StandardOutPath and StandardErrorPath) which breaks XML parsing; update
those placeholders by escaping the angle brackets (e.g., replace "<you>" with
"&lt;you&gt;&gt;" or better use a plain token like YOUR_USERNAME) so the example
is valid XML and can be parsed by launchctl/plutil.

In `@docs/operator.md`:
- Around line 72-76: The fenced code block containing the lines "planning/epics/
# one <slug>.md per Epic (README.md explains the format)", ".middle/state.md    
# the ranked dispatch state (the file-mode \"state issue\")", and
".middle/<owner>-<name>.toml   # the [epic_store] config above" should be
labeled with a language (e.g., tag the opening triple backticks as ```text) so
update that fenced block in docs/operator.md to use ```text instead of unlabeled
``` to silence markdownlint and keep the tree readable.

In `@packages/cli/src/commands/start.ts`:
- Around line 176-179: The early return on opts.foreground bypasses the pidfile
preflight in runStart; before calling runForegroundDaemon(opts) reuse the same
preflight/check logic that runStart runs (the pidfile live/stale check and
stale-pidfile cleanup) so we don't start a second daemon or leave stale files;
modify the start flow to call the existing preflight helper (or inline the same
checks used by runStart) prior to invoking runForegroundDaemon, referencing the
existing runStart preflight logic and the opts.foreground branch to ensure
identical behavior for foreground starts.

In `@packages/cli/test/start-foreground.test.ts`:
- Around line 77-79: Replace the hard-coded PORT constant (PORT = 41877) with
code that picks an ephemeral free port at runtime (e.g., using get-port or by
creating a short-lived net.Server and listening on port 0 to read
server.address().port), then write that discovered port into the same temp
config file the test uses before starting the daemon; ensure the test uses this
dynamic port wherever PORT was referenced (start-foreground.test.ts: PORT
variable and the temp config creation logic) so the daemon is launched and
asserted against the ephemeral port instead of a fixed one.

In `@planning/issues/209/plan.md`:
- Around line 60-61: The ATX heading "`#217`'s integration is realized as a
**docs↔code drift guard** via `mm doctor --vocabulary-check`, not a
deterministic replay of the recommender's *classification*:" is missing a space
after the hash and is flagged by markdownlint (MD018); fix it by inserting a
space so the heading reads "# 217's integration ..." (i.e., change "`#217`'s
integration..." to "# 217's integration..."), keeping the rest of the line
including the inline code `mm doctor --vocabulary-check` and emphasis intact.
🪄 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: ed0266d5-a94d-4152-88ab-01808daddb0c

📥 Commits

Reviewing files that changed from the base of the PR and between 31ccad3 and 919e987.

📒 Files selected for processing (17)
  • README.md
  • docs/daemon-as-a-service.md
  • docs/operator.md
  • docs/vocabulary.md
  • packages/cli/src/bootstrap-assets/skills/creating-github-issues/SKILL.md
  • packages/cli/src/bootstrap-assets/skills/implementing-github-issues/SKILL.md
  • packages/cli/src/bootstrap-assets/skills/recommending-github-issues/SKILL.md
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/start.ts
  • packages/cli/src/index.ts
  • packages/cli/test/doctor.test.ts
  • packages/cli/test/start-foreground.test.ts
  • packages/skills/creating-github-issues/SKILL.md
  • packages/skills/implementing-github-issues/SKILL.md
  • packages/skills/recommending-github-issues/SKILL.md
  • planning/issues/209/decisions.md
  • planning/issues/209/plan.md

Comment thread docs/daemon-as-a-service.md Outdated
Comment thread docs/operator.md Outdated
Comment thread packages/cli/src/commands/start.ts
Comment thread packages/cli/test/start-foreground.test.ts Outdated
Comment thread planning/issues/209/plan.md Outdated
@thejustinwalsh thejustinwalsh removed 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

🔁 Reconciled with main (merged-new-work-as-base) after 128056e

@thejustinwalsh

Copy link
Copy Markdown
Owner Author

Reviewer brief — #228 (Epic #209 operator-docs hardening)

This PR was stale + DIRTY for a few hours after #229 + #231 merged; the rebase was a single git merge origin/main resolved new-work-as-base (per the CLAUDE.md carve-out) — the only conflict was packages/cli/test/doctor.test.ts where main added Playwright-cache tests in the same area the branch added runVocabularyCheck + the file-mode round-trip tests. Resolution kept both blocks in their natural order (file-mode + vocabulary first, Playwright cache last). Force-push was --force-with-lease.

How to run it

gh pr checkout 228
bun install                                      # populate the worktree's node_modules
bun run typecheck                                # clean
bun run lint                                     # clean
bun test packages/cli/test/doctor.test.ts        # 30 / 30 pass, the merge surface
bun test                                          # 1467 / 1467 full sweep

What to verify

  • docs/operator/ — the file-mode walkthrough + the daemon-as-a-service docs landed cleanly through the merge.
  • docs/vocabulary.md — exists, lists every documented label section; mm doctor --vocabulary-check exits 0 against it.
  • mm doctor covers the new checks: file-mode round-trip + the Playwright-cache check from main both run.
  • mm start --foreground — new flag, honors the pidfile preflight, doesn't double-spawn.

Fragile bits

  • The merge commit at the tip (64be4ae) is the carve-out from CLAUDE.md — preserving it intentionally so the doctor.test.ts resolution doesn't have to be re-earned in every rebase round. If you'd rather a clean per-commit rebase, the rerere cache should now replay it.
  • The dispatcher worktree at /home/tjw/.middle/worktrees/thejustinwalsh/middle/issue-209 is what the rebase happened in (the branch was already checked out there).

…hecks

Add the 'Enable file mode on an existing repo' how-to to docs/operator.md: the
mm init --epic-store=file flip (not a hand TOML edit), the directory layout, a
round-tripping worked-example Epic file, and the operator-visible differences.

Extend the doctor file-mode check to three rows — epics_dir exists, state_file
present, and every marker-led Epic file under epics_dir round-trips byte-for-byte
— and assert it in doctor.test.ts by lifting the worked example straight from the
doc as the fixture, so docs and code can't drift apart (#216).

@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: 1

🧹 Nitpick comments (1)
packages/cli/src/commands/start.ts (1)

6-6: ⚡ Quick win

Add a type-level TSDoc block to the exported StartOptions.

The properties are documented, but the public export itself is missing a contract-level TSDoc/JSDoc comment.

Proposed patch
+/**
+ * Options for `mm start` execution modes and startup side effects.
+ * Guarantees:
+ * - background mode uses pidfile-based lifecycle (`mm stop`);
+ * - foreground mode runs in-process and writes no pid file.
+ */
 export type StartOptions = {

As per coding guidelines "Every public export in a module must carry a TSDoc/JSDoc comment. Comments must describe behavior and contracts — what it does, what it guarantees, what it assumes — not restate the identifier's name."

🤖 Prompt for 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.

In `@packages/cli/src/commands/start.ts` at line 6, Add a TSDoc block above the
exported StartOptions type that describes the contract and behavior of the
options object (what it configures, guarantees, and any assumptions), not just
restating the name; reference the StartOptions type and its existing properties
(e.g., any timeout, env, port, etc.) in the description so callers know the
effects and expectations of each field and whether fields are optional or have
defaults.
🤖 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 `@docs/operator.md`:
- Line 122: Update the documentation text that currently reads "github mode" to
use the correct product capitalization "GitHub mode"; locate the string "github
mode" in docs/operator.md (the sentence describing mm run-recommender rewriting
.middle/state.md and dispatcher poller cadence) and replace it with "GitHub
mode" to ensure consistent naming throughout the doc.

---

Nitpick comments:
In `@packages/cli/src/commands/start.ts`:
- Line 6: Add a TSDoc block above the exported StartOptions type that describes
the contract and behavior of the options object (what it configures, guarantees,
and any assumptions), not just restating the name; reference the StartOptions
type and its existing properties (e.g., any timeout, env, port, etc.) in the
description so callers know the effects and expectations of each field and
whether fields are optional or have defaults.
🪄 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: 9106b57b-1262-442f-b7d2-869eb4129c1e

📥 Commits

Reviewing files that changed from the base of the PR and between c78e1cf and 64be4ae.

📒 Files selected for processing (17)
  • README.md
  • docs/daemon-as-a-service.md
  • docs/operator.md
  • docs/vocabulary.md
  • packages/cli/src/bootstrap-assets/skills/creating-github-issues/SKILL.md
  • packages/cli/src/bootstrap-assets/skills/implementing-github-issues/SKILL.md
  • packages/cli/src/bootstrap-assets/skills/recommending-github-issues/SKILL.md
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/start.ts
  • packages/cli/src/index.ts
  • packages/cli/test/doctor.test.ts
  • packages/cli/test/start-foreground.test.ts
  • packages/skills/creating-github-issues/SKILL.md
  • packages/skills/implementing-github-issues/SKILL.md
  • packages/skills/recommending-github-issues/SKILL.md
  • planning/issues/209/decisions.md
  • planning/issues/209/plan.md
✅ Files skipped from review due to trivial changes (6)
  • packages/cli/src/bootstrap-assets/skills/recommending-github-issues/SKILL.md
  • planning/issues/209/plan.md
  • packages/skills/creating-github-issues/SKILL.md
  • docs/vocabulary.md
  • packages/cli/src/bootstrap-assets/skills/implementing-github-issues/SKILL.md
  • planning/issues/209/decisions.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • README.md
  • packages/cli/src/bootstrap-assets/skills/creating-github-issues/SKILL.md
  • packages/cli/src/index.ts
  • docs/daemon-as-a-service.md
  • packages/cli/test/doctor.test.ts

Comment thread docs/operator.md
### What changes in file mode

- **References are slugs, not `#numbers`.** `mm dispatch`, `mm status`, and the dashboard show `retry-webhooks`, not `#123`.
- **The recommender ranks files.** `mm run-recommender` ranks the Epic files under `planning/epics/` and rewrites `.middle/state.md` instead of the GitHub state issue. The dispatcher's poller picks up state changes on its normal cadence (≈120s), same as github mode.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Capitalize “GitHub” consistently.

Line 122 says “github mode”; use “GitHub mode” for correct product naming.

Proposed patch
-- **The recommender ranks files.** `mm run-recommender` ranks the Epic files under `planning/epics/` and rewrites `.middle/state.md` instead of the GitHub state issue. The dispatcher's poller picks up state changes on its normal cadence (≈120s), same as github mode.
+- **The recommender ranks files.** `mm run-recommender` ranks the Epic files under `planning/epics/` and rewrites `.middle/state.md` instead of the GitHub state issue. The dispatcher's poller picks up state changes on its normal cadence (≈120s), same as GitHub mode.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **The recommender ranks files.** `mm run-recommender` ranks the Epic files under `planning/epics/` and rewrites `.middle/state.md` instead of the GitHub state issue. The dispatcher's poller picks up state changes on its normal cadence (≈120s), same as github mode.
- **The recommender ranks files.** `mm run-recommender` ranks the Epic files under `planning/epics/` and rewrites `.middle/state.md` instead of the GitHub state issue. The dispatcher's poller picks up state changes on its normal cadence (≈120s), same as GitHub mode.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~122-~122: The official name of this software platform is spelled with a capital “H”.
Context: ... on its normal cadence (≈120s), same as github mode. - PRs are unchanged. The agen...

(GITHUB)

🤖 Prompt for 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.

In `@docs/operator.md` at line 122, Update the documentation text that currently
reads "github mode" to use the correct product capitalization "GitHub mode";
locate the string "github mode" in docs/operator.md (the sentence describing mm
run-recommender rewriting .middle/state.md and dispatcher poller cadence) and
replace it with "GitHub mode" to ensure consistent naming throughout the doc.

…lary-check

Add docs/vocabulary.md documenting every middle label (meaning, who applies it,
middle's response, when to use) as the single source of truth, and replace the
definition-shaped label prose in the three Epic-aware skills with cross-links to it
(red-flag entries stay — action-shaped).

Add 'mm doctor --vocabulary-check': a docs<->code drift guard that parses the doc
and asserts every label the code deterministically keys on (the needs-design /
agent-queue:state constants + middle-owned NON_FEATURE_LABELS) is documented, plus
the full canonical vocabulary is present. Tested in the CLI suite, booting the real
mm binary against the shipped doc (#217).
Add 'mm start --foreground': run the dispatcher in-process with no fork and no pid
file, so a service manager (systemd Restart=on-failure / launchd KeepAlive) owns the
lifecycle; runDaemon's SIGTERM handler drains and exits 0 cleanly.

Add docs/daemon-as-a-service.md with complete systemd user + system units and a
launchd plist, plus install / verify / log-tail commands; cross-link it from
operator.md's 'Start and stop' section and the command table, and from the README
setup steps as the next step after mm doctor.

Integration test boots the real mm binary via Bun.spawn against an isolated HOME +
config, confirms /health answers, asserts no pid file is written, and that SIGTERM
exits cleanly (#218).
…view edges

- checkEpicFilesRoundTrip: move the readFileSync inside the try so a directory
  named *.md (EISDIR) or an unreadable *.md symlink under epics_dir surfaces as an
  epic-files fail naming the slug, instead of throwing out of the whole mm doctor
  run (listEpicSlugs matches on name only).
- parseDocumentedLabels: skip fenced code blocks so a documentation example that
  shows a '### `label`' heading can't inflate the documented-label set.
- Soften the vocabulary-check docstring: it's a code-coverage + completeness check,
  not bidirectional equality (extra documented labels are allowed).

Tests for both edges; same class as the changes already under review.
…ing fixes

- daemon-as-a-service.md: replace literal <you> (invalid XML in the launchd
  plist example, breaks plutil/launchctl) with a plain YOUR_USERNAME token
- operator.md: label the file-layout fenced block as text (MD040)
- plan.md: prefix the decision note so it isn't a malformed ATX heading (MD018)
--foreground took an early return that skipped runStart's live/stale pidfile
check, so it would launch a second daemon over a running background dispatcher
and never clear a stale pidfile. Extract pidFilePreflight and call it from both
the background and foreground paths. Tests cover both foreground cases; the
integration test now binds an ephemeral free port instead of a fixed 41877 so
it can't collide under parallel CI.
@thejustinwalsh thejustinwalsh merged commit e690b6a into main Jun 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: Operator docs hardening (file mode flip, vocabulary, daemon-as-service)

1 participant