Skip to content

fix(dispatcher): resolve state-issue schema from the package, not per-repo#157

Merged
thejustinwalsh merged 2 commits into
mainfrom
middle-issue-107
May 28, 2026
Merged

fix(dispatcher): resolve state-issue schema from the package, not per-repo#157
thejustinwalsh merged 2 commits into
mainfrom
middle-issue-107

Conversation

@thejustinwalsh

@thejustinwalsh thejustinwalsh commented May 26, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #107

The recommender resolved the state-issue schema at <repoPath>/schemas/state-issue.v1.md, which exists only in middle's own checkout. Once auto-dispatch (Phase 8) runs the recommender against bootstrapped target repos, the schema isn't there. This resolves the schema from the @middle/state-issue package — the issue's option (b) — so no per-repo copy is needed and the single source of truth never drifts.

What changed

  • packages/state-issue/src/schema-path.ts (new)STATE_ISSUE_SCHEMA_PATH, the absolute path to the canonical schemas/state-issue.v1.md, resolved from the module's own location (import.meta.dir) so it points into the middle install regardless of cwd or which target repo is in play.
  • packages/state-issue/src/index.ts — export it; module-index frontmatter updated (Public surface: + Where things live:).
  • packages/dispatcher/src/recommender-run.tsresolveRecommenderOptions now uses the resolver; the existsSync guard is reworded (a miss = broken middle install, not a repo problem).
  • packages/dispatcher/src/main.ts — the daemon's resolveRunSettings uses the resolver.
  • Tests: new packages/state-issue/test/schema-path.test.ts; recommender-run.test.ts now asserts the schema resolves independently of repoPath; the dead per-repo fixture is removed from run-recommender.test.ts.

Why these changes

Option (a) — mm init stamping the schema into each target repo — would create one copy per repo, each able to drift from the source of truth the root CLAUDE.md state-issue contract establishes, and would need a per-repo re-sync gate. The schema is middle-internal dispatch machinery the target repo's collaborators never need in-tree (unlike skills, which the dispatched coding agent reads in-repo). Resolving from the package keeps one authoritative copy and follows the existing skills-sync.ts import.meta.dir precedent. The issue's own wording ("so no per-repo copy is needed") leans the same way. Full rationale in planning/issues/107/decisions.md, distilled into per-line review comments.

Verification

  • Typecheck: bunx tsc --noEmit — clean.
  • Lint/format: bun run lint (--deny-warnings) + bun run format — clean.
  • Full suite: bun test — 722 pass / 0 fail across 81 files.
  • The fix, specifically: recommender-run.test.ts → "resolves schemaPath from the middle install, not from repoPath" builds a target repo with no schemas/ dir and asserts resolveRecommenderOptions(...).ok === true, schemaPath === STATE_ISSUE_SCHEMA_PATH, and !schemaPath.startsWith(repoPath). schema-path.test.ts asserts the resolved path is absolute, ends in schemas/state-issue.v1.md, exists on disk, and contains the real schema header.

Acceptance evidence

Status

  • Phase 1: Resolve-from-package — STATE_ISSUE_SCHEMA_PATH, wire both readers, drop dead fixture, tests

Stumbling points

  • The schema doc's header is # Agent Queue State Issue — Schema v1 (no literal state-issue token), so the content assertion anchors on State Issue — Schema v1 instead.
  • join became unused in recommender-run.ts after the swap — caught by oxlint --deny-warnings, removed.

Suggested CLAUDE.md updates

None. The state-issue contract section already names schemas/state-issue.v1.md as the source of truth — this change makes the code resolve it accordingly.

Architectural forks

None. The decision was resolved by the CLAUDE.md contract + the skills-sync.ts precedent, not by building competing implementations.

Follow-up issues

None surfaced.

Out of scope

  • Changing mm init's stamping set (option (a) — explicitly rejected).
  • Moving the schema file's repo-root location or changing its content.
  • The recommender prompt format.

Decisions

See planning/issues/107/decisions.md (distilled into the per-line review comments on this PR).

Summary by CodeRabbit

Release Notes

  • Refactor

    • Schema path resolution now sources from the installed package rather than per-repository location, ensuring consistent schema references across environments.
  • Tests

    • Added tests validating schema path resolution from package installation.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 61b01156-8e64-4757-bd42-fe1a15c8922d

📥 Commits

Reviewing files that changed from the base of the PR and between 720044c and c11f41f.

📒 Files selected for processing (9)
  • packages/cli/test/run-recommender.test.ts
  • packages/dispatcher/src/main.ts
  • packages/dispatcher/src/recommender-run.ts
  • packages/dispatcher/test/recommender-run.test.ts
  • packages/state-issue/src/index.ts
  • packages/state-issue/src/schema-path.ts
  • packages/state-issue/test/schema-path.test.ts
  • planning/issues/107/decisions.md
  • planning/issues/107/plan.md

