diff --git a/packages/cli/test/db-scripts.test.ts b/packages/cli/test/db-scripts.test.ts index f8081e71..58e9b860 100644 --- a/packages/cli/test/db-scripts.test.ts +++ b/packages/cli/test/db-scripts.test.ts @@ -72,7 +72,7 @@ describe("backup.sh + reset-db.sh round-trip", () => { // The restored db is intact: schema migrated, and the seeded row survived. const db = openAndMigrate(join(home, "db.sqlite3")); - expect(currentSchemaVersion(db)).toBe(9); + expect(currentSchemaVersion(db)).toBe(10); const row = db.query("SELECT id FROM workflows WHERE id = 'wf-keep'").get(); expect(row).toEqual({ id: "wf-keep" }); db.close(); diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 58ac18f6..8c72f5eb 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -99,6 +99,23 @@ export type BootstrapSettings = { installedAt: string; }; +/** + * The `[epic_store]` section — selects where a repo's Epics + dispatch state live. + * Absent (or `mode = "github"`) means the default GitHub-backed store (Epics are + * issues, state is a state issue). `mode = "file"` is the file-backed store (#190): + * Epics are Markdown files under `epicsDir` and the ranked dispatch state is + * `stateFile`. Mirrors the DB `repo_config` columns (migration 008) — `mm init` + * writes both; the config-toml copy is what config-only callers (the recommender + * run resolution) read to learn a repo is file-mode without a DB handle (#200). + */ +export type EpicStoreSettings = { + mode: "github" | "file"; + /** Repo-relative Epic directory (file mode). */ + epicsDir?: string; + /** Repo-relative ranked-state file (file mode). */ + stateFile?: string; +}; + /** * The `[staleness]` section — per-repo overrides for the anti-staleness drift * check. `spec_path` is the repo-relative build-spec path the check reads; omit @@ -125,6 +142,7 @@ export type MiddleConfig = { limits?: LimitsSettings; recommender?: RecommenderSettings; stateIssue?: StateIssueSettings; + epicStore?: EpicStoreSettings; bootstrap?: BootstrapSettings; docs?: DocsSettings; staleness?: StalenessSettings; @@ -281,6 +299,19 @@ function mapBootstrap(raw: RawTable): BootstrapSettings | undefined { return { version: b.version as number, installedAt: b.installed_at as string }; } +function mapEpicStore(raw: RawTable): EpicStoreSettings | undefined { + if (!isPlainObject(raw.epic_store)) return undefined; + const e = raw.epic_store; + // Anything other than the explicit "file" string is the github default — a + // typo'd mode must never silently route a repo to the file store. + const mode = e.mode === "file" ? "file" : "github"; + return { + mode, + epicsDir: e.epics_dir as string | undefined, + stateFile: e.state_file as string | undefined, + }; +} + /** * Map the `[docs]` section. Unlike the strict per-repo mappers, the bot fields * default rather than trust presence — a tool/path-only override block (the @@ -338,6 +369,7 @@ export function loadConfig(opts: LoadConfigOptions): MiddleConfig { limits: mapLimits(merged), recommender: mapRecommender(merged), stateIssue: mapStateIssue(merged), + epicStore: mapEpicStore(merged), bootstrap: mapBootstrap(merged), docs: mapDocs(merged), staleness: mapStaleness(merged), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index faf97bc3..478dbd8f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -39,6 +39,7 @@ export type { LimitsSettings, RecommenderSettings, StateIssueSettings, + EpicStoreSettings, BootstrapSettings, DocsSettings, StalenessSettings, diff --git a/packages/dashboard/src/app/components/Epics.tsx b/packages/dashboard/src/app/components/Epics.tsx index 7470c151..f31e861b 100644 --- a/packages/dashboard/src/app/components/Epics.tsx +++ b/packages/dashboard/src/app/components/Epics.tsx @@ -7,6 +7,7 @@ */ import { useState } from "react"; import type { EpicCard } from "../../wire.ts"; +import { EpicRef } from "./EpicRef.tsx"; function ProgressBar({ closed, total }: { closed: number; total: number }) { const pct = total > 0 ? Math.round((closed / total) * 100) : 0; @@ -36,7 +37,11 @@ function DispatchControl({ // (it isn't a configured/dispatchable adapter, so the server would reject it anyway). const slot = card.dispatch.freeSlots.find((s) => s.adapter === adapter); const noSlot = slot ? !slot.available : true; - const disabled = card.dispatch.inFlight || noSlot; + // A file-mode Epic (null number) has no numeric handle for the dashboard's + // numeric dispatch route; force-dispatch it from the CLI (`mm dispatch `). + // It's still browsable here — only the in-dashboard dispatch button is gated. + const isFileEpic = card.number === null; + const disabled = card.dispatch.inFlight || noSlot || isFileEpic; return (