Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions docs/specs/2026-07-30-routing-workbuddy-host.md
Original file line number Diff line number Diff line change
@@ -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`
2 changes: 1 addition & 1 deletion templates/reporting/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
5 changes: 4 additions & 1 deletion test/better-harness-skill.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand Down
5 changes: 4 additions & 1 deletion test/style-templates.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand Down
43 changes: 43 additions & 0 deletions test/support-declarations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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], {
Expand All @@ -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");

Expand Down Expand Up @@ -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"]);
});