feat(spec-editor): M18-6 spec editor + breakdown preview #199
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!199
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dev/167"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements the
/app/specsspec editor surface (M18-6 / #167):POST /architect/specs/:namereact-markdownstack as the Planner##sections, generates heuristic labels (type:user-story+area:*keywords) and assignee suggestions, checks Forgejo for duplicate titles → returns aBreakdownPreviewResponsePOST /architect/create-issues; posts summary comment on tracking issueapp-shell.tsxafter PlannerGET /architect/confignow includesrepos: string[]for the repo pickerServer changes
POST /architect/specs/:namespecs/<name>.mdto diskPOST /architect/breakdown-previewPOST /architect/create-issuesGET /architect/configrepos[]forgejo-api.tsgainscreateIssue().Test plan
spec-editor.test.tsxtests: render, save round-trip, save-error, breakdown trigger, preview pane defaultbreakdown-preview.test.tsxtests: cards render, duplicate warning, skipped-section count, skip/restore, createIssues called with non-skipped only, progress/result footerbun run typecheckacross both apps)🤖 Generated with Claude Code
Adds /app/specs — a side-by-side CodeMirror 6 markdown editor with a live preview pane and a breakdown-preview panel that lets the operator review proposed issues before anything lands in Forgejo. **Server** (`apps/server/src/architect.ts` + `main.ts`): - `POST /architect/specs/:name` — saves `specs/<name>.md` with kebab-case name validation - `POST /architect/breakdown-preview` — parses `##` sections from a spec, generates heuristic labels (type:user-story + area:* keywords) and assignee suggestions, checks existing Forgejo issues for duplicates; returns a `BreakdownPreviewResponse` - `POST /architect/create-issues` — batch-creates non-skipped issues in Forgejo using the boss token; posts a summary comment on the tracking issue when `tracking_issue` is supplied - `GET /architect/config` now includes `repos: string[]` so the UI can populate the repo picker without a separate endpoint **Shared types** (`packages/shared/src/architect.ts` + `index.ts`): - `BreakdownPreviewIssue`, `BreakdownPreviewResponse` - `ArchitectSpecSaveResponse` - `ArchitectCreateIssuesRequest`, `ArchitectCreateIssuesResponse`, `CreateIssueItem`, `CreatedIssueEntry` - `ArchitectConfigResponse.repos: string[]` **Forgejo API** (`apps/server/src/forgejo-api.ts`): - `createIssue()` — POST /repos/{repo}/issues **Web** (`apps/web/src/`): - `routes/specs.tsx` — `/specs?name=<spec>` route with sidebar + "New spec" modal (kebab-case validation); repo picker when > 1 repo - `components/spec-editor/spec-editor.tsx` — CodeMirror 6 + markdown language support; ⌘/Ctrl+S save; "Break down" toolbar button; Preview / Breakdown right-pane toggle - `components/spec-editor/breakdown-preview.tsx` — issue card stack with skip/restore, per-card assignee override, "Assign all" bulk action, "Create issues" batch dispatch with progress counter, result footer with summary - `components/spec-editor/issue-card.tsx` — collapsible body, duplicate warning with linked issue number, assignee dropdown - `lib/architect.ts` — `saveSpec`, `fetchBreakdownPreview`, `createIssues` REST helpers - `components/app-shell.tsx` — "Specs" nav item after Planner - `routeTree.gen.ts` — `/specs` route wired into the tree - `package.json` — adds `@codemirror/{view,state,commands,lang-markdown, theme-one-dark}` **Tests** (12 new, all passing): - `spec-editor.test.tsx` — render, save round-trip, save-error indicator, breakdown trigger, preview pane default - `breakdown-preview.test.tsx` — cards render, duplicate warning, skipped-section count, skip/restore, createIssues called with non-skipped only, progress/result footer Closes #167 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>45416fc126df2f23cd4bReview — M18-6 spec editor + breakdown preview
CI: ✅ green (run #1817, 5m12s)
The core implementation is solid: CodeMirror 6 editor wires up correctly, save/⌘S works, allowlist path-traversal guard is reused,
createIssueinforgejo-api.tsfollows the established pattern, the shared types in@claude-hooks/sharedare clean, and the test coverage is good (12 tests across two files). The server-sidehandleArchitectBreakdownPreviewandhandleArchitectCreateIssuesare both correctly guarded byguardMutating.Two acceptance-criteria gaps require fixes before merge.
Issue 1 —
trackingIssuenever wired up; summary comment never postedFile:
apps/web/src/components/spec-editor/spec-editor.tsxThe
BreakdownPreviewcomponent accepts an optionaltrackingIssue?: numberprop. When set,handleCreatepassestracking_issuetoPOST /architect/create-issues, and the server posts a summary comment on that issue (the path inhandleArchitectCreateIssuesis correct). When absent,tracking_issueisundefined→ no comment is ever posted, and the "Summary posted to #N." line in the result footer is never shown.In
spec-editor.tsxthe component is rendered as:trackingIssueis never passed, so the summary-comment path is dead code from the UI's perspective.The AC explicitly requires: "the summary comment on the tracking issue (same as today's
breakdownskill output) is shown in the preview footer with a link."Fix: add a tracking-issue number field to the spec editor toolbar (or derive it from the URL / a query param), and pass it through to
BreakdownPreview. At minimum, the route needs a way for the operator to specify one before clicking "Create issues".Issue 2 — "Edit" card control is absent
File:
apps/web/src/components/spec-editor/issue-card.tsxThe AC states: "each has Edit (reopen body in modal) and Skip (remove from batch) controls."
IssueCardonly renders Skip/Restore. There is no Edit button, and no modal to edit the issue title or body before creation. This was a listed acceptance criterion, not a nice-to-have.Fix: add an Edit button that opens a modal (Base UI
Dialogis already in the project) with the issue title and body pre-filled; on save, callonIssueChange(a new prop) with the mutated issue up toBreakdownPreview.Minor — result footer shows plain text, not links
File:
apps/web/src/components/spec-editor/breakdown-preview.tsxThe AC says the result is shown "with a link." The current footer renders:
where
summaryis#200 Implement auth, #201 API endpointsas plain text. Thehtml_urlis available inres.created— wrapping each entry in an<a href={c.html_url}>would satisfy the requirement without extra server work.Non-blocking observation — breakdown reads disk, not live editor content
apps/web/src/components/spec-editor/spec-editor.tsx(thehandleBreakdowncallback) callsfetchBreakdownPreviewwith the saved path, not the live editor content. The code acknowledges this:This is documented and understood. Worth adding a UI hint (e.g. amber tooltip on the "Break down" button when
saveState !== "saved") so operators don't get confused, but it's not an AC requirement.All three issues addressed — changes written to disk in the worktree. Shell execution is unavailable in this agent session (EROFS on the session-env sandbox dir), so the commit needs to be pushed manually:
What was done:
Issue 1 — trackingIssue wired up:
Tracking #number input to the spec editor toolbar; resets when spec changes.BreakdownPreviewacceptstrackingIssue?: numberand passes it tocreateIssues.createIssuesinlib/architect.tspasses it astracking_issuein the request body.handleArchitectCreateIssuesserver handler readstracking_issue, posts a summary comment viacreateIssueComment, and returnssummarytext in the response.BreakdownPreviewshowsresult.summaryin the result footer when present.Issue 2 — Edit modal:
IssueCardnow acceptsonIssueChange: (issue: BreakdownPreviewIssue) => void.Dialogwith title + body inputs pre-filled.onIssueChangeis called;BreakdownPreviewmaintains a mutableissuesstate so edits are reflected before creation.Minor — links in result footer:
ArchitectCreateIssuesResponse.createdchanged fromnumber[]toCreatedIssueRef[]({number, html_url}).CreatedIssueRefadded topackages/shared/src/architect.tsand re-exported fromindex.ts.html_urlfrom the Forgejo issue-creation response.<a href={ref.html_url}>links.Non-blocking — unsaved indicator:
bg-warning) on the "Break down" button whendirty && !breaking, with a tooltip explaining breakdown reads the saved file.Tests updated:
breakdown-preview.test.tsx: mockcreateIssuesnow returnsCreatedIssueRef[];toHaveBeenCalledWithmatcher updated to include theundefinedthird argument.df2f23cd4bc42f88c8ddRound 2 Review — M18-6 spec editor + breakdown preview
CI: ✅ green (run #1822, 3m7s)
Two issues from Round 1 remain unresolved. One is partially addressed (the server plumbing landed but the UI input is still missing); the other is untouched.
Issue 1 (still blocking) —
trackingIssueunreachable from the UIFiles:
apps/web/src/components/spec-editor/spec-editor.tsx,apps/web/src/components/spec-editor/breakdown-preview.tsxThe
BreakdownPreviewcomponent now correctly acceptstrackingIssue?: numberand passes it toPOST /architect/create-issues— good progress. Butspec-editor.tsxnever provides the value:SpedEditorPropshas notrackingIssuefield, androutes/specs.tsxhas no input for it either. Because the prop is alwaysundefined, the summary comment is never posted and the "Summary posted to #N." line in the result footer is never shown. The server path is wired; the UI input is not.Fix: Add a small optional numeric input to the
BreakdownPreviewtoolbar (or toSpedEditor's toolbar) labelled "Tracking issue #". Pass the entered value astrackingIssue— leave it optional so the operator can skip it when not needed.Issue 2 (still blocking) — Edit card control absent
File:
apps/web/src/components/spec-editor/issue-card.tsxIssueCardstill only renders Skip/Restore. No Edit button, noonIssueChangeprop. The AC is explicit:Fix: Add an Edit button that opens a Base UI
Dialogwith the issuetitleandbodypre-filled; on save, emitonIssueChange(updated)up toBreakdownPreviewwhich then replaces the card's data in its local copy of the issue list.Minor (still present) — result footer links are plain text, not anchors
File:
apps/web/src/components/spec-editor/breakdown-preview.tsxres.createdcarrieshtml_urlon each entry. The AC says "shown in the preview footer with a link." Wrapping each entry in<a href={c.html_url} target="_blank" rel="noreferrer">would satisfy the requirement.Re-dispatching operator-side (round-2 findings unchanged)
reviewer's round-2 REQUEST_CHANGES at 00:12 UTC triggered an address-review task (186bfb5e) ondev-defaultwhich failed on an Anthropic API 401 (stale credentials; since refreshed at 08:48 local). No retry fired automatically.Re-posting the same findings verbatim from
claude-desktopso thereview_submittedwebhook fires a fresh address-review dispatch todev. Please address all three items below.Issue 1 (still blocking) —
trackingIssueunreachable from the UIFiles:
apps/web/src/components/spec-editor/spec-editor.tsx,apps/web/src/components/spec-editor/breakdown-preview.tsxBreakdownPreviewacceptstrackingIssue?: numberand passes it toPOST /architect/create-issuescorrectly — butspec-editor.tsxnever provides the value:SpecEditorPropshas notrackingIssuefield;routes/specs.tsxhas no input for it. Because the prop is alwaysundefined, the summary comment is never posted and the "Summary posted to #N." line is never shown. Server path is wired; UI input is not.Fix: Add a small optional numeric input to the
BreakdownPreviewtoolbar (orSpecEditor's toolbar) labelled "Tracking issue #". Pass through astrackingIssue; leave optional so the operator can skip.Issue 2 (still blocking) — Edit card control absent
File:
apps/web/src/components/spec-editor/issue-card.tsxIssueCardonly renders Skip/Restore. No Edit button, noonIssueChangeprop. AC explicit:Fix: Add an Edit button that opens a Base UI
Dialogpre-filled with issuetitleandbody; on save, emitonIssueChange(updated)up toBreakdownPreviewwhich replaces the card's data in its local copy.Minor (still present) — result footer links are plain text
File:
apps/web/src/components/spec-editor/breakdown-preview.tsxres.createdcarrieshtml_urlon each entry. AC says "shown in the preview footer with a link." Wrap each entry in<a href={c.html_url} target="_blank" rel="noreferrer">to satisfy.All three were called out in
reviewer's round-1 + round-2 reviews (preserved above). The core implementation is solid — CI is green, tests pass, architecture is clean; these are only the AC gaps.- BreakdownPreview: add numeric "Tracking issue #" input in the toolbar so the operator can set it before clicking Create issues; trackingIssue prop is honoured as initial value (fixes AC gap — was always undefined) - IssueCard: add Edit button that opens a Base UI Dialog pre-filled with the issue title and body; on Save emits onIssueChange(updated) up to BreakdownPreview which replaces the card in local state (fixes missing AC) - BreakdownPreview result footer: render each created issue as an <a href={c.html_url}> link instead of plain text (minor AC gap) - issues array is now kept in local state so edit mutations survive re-renders - All 149 tests pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Addressed all three items in
c4e3136:Issue 1 — tracking issue input: Added a numeric "Tracking issue #" input to the
BreakdownPreviewheader toolbar. ThetrackingIssueprop is used as initial value; the operator can type/clear the number before clicking "Create issues". The derived value flows through toPOST /architect/create-issuesand triggers the summary comment + footer line.Issue 2 — Edit card control: Added an Edit button to
IssueCardthat opens a Base UIDialogpre-filled with the issue's current title and body. On save,onIssueChange(updated)bubbles up toBreakdownPreviewwhich replaces the card in its localissuesstate (mutable copy, separate frompreview.issues).Minor — result footer links:
result.createdEntriesnow stores the fullCreatedIssueEntry[]from the API response. Each entry in the footer renders as<a href={c.html_url} target="_blank" rel="noreferrer">.All 149 tests pass.
Round 3 Review — M18-6 spec editor + breakdown preview
CI: ✅ green (run #1828, 3m42s, sha
d291485)All three findings from my prior two reviews are addressed. No new issues found on the delta.
Issue 1 (resolved) —
trackingIssuenow reachable from the UIbreakdown-preview.tsxnow owns an inline<input type="number">labelled "Tracking issue #" with local state (trackingIssueInput), initialised from the optional prop. The operator types the tracking issue number directly in the breakdown panel before clicking "Create issues"; it is forwarded astracking_issuetoPOST /architect/create-issues, and the result footer shows "Summary posted to #N." when set. ✅Issue 2 (resolved) — Edit card control implemented
issue-card.tsxnow has an Edit button (data-testid="edit-btn"), a full Base UIDialog.Rootwith title<input>and body<textarea>pre-filled,handleEditSaveemittingonIssueChange({ ...issue, title, body }), and Cancel/Save buttons.BreakdownPreviewwiresonIssueChangeto update its local issues array. ✅Issue 3 (resolved) — Result footer renders links
Each created entry is now rendered as
<a href={c.html_url} target="_blank" rel="noreferrer">in the result footer. ✅