📝 Walkthrough

Walkthrough

This PR implements schema path resolution for the state-issue recommender by exporting an absolute filesystem path constant from the @middle/state-issue package, updating the recommender dispatcher to use this constant instead of constructing repo-relative paths, and validating the behavior with new tests while removing obsolete per-repo fixtures.

Changes

Schema Path Resolution Implementation

Layer / File(s) Summary
Schema path export and validation
packages/state-issue/src/schema-path.ts, packages/state-issue/src/index.ts, packages/state-issue/test/schema-path.test.ts
New STATE_ISSUE_SCHEMA_PATH constant resolves schemas/state-issue.v1.md using import.meta.dir relative to the installed package. Module is exported from the package index and validated with tests checking path absoluteness, filename correctness, file existence, and schema content.
Recommender dispatcher schema resolution
packages/dispatcher/src/main.ts, packages/dispatcher/src/recommender-run.ts
Dispatcher imports and passes STATE_ISSUE_SCHEMA_PATH to recommender configuration. resolveRecommenderOptions now derives schema path from the package constant instead of repo-relative construction, and rewords the missing-schema error to reference the installation location as a packaging issue.
Test updates and fixture removal
packages/dispatcher/test/recommender-run.test.ts, packages/cli/test/run-recommender.test.ts
New test verifies resolveRecommenderOptions resolves schemaPath to STATE_ISSUE_SCHEMA_PATH independent of repoPath. Per-repo schemas/state-issue.v1.md fixture is removed from CLI test setup with explanatory comments.

Planning and Decisions

Layer / File(s) Summary
Issue planning and decisions
planning/issues/107/plan.md, planning/issues/107/decisions.md
Issue #107 planning document outlines approach to resolve schema from installed package across dispatcher, recommender, and tests. Three decision records document the primary schema resolution approach, error message semantics change, and fixture removal rationale.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • thejustinwalsh/middle#107: This PR directly implements the resolution described in the issue by adding STATE_ISSUE_SCHEMA_PATH export and updating resolveRecommenderOptions to use the installed package schema path instead of per-repo copies.

Possibly related PRs

  • thejustinwalsh/middle#105: Both PRs address schema path handling for the recommender; #105 currently requires per-repo schemas/state-issue.v1.md while this PR sources it from the installed @middle/state-issue package.
  • thejustinwalsh/middle#150: Both PRs update recommender wiring through the dispatcher pipeline; #150 refactors workflow setup to thread per-run settings while this PR changes how schemaPath is resolved to use the package constant.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: moving state-issue schema resolution from per-repo paths to the @middle/state-issue package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

…-repo

The recommender resolved <repoPath>/schemas/state-issue.v1.md, which only
exists in middle's own checkout — so a recommender run against any bootstrapped
target repo would fail to find the schema once auto-dispatch lands.

Add STATE_ISSUE_SCHEMA_PATH to @middle/state-issue, resolved from the package's
own location (import.meta.dir) like skills-sync.ts resolves middle-tree assets.
Both recommender readers (resolveRecommenderOptions, the daemon's
resolveRunSettings) now point at it instead of <repoPath>/schemas. The existsSync
guard is reworded: a miss now means a broken middle install, not a repo problem.

Drops the dead per-repo schema fixture from run-recommender.test.ts (the thin
client never reads it). Closes #107.
* (its `..` count differs because it sits one directory deeper). This file lives
* at `packages/state-issue/src/`, so the repo root is three levels up.
*/
export const STATE_ISSUE_SCHEMA_PATH = join(

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: resolve the schema from the package, not per-repo stamping (issue #107 option b, not a).

The issue offered two fixes: have mm init copy the schema into each target repo, or resolve it from the @middle/state-issue package. We chose the package resolver because:

  • Single source of truth. The root CLAUDE.md state-issue contract makes schemas/state-issue.v1.md authoritative. Per-repo copies drift from it — the exact failure that rule prevents — and would need a per-repo re-sync gate (skills already carry this "two-copies invariant"; stamping would multiply it by every target repo).
  • The schema is middle-internal. Skills are stamped because the dispatched coding agent runs in the target repo and collaborators share them. The state-issue schema is dispatch machinery the recommender agent reads; the target repo never needs it in-tree.
  • Established precedent. packages/cli/src/bootstrap/skills-sync.ts already resolves middle-tree assets from import.meta.dir. This mirrors that pattern (different directory depth, same idea).

return {
ok: false,
error: `state-issue schema not found at ${schemaPath} (Phase 7 runs against middle's own repo)`,
error: `state-issue schema missing from the middle installation at ${schemaPath} — this is a packaging bug, not a repo problem`,

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: reworded the existsSync guard as a packaging-integrity check. Post-fix the path points into the middle installation, which always ships the schema, so a miss now means a corrupt/partial middle install — a different failure mode than the old per-repo "Phase 7 runs against middle's own repo" condition. The cheap guard stays so the failure surfaces here with a clear cause instead of as an agent-side cat: no such file mid-run.

// Phase 7 schema lives at the repo root.
mkdirSync(join(repoPath, "schemas"), { recursive: true });
writeFileSync(join(repoPath, "schemas", "state-issue.v1.md"), "# schema\n");
// No per-repo schema fixture: runRecommender is a thin daemon client (it never

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: dropped the dead per-repo schema fixture. runRecommender is a thin daemon client — it never calls resolveRecommenderOptions (validation happens daemon-side), so this fixture was never read even before this change. After option (b), no reader looks in the target repo at all, so keeping it would be doubly misleading.

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

Copy link
Copy Markdown
Owner Author

Reviewer's brief — PR #157 (closes #107)

What this does. The recommender resolved the state-issue schema at <repoPath>/schemas/state-issue.v1.md, which exists only in middle's own checkout. This swaps both readers to STATE_ISSUE_SCHEMA_PATH — a new export on @middle/state-issue that resolves the canonical schemas/state-issue.v1.md from the package's own location (import.meta.dir), the same source-tree pattern as skills-sync.ts. That's the issue's option (b); option (a) (per-repo stamping) was rejected because it creates copies that drift from the source of truth.

How to run it.

bunx tsc --noEmit          # typecheck — clean
bun run lint               # oxlint --deny-warnings — clean
bun test                   # 722 pass / 0 fail
bun test packages/state-issue/test/schema-path.test.ts \
         packages/dispatcher/test/recommender-run.test.ts   # the fix, focused

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

  • packages/state-issue/src/schema-path.ts — the .. count: from packages/state-issue/src/, three .. reach the repo root, then schemas/state-issue.v1.md. schema-path.test.ts asserts the resolved path is absolute, ends in schemas/state-issue.v1.md, exists, and contains the real schema header — so a wrong depth fails loudly.
  • packages/dispatcher/src/recommender-run.ts + main.ts — both now point at STATE_ISSUE_SCHEMA_PATH, not join(repoPath, "schemas", …). The recommender-run.test.ts case "resolves schemaPath from the middle install, not from repoPath" builds a target repo with no schemas/ dir and asserts resolution still succeeds and is outside repoPath — this is the regression guard for the bug.
  • The existsSync guard message now frames a miss as a broken middle install (packaging bug), not a per-repo condition.

How to review it. Small, contained diff (+199/−9, 9 files). Per-line review comments on the PR carry the decision rationale. Confirm no other reader still uses join(repoPath, "schemas", …) (grep state-issue.v1.md/schemaPath/"schemas" — none survive).

Fragile / extra eyes. The only load-bearing assumption is that middle runs from its source tree (Bun runs .ts natively; no build/dist step moves the schema relative to the module) — same assumption the existing skills-sync.ts already relies on. If middle ever gains a bundling/publish step that relocates package files, this resolution and the skills mirror would both need revisiting together.

@thejustinwalsh thejustinwalsh merged commit 4ac613f into main May 28, 2026
1 check passed
@thejustinwalsh thejustinwalsh deleted the middle-issue-107 branch May 28, 2026 19:23
thejustinwalsh added a commit that referenced this pull request May 29, 2026
Single-pass new-work-as-base merge of origin/main after rebase kept
re-conflicting on the same hunks across multiple commits (CLAUDE.md
escape hatch).

- packages/dispatcher/src/poller-cron.ts — unified `startPoller(deps,
  opts)` signature; folded `ReconcilerHooks` into `StartPollerOptions`
  as `opts.reconcilers` (alongside `opts.checkboxRevert` and
  `opts.intervalMs`).
- packages/dispatcher/src/main.ts — unified daemon-startup: keeps the
  durable engine + `recoverEngine` + `reconcileOrphanedSignals` from
  #160, the notification-failsafe watchdog comment from #162, and adds
  the `reconcileOpenPRsForRepo` block + `reconcilers` config in the
  `startPoller` call. Dropped the now-unused `Engine` import (main
  routes through `createDurableEngine`).
- packages/core/src/index.ts — kept both export blocks: integration
  rubric from #163, `selectAdapter` from this PR.
- packages/dispatcher/test/recommender-run.test.ts — kept both describe
  blocks (adapter-enabled gate from this PR, schema-resolution from
  #157); added `enabled: true` to the schema test's adapter config so
  it passes the new gate.
- packages/dispatcher/test/gates/checkbox-revert-pass.test.ts — added
  the five new `GitHubGateway` methods to the test stub
  (`listOpenIssues`, `addLabel`, `listMergedPrsClosingRefs`,
  `closeIssue`, `createIssue`) main grew during the marathon.

Gates re-verified locally: `bun run typecheck` clean, `bun test
packages/dispatcher` 620/620 pass, `bun run lint` clean, `bun run
format` clean (no changes).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mm init: stamp the state-issue schema into target repos

1 participant