feat: trigger-system-event + composed forward (goal 0027, ADR-0035 build half) - #13
Merged
Conversation
…0027-0031 ADR-0035 (accepted) and the goal-0027 file were sitting untracked from last session's audit; committing them as the first act on this feature branch per the goal's own instructions, plus queue rows for 0028-0031 (also untracked goal files) so BACKLOG.md carries the true priority order per CLAUDE.md's standing rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft
…ild half) Unparks SPEC §3.4's System/meta trigger row: a trigger-system-event NodeType family (decision-parked/run-completed/run-failed/run-cancelled) fired through the single execution path (ADR-0008), dispatched via an injected seam (ExecutionService.SetSystemEventSink / TriggerService.DispatchSystemEvent, wired from main.go so executionsvc never imports triggersvc) with a loop rule enforced at emission: a system-event-triggered run never emits a system event of its own (n8n's Error Trigger precedent, one hop max). Refactors the forward-toggle violation ADR-0035 was written to fix: ForwardPendingApproval's private send path + Settings section (checkbox, request picker, four RPCs) are deleted, replaced by a seeded, DISABLED "Example: Forward pending approvals" workflow (trigger-system-event -> integration-http, reusing the same seeded HTTPRequest the guarded-HTTP example already references -- 1:many Configure-entity reuse proven directly). integration-http's body resolution now falls back to ctx.Payload when nothing else configures one, so the trigger's JSON event becomes the POST body with zero templating. A startup migration note logs once if the old Settings key was present, never silently dropping config. SettingsView's silently-caught mount-fetch failures (the audit's robustness finding) now surface a visible error banner instead of a permanently-disabled control with no explanation. The core/composition decision test is written into .claude/rules/architecture.md, with the forward toggle as the recorded counterexample; docs/SPEC.md §3.4/§3.7/§9.5 updated; goal 0027 archived delivered; BACKLOG gains rows for goals 0027-0031. Proofs: triggersvc/systemevent_seed_test.go (decision-parked fires the real seed end-to-end against a fixture HTTP server and asserts the POST body; the loop rule; run-completed for both a manual and a triggered run) + e2e (seed presence/trigger label, Settings section gone). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft
Caught by the full Playwright run: adding trigger-system-event (previous commit) makes the canvas palette 26 items, not 25 -- the seed's own presence, correctly reflected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft
alicoding
enabled auto-merge
August 12, 2026 05:24
alicoding
added a commit
that referenced
this pull request
Aug 15, 2026
…y (task #12) Mill as MCP server for the first time -- §3.1 named three possible MCP roles (server, client, host) back when only client was ever built (§3.6, wrapping external tools as workflow nodes); this closes the server role, already locked as fine ("fits §1.1 cleanly with zero tension") since no LLM/agent loop runs inside Mill here -- an external agent's own host connects and reads, the same shape as httpconnector being an HTTP client. Does not touch or reopen the still-disputed MCP-host question. internal/adapters/mcpserving wraps modelcontextprotocol/go-sdk's server role (the same SDK Mill already depends on for the client role) behind a thin New()/Serve() pair -- new role, not a new dependency. millmcpservice.go registers two URIs per entity type: a plain index resource (mill://workflows, mill://requests, mill://lists, mill://mcpservers) listing every current ID/Label/Description, and a ResourceTemplate (mill://workflows/{id}, etc.) whose read returns the full definition by calling the SAME Export* methods tasks #10/#11 just built for the UI's own Export buttons -- one read-model, reused, not a second one built for this. Secrets stay excluded by the same construction Export* already guarantees, verified independently through this new code path rather than assumed inherited: a real test connects a genuine MCP client over real HTTP, sets a real secret on a real HTTPRequest, reads it back through mill://requests/{id}, and asserts the secret never appears in the wire response. Binds 127.0.0.1:8090 by default (MILL_MCP_ADDR overrides), loopback-only deliberately -- a new unauthenticated local listener, same conservative- default reasoning as the LAN-exposure question elsewhere in this repo. Runs in both desktop and server-mode builds (no build tag); confirmed the second listener doesn't conflict with server mode's existing :8080 via a real Playwright smoke run. A bind failure is logged, not fatal. Explicitly NOT built here, named directly rather than silently dropped: the write side (create/import via MCP Tools). §8's guardrail policy is still OPEN, and programmatic writes are a materially different risk than read-only exposure -- tracked as its own task (#13), not folded into this one. Verified: two new Go tests connect a genuine MCP client (the SDK's own client role, not a mock, not a direct call into the handler functions) over real HTTP -- resources/list returns the real registered set, resources/read against a just-created real workflow returns its real node data, an unknown ID returns a real resource-not-found error, and the secret-exclusion test above. Full existing suite passes with -race. go vet, golangci-lint (0 issues after fixing two real errcheck findings on deferred session.Close() -- fixed using this repo's own existing defer func() { _ = x.Close() }() convention, not suppressed), ls_lint, check-loc.sh all clean. Documented in docs/SPEC.md §3.6. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft
alicoding
added a commit
that referenced
this pull request
Aug 15, 2026
feat: trigger-system-event + composed forward (goal 0027, ADR-0035 build half)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
trigger-system-eventNodeType family (decision-parked/run-completed/run-failed/run-cancelled), fired through the single execution path (ADR-0008) via an injected dispatch seam (ExecutionService.SetSystemEventSink/TriggerService.DispatchSystemEvent, wired frommain.gosoexecutionsvcnever importstriggersvc). Loop rule enforced at emission: a system-event-triggered run never emits a system event of its own (n8n's Error Trigger precedent, one hop max).ForwardPendingApproval's private send path + its Settings section (checkbox, request picker, 4 RPCs) are deleted, replaced by a seeded, DISABLED "Example: Forward pending approvals" workflow (trigger-system-event→integration-http, reusing the same seeded HTTPRequest the guarded-HTTP example already references).integration-http's body resolution now falls back toctx.Payloadwhen nothing else configures a body, so the trigger's JSON event becomes the POST body with zero templating. A startup migration note logs once if the old Settings key was present — config is never silently dropped.SettingsView's silently-caught mount-fetch failures (the audit's robustness finding) now surface a visible error banner instead of a permanently-disabled control with no explanation..claude/rules/architecture.md, with the forward toggle as the recorded counterexample;docs/SPEC.md§3.4/§3.7/§9.5 updated; goal 0027 archived delivered;BACKLOG.mdgains rows for goals 0027-0031 (0028-0031 were untracked goal files from a prior session, committed here too).Test plan
go vet ./...,golangci-lint run ./...— cleango test -race ./...— all packages greengo build(desktop) andgo build -tags server(server) — both succeedscripts/check-loc.sh,check-rules-frontmatter.sh,ls_lint— cleantsc --noEmit,eslint .,npm run boundaries,vitest run— all clean/greentriggersvc.TestSeededForwardApprovalsExample_DecisionParked_PostsRealHTTPCall(real DBOS run parks, fires the real seed end-to-end against a fixture HTTP server, asserts the POST body is the realSystemEventJSON),TestSystemEvent_LoopRule_SystemEventTriggeredRunEmitsNothing,TestSystemEvent_RunCompleted_FiresForManualAndTriggeredRunsseed-completeness.spec.ts), Settings no longer shows the Forward section (settings.spec.ts)wails3 generate bindings -clean=true -ts -i) — only the 5 deletedSettingsServicemethods changedCo-Authored-By: Claude Fable 5 noreply@anthropic.com
https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft