Skip to content

feat: trigger-system-event + composed forward (goal 0027, ADR-0035 build half) - #13

Merged
alicoding merged 3 commits into
mainfrom
goal/0027-composed-events
Aug 12, 2026
Merged

feat: trigger-system-event + composed forward (goal 0027, ADR-0035 build half)#13
alicoding merged 3 commits into
mainfrom
goal/0027-composed-events

Conversation

@alicoding

Copy link
Copy Markdown
Owner

Summary

  • Unparks SPEC §3.4's System/meta trigger row: trigger-system-event NodeType 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 from main.go so executionsvc never imports triggersvc). 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 core/composition violation ADR-0035 was written to fix: 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-eventintegration-http, reusing the same seeded HTTPRequest the guarded-HTTP example already references). integration-http's body resolution now falls back to ctx.Payload when 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.
  • 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.md gains 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 ./... — clean
  • go test -race ./... — all packages green
  • go build (desktop) and go build -tags server (server) — both succeed
  • scripts/check-loc.sh, check-rules-frontmatter.sh, ls_lint — clean
  • Frontend: tsc --noEmit, eslint ., npm run boundaries, vitest run — all clean/green
  • Full Playwright suite (164 tests, real desktop/server-mode parity): 162 clean pass + 2 pre-existing flaky (unrelated timing tests, passed on retry)
  • New Go proofs: triggersvc.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 real SystemEvent JSON), TestSystemEvent_LoopRule_SystemEventTriggeredRunEmitsNothing, TestSystemEvent_RunCompleted_FiresForManualAndTriggeredRuns
  • New e2e: seed presence + trigger-system-event label on canvas (seed-completeness.spec.ts), Settings no longer shows the Forward section (settings.spec.ts)
  • Bindings regenerated (wails3 generate bindings -clean=true -ts -i) — only the 5 deleted SettingsService methods changed

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

https://claude.ai/code/session_01FYwojT8GdUbYSoggbvEFft

alicoding and others added 3 commits August 12, 2026 00:53
…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
alicoding enabled auto-merge August 12, 2026 05:24
@alicoding
alicoding merged commit 4d109da into main Aug 12, 2026
16 checks passed
@alicoding
alicoding deleted the goal/0027-composed-events branch August 12, 2026 05:29
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant