Skip to content

Commit 1f7a92d

Browse files
alicodingclaude
andcommitted
feat: Review kind filter + polish; audit LOW items — goals 0002 and 0025 close
Review queue (goal 0002 item 4, its last): a kind Select over the four pending kinds (policy ask / human review / breakpoint / MCP write — discriminated off the same fields each row's own icon and badge already key on, wording reused verbatim from row prose, fixed option order), shown only at 2+ kinds present (the single-option-select-is-noise rule); Blankslate empty state + Spinner loading replace the bare div/ nothing. E2e: kind-filter narrowing + emptied-queue Blankslate (mcpTestClient.ts now the shared real-MCP-client helper across three specs). Audit LOW items (goal 0025 item 9, its last): WorkflowHoverPreview clears its hover timer on unmount; unused PageContainerVariant export removed; authoring-validation's single-node canvas click hardened with the sibling specs' candidate-point pattern. Both goals archived (0002's badge item was 0005's build; every other box checked). Rides along per the intake rule: SPEC §4 gains the owner-named communication-preset design input (Discord/Telegram/ Twilio/ntfy — goal 0023's generic forward already reaches them all; a preset earns a build on real friction). Gates: tsc/eslint/loc clean, 178 vitest, suite 153 passed (the new kind-filter test was flaky-then-passed once — first observation, logged against ADR-0034's quarantine threshold, watched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft
1 parent c7738af commit 1f7a92d

11 files changed

Lines changed: 322 additions & 126 deletions

docs/SPEC.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2864,6 +2864,12 @@ choice doesn't).
28642864
UI — Phase B's own bigger, separate design surface.
28652865
- Jira/Confluence as a first-class example: still `OPEN`, unbuilt — the
28662866
generic request type is real, but no named-vendor preset exists yet.
2867+
Same future-preset category, owner-named 2026-08-11 after goal 0023's
2868+
generic forward shipped: **communication presets** (Discord webhook /
2869+
Telegram bot / Twilio SMS / ntfy push — n8n's communication node
2870+
category) — the generic HTTPRequest already reaches all of them; a
2871+
preset earns a build when real wiring hits real friction, not
2872+
speculatively.
28672873
- Whether connectors are built-in or a plugin surface: still `OPEN`.
28682874
- **Backend CRUD + Configure UI — `LOCKED` and built, end-to-end.**
28692875
`ConfigureService` (`configureservice.go`) is the Wails-bound service —

docs/goals/0002-review-queue-maturation.md

Lines changed: 0 additions & 40 deletions
This file was deleted.

docs/goals/BACKLOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ this pipeline and on this code)**
3434
1. [ ] [0024 — CI/CD target architecture + operating model](0024-cicd-target-architecture.md)
3535
— IN FLIGHT: catch-up pushed, e2e triage + target-architecture
3636
build land next, then the ruleset on a green main (ADR-0034)
37-
2. [ ] [0025 — Substance hardening](0025-substance-hardening.md)
37+
2. [x] [0025 — Substance hardening](archive/0025-substance-hardening.md) — DELIVERED 2026-08-12 (both audit waves + LOW items) —
3838
fix the audited green-but-wrong class (12 silent persistence
3939
sites, unfailable test fake, uncovered safety-badge source);
4040
frontend half already largely clean
@@ -58,7 +58,7 @@ live-review material, interleaved during owner reviews, not a lane.**
5858

