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
49 changes: 49 additions & 0 deletions .claude/rules/delivery-discipline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Delivery discipline — Definition of Ready / Definition of Done

No `paths` frontmatter — applies to every goal, every language. Terms
are Scrum's (the 2020 Scrum Guide formalized DoD as an artifact
commitment) and Kanban's (Kanban University's own "ready for delivery"
pull criteria) — adopted as the converged NAMES for gates this repo
already runs, consolidated here from CLAUDE.md/testing.md/ADR-0034 so
an agent checks ONE list, not four documents (researched 2026-08-12;
owner-mandated: "DoR should be industry standard").

## Definition of Ready — before a BACKLOG.md item enters a session
- Precedent checked, not assumed (CLAUDE.md Research→Plan→Implement).
- A capability map exists for any schema/adopt-vs-build call with more
than one real future use (CLAUDE.md Plan step, SPEC §3.3's worked
example).
- A goal file exists: Goal/Plan/**Acceptance stated as a checkable
predicate**, not a vibe.
- No SPEC.md `OPEN` dependency is silently resolved by starting —
surface the choice, or name it an explicit blocker and don't start.
- No estimate/story-point step — deliberately excluded: estimation
negotiates shared *team* capacity, which a solo-owner +
agent-execution loop doesn't have.

## Definition of Done — before the checkbox flips and the file moves to archive/
- Local lefthook suite green, never bypassed.
- CI's `ci-gate` required check green on the **merged** PR (ADR-0034).
- Every new capability carries a seeded example + a proof at the right
layer (testing.md's layering) — the seed is part of DoD, never a
follow-up.
- Any bug fixed via live/manual repro is now a committed test
(testing.md).
- SPEC.md updated in the same change, for anything that shifts what it
describes (mechanical-only changes exempted).
- The goal file's own Acceptance criteria checked against what
SHIPPED, not what was planned.
- Nothing secret-shaped staged; a real commit message.

## Build-health visibility
The ruleset (ADR-0034) already makes main unmergeable-red; confirm,
don't re-enforce:
- `gh pr checks <n> --watch` right after opening a goal's PR.
- `gh run list -b main -L 1` once, when picking up the next goal.
No richer signal is worth building — a webhook receiver is a second
deployable, already forbidden (SPEC §1.1).

## Tech debt
A BACKLOG.md entry with the same DoR/DoD as any goal — never a second
register, never a bare TODO as the record (a comment may point at a
goal/ADR id, never stand alone).
22 changes: 21 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ permissions:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Conditional, per GitHub's own docs pattern: a superseded PR run
# SHOULD die (per-PR synthetic ref), but main's post-merge runs must
# each complete — with `true` here, two merges landing close together
# cancelled the earlier merge commit's own verification run, leaving
# that main SHA's CI status permanently incomplete (observed live
# 2026-08-12: merges #1/#10 showed cancelled; the mirror image of
# ADR-0034's bisect-blind-spot concern).
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
# goal 0024 / ADR-0034's un-deferred path-filtering: skip the heavy
Expand Down Expand Up @@ -269,6 +276,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
# fail-fast: false -- real incident, not a hypothetical (PR 11,
# run 31557343422, 2026-08-12): shard 3 failed on a real e2e
# regression, and GitHub Actions' own fail-fast DEFAULT (true when
# unset) immediately cancelled shards 1 and 2 mid-run rather than
# letting them finish -- both were killed ~2.5 minutes into a
# passing-so-far run, so their own genuine pass/fail signal was
# simply never collected ("shard-2-cancelled-while-green": it
# wasn't red, it never got the chance to report). A 3-way sharded
# suite needs every shard's own verdict to know what's actually
# broken, not just "at least one shard is red" -- the same
# reasoning build-go's own 2-platform matrix comment already
# states for its own fail-fast: false, applied here too.
fail-fast: false
matrix:
shardIndex: [1, 2, 3]
shardTotal: [3]
Expand Down
60 changes: 60 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -2375,6 +2375,44 @@ turns out to solve this without touching that dispute).
all Go-tested; a Review-queue row appearing and its Approve executing
the write is e2e-tested (`mcp-write-approval.spec.ts`).

**A fourth verb — `cancel_write` — and a requester-liveness heartbeat
are now built (docs/goals/archive/0026-request-lifecycle-honesty.md),
completing park/poll/resolve/**WITHDRAW**.** `cancel_write {id}`
(`mcpsvc`, ungated — cancelling your own request only ever reduces
pending work) lets the requesting client withdraw its own still-pending
write; `cancelled` is a distinct `MCPWriteStatus`, never conflated with
`denied`, sharing `ResolveMCPWrite`'s at-most-once locking discipline.
`check_write_status` now also records `LastPolledAt` on every real poll
— the requester's own heartbeat — surfaced to Review as a muted
"requester last checked Nm ago" hint, shown only past a 5-minute
staleness bar (never on fresh polling, no noise). **A real BUG found
live in the same pass**: `ResolveMCPWrite` and the lazy expiry sweep
never fired the `mcp-write-approval` pending-count signal at all — a
resolved write's Activity/Review traces were correct, but the sidebar
badge and any poll-less surface (the banner, the floating prompt) could
hold a phantom pending count against an already-empty queue. Root
cause, once traced: `main.go`'s `application.RegisterEvent[MCPWriteRequest]
("mcp-write-approval")` binds that event name to an exact Go type
(Wails3's own documented behavior — "data types are matched exactly and
no conversion is performed") — emitting a bare `struct{}{}` silently
failed that check and the event was dropped before ever reaching a
browser client. Fixed by emitting a zero-value `MCPWriteRequest`
instead; every resolution path (approve/deny/cancel/expiry) now pings
it unconditionally. **Resolved MCP writes are now durable in Review's
own Recently-resolved section** (`MCPWriteResolved`/`ResolvedMCPWrites`,
merged newest-first alongside resolved runs, distinct `PlugIcon`
identity, not clickable) — previously the only trace of a
denied/expired write was session-only Activity, gone on restart, even
though the 24h outcome record was already persisted. **Activity's own
MCP-write rows are no longer action-dead**: expandable (the existing
`canExpand`/`result` mechanism) with a jump-to-target-workflow
`WorkflowHoverPreview` icon when the gated tool named an existing
workflow (`update_workflow`/`publish_workflow`/`delete_workflow`'s own
`id` argument — empty for `import_*` tools, which mint a new entity).
Proven end-to-end against a real MCP client, including the exact
phantom-badge regression (deny from Review, assert the sidebar badge
clears with no other page event) — `mcp-write-cancel.spec.ts`.

### 3.7 Global app settings

`SettingsService` (`settingsservice.go`) owns Mill's global settings
Expand Down Expand Up @@ -2640,6 +2678,28 @@ the integration-http/decision-outcome transport tail) +
HTTPRequest, independent of the presence gate — the layer that reaches
the owner with no local Mac to notify on at all.

**Staleness presentation — `LOCKED` and built
(docs/goals/archive/0026-request-lifecycle-honesty.md), applying the §1
thesis to time-honesty of a pending ask.** Owner-observed 2026-08-11: a
4-hour-old test write sat in Review looking exactly as urgent as a
fresh one ("feels like I missed something"). Age-tiered, not binary:
fresh (<15 minutes) renders as-is; older gets visible emphasis (an
attention-colored age badge) plus an "expires in Nh" caption counting
down the shared 24h clock (`frontend/src/shared/staleness.ts`'s
`ageTier`/`formatExpiresIn`, one implementation reused by ReviewView's
pending rows — both guardrail/human-review/debug parks and MCP write
requests — the `MCPWriteApprovals` banner, and the floating approval
prompt, `shared/StalenessBadge.tsx`). **No auto-dismiss** — the VS Code
severity-rule precedent (§9.1-adjacent) holds; expiry is the only
terminal timer, staleness is presentation only. The same age-tier
mechanism, at its own 5-minute bar, now also flags a **stuck-ENQUEUED
run** (a real zombie run — queued forever, never dequeued — found in
production data) in `WorkflowRunsPanel` and Activity's runs explorer,
alongside the Stop/`CancelRun` affordance DBOS's own `CancelWorkflow`
already supports for that status ("cancels a running or enqueued
workflow," confirmed against a real constructed ENQUEUED run, not just
trusted from the doc comment — `TestListRuns_EnqueuedRun_PresentationFieldsAndCancelPath`).

**Still `OPEN`, real named gaps:** a menu-bar/dock presence toggle (see
above); appearance settings beyond light/dark; a default working
directory/scope (blocked on §6); fullscreen window-state tracking
Expand Down
7 changes: 7 additions & 0 deletions docs/adr/0034-git-ci-operating-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,10 @@ Both are resolved now, the second one reversed.
build now that the mechanism is understood and the felt cost (a
15-minute-plus e2e matrix run on a pure `docs/goals/*.md` edit) is
real.
- **`e2e` job's shard matrix gained `fail-fast: false`** (goal 0026),
after a real incident (PR 11, run 31557343422): shard 3 failed
legitimately, and GitHub Actions' own fail-fast default immediately
cancelled shards 1 and 2 mid-run rather than letting them report
their own verdicts — signal lost, not just noise, the same class of
problem `build-go`'s own 2-platform matrix already carried this flag
for.
17 changes: 13 additions & 4 deletions docs/goals/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,19 @@ this pipeline and on this code)**
backend-side `isAway`), alert-style authorization request (notify.Start),
cross-device forward (`composition.SendJSONWebhook`,
`ForwardPendingApproval`) — see ADR-0032's Update note
4. [ ] [0026 — Request lifecycle honesty](0026-request-lifecycle-honesty.md)
— withdrawal verb (`cancel_write`, the MCP Tasks `tasks/cancel`
precedent ADR-0032 already mirrors) + staleness/expiry presentation
(owner-observed 2026-08-11: a 4h-old stale ask reads as breakage)
4. [x] [0026 — Request lifecycle honesty](archive/0026-request-lifecycle-honesty.md)
— delivered 2026-08-12: `cancel_write` MCP tool (a distinct
outcome from denied, ungated, at-most-once); age-tiered staleness
presentation (Review/banner/floating prompt) + "expires in Nh";
requester-liveness hint (`lastPolledAt`, >5m-stale gate); the
phantom-badge BUG fixed (every resolution path — approve/deny/
cancel/expiry — now pings the pending-changed signal, found live:
an empty-struct payload silently failed Wails3's own registered-
event type check); resolved MCP writes now durable in Review's
Recently-resolved; Activity MCP-write rows are expandable with a
jump-to-workflow preview; stuck-ENQUEUED runs get age emphasis +
Stop in WorkflowRunsPanel/Activity's runs explorer. Item 4
(session-side hygiene) intentionally not a Mill code change.

**Ratified 2026-08-10 (owner): three groups, A→B→C. 0001 stays standing
live-review material, interleaved during owner reviews, not a lane.**
Expand Down
56 changes: 56 additions & 0 deletions docs/goals/archive/0026-request-lifecycle-honesty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 0026 — Request lifecycle honesty: withdrawal + staleness

## Goal
A pending decision never lies about its relevance (owner-observed
2026-08-11: a 4-hour-old test write sat in Review looking as urgent as
a fresh ask — "feels like I missed something / it's not working").
An actionable item must carry its freshness, and the requester must be
able to take back a request that stopped mattering.

## Plan
1. [x] **`cancel_write` MCP tool** (the missing fourth verb —
park/poll/resolve/WITHDRAW; `tasks/cancel` in the MCP Tasks spec is
the direct precedent ADR-0032 already mirrors): the requesting
client cancels its own still-pending write by id; cancelled ≠
denied (a distinct outcome, recorded in Activity like
denied/expired — never traceless). Ungated (cancelling your own
request needs no human approval; it only ever REDUCES pending
work). At-most-once semantics shared with resolve.
2. [x] **Staleness presentation** in Review + banner + floating
prompt: age-tiered treatment (fresh <15m renders as-is; older gets
a visible age emphasis + "expires in Nh" from the 24h clock) — the
§1 thesis applied to time-honesty of asks. No auto-dismiss of
actionable items (the VS Code severity rule holds; expiry is the
only terminal timer).
3. [x] **Requester liveness hint** (design question, research first):
should a pending write surface "requester last polled Nm ago" —
check_write_status calls are the natural heartbeat — so an
abandoned request is visibly abandoned? Cheap to record; decide
presentation against the no-noise bar.
4. [ ] Session-side hygiene rule for THIS workflow (rides the memory,
not Mill): a test write parked for demonstration gets cancelled by
its requester when the demonstration ends. (Session-side, not a
Mill code change — outside this delivery's own scope.)
5. [x] **Badge staleness on write resolution (BUG, diagnosed live
2026-08-11)**: ResolveMCPWrite (and the expiry sweep) never emits on
the pending-changed channel, so the sidebar badge held a phantom 1
against an empty queue — resolution paths must ping the same ONE
signal parks do (goal 0005's model, missing emit).
6. [x] **Resolved writes appear in Review's Recently-resolved** — today
a denied/approved/cancelled write vanishes from Review entirely
(only trace: session-only Activity, gone on restart). The queue's
own history must include write resolutions, durably (the persisted
24h outcome records already exist — surface them).
7. [x] **Activity MCP-write rows get actions**: expandable detail +
jump-to-the-target-workflow (runs already drill down; writes are
action-dead — owner: "so what I can do and nothing I can do").
8. [x] **Stuck-ENQUEUED runs surface honestly** (a run enqueued-forever
reads as live; found: a zombie ENQUEUED run from a morning error) —
age-visible like item 2, plus a Stop affordance where cancel is
legal.

## Acceptance
A parked-then-obsolete request can be withdrawn by its requester and
shows as cancelled in Activity; a lingering pending item visibly
communicates its age and time-to-expiry; the owner never again reads
staleness as breakage.
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
export type {
DataChanged,
MCPWriteActivity,
MCPWriteRequest
MCPWriteRequest,
MCPWriteResolved
} from "./models.js";
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,37 @@ export interface DataChanged {
}

/**
* MCPWriteActivity is pushed for a missed (expired) or denied MCP
* write so it's no longer traceless (docs/goals/0005-pending-attention-
* model.md item 3). Reuses the same activity-push shape App.tsx's
* hotkey-activity handler already established, under a distinct
* "mcp-write" ActivitySource so it's filterable, not conflated with a
* workflow trigger.
* MCPWriteActivity is pushed for a resolved (denied/cancelled/expired,
* or approved-but-failed) MCP write so it's no longer traceless
* (docs/goals/0005-pending-attention-model.md item 3). Reuses the same
* activity-push shape App.tsx's hotkey-activity handler already
* established, under a distinct "mcp-write" ActivitySource so it's
* filterable, not conflated with a workflow trigger.
*/
export interface MCPWriteActivity {
"description": string;

/**
* Outcome is "denied" or "expired" -- an approved write never
* reaches here, there's nothing traceless about it.
* Outcome is "denied", "cancelled", or "expired" -- an
* approved-and-succeeded write never reaches here, there's nothing
* traceless about it.
*/
"outcome": string;

/**
* ToolName/WorkflowID/Result back Activity's own MCP-write row
* actions (docs/goals/0026 item 7: "so what I can do and nothing I
* can do") -- ToolName is the gated tool this record was for;
* WorkflowID is the workflow it targeted, when the tool names one
* (update_workflow/publish_workflow/delete_workflow's own "id"
* argument -- empty for import_* tools, which mint a NEW entity
* rather than referencing an existing one, so there's nothing to
* jump to); Result is what the Activity row's expandable detail
* panel shows.
*/
"toolName"?: string;
"workflowID"?: string;
"result"?: string;
}

