diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 206b4622..4083f426 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -39,6 +39,30 @@ a named library — check before reaching for a `.map()` + custom markup or a hand-rolled parser (see `.claude/rules/frontend.md` for the concrete UI-collection instance of this). +**The core/composition boundary — before building ANY new capability, +ask: is this a node, a trigger, a connector, or a true kernel +change?** ([ADR-0035](../../docs/adr/0035-core-vs-composition-boundary.md), +`docs/SPEC.md` §9.5's Update.) If a user could plausibly say "I want +that, but to a different channel / with a condition / on a different +event," it's composition-shaped and MUST arrive as composition — a +self-registered `NodeType`, a trigger event, a Configure entity — +never a bespoke service path plus a Settings toggle. Settings toggles +configure the kernel; they never implement a side effect. Recorded +counterexample, the reason this rule exists: cross-device notification +shipped as a Settings checkbox wired to a private send path +(`ForwardPendingApproval`) instead of a connector + trigger +composition, caught live and refactored into a seeded, editable +workflow. The flip side of the same rule: platform-internal behavior +MAY and SHOULD consume Mill's own composition surface (a built-in, +seeded, fully-editable workflow) rather than hand-rolling a parallel +mini-pipeline for something the surface can already express — the app +dogfooding its own platform, inspectable and guarded like anything a +user builds. `docs/SPEC.md` §9.5 carries the protected-kernel list +(graph engine, guardrail gate, durable execution, registries, the +Configure recipe, the MCP plane) that composition never reaches into; +changes there need an ADR, same bar this file's other architecture +decisions already carry. + **Max 500 lines per hand-written source file (`.go`/`.ts`/`.tsx`).** Enforced by `scripts/check-loc.sh`, run by both Lefthook (pre-commit) and CI's `file-loc-limit` job, so it can't land un-caught either way. A diff --git a/docs/SPEC.md b/docs/SPEC.md index 8e9418ac..7a8e8e67 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1645,7 +1645,7 @@ Plan step for this as a standing rule. |---|---|---|---| | **Capture / Process / Apply** | Read structured state from a source, transform it, deliver it | Build (core domain) | `LOCKED`, §2 — built for clipboard/markdown | | **Text injection** (a fixed hint/instruction pasted alongside a workflow's real output — e.g. telling an M365 Copilot chat what other tools are available) | Prepend or append configured static text to the payload | Build (core domain, `process-inject-text`, ADR-0006's self-registration pattern) — no templating engine; conditional injection composes for free with an upstream Decision node instead of adding branching logic to the node itself | `LOCKED`, built — `internal/domain/composition/processinjecttext.go`, e2e-verified (`composition-canvas-interactions.spec.ts`) end-to-end including via the generic ConfigField Inspector, no bespoke UI | -| **Trigger** | Entry-point node: listen for *any* event source (hotkey, clipboard change, a browser-bridge DOM event per §5, an incoming MCP `tools/call` per §3.1, a schedule) and emit its data as the workflow's starting input — not "the hotkey mechanism," a general category the hotkey is one instance of. A trigger's output *is* the workflow's input; these are one concept, not two. | Each concrete event source adopts its own library behind an adapter (hotkey/schedule/filesystem-watch do; clipboard-watch is a small build); the abstraction unifying them into one node kind, and `TriggerService`'s registry/exclusivity, are Mill's own | `LOCKED`, built (manual/hotkey/schedule/clipboard-watch/filesystem-watch) — see §3.4 for the fuller map. DOM-event and MCP-call triggers remain unbuilt, gated on §5/§3.1 | +| **Trigger** | Entry-point node: listen for *any* event source (hotkey, clipboard change, a browser-bridge DOM event per §5, an incoming MCP `tools/call` per §3.1, a schedule, Mill's own execution engine) and emit its data as the workflow's starting input — not "the hotkey mechanism," a general category the hotkey is one instance of. A trigger's output *is* the workflow's input; these are one concept, not two. | Each concrete event source adopts its own library behind an adapter (hotkey/schedule/filesystem-watch do; clipboard-watch is a small build); the abstraction unifying them into one node kind, and `TriggerService`'s registry/exclusivity, are Mill's own | `LOCKED`, built (manual/hotkey/schedule/clipboard-watch/filesystem-watch/callable/system-event) — see §3.4 for the fuller map, including `trigger-system-event`'s [ADR-0035](adr/0035-core-vs-composition-boundary.md) unparking. DOM-event and MCP-call triggers remain unbuilt, gated on §5/§3.1 | | **Branch / routing** (UI-renamed from "Decision: route", ADR-0027) | Route execution down one of several named output edges based on a condition evaluated against the running payload | Node/graph semantics: build (core domain — composition rules). Expression evaluation underneath: adopt (`expr-lang/expr`, MIT, sandboxed/side-effect-free/loop-bounded by design — verified directly, not assumed) rather than hand-writing a condition parser | `LOCKED` (execution engine + authoring) — `internal/domain/composition`'s `ExecContext`/`ValidateGraph`/`nextNode` walk real branches end-to-end; `KindDecision` + `decision-route` NodeType render and connect on the canvas. Conditions are authored visually via a `react-querybuilder` rule builder (`DecisionEdgeInspector.tsx`), translated to `expr-lang/expr` — see §3.5's Branch row | | **Decision (terminal outcome)** | Terminate a branch with a reusable, Configure-authored typed outcome: category + typed outputs + optional webhook; manual-review category parks into the Review queue first | Entity/CRUD/terminal-node semantics: build (core domain, the List/MCP-Server pattern). Webhook transport: reuse (the referenced HTTPRequest's own execution path via the extracted `httpsend.go` — never a second HTTP client). Park mechanism: reuse (the same `waitForApprovalFn` human-review uses) | `LOCKED`, built — [ADR-0027](adr/0027-decision-terminal-outcome.md): `internal/domain/decision`, `KindTerminal` + `decision-outcome` (no source handle, three-layer outgoing-edge rejection), Configure → Decisions tab, typed `outputBindings`, seeded branch-to-decision + manual-review examples proven against real DBOS, 96/96 e2e twice. Building it surfaced and fixed a real latent bug: the guardrail gate and dry-run tester read the *static* per-NodeType effect class, which would have hung a manual-review Decision run — generalized to `EffectForNode` (dynamic: a webhook-bearing Decision is `external`, a plain one `local`) and `NodeAlwaysParks` (human-review's hardcoded check, generalized). MCP write-tools for Decisions (`import_decision`/`export_decision`) are a named, mechanical follow-up — read Resources (`mill://decisions`) shipped | | **Parallel Steps** | Fan out to multiple steps concurrently, then join | Graph/fan-in semantics: build. Concurrency execution: DBOS's `Queue`/`WithWorkerConcurrency` (§7) is a plausible real backing mechanism once designed, not hand-rolled goroutine management | ADR-0005 names it, deferred | @@ -1809,10 +1809,12 @@ Zapier, Raycast — chosen because they're the platforms already anchoring this design elsewhere in this doc) rather than invented from Mill's two existing entry points (hotkey, manual click). -**Built** — `KindTrigger`, five `NodeType`s, `TriggerService`, typed -`ConfigField`s, hotkey exclusivity, and payload generation are all -real code; the design reasoning below is accurate as originally -written, not a later correction. +**Built** — `KindTrigger`, six `NodeType`s (a sixth, `trigger-system-event`, +added by [ADR-0035](adr/0035-core-vs-composition-boundary.md) — see the +System/meta row below), `TriggerService`, typed `ConfigField`s, hotkey +exclusivity, and payload generation are all real code; the design +reasoning below is accurate as originally written, not a later +correction. **Grouping is by delivery mechanism, not business domain** — this is the axis that actually determines config shape and the adopt-vs-build call @@ -1842,7 +1844,7 @@ regular interval) or webhook/real-time (service pushes events instantly)" | **Incoming MCP tool call** | C | An agent/chat client invokes one of Mill's exposed tools | Adopt (Go SDK's `Server.AddReceivingMiddleware`, already `LOCKED`, §3.1) | `OPEN` as a graph Trigger kind — validated as a real, established category (not a Mill invention) by n8n shipping its own dedicated MCP Server Trigger node | | **Webhook / incoming HTTP** | C | External service POSTs an event to a Mill-owned endpoint | Not a library gap — Mill already runs an HTTP server in server-mode (Wails3 + stdlib `net/http`); the open question is purely whether Mill should run a public listener at all | `OPEN` — a scope/threat-model decision, not an adoption decision | | **App/connector-specific** (e.g. email/IMAP) | B or C | Poll or push scoped to one external service | Depends on §4 Connectors | `PARKED` until §4 resolves — not a distinct Trigger *kind*, a connector-scoped instance of Group B/C | -| **System/meta** (run failed, workflow updated) | D | Fired by Mill's own execution engine | Build, depends on §7 | `PARKED` until §7's execution engine lands — direct analog to n8n's Error Trigger / Workflow Trigger | +| **System/meta** (decision-parked, run-completed/-failed/-cancelled) | D | Fired by Mill's own execution engine | Build (`trigger-system-event`, §7's engine) | `LOCKED`, built ([ADR-0035](adr/0035-core-vs-composition-boundary.md)) — direct analog to n8n's Error Trigger / Workflow Trigger, unparked once §7 landed. Config: `event` (options, one of the four above) + `workflowScope` (empty/"all", or one specific workflow's ID via the ADR-0009 picker, `RefKind: "workflow-scope"`). Fire payload (`InitialPayload`, JSON): `{event, runId, workflowId, workflowLabel, nodeId?, timestamp}` — `nodeId` only set for `decision-parked`. **Loop rule** (n8n's Error Trigger precedent, enforced at emission): a run whose OWN root trigger is `trigger-system-event` never emits a system event of its own, of ANY kind — a chain always bottoms out after one hop. Dispatch seam: `ExecutionService` exposes `SetSystemEventSink` (an injected-function seam, mirrors `SetConnectorLookup`); `TriggerService.DispatchSystemEvent` is wired in from `main.go`, keeping the import direction one-way (`executionsvc` never imports `triggersvc`). Emission sites: `parkForApproval` (decision-parked, `executionservice_guardrail.go`), `runWorkflow` (run-completed/run-failed, the one DBOS-registered function every run kind executes through), `CancelRun` (run-cancelled). First composed consumer: the seeded "Example: Forward pending approvals" workflow (§3.7's Update) — the forward-refactor proof. | | **Callable by another workflow** | D | Fired only when a Child Workflow node (docs/adr/0010) invokes this workflow — never a real external event | Build (composition rule; execution rides on DBOS's native parent/child call, already adopted §7) | `LOCKED`, built — `trigger-callable` NodeType, no listener process (same shape as `trigger-manual`); direct analog to n8n's Execute Workflow Trigger | **Architecture conclusion: each trigger type is its own `NodeType` under @@ -2652,8 +2654,12 @@ Update has the full writeup. A menu-bar/dock *presence toggle* (hiding the dock icon entirely) stays unbuilt — a different capability than the badge — and **trigger-fire notifications remain a named future use of the now-existing mechanism**, not built yet: the same `notify.SendPlain`/ -`SendActionable` primitives this pass added would carry it, once a -concrete "fire on X" event is chosen. +`SendActionable` primitives this pass added would carry it. **Update +(ADR-0035): the concrete "fire on X" event this was blocked on now +exists** — `trigger-system-event`'s four events — but wiring an +OS-notification NodeType (rather than `NotifyPendingApproval` staying +Settings-governed kernel chrome) is still unbuilt; the forward's own +HTTP path is the first composed consumer, not this one. **Attention escalation — `LOCKED` and built (docs/goals/archive/0023- attention-escalation.md, ADR-0032's Update).** The `document.hasFocus()`-only @@ -2671,12 +2677,28 @@ write or "Open in Mill" for a guardrail park. **(3)** alert-style authorization is now actually requested (`notify.Start` previously never called `RequestNotificationAuthorization` at all); Settings documents the System Settings → Notifications → Mill → Alerts toggle. -**(4)** a cross-device forward, `composition.SendJSONWebhook` (reuses -the integration-http/decision-outcome transport tail) + -`SettingsService.ForwardPendingApproval`, default-off, POSTs -`{kind, id, description, createdAt}` to a Settings-configured -HTTPRequest, independent of the presence gate — the layer that reaches -the owner with no local Mac to notify on at all. +**(4)** a cross-device forward — **Update ([ADR-0035](adr/0035-core-vs-composition-boundary.md), +2026-08-12): moved from a Settings toggle + private send path +(`SettingsService.ForwardPendingApproval`, `composition.SendJSONWebhook` +— both deleted) to composition**, the forward-refactor's own proof: a +seeded, DISABLED-by-default workflow, "Example: Forward pending +approvals" (`trigger-system-event(decision-parked)` → +`integration-http` against the same seeded no-auth HTTPRequest +"Example: Approval-gated HTTP call" already uses, re-pointed by the +user at their real endpoint). `integration-http`'s body resolution +(`integration.go`) now falls back to `ctx.Payload` when neither the +node nor the integration configures one, so the trigger's own JSON +event becomes the POST body with zero templating needed. Same fail-safe +default as any other external-effect step (SPEC §8): parks awaiting +approval until the user adds a Configure > Guardrails allow rule +scoped to this one node, exactly like the guarded-HTTP example already +demonstrates. A migration note logs once at startup if a pre-refactor +`settings-forward-approvals-enabled` key is present, naming the +replacement — config is never silently dropped. The OS-notification +half (`NotifyPendingApproval`) is unchanged, staying a Settings- +governed kernel default (the away-user attention layer, §9.5's +protected-kernel list) — only the HTTP forward moved; full +notification-as-a-node is named future work, not built. **Staleness presentation — `LOCKED` and built (docs/goals/archive/0026-request-lifecycle-honesty.md), applying the §1 @@ -3944,6 +3966,27 @@ infrastructure. This inheritance list IS the working definition of the Accessibility re-grant tax on every reinstall); Configure- entity draft/live lifecycle (workflows have it; entities don't). +**Update ([ADR-0035](adr/0035-core-vs-composition-boundary.md), +2026-08-12): the core/composition BOUNDARY, sharpened.** This +section's extension contract said what a new capability brings/ +inherits; ADR-0035 adds the decision test that determines whether a +capability should even reach for that contract vs. a true kernel +change: **is this a node, a trigger, a connector — or a true kernel +change?** If a user could plausibly say "I want that, but to a +different channel / with a condition / on a different event," it's +composition-shaped and MUST arrive as composition, never a bespoke +service path plus a Settings toggle. Recorded counterexample: cross- +device notification shipped hours after this section was first +written, AS a Settings toggle + private send path +(`ForwardPendingApproval`) — caught live, refactored into the seeded +"Example: Forward pending approvals" workflow (§3.7's Update). The +kernel list above stays the protected-kernel definition; ADR-0035's +second contract is new: platform-internal behavior MAY and SHOULD +consume the same composition surface as built-in, seeded, editable +workflows (the app dogfooding its own platform) — what the platform +never does is hand-roll a parallel mini-pipeline for something the +surface can already express. + ## 10. Open questions log - Decision as a reusable typed terminal outcome (§3.3/§3.5) — diff --git a/docs/adr/0035-core-vs-composition-boundary.md b/docs/adr/0035-core-vs-composition-boundary.md new file mode 100644 index 00000000..147360be --- /dev/null +++ b/docs/adr/0035-core-vs-composition-boundary.md @@ -0,0 +1,82 @@ +# ADR-0035 — The core/composition boundary: capabilities arrive as composition + +Status: accepted (owner-mandated 2026-08-11 — "worth aligning on the +principle more explicitly"; evidence base: the goal-0027 audit, same +night). + +## Context + +Hours after SPEC §9.5 wrote down the kernel/extension contract, we +violated it: cross-device notification shipped as a Settings toggle +wired to a private send path (`ForwardPendingApproval`) instead of a +connector + trigger composition — n8n's communication *node* built as +a preference checkbox. The owner caught it from the UX alone ("this is +incorrect way of working... it should have been a connector"). The +audit then found exactly one sibling (the OS-notification delivery +half of `NotifyPendingApproval`) and confirmed everything else +currently wired is legitimate kernel chrome — the boundary is +recoverable cheaply now, and expensive later. + +## Decision + +**1. The decision test, applied before building ANY capability:** +*is this a node, a trigger, a connector — or a true kernel change?* +If a user could plausibly say "I want that, but to a different +channel / with a condition / on a different event," it is +composition-shaped and MUST arrive as composition: a self-registered +NodeType, a trigger event, or a Configure entity — never a bespoke +service path plus a Settings toggle. Settings toggles configure the +kernel; they never implement side effects. + +**2. The protected kernel (changes require an ADR):** the graph +engine + validation; the guardrail gate + effect classes; durable +execution (ADR-0004/0008/0021/0026 boundaries); the registries and +injected-lookup seams (ADR-0006/0009); the Configure-entity recipe; +the MCP plane (ADR-0025/0032); the attention *presence* logic +(isAway/idle — session state, not deliverable); app chrome (badges, +panels, navigation, Activity/data-changed refetch signals). §9.5's +list, now with an explicit stability bar. Everything else is +composition space. + +**3. The two contracts (owner's own framing):** platform-internal +behavior MAY and SHOULD consume the same composition surface — as +built-in, seeded, fully-editable workflows (inspectable, guarded, in +Runs) — the app dogfooding its own platform. What the platform never +does is hand-roll a parallel mini-pipeline for something the surface +can express. Connector/credential reuse follows automatically: one +Configure entity, 1:many, exactly the existing model. + +**4. System-event triggers unparked** (§3.4's parked row; its stated +blocker — §7's engine — landed long ago): a `trigger-system-event` +family. Emission points confirmed by audit: decision-parked/resolved +already emit (`executionservice_guardrail.go:227,239,245,250` — as +refetch signals to be upgraded to typed events); +run-completed/failed/cancelled DO NOT exist yet (the completion +paths in `executionservice.go:361`, `triggerservice.go:227-233`, +`executionservice_cancel.go` return/log without emitting) — real +plumbing, routed through the single execution path. **Loop rule, +enforced at the emission site**: a run whose own trigger is a +system-event never emits system events for itself (n8n's Error +Trigger one-level precedent) — built in from day one, not discovered. + +**5. The forward refactor is the proof**: `ForwardPendingApproval`'s +private path is REPLACED by a seeded, editable workflow +(decision-parked → integration-http via a user-picked connector); +the OS-notification delivery becomes the same event's second +composed consumer. The Settings section shrinks to kernel config +(the idle threshold) plus, at most, a shortcut that opens/authors +the workflow. Acceptance: the next channel (Discord/Telegram/ +Twilio) is connector config with zero new Go. + +## Consequences + +- Feature cost collapses toward "an adapter or a connector entry" — + the owner's stated bar ("not taking days to build when it is just + adapters/plugin"). +- The decision test enters CLAUDE.md/rules as a standing check with + the forward toggle as the recorded counterexample. +- Known hazards carried into the build: silent mount-effect RPC + failures in Settings get visible error states (the audit's + robustness finding); the DEV·live badge's Go-liveness blind spot + (task #6) rises in priority after claiming its second scalp + (the owner's "untogglable" report was a 15-commit-stale binary). diff --git a/docs/goals/0028-public-repo-hygiene.md b/docs/goals/0028-public-repo-hygiene.md new file mode 100644 index 00000000..c78842d4 --- /dev/null +++ b/docs/goals/0028-public-repo-hygiene.md @@ -0,0 +1,50 @@ +# 0028 — Public-repo hygiene: the converged-standard baseline + +## Goal +Owner-mandated ("configure rules for anything missing to keep the +codebase tight since we are now on public repo"). Research delivered +2026-08-12 (community-profile score 28%; exposure sweep CLEAN; LICENSE +already correct — Apache-2.0 with reasoning recorded): close the real +gaps, skip the ceremony. + +## Plan (each item cites its standard; the research report is the brief) +1. [ ] README rewrite replacing the Wails scaffold: what Mill is + (SPEC §1's positioning), honest pre-1.0/UX-PROTOTYPE status, git + clone + task setup:hooks + task dev install, pointers to + SPEC/CLAUDE.md (never duplicating them), keep the CI badge. + No screenshots of surfaces SPEC itself tags as PROTOTYPE. +2. [ ] SECURITY.md: GitHub private-vulnerability-reporting as the + channel (enable in repo settings — no email PII); an honest + scope paragraph (guardrailed command execution, keychain secrets, + loopback unauthenticated MCP listener); pre-1.0 rolling-main + support note. +3. [ ] Minimal CONTRIBUTING.md (solo-maintained; CLAUDE.md is the + process; task setup:hooks mirrors CI; issue-before-large-PR). + One minimal bug-report issue template (reuses the build-identity + badge value as the version field — SPEC §3.8's own signal). +4. [ ] OpenSSF Scorecard workflow (official template, scheduled, + README badge) — the repo's ADR-0034 posture should score well + immediately; zero ongoing maintenance. +5. [ ] golangci-lint strengthening, two passes: first + gosec/bodyclose/noctx/revive/unparam (security + HTTP-client + hygiene matching what the code actually does) + full triage; + second pass gocritic/prealloc/contextcheck/sqlclosecheck once + clean. NEVER the `all` preset (ceremony linters fight house + conventions). +6. [ ] dependency-review-action gains deny-licenses (GPL/AGPL + variants) — one line in ci.yml. +7. [ ] The elkjs EPL-2.0 verdict note (SPEC flags it, never + resolves): dynamic-import-as-separate-chunk under EPL-2.0, a + short recorded paragraph. +8. [ ] Cosmetic: de-literal the two /Users/ali paths + (.claude/agents/test-investigator.md, launchatlogin test). + +## Skip list (recorded so nobody re-litigates) +CODE_OF_CONDUCT (until a second contributor exists), PR templates, +FUNDING.yml, go-licenses as standing CI, golangci `all` preset — +each with reasons in the research report. + +## Acceptance +Community profile score jumps; a security researcher knows where to +report and what's in scope; gosec-first-pass clean; a GPL dependency +cannot enter via PR; README describes Mill truthfully. diff --git a/docs/goals/0029-dev-liveness-honesty.md b/docs/goals/0029-dev-liveness-honesty.md new file mode 100644 index 00000000..f4f2ffb8 --- /dev/null +++ b/docs/goals/0029-dev-liveness-honesty.md @@ -0,0 +1,32 @@ +# 0029 — Dev-liveness honesty: the DEV·live badge must not vouch for a dead watcher + +## Goal +The green DEV·live badge claimed liveness twice while the Go binary +was stale (once wedged by disk-full, once 15 commits behind — the +owner's "can't toggle" Settings report was this, not a bug). Goal +0019's "DEV wins unconditionally" traded away Go-rebuild honesty for +no-false-alarms; both scalps prove the trade needs a third state. + +## Plan +1. [ ] Research-first (small): the cheapest honest Go-liveness signal + in dev — candidates: the binary self-reports its build time via + GetBuildInfo (exists) and the frontend compares against the + NEWEST mtime of internal/**/*.go at bundle-serve time (vite can + compute at request time in dev middleware); or task dev writes a + heartbeat file the binary's own watcher-restart updates. Must NOT + false-alarm on docs-only commits (goal 0019's original trap) — + compare against Go-source state only, never git HEAD. +2. [ ] A third badge state: green DEV·live (both live), amber + DEV·go-stale ("Go changes not yet in this binary — the watcher + may be wedged; restart task dev"), red STALE unchanged. The amber + text names the remedy. +3. [ ] Dev-loop guards from tonight's incidents: the task dev + start-sweep also clears the orphaned vite port (lsof -ti :9245); + a pre-build disk-space check (< 2GB free → loud warning naming + `go clean -cache`, the recurring silent killer). +4. [ ] E2e where testable; manual-only registry for the rest. + +## Acceptance +A wedged watcher shows amber within one poll interval while docs-only +commits stay green; the owner never again debugs a working feature +against a stale binary. diff --git a/docs/goals/0030-node-standard.md b/docs/goals/0030-node-standard.md new file mode 100644 index 00000000..616d0092 --- /dev/null +++ b/docs/goals/0030-node-standard.md @@ -0,0 +1,48 @@ +# 0030 — Node standard: minimum requirements every NodeType meets + +## Goal +Owner-mandated 2026-08-12: "define what is a standard for all plugins +going forward including minimum requirement schemas... when I looked +into some nodes it hasn't been reviewed against industry standards." +Adopt (never invent) a node/plugin conformance standard — the checklist +every existing and future NodeType is reviewed against — modeled on the +published standards real platforms enforce (n8n's community-node +verification guidelines are the named precedent to research first). + +## Plan +1. [x] Research DONE 2026-08-12 (primary sources: n8n verification/UX/ + error-handling guidelines, Zapier publishing requirements, Raycast + store checklist — full report in session; key verdicts below). + Converged 8-item checklist mapped onto Mill: items 1/4/5/6/8 already + enforced (TestNodeTypes, keyring credentials, seed-per-capability — + stricter than all three platforms; Effect = a machine-enforced + version of n8n's no-unscoped-access rule). NEW machine-checkable + items: (a) ConfigField.Description non-empty; (b) **Effect must be + explicitly set — the zero value silently becomes ClassNone = NO + GUARDRAIL GATE, the one dangerous gap** (priority); (c) every + nodeExec error prefixed with its NodeType ID (already convention, + un-checked); (d) ID prefixed by Kind's prefix (allow-list for + pre-pattern IDs); (e) Output non-empty for non-terminal kinds. + Rejected with reasons: n8n publishing/license ceremony, CRUD + completeness, Zapier marketing copy, Raycast store assets, and + Raycast's no-keychain rule (contradicts Mill's deliberate go-keyring + design). NodeType-level versioning named as real-but-latent — not + built speculatively. +2. [ ] Write the Mill Node Standard (a rules/ file or docs/ page + + ADR): minimum per-NodeType requirements — typed ConfigFields with + descriptions/defaults, declared effect class, Output description, + payload contract documented, error semantics (fail-safe, named + errors with remedies), seeded proof at the right layer, SPEC row, + naming conventions (Kind prefixes, label style), Inspector UX bar + (no raw-JSON-only config where typed fields are expressible). +3. [ ] Conformance audit: every existing NodeType reviewed against the + standard; gaps become checklist items fixed in the same wave or + recorded as explicit debt entries (delivery-discipline rule). +4. [ ] Enforcement where mechanical: extend seedproof-style checks if + any standard item is machine-checkable (e.g. every NodeType has a + nonempty Description + effect class — a Go test over the registry). + +## Acceptance +A written standard citing its precedents; every current NodeType either +conforms or has a named debt entry; a new node's DoR includes the +standard; at least one machine-check enforces the checkable subset. diff --git a/docs/goals/0031-ai-node-family.md b/docs/goals/0031-ai-node-family.md new file mode 100644 index 00000000..d0348c2d --- /dev/null +++ b/docs/goals/0031-ai-node-family.md @@ -0,0 +1,66 @@ +# 0031 — AI node family: guardrailed AI steps, designed from the converged taxonomy + +## Goal +Owner-engaged 2026-08-12 on the "what's missing vs top apps" answer: +the AI node is the category-defining capability (n8n's growth came +from AI-workflow positioning; Mill's unique differentiator is the +guardrail between AI output and real action, already built). Owner's +own requirement: "when we say AI node in n8n and many no code +platforms they have different ways and they are all useful to have +even in a single platform" — so this is a node FAMILY designed from +the converged taxonomy, not one blob node. §1.1's invariant is locked: +user-configured endpoint (local Ollama / BYO key), one deterministic +call per step, never an agent loop inside Mill. + +## Plan +1. [x] Research DONE 2026-08-12 (primary sources: n8n cluster-node/ + Ollama-credential docs, Ollama native+OpenAI-compat APIs, Anthropic + Messages API + its compat shim, Zapier/Make/Dify AI nodes). Verdicts: + - **Shapes**: completion + extract-structured are the unanimous + 3-platform convergences → build ai-completion + + ai-extract-structured first. Summarize = a completion preset, not + a shape. Embed = converged but always RAG infrastructure — DEFERRED + until a vector-store exists (point-solution trap otherwise). + Vision = input variant, not a node. Agent-loop = out by §1.1's + locked invariant. ai-classify = OPEN design choice (Dify has a + dedicated node; Mill could compose extract-structured + Branch). + - **Config sharing**: n8n's Chat-Model-sub-node/credential pattern + translated to Mill's idiom = AIProvider Configure entity + {Kind: "openai-compatible"|"anthropic", BaseURL, Model, + AuthSecret(keyring)} — 1:many, RefKind picker. n8n's typed-edge + cluster mechanism itself rejected (doesn't fit Mill's graph model). + - **Transport**: exactly TWO adapters — openaicompat (covers Ollama's + /v1 + LM Studio/vLLM/any BYO endpoint, zero per-provider code) + + anthropic native Messages (Anthropic's own docs disqualify their + compat shim for production). Dedicated adapters, NOT routed + through Connector (AuthType has no chat/schema concept — the + templating anti-pattern §3.3 already rejected). + - **Effect class — NEEDS OWNER RATIFICATION, not silently resolved**: + recommendation = static ClassExternal (consistent with + integration-http/mcp-tool-call's local-subprocess precedent), + with the existing EffectForNode dynamic-override downgrading to + ClassLocal only for loopback (localhost/127.0.0.1/::1) BaseURLs — + keeps local Ollama frictionless per §1's not-harder-than-baseline + invariant while remote/BYO-key asks by default. Two in-repo + precedents pull opposite ways; this is a product/security taste + call → morning handoff item. +2. [ ] Capability map + ADR: which family members Mill builds now vs + later; the AI-provider Configure entity (1:many, stamped recipe — + endpoint, model, BYO key in keychain); how the guardrail treats AI + steps (effect class; an AI call is external egress unless + localhost — verdict needed); structured-output handling into + Attributes (the typed-payload story). +3. [ ] Build the first members against goal 0030's node standard + (conformance from birth): likely `ai-completion` + + `ai-extract-structured` (writes typed Attributes — the one that + composes with Decision routing for real decisioning workflows). +4. [ ] Seeded proof: an example workflow using a local Ollama + endpoint (documented as requiring user setup, seeded DISABLED like + the fs-watch example) + e2e against a fixture HTTP endpoint + (deterministic, no real model in CI). + +## Acceptance +A user with Ollama running composes "capture → AI step → guarded +action" with zero Mill code changes beyond config; the AI provider +entity is reusable across workflows; every family member passes the +0030 standard; CI proves the path with a fixture endpoint. diff --git a/docs/goals/BACKLOG.md b/docs/goals/BACKLOG.md index 0e8007bf..0d6d5bc6 100644 --- a/docs/goals/BACKLOG.md +++ b/docs/goals/BACKLOG.md @@ -57,6 +57,30 @@ this pipeline and on this code)** 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. +5. [x] [0027 — Core vs composition boundary](archive/0027-core-vs-composition-boundary.md) + — DELIVERED 2026-08-12: ADR-0035's build half — `trigger-system-event` + unparked (four events, loop-rule enforced at emission), the forward + refactored from a Settings toggle + private send path into a seeded, + editable "Example: Forward pending approvals" workflow; the decision + test written into `.claude/rules/architecture.md`; SettingsView's + silent-mount-fetch class fixed alongside. +6. [ ] [0028 — Public-repo hygiene](0028-public-repo-hygiene.md) — + research delivered 2026-08-12 (community-profile score 28%, + exposure sweep clean, LICENSE already correct); the close-the-gaps + build (README/SECURITY/CONTRIBUTING/Scorecard/lint hardening) not + started. +7. [ ] [0029 — Dev-liveness honesty](0029-dev-liveness-honesty.md) — + the DEV·live badge's Go-liveness blind spot, now having claimed a + second scalp (ADR-0035's Consequences note); a third badge state + (amber DEV·go-stale) not yet built. +8. [ ] [0030 — Node standard](0030-node-standard.md) — owner-mandated + 2026-08-12: a written, precedent-researched (n8n community-node + review) conformance standard every NodeType is checked against; + not started. +9. [ ] [0031 — AI node family](0031-ai-node-family.md) — owner-engaged + 2026-08-12: the guardrailed AI-node family (n8n/Make/Zapier/ + Dify taxonomy convergence), Mill's category-defining capability; + research not started. **Ratified 2026-08-10 (owner): three groups, A→B→C. 0001 stays standing live-review material, interleaved during owner reviews, not a lane.** diff --git a/docs/goals/archive/0027-core-vs-composition-boundary.md b/docs/goals/archive/0027-core-vs-composition-boundary.md new file mode 100644 index 00000000..3954e439 --- /dev/null +++ b/docs/goals/archive/0027-core-vs-composition-boundary.md @@ -0,0 +1,82 @@ +# 0027 — Core vs composition: the boundary that keeps features cheap + +## Goal +Owner-mandated 2026-08-11 ("very critical... worth aligning on the +principle more explicitly"): a hard, written boundary between the +protected platform kernel (stable, changes rarely) and everything +else (which MUST arrive as composition — nodes, triggers, connectors — +never bespoke wiring), so a future capability is an adapter/plugin +afternoon, not a days-long build. Triggered by a live violation WE +shipped hours after writing §9.5's kernel doc: cross-device +notification built as a Settings toggle + private ForwardPendingApproval +code path instead of a communication connector + a composable trigger +(n8n's model, the owner's own framing). + +## Plan +1. [x] **Audit** (read-only, first): inventory every current + bespoke-wired capability that is composition-shaped — the forward + toggle (exhibit A), trigger-fire notifications (§3.7's open item), + the in-app attention wiring, anything else where "the app does X on + event Y" bypasses the workflow engine. Also: the broken Settings UX + found live (Forward checkbox untogglable, Away-after stepper renders + empty) — diagnose as part of understanding the surface. DONE: + ADR-0035's Context/Decision recorded the audit findings — + `ForwardPendingApproval` + `NotifyPendingApproval`'s delivery half + were the only two violations; the "untogglable" report was a + 15-commit-stale binary (goal 0029 tracks the badge fix), not a real + bug — but the underlying silent-mount-fetch-failure class was real + (item 4 below). +2. [x] **Unpark §3.4's System/meta trigger row**: `trigger-system-event` + NodeType (`internal/domain/composition/triggers.go`), four events + (decision-parked/run-completed/run-failed/run-cancelled), fired + through the same single execution path (ADR-0008). Dispatch seam: + `ExecutionService.SetSystemEventSink` / `TriggerService. + DispatchSystemEvent` (`executionservice_systemevent.go`, + `triggersystemevent.go`), wired from `main.go`. Loop rule enforced + at emission: a system-event-triggered run emits no system events of + its own (n8n's Error Trigger precedent, one hop max). +3. [x] **Refactor the forward as composition**: `ForwardPendingApproval` + + `composition.SendJSONWebhook` deleted; seeded DISABLED workflow + "Example: Forward pending approvals" (trigger-system-event + (decision-parked) → integration-http, reusing the SAME seeded + HTTPRequest "Example: Approval-gated HTTP call" already references — + the 1:many Configure-entity reuse proven directly). A startup + migration note logs once if the old Settings key was present, never + silently dropping config. +4. [x] **The two contracts, written into SPEC**: `docs/SPEC.md` §9.5's + Update + §3.7's Update record both — platform-internal behavior may + consume the composition surface via seeded workflows; the protected + kernel list (already in §9.5) now carries the explicit "changes + require an ADR" bar (ADR-0035 itself). +5. [x] **The decision test, one paragraph in `.claude/rules/ + architecture.md`**: "is this a node, a trigger, a connector, or a + true kernel change?" with the forward toggle as the recorded + counterexample. + +Also delivered, riding this PR (the audit's own robustness finding, +scoped in alongside): `SettingsView.tsx`'s silently-caught mount +fetches (`GetSummonHotkey`/`GetMCPWriteEnabled`/ +`GetMCPWriteApprovalRequired`/`GetAttentionIdleThreshold`) now surface +a visible "Couldn't load — the app may need a restart" banner instead +of a permanently-disabled control with no explanation. + +## Acceptance +The forward works as a visible, editable workflow using a real +connector — MET (the seeded workflow, disabled by default, re-pointed +by the user). A second workflow can reuse that connector — MET (the +guarded-HTTP example and the forward example share one HTTPRequest). +The system-event trigger family exists with seeded proof — MET +(`triggersvc.TestSeededForwardApprovalsExample_DecisionParked_ +PostsRealHTTPCall`, the loop-rule test, the run-completed-both-RunKinds +test, `e2e: seed-completeness.spec.ts`). The kernel list + decision +test are written — MET. The broken Settings controls are fixed or +gone — MET (Forward section deleted; the audited silent-fetch class +fixed for the remaining controls). The next communication channel +(Discord/Telegram/Twilio) is buildable as connector-config only, zero +new Go paths — MET by construction: point a new HTTPRequest at the +target service and re-point the seeded workflow's Integration field. + +**DELIVERED 2026-08-12** (this session/PR) — goal 0029 (DEV·live +badge honesty) and goal 0030 (node standard, which +`trigger-system-event` should be reviewed against once written) remain +separately queued, not blocking this goal's own closure. diff --git a/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/settingsservice.ts b/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/settingsservice.ts index 97195e51..af14b99d 100644 --- a/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/settingsservice.ts +++ b/frontend/bindings/github.com/alicoding/mill/internal/services/settingssvc/settingsservice.ts @@ -99,20 +99,6 @@ export function DismissPanel(): $CancellablePromise { return $Call.ByID(3877156882); } -/** - * ForwardPendingApproval fires the configured cross-device forward for - * one new pending item -- fire-and-forget: it returns immediately, and - * a delivery failure is only slog-logged, never surfaced back to the - * caller or allowed to block anything. App.tsx's own per-new-item loop - * calls this alongside NotifyPendingApproval, unconditionally -- - * unlike the presence gate, forwarding doesn't depend on local - * focus/idle at all, since the whole point is reaching the owner when - * there may be no local Mac attention to gate on in the first place. - */ -export function ForwardPendingApproval(id: string, description: string, kind: string): $CancellablePromise { - return $Call.ByID(4270514639, id, description, kind); -} - /** * GetAttentionIdleThreshold returns the configured idle-seconds * threshold: the presence gate below treats the user as away once @@ -134,21 +120,6 @@ export function GetBuildInfo(): $CancellablePromise<$models.BuildInfo> { return $Call.ByID(2673585232); } -/** - * GetForwardApprovalsEnabled reports whether the forward is armed. - */ -export function GetForwardApprovalsEnabled(): $CancellablePromise { - return $Call.ByID(3422319504); -} - -/** - * GetForwardApprovalsRequestID returns the configured HTTPRequest's ID - * (empty means unconfigured). - */ -export function GetForwardApprovalsRequestID(): $CancellablePromise { - return $Call.ByID(1205123449); -} - /** * GetLaunchAtLogin queries the real OS state (System Events' login * items list) rather than a persisted preference -- authoritative even @@ -338,24 +309,6 @@ export function SetAttentionIdleThreshold(seconds: number): $CancellablePromise< return $Call.ByID(454955395, seconds); } -/** - * SetForwardApprovalsEnabled persists the toggle, returning the persist - * error (same reasoning as every other settings toggle here: a save - * that silently didn't take effect leaves the user believing the - * forward is armed when it isn't, or vice versa). - */ -export function SetForwardApprovalsEnabled(enabled: boolean): $CancellablePromise { - return $Call.ByID(1952175932, enabled); -} - -/** - * SetForwardApprovalsRequestID persists which Configure-authored - * HTTPRequest to forward pending-approval events through. - */ -export function SetForwardApprovalsRequestID(id: string): $CancellablePromise { - return $Call.ByID(60838469, id); -} - /** * SetKeybinding overrides commandID's binding to mods+key, rejecting a * combo already claimed by another command's override, or by a diff --git a/frontend/e2e/composition.spec.ts b/frontend/e2e/composition.spec.ts index f6500c0a..a5611abf 100644 --- a/frontend/e2e/composition.spec.ts +++ b/frontend/e2e/composition.spec.ts @@ -192,8 +192,9 @@ test('Composition page lists built-in workflows; node primitives live in a colla // process-extract-html, capture-clipboard-info (the save-page // capture floor + clipboard inspector, docs/adr/0030 / SPEC.md §5) + // list-search (docs/goals/0011-lists-maturation.md's richer, typed - // successor to list-lookup). - await expect(activePanel(page).getByTestId('palette-item')).toHaveCount(25) + // successor to list-lookup) + trigger-system-event (docs/adr/0035's + // unparked System/meta trigger). + await expect(activePanel(page).getByTestId('palette-item')).toHaveCount(26) }) test('A new workflow starts with a starter node placed, not a blank canvas', async ({ page }) => { diff --git a/frontend/e2e/seed-completeness.spec.ts b/frontend/e2e/seed-completeness.spec.ts index 603935b3..6c68eab3 100644 --- a/frontend/e2e/seed-completeness.spec.ts +++ b/frontend/e2e/seed-completeness.spec.ts @@ -157,3 +157,24 @@ test('Example: Saved page to Markdown workflow is present and ships disabled', a await expect(row).toBeVisible() await expect(row.getByText('disabled', { exact: true })).toBeVisible() }) + +// docs/adr/0035: the composed replacement for ForwardPendingApproval's +// deleted private send path -- real execution semantics (the decision- +// parked emission, the loop rule, run-completed firing for both RunKinds) +// are proven at the Go layer (triggersvc's systemevent_seed_test.go); +// this confirms the seed is actually reachable through the live app and +// shows its real trigger-system-event label, same presence-only bar +// every other real-event-driven seed above already sets. +test('Example: Forward pending approvals workflow is present, disabled, with the real trigger-system-event node on canvas', async ({ page }) => { + await page.goto('/') + await page.getByRole('link', { name: 'Workflows' }).click() + + const row = workflowRow(page, 'Example: Forward pending approvals') + await expect(row).toBeVisible() + await expect(row.getByText('disabled', { exact: true })).toBeVisible() + await row.click() + + const nodes = activePanel(page).locator('.react-flow__node') + await expect(nodes).toHaveCount(2) + await expect(nodes.filter({ hasText: 'Trigger: system event' })).toBeVisible() +}) diff --git a/frontend/e2e/settings.spec.ts b/frontend/e2e/settings.spec.ts index 1aea5c00..68787650 100644 --- a/frontend/e2e/settings.spec.ts +++ b/frontend/e2e/settings.spec.ts @@ -18,6 +18,20 @@ test('Settings page shows Launch at login and Global hotkey sections', async ({ await expect(page.getByText('Global hotkey')).toBeVisible() }) +// docs/adr/0035: the forward-refactor proof's Settings half -- +// ForwardPendingApproval's private send path and its own Settings +// section (checkbox + request picker) are deleted, replaced by the +// seeded "Example: Forward pending approvals" workflow (proven in +// seed-completeness.spec.ts). This is the negative half: the old +// section must actually be GONE, not just unused. +test('Settings no longer shows the Forward pending approvals section', async ({ page }) => { + await page.goto('/') + await page.getByRole('button', { name: 'Settings' }).click() + await expect(page.getByTestId('settings-view')).toBeVisible() + await expect(page.getByText('Forward pending approvals')).toHaveCount(0) + await expect(page.getByTestId('forward-approvals-enabled-checkbox')).toHaveCount(0) +}) + test('Launch at login checkbox reflects the real server-mode error', async ({ page }) => { await page.goto('/') await page.getByRole('button', { name: 'Settings' }).click() diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 90c6dd09..bbcc2885 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -330,10 +330,10 @@ function App() { // "present" (focused AND recently-active) or "away" (idle past the // configured threshold, or unfocused), fixing the previously-observed // focused-but-idle suppression bug a frontend-only gate couldn't see. - // ForwardPendingApproval (item 4, the cross-device forward) fires - // unconditionally alongside it -- forwarding doesn't depend on local - // focus/idle at all, since its whole point is reaching the owner when - // there may be no local Mac attention to gate on in the first place. + // The cross-device forward moved to composition (docs/adr/0035): a + // decision-parked system event now reaches the seeded "Example: Forward + // pending approvals" workflow through TriggerService, not a call from + // here -- ForwardPendingApproval's own private send path is deleted. const [reviewPendingCount, setReviewPendingCount] = useState(0); const notifiedIds = useRef>(new Set()); useEffect(() => { @@ -358,7 +358,6 @@ function App() { for (const item of items) { if (notifiedIds.current.has(item.key)) continue; notifiedIds.current.add(item.key); - void SettingsService.ForwardPendingApproval(item.id, item.description, item.kind).catch(() => {}); void SettingsService.NotifyPendingApproval(item.id, item.description, item.kind, document.hasFocus()).catch(() => {}); } }); diff --git a/frontend/src/configure/EntityRefField.tsx b/frontend/src/configure/EntityRefField.tsx index 86ed1b55..e55ee33f 100644 --- a/frontend/src/configure/EntityRefField.tsx +++ b/frontend/src/configure/EntityRefField.tsx @@ -41,6 +41,12 @@ async function fetchEntities(refKind: string): Promise { return (await ConfigureService.MCPServers()) ?? [] case 'workflow': return ((await CompositionService.Workflows()) ?? []).filter(isCallableWorkflow) + // docs/adr/0035: trigger-system-event's workflowScope picker -- every + // workflow is a valid scope target (not just callable ones, unlike + // 'workflow' above), since any workflow can be the SOURCE of a + // decision-parked/run-completed/run-failed/run-cancelled event. + case 'workflow-scope': + return (await CompositionService.Workflows()) ?? [] case 'decision': return ((await ConfigureService.Decisions()) ?? []).map((d) => ({ ID: d.ID, Label: `${d.Label} (${d.Category})` })) case 'execenv': @@ -55,6 +61,7 @@ const KIND_NOUN: Record = { list: 'list', mcpserver: 'MCP server', workflow: 'callable workflow', + 'workflow-scope': 'workflow', decision: 'decision', execenv: 'execution environment', } @@ -96,7 +103,13 @@ export function EntityRefField({ refKind, value, onChange }: { refKind: string; onChange={(e) => handleSelect(e.target.value)} > - {entities === null ? 'Loading…' : value ? `Unknown ${KIND_NOUN[refKind]} (${value})` : `Select a ${KIND_NOUN[refKind]}…`} + {entities === null + ? 'Loading…' + : value + ? `Unknown ${KIND_NOUN[refKind]} (${value})` + : refKind === 'workflow-scope' + ? 'All workflows' + : `Select a ${KIND_NOUN[refKind]}…`} {(entities ?? []).map((entity) => ( {entity.Label} diff --git a/frontend/src/views/SettingsView.tsx b/frontend/src/views/SettingsView.tsx index f0b6c094..752c95e7 100644 --- a/frontend/src/views/SettingsView.tsx +++ b/frontend/src/views/SettingsView.tsx @@ -5,7 +5,6 @@ import { SunIcon, MoonIcon, DeviceDesktopIcon, KeyIcon } from '@primer/octicons- import { SettingsService } from '../shared/bindings' import { describeCombo, keyFromEventCode, modsFromEvent, reservedByMacOS } from '../shared/keybinding' import { isAccessibilityError, ACCESSIBILITY_SETTINGS_URL } from '../composition/hotkeyCapture' -import { EntityRefField } from '../configure/EntityRefField' import KeyboardShortcutsSection from './KeyboardShortcutsSection' import styles from '../shared/ListCard.module.css' import PageContainer from '../shared/PageContainer' @@ -49,12 +48,21 @@ function SettingsView() { const [mcpWriteEnabled, setMCPWriteEnabledState] = useState(null) const [mcpApprovalRequired, setMCPApprovalRequiredState] = useState(null) - // Attention/notifications (docs/goals/0023-attention-escalation.md - // items 2/3/4): the idle-aware presence-gate threshold, and the - // cross-device forward's own enable toggle + configured HTTPRequest. + // Attention/notifications (docs/goals/0023-attention-escalation.md item + // 2): the idle-aware presence-gate threshold. The cross-device forward's + // own toggle moved to composition (docs/adr/0035) -- see the seeded + // "Example: Forward pending approvals" workflow instead. const [idleThreshold, setIdleThresholdState] = useState(null) - const [forwardEnabled, setForwardEnabledState] = useState(null) - const [forwardRequestID, setForwardRequestIDState] = useState('') + + // docs/adr/0035's audit finding: these mount fetches used to fail + // silently (console.error only), leaving their controls disabled + // forever with no visible explanation -- the "untogglable checkbox"/ + // "empty stepper" bugs reported live were a stale binary, but a real + // fetch failure (a genuinely broken RPC) would have looked identical. + // One shared banner rather than a per-field message: these three all + // fail for the same reason (the backend didn't answer), and a build + // this small doesn't need per-control diagnosis. + const [settingsLoadError, setSettingsLoadError] = useState(false) useEffect(() => { SettingsService.GetLaunchAtLogin() @@ -62,22 +70,16 @@ function SettingsView() { .catch((err) => setLaunchAtLoginError(String(err))) SettingsService.GetSummonHotkey() .then((label) => setSummonBinding(label || null)) - .catch(console.error) + .catch((err) => { console.error(err); setSettingsLoadError(true) }) SettingsService.GetMCPWriteEnabled() .then(setMCPWriteEnabledState) - .catch(console.error) + .catch((err) => { console.error(err); setSettingsLoadError(true) }) SettingsService.GetMCPWriteApprovalRequired() .then(setMCPApprovalRequiredState) - .catch(console.error) + .catch((err) => { console.error(err); setSettingsLoadError(true) }) SettingsService.GetAttentionIdleThreshold() .then(setIdleThresholdState) - .catch(console.error) - SettingsService.GetForwardApprovalsEnabled() - .then(setForwardEnabledState) - .catch(console.error) - SettingsService.GetForwardApprovalsRequestID() - .then((id) => setForwardRequestIDState(id ?? '')) - .catch(console.error) + .catch((err) => { console.error(err); setSettingsLoadError(true) }) }, []) // Same menu-accelerator-suspension bracket as @@ -157,15 +159,6 @@ function SettingsView() { .catch(console.error) } - const toggleForwardEnabled = (enabled: boolean) => { - SettingsService.SetForwardApprovalsEnabled(enabled).then(() => setForwardEnabledState(enabled)).catch(console.error) - } - - const setForwardRequestID = (id: string) => { - setForwardRequestIDState(id) - SettingsService.SetForwardApprovalsRequestID(id).catch(console.error) - } - const checkForUpdates = () => { setUpdateChecking(true) setUpdateStatus('') @@ -184,6 +177,11 @@ function SettingsView() { App-level preferences -- not workflow or Configure-authored data (that lives in Composition/Configure), a UI preference persisted locally to this machine. + {settingsLoadError && ( + + Couldn't load some settings -- the app may need a restart. + + )} Appearance setColorMode(COLOR_MODES[i])}> @@ -320,29 +318,6 @@ function SettingsView() { permission on first launch, but macOS still defaults new apps to Banners, which auto-dismiss. - Forward pending approvals - - toggleForwardEnabled(e.target.checked)} - data-testid="forward-approvals-enabled-checkbox" - /> - Forward to another device - - Off by default (docs/goals/0023 item 4). When on, every new pending guardrail ask or MCP write fires - the integration below (e.g. ntfy/Telegram/a webhook receiver you configure) with{' '} - {'{kind, id, description, createdAt}'} as the body -- the only layer that reaches you - entirely away from this Mac. Fire-and-forget: a delivery failure never blocks the park. - - - {forwardEnabled && ( - - Integration - - - )} - Updates