Skip to content

feat: add manual human acceptance gate#44

Merged
rhoninl merged 1 commit into
mainfrom
feat/human-acceptance-gate
May 25, 2026
Merged

feat: add manual human acceptance gate#44
rhoninl merged 1 commit into
mainfrom
feat/human-acceptance-gate

Conversation

@rhoninl

@rhoninl rhoninl commented May 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a generic manual / human acceptance workflow node with accepted, needs_changes, and rejected decision handles.
  • Parks runs as waiting_for_human without marking them failed, exposes reviewer brief/decision metadata, and resumes through the selected handle.
  • Adds decision API, run-list/detail status projection, SSE event support, RunView decision UI, canvas node/config UI, and MCP waiting-state metadata.
  • Adds validation for manual decisions, timeout fallback safety, multi-gate pending visibility, and bounded reviewer comments.

Closes #43

Acceptance evidence

Evidence was collected locally before opening this PR and is also posted as a PR comment.

  • bun test → 1286 pass / 1 skip / 0 fail, 3109 expect calls across 119 files
  • bun run typecheck → clean (tsc --noEmit)
  • bun run build → success; /api/runs/[workflowId]/[runId]/decision compiled
  • Static added-line security scan → no hardcoded secret / eval / shell-injection patterns found
  • Independent review blockers found and fixed:
    • Added node_waiting_for_human / node_human_decided to SSE allowlist
    • Required rejected when timeoutMs > 0
    • Projected waiting_for_human from pendingHumanDecisions() for multi-gate runs
    • Capped reviewer comments at 4000 chars

Notes

  • Restart-persistent pending human gates are intentionally not implemented here. If the server restarts while a gate is parked, existing crash recovery fails the run record rather than silently losing the pending gate. Full rehydration of parked gate promises should be a separate runtime-persistence feature.

- Add generic manual workflow node with decision-specific routing
- Add decision API, waiting_for_human status, and run-view UI
- Add validation, SSE event support, and acceptance coverage

Closes #43
@rhoninl

rhoninl commented May 24, 2026

Copy link
Copy Markdown
Owner Author

Acceptance evidence collected by Hermes

Verdict: ready for review. I verified the implementation after Claude Code fixed the independent-review blockers.

Commands run

  • bun test
    • Result: 1286 pass / 1 skip / 0 fail
    • 3109 expect calls across 119 files
  • bun run typecheck
    • Result: clean (tsc --noEmit)
  • bun run build
    • Result: success
    • Confirmed route compiled: /api/runs/[workflowId]/[runId]/decision
  • Added-line static security scan
    • Result: no findings for hardcoded secrets, eval/exec, shell injection, unsafe pickle, or string-formatted SQL patterns

Acceptance coverage

  • Manual node parks run as waiting_for_human without failing it.
  • accepted, needs_changes, and rejected decisions route through decision-specific handles.
  • Decision metadata is recorded: decision, decidedBy, decidedAt, optional comment, and timedOut.
  • Decision API supports pending-gate lookup and decision submission.
  • Run list/detail APIs project waiting_for_human when any human gate is pending.
  • RunView renders a human decision panel and submits decisions without browser alert/confirm/prompt dialogs.
  • SSE client allowlist includes node_waiting_for_human and node_human_decided, so live UI events are not dropped.
  • timeoutMs > 0 requires rejected to be configured, matching the fail-closed timeout behavior.
  • Multiple pending gates are exposed through pendingHumanDecisions().
  • Reviewer comments are capped at 4000 chars.

Independent review blockers resolved

An independent review initially blocked acceptance on four issues:

  1. Live UI would drop manual-gate SSE events — fixed by adding both manual events to VALID_EVENT_TYPES and adding regression coverage.
  2. Timeout could fabricate rejected when disabled — fixed by validation requiring rejected when timeoutMs > 0; UI also auto-adds/locks it.
  3. Multi-gate status could fall back to plain running — fixed by projecting from pendingHumanDecisions() and promoting remaining gates in the single-slot waitingFor mirror.
  4. Reviewer comment was uncapped — fixed with a 4000-char server cap and matching UI maxLength.

Known limitation

Restart-persistent pending human gates are not implemented in this PR. If the server restarts while a gate is parked, existing crash recovery fails the run record rather than silently losing the pending gate. Full rehydration of parked gate promises should be handled as a separate runtime-persistence feature.

@rhoninl

rhoninl commented May 24, 2026

Copy link
Copy Markdown
Owner Author

CI evidence update

GitHub checks for this PR are now green:

  • cipass
  • Production-smoke E2Epass
  • PR merge state → CLEAN / MERGEABLE

This is in addition to the local evidence already posted above:

  • bun test → 1286 pass / 1 skip / 0 fail
  • bun run typecheck → clean
  • bun run build → success
  • static added-line security scan → no findings

@rhoninl

rhoninl commented May 24, 2026

Copy link
Copy Markdown
Owner Author

Human-gate runtime smoke evidence

Yuki was right that CI/typecheck alone is not enough evidence for this feature. I ran a real local runtime smoke probe on this PR branch and verified the manual gate end-to-end.

Runtime setup

  • Branch: feat/human-acceptance-gate
  • Commit: 0eeca4e
  • Server: production build started locally with isolated runtime data
  • Base URL: http://127.0.0.1:33123
  • Health check: GET /api/health{"status":"ok","version":"0.1.0"...}

Smoke workflow created

Workflow id: manual-gate-smoke-20260524

Shape:

start-1
  → manual-1
      accepted      → end-accepted      (succeeded)
      needs_changes → end-needs-changes (failed)
      rejected      → end-rejected      (failed)

Manual gate config used:

{
  "brief": "Smoke proof: confirm manual gate pauses and resumes through accepted.",
  "reviewerHint": "Hermes smoke probe",
  "decisions": [
    { "key": "accepted", "label": "Accept smoke" },
    { "key": "needs_changes", "label": "Needs changes" },
    { "key": "rejected", "label": "Reject smoke" }
  ]
}

Workflow save proof:

  • POST /api/workflows → saved workflow with manual-1 node present

Waiting gate proof

Started run:

  • POST /api/run with workflowId=manual-gate-smoke-20260524
  • Run id: eb9c5a53-f9ef-453b-9a89-a244d53ed6cc

Then polled:

  • GET /api/runs/manual-gate-smoke-20260524/eb9c5a53-f9ef-453b-9a89-a244d53ed6cc

Observed:

{
  "status": "waiting_for_human",
  "waitingFor": {
    "kind": "human",
    "nodeId": "manual-1",
    "brief": "Smoke proof: confirm manual gate pauses and resumes through accepted.",
    "reviewerHint": "Hermes smoke probe",
    "decisions": ["accepted", "needs_changes", "rejected"]
  },
  "pendingHumanDecisions": [
    { "nodeId": "manual-1" }
  ]
}

Also verified:

  • GET /api/runs/manual-gate-smoke-20260524/eb9c5a53-f9ef-453b-9a89-a244d53ed6cc/decision
  • Returned exactly one pending gate: manual-1

Human decision / resume proof

Submitted decision:

POST /api/runs/manual-gate-smoke-20260524/eb9c5a53-f9ef-453b-9a89-a244d53ed6cc/decision

Payload:

{
  "nodeId": "manual-1",
  "decision": "accepted",
  "comment": "Hermes smoke probe accepted the manual gate."
}

Response proof:

{
  "ok": true,
  "decision": "accepted",
  "decidedBy": "open",
  "comment": "Hermes smoke probe accepted the manual gate."
}

Final run proof:

  • Final status: succeeded
  • scope.manual-1.decision: accepted
  • scope.manual-1.comment: Hermes smoke probe accepted the manual gate.
  • waitingFor: cleared
  • Event stream contained:
    • node_waiting_for_human
    • node_human_decided
    • node_finished for end-accepted
    • run_finished

Runtime smoke verdict

✅ The human acceptance gate works in a real runtime flow:

  1. workflow can be saved with a manual gate
  2. run pauses as waiting_for_human
  3. pending gate is visible through the decision API
  4. human decision API records the decision/comment
  5. run resumes through the selected accepted handle
  6. final workflow succeeds through end-accepted

@rhoninl rhoninl merged commit 71caabf into main May 25, 2026
2 checks passed
@rhoninl rhoninl deleted the feat/human-acceptance-gate branch May 25, 2026 01:03
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.

Add generic human acceptance gate for workflow approvals

1 participant