5959
**Group C — Attention layer**
6060
5. [x] [0005 — Pending-attention model](0005-pending-attention-model.md) — core delivered 2026-08-10 (unified guardrail event + sidebar badge + traceless-timeout fix; OS-notification future named)
61-
6. [ ] [0002 — Review queue maturation](0002-review-queue-maturation.md) (remainder: pending badge built on 0005's eventing; kind filter/polish)
61+
6. [x] [0002 — Review queue maturation](archive/0002-review-queue-maturation.md) — DELIVERED 2026-08-12 (kind filter over four pending kinds, Blankslate/loading polish; badge came via 0005)
6262

6363
**Unscheduled (reorder into a group when prioritized)**
6464
7. [x] [0012 — Authoring hot-exit](archive/0012-authoring-hot-exit.md) — canvas half delivered 2026-08-10 (scratch persistence + restored-unsaved banner + dirty dots; Configure forms recorded-remaining in the archived file)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# 0002 — Review queue maturation
2+
3+
## Goal
4+
The Review queue works as the case-management-style inbox from
5+
`docs/SPEC.md` §3.2/§8 (ADR-0023) for real daily use — without
6+
crossing the no-Camunda line (no assignment/SLA/notes).
7+
8+
## Plan
9+
1. [x] Workflow filter over pending + resolved (delivered 2026-08-10).
10+
2. [x] Recently-resolved visibility — RunSummary.Resolution read from
11+
the same park event after resolution; Review shows the last 10
12+
with approved/denied/timed-out labels (delivered 2026-08-10,
13+
e2e-covered: deny → resolved section → filter).
14+
3. [x] Pending-count sidebar badge — delivered alongside 0005's unified
15+
event, same build as planned (`guardrail-pending-changed`, consumed
16+
in `AppSidebar.tsx` via `review-pending-count`, summed with the
17+
mcp-write-approval channel, no polling); e2e-covered
18+
(`guardrail.spec.ts`'s "Sidebar Review badge..." test). Checkbox was
19+
stale — found already-built while working item 4, corrected here
20+
rather than left inaccurate.
21+
4. [x] Kind filter + empty/loading polish (delivered 2026-08-11):
22+
`ReviewView.tsx` gained a "Filter by kind" Select (policy ask /
23+
human review checkpoint / debug park / MCP write request,
24+
discriminated off `pending.source`/`pending.nodeTypeID` plus the
25+
separate pendingWrites list), shown only when 2+ kinds are
26+
present (SPEC §3.5's single-option-select-is-noise rule); a
27+
Human review checkpoint also got its own leading icon
28+
(`PersonIcon`, distinct from the ambient-ask `ShieldIcon`) since
29+
filtering it as a separate kind needed a real recognition cue, not
30+
just a new Select bucket. Empty state is now the shared Primer
31+
`Blankslate` (`InboxIcon` + one heading line, same pattern as
32+
`HomeView.tsx`/`InventoryList.tsx`); loading (`pending === null`)
33+
now shows `HomeView.tsx`'s own centered-`Spinner`-under-the-Heading
34+
treatment instead of rendering nothing. E2e-covered end-to-end
35+
(`guardrail.spec.ts`): all three kinds parked at once (a policy
36+
ask, a human-review checkpoint, and a real MCP write request over
37+
a live MCP client), the Select narrows to each, and the Blankslate
38+
shows once every kind clears back to zero. The MCP-client test
39+
helpers (`connectMCPClient`/`findWorkflowIdByLabel`/
40+
`exportWorkflowViaMCP`/`enableMCPWritesWithApprovalRequired`/
41+
`restoreMCPWriteDefaults`) were promoted out of
42+
`mcp-write-approval.spec.ts` into a shared `e2e/mcpTestClient.ts`
43+
module so this new test didn't hand-roll a second copy.
44+
5. [x] Row drill-down (delivered 2026-08-10): every Review row opens
45+
its run in the work-tab shell at the workflow's Runs tab, run
46+
detail preselected (consumed-once `pendingRunFocus` store seam);
47+
Approve/Deny stopPropagation-protected; e2e ×3. The "1-12-31"
48+
timestamp root cause was deeper than display: DBOS only writes
49+
StartedAt on queue-dequeue, which Mill never uses — EVERY run had a
50+
zero start; `summaryFromStatus` now falls back to CreatedAt
51+
(regression-tested), fixing Review, Runs, Activity, and MCP
52+
`get_run`/`list_runs` responses all at once.
53+
6. [ ] The fuller case-management growth path (durable Case entity,
54+
Statuses/Queues/Checklists/Automations, the MCP-written AI summary)
55+
is recorded as design input in SPEC §3.2.1 — future research, not
56+
this goal's scope.
57+
58+
This goal's Acceptance is now fully met (items 1-5 delivered; item 6
59+
is recorded future design input, not build scope) — left for the
60+
completing commit to archive per CLAUDE.md's own convention.
61+
62+
## Acceptance
63+
Run the seeded review example plus a guardrail ask; both flows are
64+
filterable, resolutions are visible after the fact, and the sidebar
65+
shows pending count — judged live by the owner.

docs/goals/0025-substance-hardening.md renamed to docs/goals/archive/0025-substance-hardening.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,19 @@ Frontend wave (audit came back largely clean; remainder):
3434
8. [x] Vitest for `canvasScratch.normalize/draftsEqual` (hot-exit
3535
dirty-detection — data-loss-adjacent pure logic) and
3636
`requestDraft.authConfigFrom/joseConfigFrom`.
37-
9. [ ] LOW items: WorkflowHoverPreview unmount timer, unused
38-
PageContainerVariant export, authoring-validation.spec's unhardened
39-
canvas click.
37+
9. [x] LOW items (delivered 2026-08-11): `WorkflowHoverPreview.tsx`'s
38+
`scheduleOpen` timeout now clears on unmount (a real `useEffect`
39+
cleanup, alongside the existing mouseleave-triggered `cancelOpen`);
40+
`PageContainer.tsx`'s `PageContainerVariant` type is no longer
41+
exported (verified zero references outside the file first — it was
42+
dead public surface, kept as an internal type alias);
43+
`authoring-validation.spec.ts`'s `deleteStarterNode` now uses the
44+
same candidate-point/`elementFromPoint` verification the sibling
45+
specs (`composition-canvas-interactions.spec.ts`,
46+
`child-workflow.spec.ts`) already use for canvas-node clicks,
47+
rather than a plain `.click()` that could land on React Flow's own
48+
Controls/MiniMap chrome — this file's own local copy, per the
49+
suite's per-file-helper convention.
4050

4151
## Acceptance
4252
Every accepted finding fixed with its committed repro (a persist

frontend/e2e/authoring-validation.spec.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,38 @@ async function dragPaletteItemToCanvas(page: import('@playwright/test').Page, no
5050

5151
// Removes the pre-populated starter node -- test (a) needs an exact,
5252
// known single-node graph (a lone Capture, no Trigger at all), not
53-
// "the starter plus whatever I added."
53+
// "the starter plus whatever I added." Selects it by clicking a point
54+
// PROVEN to land inside its own card, not a fixed offset -- see
55+
// composition-canvas-interactions.spec.ts's/child-workflow.spec.ts's own
56+
// copies of this pattern for the full MiniMap/Controls-overlap reasoning
57+
// (a plain `.click()` targets the card's center, which React Flow's own
58+
// Controls/MiniMap chrome can sit under depending on layout); this
59+
// file's own copy, since the helper is deliberately per-file. There's
60+
// only ever one node at this point (a brand-new workflow's starter), so
61+
// no label filter is needed.
5462
async function deleteStarterNode(page: import('@playwright/test').Page) {
55-
await activePanel(page).locator('.react-flow__node').click()
63+
const node = activePanel(page).locator('.react-flow__node').first()
64+
const box = await node.boundingBox()
65+
if (!box) throw new Error('deleteStarterNode: starter node has no bounding box')
66+
const candidates = [
67+
{ x: box.x + 10, y: box.y + 10 },
68+
{ x: box.x + box.width - 10, y: box.y + 10 },
69+
{ x: box.x + box.width / 2, y: box.y + box.height / 2 },
70+
{ x: box.x + 10, y: box.y + box.height - 10 },
71+
]
72+
let clicked = false
73+
for (const point of candidates) {
74+
const insideNode = await page.evaluate(({ x, y }) => {
75+
const el = document.elementFromPoint(x, y)
76+
return !!el?.closest('.react-flow__node')
77+
}, point)
78+
if (insideNode) {
79+
await page.mouse.click(point.x, point.y)
80+
clicked = true
81+
break
82+
}
83+
}
84+
if (!clicked) throw new Error('deleteStarterNode: no candidate point resolved inside the starter node\'s own card')
5685
await activePanel(page).getByRole('button', { name: 'Delete selected' }).click()
5786
await expect(activePanel(page).locator('.react-flow__node')).toHaveCount(0)
5887
}

frontend/e2e/guardrail.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { test, expect } from './fixtures/server'
2+
import { clickRowAction } from './inventoryRow'
3+
import {
4+
connectMCPClient, exportWorkflowViaMCP, findWorkflowIdByLabel,
5+
enableMCPWritesWithApprovalRequired, restoreMCPWriteDefaults,
6+
} from './mcpTestClient'
27

38
// The guardrail execution gate end-to-end in the live app (docs/SPEC.md
49
// §8, ADR-0019/0022), driven through the seeded "Example:
@@ -240,3 +245,98 @@ test('Review row drill-down: pending-row Approve/Deny still resolve in place, wi
240245
await expect(page.getByRole('tab', { name: 'Runs' })).toHaveCount(0)
241246
await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toHaveCount(0, { timeout: 10_000 })
242247
})
248+
249+
// Kind filter + empty/loading polish (docs/goals/0002-review-queue-
250+
// maturation.md item 4): three real pending kinds parked at once
251+
// (a policy ask, a human-review checkpoint, and an MCP write request --
252+
// docs/adr/0032), proving the Select appears only once 2+ kinds are
253+
// present, narrows correctly per kind, and the calm Blankslate empty
254+
// state shows once every kind is cleared back to zero.
255+
test('Review kind filter narrows pending rows by kind, and the Blankslate empty state shows once cleared', async ({ page }, testInfo) => {
256+
await page.goto('/')
257+
await page.getByRole('link', { name: 'Workflows' }).click()
258+
259+
// Kind 1: a policy ask.
260+
const askRow = page.locator('[data-testid="inventory-row"][data-entity="workflow"]').filter({ has: page.getByText(GUARDED, { exact: true }) })
261+
await askRow.getByRole('button', { name: 'Run' }).click()
262+
263+
// Kind 2: a human-review checkpoint.
264+
const reviewSeed = 'Example: Human review with input'
265+
const reviewRow = page.locator('[data-testid="inventory-row"][data-entity="workflow"]').filter({ has: page.getByText(reviewSeed, { exact: true }) })
266+
await reviewRow.getByRole('button', { name: 'Run' }).click()
267+
const dialog = page.getByRole('dialog')
268+
await dialog.getByLabel('Note').fill('')
269+
await dialog.getByRole('button', { name: 'Run' }).click()
270+
271+
// Kind 3: an MCP write request -- export/re-import a throwaway
272+
// workflow over a real MCP client, same shape mcp-write-approval.spec.ts
273+
// uses (mcpTestClient.ts is the shared helper both now import).
274+
await enableMCPWritesWithApprovalRequired(page)
275+
await page.getByRole('link', { name: 'Workflows' }).click()
276+
await page.getByTestId('new-workflow').click()
277+
await page.locator('[role="tabpanel"]:not([hidden])').last().getByLabel('Label').fill('E2E kind-filter MCP source')
278+
await page.locator('[role="tabpanel"]:not([hidden])').last().getByTestId('save-workflow').click()
279+
const mcpSourceRow = page.locator('[data-testid="inventory-row"][data-entity="workflow"]').filter({ has: page.getByText('E2E kind-filter MCP source', { exact: true }) })
280+
await expect(mcpSourceRow).toBeVisible()
281+
282+
const client = await connectMCPClient(testInfo.parallelIndex)
283+
const sourceId = await findWorkflowIdByLabel(client, 'E2E kind-filter MCP source')
284+
const exported = await exportWorkflowViaMCP(client, sourceId)
285+
const importResultPromise = client.callTool({ name: 'import_workflow', arguments: { json: exported } })
286+
287+
// All three kinds pending at once -- the Select appears (2+ kinds).
288+
await page.getByRole('link', { name: 'Review' }).click()
289+
const kindSelect = page.getByTestId('review-kind-filter')
290+
await expect(kindSelect).toBeVisible({ timeout: 15_000 })
291+
292+
// "Awaiting approval" narrows to just the guardrail ask.
293+
await kindSelect.selectOption({ label: 'Awaiting approval' })
294+
await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toBeVisible()
295+
await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toHaveCount(0)
296+
await expect(page.getByTestId('review-mcp-write-item')).toHaveCount(0)
297+
298+
// "Human review" narrows to just the checkpoint.
299+
await kindSelect.selectOption({ label: 'Human review' })
300+
await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toBeVisible()
301+
await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toHaveCount(0)
302+
await expect(page.getByTestId('review-mcp-write-item')).toHaveCount(0)
303+
304+
// "MCP write request" narrows to just the pending write.
305+
await kindSelect.selectOption({ label: 'MCP write request' })
306+
await expect(page.getByTestId('review-mcp-write-item')).toBeVisible()
307+
await expect(page.getByTestId('review-item')).toHaveCount(0)
308+
309+
// Back to "All kinds": every row is visible again.
310+
await kindSelect.selectOption({ label: 'All kinds' })
311+
await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toBeVisible()
312+
await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toBeVisible()
313+
await expect(page.getByTestId('review-mcp-write-item')).toBeVisible()
314+
315+
// Clear every kind back to zero: deny both runs, approve the write.
316+
await page.getByTestId('review-item').filter({ hasText: GUARDED }).getByTestId('review-deny').click()
317+
await expect(page.getByTestId('review-item').filter({ hasText: GUARDED })).toHaveCount(0, { timeout: 10_000 })
318+
await page.getByTestId('review-item').filter({ hasText: reviewSeed }).getByTestId('review-deny').click()
319+
await expect(page.getByTestId('review-item').filter({ hasText: reviewSeed })).toHaveCount(0, { timeout: 10_000 })
320+
await page.getByTestId('review-mcp-write-item').getByTestId('review-mcp-write-approve').click()
321+
await expect(page.getByTestId('review-mcp-write-item')).toHaveCount(0, { timeout: 10_000 })
322+
323+
const result = await importResultPromise
324+
await client.close()
325+
if (result.isError) throw new Error(`import_workflow ultimately errored: ${JSON.stringify(result.content)}`)
326+
327+
// Nothing pending: the kind Select disappears (fewer than 2 kinds
328+
// present) and the calm Blankslate empty state shows.
329+
await expect(page.getByTestId('review-kind-filter')).toHaveCount(0)
330+
await expect(page.getByTestId('review-empty')).toBeVisible({ timeout: 10_000 })
331+
332+
// Cleanup: both minted workflows (import always mints a new ID), and
333+
// the MCP-write settings toggle.
334+
await page.getByRole('link', { name: 'Workflows' }).click()
335+
let remaining = await mcpSourceRow.count()
336+
while (remaining > 0) {
337+
await clickRowAction(page, mcpSourceRow.first(), 'Delete')
338+
remaining -= 1
339+
await expect(mcpSourceRow).toHaveCount(remaining)
340+
}
341+
await restoreMCPWriteDefaults(page)
342+
})

0 commit comments

Comments
 (0)