/**
Expand All @@ -41,4 +57,33 @@ export interface MCPWriteRequest {
"id": string;
"description": string;
"createdAt": string;

/**
* LastPolledAt mirrors MCPWriteRecord's own field (docs/goals/0026
* item 3) -- nil when the requester has never called
* check_write_status on this id yet.
*/
"lastPolledAt"?: string | null;
}

/**
* MCPWriteResolved is the frontend-facing shape for an already-resolved
* write (docs/goals/0026 item 6) -- Review's Recently-resolved section
* reads this alongside RunSummary's own resolved rows, merged
* newest-first. Retained for the same 24h window check_write_status
* already promises (sweepLocked's own retention) -- "durable across a
* restart" and "still visible for the same window an MCP client can
* still poll" are the same guarantee, not two.
*/
export interface MCPWriteResolved {
"id": string;
"description": string;

/**
* approved / denied / cancelled / expired
*/
"status": string;
"error"?: string;
"createdAt": string;
"resolvedAt": string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ export function ClearKeybinding(commandID: string): $CancellablePromise<void> {
return $Call.ByID(3036020909, commandID);
}

/**
* DebugBackdatePendingMCPWrite is an e2e-only test knob (docs/goals/0026
* item 2's staleness presentation) -- see MillMCPService.
* DebugBackdatePendingWrite's own doc comment for why this has to be an
* in-process call rather than an external settings-file edit. Refuses
* outside isolated test data (the same IsIsolatedData signal every
* e2e run already sets via MILL_SETTINGS_PATH) -- never reachable
* against a real production instance.
*/
export function DebugBackdatePendingMCPWrite(id: string, ageMinutes: number): $CancellablePromise<void> {
return $Call.ByID(561097732, id, ageMinutes);
}

/**
* DismissApprovalPrompt hides the floating approval prompt and applies
* the same focus-yield mitigation DismissPanel already uses -- called
Expand Down Expand Up @@ -283,6 +296,15 @@ export function ResolveMCPWrite(id: string, approve: boolean): $CancellablePromi
return $Call.ByID(2304068895, id, approve);
}

/**
* ResolvedMCPWrites lists every already-resolved MCP write still in its
* 24h retention window (docs/goals/0026 item 6) -- Review's
* Recently-resolved section reads this alongside its own resolved runs.
*/
export function ResolvedMCPWrites(): $CancellablePromise<mcpsvc$0.MCPWriteResolved[] | null> {
return $Call.ByID(3844643736);
}

/**
* RestoreMenuAccelerators reverses SuspendMenuAccelerators -- see its
* doc comment for the full reasoning. Safe to call more times than
Expand Down
Loading
Loading