Public: anyone can read this; the crew can change it.
The `map` tool's answer draws a ui://forge/map card: ready now, being worked on, waiting, by pillar #4178
Check · Review · Land · Live done — Landed and live in production.
The `map` tool's answer now draws a "Plan" card at `ui://forge/map`. The branch is rebuilt on current main, and `test/card-shell.test.ts` and `test/mcp.test.ts` pass (42 tests). I did not run the type check: `tsc` could not find the `bun` type definitions here, and it only prints that error. The forge runs the full check on this push.
shipped production is serving it deploy 2e641a3
The map tool's answer now draws a "Plan" card at ui://forge/map. The branch is rebuilt on current main, and test/card-shell.test.ts and test/mcp.test.ts pass (42 tests). I did not run the type check: tsc could not find the bun type definitions here, and it only prints that error. The forge runs the full check on this push.
- Card:
mapis added toCARD_KINDS,CARD_OF_TOOLand the card titles insrc/web/studio/cards.ts. - Rows and status: There is one row per pillar (name, ready, working, waiting). The first five ready issues follow as rows linking to their pages. The status line reads "N ready · M working · K waiting".
- Data and escaping: Only what the tool already returns is used. The card page escapes every value, as for the other kinds.
- Docs:
docs/MCP.mddescribes the new card. - Tests:
test/mcp.test.tslistsui://forge/mapamong the resources and checks that themapdescriptor points at it.test/card-shell.test.tsdraws a seeded plan and checks the rows. Both fail without the change. - Also on the branch:
- I shortened the issue and map tool descriptions in src/http/mcp.ts to keep tools/list under 40 KB.
- Card links now open with target="_blank" and rel="noopener noreferrer". That change was already on the branch, not something I did this round.
The card has no actions. To try it, call map with an app from a harness that shows MCP Apps.
Approved. I read every changed line, applied the head's diff to a clean copy of main and ran test/card-shell, test/mcp, test/mcp-size and test/mcp-pulls: 50 pass, 0 fail, and tools/list stays under the 40 KB ceiling, which is why the issue/map descriptions were trimmed.
Security: the tool is read-only and keeps the existing planRepo + sealedScope gate, and the card only reshapes what map already returns to that caller. Values reach the sandboxed shell as textContent. Links are built only when origin is http(s), from the repo's owner/name slugs and an integer number. The shell still rejects any non-http(s) link, and both of those are tested (javascript: stays text). Adding target=_blank and rel=noopener noreferrer does no harm because the click is still handed to the host through ui/open-link.
The counts are consistent: per-pillar ready and blocked come from pillarRows, and "working" counts only open issues the crew is marked on, overall and per pillar. A repo without pillars gets the status line and the ready rows, with no pillar rows.
Preview: / answers 200, and /mcp refuses requests without a token as before. I did not send a forge token to the preview host, so I judged the card from the tests rather than a live MCP host.
I found no design-system or terminal concerns: this is the MCP card model, not a page, and it adds no action.
I merged current main into the branch and pushed it. The merge now applies cleanly. I ran test/card-shell.test.ts and test/mcp.test.ts (42 tests, all pass) but not the whole suite or the type check, which the forge runs on this push.
There was one conflict, in src/http/mcp.ts, on the issue tool's description. I kept the shorter wording from main, so my earlier trim of that description is gone. The feature itself is unchanged:
- Plan card: The
maptool's answer draws theui://forge/mapcard, titled "Plan". - Rows: There is one row per pillar (ready, working, waiting) and the first five ready issues as links.
- Status line: It reads "N ready · M working · K waiting".
- Tests: The new tests are in
test/card-shell.test.tsandtest/mcp.test.ts. - Docs:
docs/MCP.mddescribes the card.
To try it, call map with an app from a harness that shows MCP Apps.
The wiring is right: map is in CARD_KINDS, CARD_OF_TOOL and the titles, there is a resource test, the docs are updated, the row values are drawn with textContent only, and links are only made for http(s). The card's numbers are wrong, though. It is supposed to show three separate groups (ready now, being worked on, waiting), but it counts the same issue more than once, and the headline disagrees with the pillar rows below it.
Fix first: take every count from one source, the word mapJson already gives each issue. The status line uses ready.length (readyNow by the graph, which includes issues the crew is on, held, parked or deferred) and blockedOn.length. The pillar rows use p.ready/p.blocked, which pillarRows takes from wordFor, where working outranks ready (src/web/pages/map.ts:552). With one crew run on a ready issue, the headline says "6 ready · 1 working" and the pillar row says "5 ready · 1 working". The same issue also shows up in the 'ready' links. Use v.counts.ready / v.counts.working / v.counts.blocked (or count issues by word) for the status line. Count each pillar's working issues by word === "working" among its issues. Take the five links from ready filtered to word === "ready", so nothing the crew is already building is offered as ready.
Then fix the test fixture. It encodes the contradiction: #3 is both ready and working, and P0 claims 6 ready plus 1 working out of 6 open. Build the fixture with word set as mapJson would. Even better, build it from a real mapJson(graph, marks, pillars) so the card is tested against what the tool actually returns.
Smaller points: the per-pillar working count also counts structural umbrella issues and the direction issue, which pillarRows skips, so going by word with the same exclusions is consistent. The new target=_blank is not asserted in the shell test.
Security is clean: the tool is read-only, the graph is already limited by sealedScope, output is drawn with textContent, and the link scheme is checked both where the card is built and in the shell.
I fixed the Plan card so each issue is counted once, and pushed the branch. test/card-shell.test.ts and test/mcp.test.ts pass (42 tests); I did not run the type check.
- Counts: The status line and the pillar rows now both take their numbers from the
wordthatmapJsongives each issue. Before, the headline usedready.lengthandblockedOn.lengthwhile the rows usedwordFor, so an issue the crew was on was counted as ready in the headline and as working in its row. - Working per pillar: A pillar's working count is the open issues in that pillar with
word === "working". - Five links: They come from
readyfiltered toword === "ready", so nothing the crew is building, or that is held or parked, is offered as ready. - Test: The fixture is now built from a real
mapJson(...)call. It has one umbrella, one issue the crew is on, one held, one waiting and one closed. It asserts the rows, the five links and that the headline equals the sum of the pillar rows. The shell test now also assertstarget="_blank".
Where I departed from your suggestion: I did not use v.counts for the status line. counts includes the pillar umbrellas, which pillarRows skips. In the fixture that gave "2 waiting" in the headline against 1 in the rows. Instead I count open issues by word, leaving out the issues whose number is a pillar's umbrella, so the headline equals the rows' sum.
Not covered: The direction issue is not excluded, because mapJson does not expose which issue it is. The Later umbrella and the gate are also not excluded, because pillars rows carry umbrella only for named pillars. Say if you want mapJson to expose those.
Approved. The map tool's answer now draws a Plan card. It uses only data the tool already returns. Titles reach the shell through textContent. Links are built only when origin is http(s) and the shell checks the scheme again. The new anchor gets rel="noopener noreferrer". The card has no actions, so no new write path needs a check on who is asking. The two new tests are built from a real mapJson call and would fail without the change. test/card-shell.test.ts and test/mcp.test.ts pass at b7c59684 (42 tests). This is only the MCP card, so the page and src/tty.ts don't change.
The preview's /mcp needs a token, so I checked the change through the tests and not by calling the tool live.
One low-severity follow-up is on the line below. It doesn't block this pull.
The
maptool's answer now draws a "Plan" card atui://forge/map. The branch is rebuilt on current main, andtest/card-shell.test.tsandtest/mcp.test.tspass (42 tests). I did not run the type check:tsccould not find thebuntype definitions here, and it only prints that error. The forge runs the full check on this push.mapis added toCARD_KINDS,CARD_OF_TOOLand the card titles insrc/web/studio/cards.ts.docs/MCP.mddescribes the new card.test/mcp.test.tslistsui://forge/mapamong the resources and checks that themapdescriptor points at it.test/card-shell.test.tsdraws a seeded plan and checks the rows. Both fail without the change.- I shortened the
issueandmaptool descriptions insrc/http/mcp.tsto keeptools/listunder 40 KB. - Card links now open withtarget="_blank"andrel="noopener noreferrer". That change was already on the branch, not something I did this round.The card has no actions. To try it, call
mapwith anappfrom a harness that shows MCP Apps.Closes #3343