diff --git a/CHANGELOG.md b/CHANGELOG.md index 0806589..b4a7268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ observable behavior and compatibility, not every internal refactor. ## Unreleased +### Fixed + +- The Portable HTML report route in `templates/reporting/routing.md` now + lists WorkBuddy, so agents on WorkBuddy are routed to the self-contained + HTML + Markdown output the 0.4.0 host adapter already ships. A derived + support-declaration check now requires every adapter-matrix host claiming + portable HTML output to appear in that routing row. + ## 0.4.0 - 2026-07-30 ### Added diff --git a/docs/specs/2026-07-30-routing-workbuddy-host.md b/docs/specs/2026-07-30-routing-workbuddy-host.md new file mode 100644 index 0000000..8aca132 --- /dev/null +++ b/docs/specs/2026-07-30-routing-workbuddy-host.md @@ -0,0 +1,76 @@ +# Report Routing WorkBuddy Host Gap + +Add WorkBuddy to the Portable HTML report route in +`templates/reporting/routing.md` and add a derived consistency test so a +future host with portable HTML output cannot be omitted from report routing +again. + +## Traceability + +- Spec ID: 2026-07-30-routing-workbuddy-host +- Story: follow-up to the WorkBuddy host adapter (`4b871d9`, + CHANGELOG 0.4.0) +- Status: Implemented + +## Intent + +The WorkBuddy host adapter shipped with a `self-contained HTML + Markdown` +Default Output claim in the [Host Adapter Matrix](../adapters/README.md), and +the matrix Output Modes section, the docs site (English and zh-Hans), and +CHANGELOG 0.4.0 all state that WorkBuddy portable HTML routing is +implemented. However, the Portable HTML report route in +[`templates/reporting/routing.md`](../../templates/reporting/routing.md) +still lists only "Claude Code, Codex, Cursor, Qwen Code, GitHub Copilot, or +Pi". Because `routing.md` is the routing switchboard agents load through +`SKILL.md`, an agent running on WorkBuddy is never routed to the portable +HTML report. + +The omission survived because the two guarding assertions +(`test/better-harness-skill.test.mjs`, `test/style-templates.test.mjs`) +hard-code the host list, and the A-06 support-declaration consistency tests +(`test/support-declarations.test.mjs`) never compare the adapter matrix +against `routing.md`. + +## Acceptance Scenarios + +- AC-1: The Portable HTML report row in `templates/reporting/routing.md` + lists WorkBuddy alongside the existing hosts; no other route changes. +- AC-2: The hard-coded routing assertions in + `test/better-harness-skill.test.mjs` and `test/style-templates.test.mjs` + match the updated host list. +- AC-3: `test/support-declarations.test.mjs` gains a derived check: every + adapter-matrix host whose Default Output cell claims + `self-contained HTML + Markdown` must appear as an exact host entry in the + Portable HTML report route of `routing.md`. A prefix collision such as + `WorkBuddy` versus `WorkBuddy Enterprise` must fail. The check is + one-directional so a host may be removed from the matrix claim first (as the + in-flight A-05 change does for Cursor) without breaking routing. +- AC-4: `npm test` passes; `node --test test/doc-link-graph.test.mjs` passes + with this spec's links resolving. + +## Non-Goals + +- No change to the Cursor entries in `routing.md` or the adapter matrix; the + Cursor durable-report gap stays tracked by the in-flight A-05 pull request + and roadmap item HA-03. +- No new WorkBuddy capability, output mode, or renderer change; this only + routes the already-shipped portable HTML output. + +## Plan + +1. Add WorkBuddy to the Portable HTML report row in + `templates/reporting/routing.md`. +2. Update the two hard-coded host-list assertions to match. +3. Add the derived matrix-to-routing consistency test to + `test/support-declarations.test.mjs`, parsing the route declaration into + normalized host entries and covering a host-prefix collision. + +## Test Evidence + +- `node --test test/support-declarations.test.mjs` +- Mutation regression: replacing the exact `WorkBuddy` route entry with + `WorkBuddy Enterprise` must report WorkBuddy as missing. +- `node --test test/better-harness-skill.test.mjs` +- `node --test test/style-templates.test.mjs` +- `node --test test/doc-link-graph.test.mjs` +- Full suite: `npm test` diff --git a/templates/reporting/routing.md b/templates/reporting/routing.md index 862749d..c011b50 100644 --- a/templates/reporting/routing.md +++ b/templates/reporting/routing.md @@ -28,6 +28,6 @@ files from other routes. | Route | Use when | Artifacts | Runtime owner | | --- | --- | --- | --- | | Qoder Canvas report | Active host is Qoder | renderer-owned `findings.json`, `canvas.json`, `report.canvas.tsx` | `qoder-canvas.md` | -| Portable HTML report | Active host is Claude Code, Codex, Cursor, Qwen Code, GitHub Copilot, or Pi, or a portable visual is explicitly requested | renderer-owned `findings.json`, `report.md`, `report.html` | `html-visual.md` | +| Portable HTML report | Active host is Claude Code, Codex, Cursor, Qwen Code, GitHub Copilot, Pi, or WorkBuddy, or a portable visual is explicitly requested | renderer-owned `findings.json`, `report.md`, `report.html` | `html-visual.md` | | Markdown only | Markdown without a visual companion is explicitly requested | `report.md`, `findings.json` | none | | Inline only | Inline or no-files output is explicitly requested | none; inline analysis writes nothing | none | diff --git a/test/better-harness-skill.test.mjs b/test/better-harness-skill.test.mjs index 1859645..dc801ee 100644 --- a/test/better-harness-skill.test.mjs +++ b/test/better-harness-skill.test.mjs @@ -77,7 +77,10 @@ test("non-Qoder providers default to validated durable HTML with an explicit inl assert.match(skill, /HTML artifacts: findings\.json, report\.md, report\.html/); assert.match(skill, /Succeed only on\s+`status: pass`/); assert.match(skill, /Never hand-write\s+Canvas, Markdown, or HTML/); - assert.match(routing, /Portable HTML report \| Active host is Claude Code, Codex, Cursor, Qwen Code, GitHub Copilot, or Pi/); + assert.match( + routing, + /Portable HTML report \| Active host is Claude Code, Codex, Cursor, Qwen Code, GitHub Copilot, Pi, or WorkBuddy, or a portable visual is explicitly requested \|/, + ); assert.match(routing, /Inline only \| Inline or no-files output is explicitly requested \| none; inline analysis writes nothing/); assert.match(adapters, /Claude Code[^\n]+scripts\/session-analysis\/platforms\/claude\.mjs[^\n]+self-contained HTML \+ Markdown/); assert.match(adapters, /Cursor[^\n]+scripts\/session-analysis\/platforms\/cursor\.mjs[^\n]+self-contained HTML \+ Markdown/); diff --git a/test/style-templates.test.mjs b/test/style-templates.test.mjs index c96126c..efd98d6 100644 --- a/test/style-templates.test.mjs +++ b/test/style-templates.test.mjs @@ -75,7 +75,10 @@ test("harness report routing owns output-mode selection and exclusions", () => { assert.match(reportRouting, /Choose exactly one output route/); assert.match(reportRouting, /Qoder Canvas report/); assert.match(reportRouting, /Portable HTML report/); - assert.match(reportRouting, /Active host is Claude Code, Codex, Cursor, Qwen Code, GitHub Copilot, or Pi/); + assert.match( + reportRouting, + /Active host is Claude Code, Codex, Cursor, Qwen Code, GitHub Copilot, Pi, or WorkBuddy, or a portable visual is explicitly requested \|/, + ); assert.match(reportRouting, /Markdown only/); assert.match(reportRouting, /Inline only/); assert.match(reportRouting, /inline analysis writes nothing/); diff --git a/test/support-declarations.test.mjs b/test/support-declarations.test.mjs index ea0ebb7..14136d2 100644 --- a/test/support-declarations.test.mjs +++ b/test/support-declarations.test.mjs @@ -13,6 +13,7 @@ const SUPPORTED_PLATFORMS = ["qoder", "codex", "claude", "cursor", "qwen", "copi const cliPath = path.join(process.cwd(), "scripts", "better-harness.mjs"); const adapterMatrixPath = path.join(process.cwd(), "docs", "adapters", "README.md"); +const reportRoutingPath = path.join(process.cwd(), "templates", "reporting", "routing.md"); function runBetterHarness(args) { return spawnSync(process.execPath, [cliPath, ...args], { @@ -29,6 +30,29 @@ function assertSameSet(actual, label) { assert.deepEqual(sortedSet(actual), sortedSet(SUPPORTED_PLATFORMS), `${label} disagrees with the supported platform set`); } +function portableHtmlMatrixHosts(matrix) { + return matrix + .split("\n") + .map((line) => line.split("|").map((cell) => cell.trim())) + .filter((cells) => cells[6] === "self-contained HTML + Markdown") + .map((cells) => cells[1]); +} + +function portableHtmlRouteHosts(routing) { + const declaration = routing.match( + /\| Portable HTML report \| Active host is (.+?), or a portable visual is explicitly requested \|/u, + )?.[1]; + assert.ok(declaration, "reporting/routing.md does not declare a Portable HTML report route"); + return new Set(declaration.split(/,\s*(?:or\s+)?/u).map((host) => host.trim())); +} + +function missingPortableHtmlRouteHosts(matrix, routing) { + const htmlHosts = portableHtmlMatrixHosts(matrix); + assert.ok(htmlHosts.length > 0, "adapter matrix declares no self-contained HTML + Markdown hosts"); + const routeHosts = portableHtmlRouteHosts(routing); + return htmlHosts.filter((host) => !routeHosts.has(host)); +} + test("agent-customize provider registry declares exactly the supported platforms", () => { assertSameSet([...PROVIDER_COLLECTORS.keys()], "PROVIDER_COLLECTORS"); @@ -119,3 +143,22 @@ test("host adapter matrix documents exactly the supported platforms", () => { assertSameSet(documentedProviders, "adapter matrix configured-asset providers"); assertSameSet(documentedPlatforms, "adapter matrix session platforms"); }); + +test("adapter-matrix portable HTML hosts appear in the portable HTML report route", () => { + const matrix = readFileSync(adapterMatrixPath, "utf8"); + const routing = readFileSync(reportRoutingPath, "utf8"); + + // Hosts whose matrix Default Output cell claims the portable HTML pipeline. + // One-directional on purpose: a host may drop the matrix claim first (for + // example a pending durable-report gap) without breaking report routing. + for (const host of missingPortableHtmlRouteHosts(matrix, routing)) { + assert.fail(`Portable HTML report route is missing matrix HTML host: ${host}`); + } + + const prefixCollisionRouting = routing.replace( + ", or WorkBuddy, or a portable visual is explicitly requested", + ", or WorkBuddy Enterprise, or a portable visual is explicitly requested", + ); + assert.notEqual(prefixCollisionRouting, routing, "prefix-collision fixture did not replace the WorkBuddy route entry"); + assert.deepEqual(missingPortableHtmlRouteHosts(matrix, prefixCollisionRouting), ["WorkBuddy"]); +});