From 481c21ac9978a738ebd31c2defc0909f9201ecfc Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 11:13:26 +0100 Subject: [PATCH 1/5] feat(workflows): add postAction hook workflow for issue #500 Add agent-relay workflow YAML that coordinates agents to implement the postAction hook feature on WorkflowStep, supporting shell commands and HTTP webhooks after step completion. Closes #500 Co-Authored-By: Claude Opus 4.6 --- relay.post-action-hook.yaml | 517 ++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 relay.post-action-hook.yaml diff --git a/relay.post-action-hook.yaml b/relay.post-action-hook.yaml new file mode 100644 index 000000000..9e9aefacf --- /dev/null +++ b/relay.post-action-hook.yaml @@ -0,0 +1,517 @@ +version: '1.0' +name: post-action-hook +description: > + Implements GitHub issue #500: Add optional postAction hook to WorkflowStep. + postAction supports shell commands and HTTP webhooks that run after a step + completes successfully, with configurable failAction (fail vs warn). + + Pipeline: read context -> add types -> implement runner logic -> update + validator -> write tests -> type-check -> run tests -> fix if broken -> + final tests -> capture diff -> review. + +swarm: + pattern: pipeline + maxConcurrency: 3 + timeoutMs: 2400000 # 40 min + channel: post-action-hook + +agents: + - name: types-author + cli: codex + preset: worker + role: 'Adds PostAction interface and updates WorkflowStep in types.ts and schema.json.' + constraints: + model: o4-mini + + - name: implementer + cli: codex + preset: worker + role: 'Implements postAction execution logic in runner.ts (shell + HTTP + interpolation).' + constraints: + model: o4-mini + + - name: validator-author + cli: codex + preset: worker + role: 'Updates the YAML validator to accept and validate postAction fields.' + constraints: + model: o4-mini + + - name: test-writer + cli: codex + preset: worker + role: 'Writes unit tests for postAction execution: shell, webhook, interpolation, failAction.' + constraints: + model: o4-mini + + - name: fixer + cli: codex + preset: worker + role: 'Fixes TypeScript errors or failing tests found during verification.' + constraints: + model: o4-mini + + - name: reviewer + cli: claude + preset: reviewer + role: 'Reviews the full diff for correctness, security, edge cases, and backwards compatibility.' + constraints: + model: sonnet + +workflows: + - name: default + description: 'Add postAction hook to workflow steps with shell command and webhook support.' + onError: continue + + preflight: + - command: test -f packages/sdk/src/workflows/types.ts + description: 'Workflow types file exists' + - command: test -f packages/sdk/src/workflows/runner.ts + description: 'Workflow runner file exists' + - command: test -f packages/sdk/src/workflows/validator.ts + description: 'Workflow validator file exists' + + steps: + # ── Phase 1: Read existing code for context ──────────────────────────── + + - name: read-types + type: deterministic + command: cat packages/sdk/src/workflows/types.ts + captureOutput: true + failOnError: false + + - name: read-runner-spawn + type: deterministic + command: > + awk '/private async spawnAndWait/,/^ \}$/{print NR": "$0}' + packages/sdk/src/workflows/runner.ts | head -120 + captureOutput: true + failOnError: false + + - name: read-runner-interpolate + type: deterministic + command: > + awk '/private interpolateStepTask/,/^ \}$/{print NR": "$0}' + packages/sdk/src/workflows/runner.ts | head -60 + captureOutput: true + failOnError: false + + - name: read-runner-deterministic + type: deterministic + command: > + grep -n "deterministic\|execSync\|child_process\|captureOutput" + packages/sdk/src/workflows/runner.ts | head -30 + captureOutput: true + failOnError: false + + - name: read-validator + type: deterministic + command: cat packages/sdk/src/workflows/validator.ts + captureOutput: true + failOnError: false + + - name: read-schema + type: deterministic + command: cat packages/sdk/src/workflows/schema.json + captureOutput: true + failOnError: false + + - name: read-existing-tests + type: deterministic + command: cat packages/sdk/src/__tests__/workflow-runner.test.ts + captureOutput: true + failOnError: false + + # ── Phase 2: Add type definitions ────────────────────────────────────── + + - name: add-types + type: agent + agent: types-author + dependsOn: [read-types, read-schema] + task: | + Add the PostAction interface and update WorkflowStep in two files. + + CURRENT types.ts: + {{steps.read-types.output}} + + CURRENT schema.json: + {{steps.read-schema.output}} + + ── Task 1: Update packages/sdk/src/workflows/types.ts ── + + Add this interface BEFORE the WorkflowStep interface: + + ```typescript + /** Webhook configuration for a postAction. */ + export interface PostActionWebhook { + /** URL to call. Supports {{step.output}}, {{step.name}}, {{run.id}} interpolation. */ + url: string; + /** HTTP method. Default: POST. */ + method?: 'POST' | 'PUT' | 'PATCH'; + /** Additional headers to send. */ + headers?: Record; + /** Request body. Supports {{step.output}}, {{step.name}}, {{run.id}} interpolation. */ + body?: string; + } + + /** Action to run after a step completes successfully. */ + export interface PostAction { + /** Shell command to execute. */ + command?: string; + /** HTTP webhook to call. */ + webhook?: PostActionWebhook; + /** Behavior on failure: 'fail' aborts the workflow, 'warn' logs and continues. Default: 'warn'. */ + failAction?: 'fail' | 'warn'; + } + ``` + + Then add this field to the WorkflowStep interface, after the `captureOutput` field: + + ```typescript + // ── Post-action hook ──────────────────────────────────────────────────── + /** Optional action to run after the step completes successfully. */ + postAction?: PostAction; + ``` + + ── Task 2: Update packages/sdk/src/workflows/schema.json ── + + Add "postAction" to the step properties in the JSON schema. Add definitions + for PostAction and PostActionWebhook objects matching the TypeScript types. + The postAction property should be optional (do NOT add it to "required"). + + Only modify these two files. Do not change anything else. + verification: + type: exit_code + + # ── Phase 3: Implement runner logic ──────────────────────────────────── + + - name: implement-runner + type: agent + agent: implementer + dependsOn: [add-types, read-runner-spawn, read-runner-interpolate, read-runner-deterministic] + task: | + Implement postAction execution in packages/sdk/src/workflows/runner.ts. + + CONTEXT — spawnAndWait method: + {{steps.read-runner-spawn.output}} + + CONTEXT — interpolation method: + {{steps.read-runner-interpolate.output}} + + CONTEXT — deterministic/exec patterns: + {{steps.read-runner-deterministic.output}} + + ── What to implement ── + + Add a private method `executePostAction` to the WorkflowRunner class: + + ```typescript + /** + * Execute a step's postAction hook (shell command and/or webhook). + * Called after a step completes successfully. + */ + private async executePostAction( + step: WorkflowStep, + stepOutput: string, + runId: string, + ): Promise { + const postAction = step.postAction; + if (!postAction) return; + + const failAction = postAction.failAction ?? 'warn'; + + // Interpolate postAction-specific variables + const interpolatePostAction = (template: string): string => { + return template + .replace(/\{\{step\.output\}\}/g, stepOutput) + .replace(/\{\{step\.name\}\}/g, step.name) + .replace(/\{\{run\.id\}\}/g, runId); + }; + + // Execute shell command if present + if (postAction.command) { + try { + const resolvedCmd = interpolatePostAction(postAction.command); + this.log(`[${step.name}] Running postAction command: ${resolvedCmd}`); + const { execSync } = await import('node:child_process'); + execSync(resolvedCmd, { + timeout: step.timeoutMs ?? 30_000, + stdio: 'pipe', + cwd: process.cwd(), + }); + this.log(`[${step.name}] postAction command completed`); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + if (failAction === 'fail') { + throw new Error(`postAction command failed for step "${step.name}": ${msg}`); + } + this.log(`[${step.name}] postAction command warning: ${msg}`); + } + } + + // Execute webhook if present + if (postAction.webhook) { + try { + const webhook = postAction.webhook; + const url = interpolatePostAction(webhook.url); + const method = webhook.method ?? 'POST'; + const body = webhook.body ? interpolatePostAction(webhook.body) : undefined; + const headers: Record = { + 'Content-Type': 'application/json', + ...(webhook.headers ?? {}), + }; + + this.log(`[${step.name}] Running postAction webhook: ${method} ${url}`); + const response = await fetch(url, { method, headers, body }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + this.log(`[${step.name}] postAction webhook completed (${response.status})`); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + if (failAction === 'fail') { + throw new Error(`postAction webhook failed for step "${step.name}": ${msg}`); + } + this.log(`[${step.name}] postAction webhook warning: ${msg}`); + } + } + } + ``` + + Then call this method in two places: + + 1. In the agent step execution path — after the step completes successfully + (after verification passes, before updating the step status to 'completed'), + find where the step output is captured and add: + ```typescript + await this.executePostAction(step, output, runId); + ``` + + 2. In the deterministic step execution path — after the command runs successfully + and output is captured, add the same call: + ```typescript + await this.executePostAction(step, output, runId); + ``` + + Make sure to import the PostAction type if needed. Use the existing import + from './types.js' which already imports WorkflowStep. + + Only modify packages/sdk/src/workflows/runner.ts. + verification: + type: exit_code + + # ── Phase 4: Update validator ────────────────────────────────────────── + + - name: update-validator + type: agent + agent: validator-author + dependsOn: [add-types, read-validator] + task: | + Update the YAML validator to accept and validate postAction fields. + + CURRENT validator.ts: + {{steps.read-validator.output}} + + ── What to implement ── + + In packages/sdk/src/workflows/validator.ts, add validation for the + postAction field on workflow steps: + + 1. If a step has a `postAction` field, validate that: + - At least one of `command` or `webhook` is present + - If `webhook` is present, `url` is a non-empty string + - If `webhook.method` is present, it is one of 'POST', 'PUT', 'PATCH' + - If `failAction` is present, it is one of 'fail', 'warn' + + 2. Add warnings (not errors) for: + - postAction with both command and webhook (valid but worth noting) + + Follow the existing validation patterns in the file. Use the same + error/warning collection approach already in use. + + Only modify packages/sdk/src/workflows/validator.ts. + verification: + type: exit_code + + # ── Phase 5: Write tests ─────────────────────────────────────────────── + + - name: write-tests + type: agent + agent: test-writer + dependsOn: [implement-runner, update-validator, read-existing-tests] + task: | + Write unit tests for the postAction feature. Create a NEW test file: + packages/sdk/src/__tests__/post-action.test.ts + + EXISTING test patterns (for reference on mock setup): + {{steps.read-existing-tests.output}} + + ── Tests to write ── + + Use the same mock infrastructure as workflow-runner.test.ts (mock fetch, + mock Relaycast SDK, mock DB adapter, WorkflowRunner import). + + Create these test cases in a describe('postAction hook') block: + + 1. 'executes shell command after deterministic step completes' + - Config with a deterministic step that has postAction.command + - Verify the command runs (mock execSync) + - Step should succeed + + 2. 'executes webhook after agent step completes' + - Config with an agent step that has postAction.webhook + - Verify fetch is called with the correct URL, method, headers, body + - Step should succeed + + 3. 'interpolates {{step.output}}, {{step.name}}, {{run.id}} in webhook body' + - Verify the interpolation replaces placeholders in URL and body + + 4. 'failAction: warn logs warning and continues on command failure' + - Mock execSync to throw + - Step should still succeed (not fail the workflow) + + 5. 'failAction: fail throws and fails the step on command failure' + - Mock execSync to throw + - postAction has failAction: 'fail' + - Step should fail + + 6. 'failAction: warn logs warning and continues on webhook failure' + - Mock fetch to return a 500 response for the webhook URL + - Step should still succeed + + 7. 'skips postAction when not defined on step' + - A step with no postAction field + - Verify no extra execSync or fetch calls + + 8. 'executes both command and webhook when both are defined' + - postAction has both command and webhook + - Verify both execute in order + + Use vitest (describe, it, expect, vi) and the same patterns as the + existing test file. Make sure mocks are properly set up and cleaned up. + verification: + type: exit_code + + # ── Phase 6: Type-check ──────────────────────────────────────────────── + + - name: type-check + type: deterministic + dependsOn: [implement-runner, update-validator, write-tests] + command: > + cd packages/sdk && + npx tsc --noEmit 2>&1 | tail -30 && + echo "TYPE_CHECK_PASSED" || echo "TYPE_CHECK_FAILED" + captureOutput: true + failOnError: false + + # ── Phase 7: Run tests ───────────────────────────────────────────────── + + - name: run-tests + type: deterministic + dependsOn: [type-check] + command: >- + cd packages/sdk && { npx vitest run 2>&1; echo "EXIT:$?"; } | tail -80 + captureOutput: true + failOnError: false + + # ── Phase 8: Fix failures ────────────────────────────────────────────── + + - name: fix-if-broken + type: agent + agent: fixer + dependsOn: [run-tests, type-check] + task: | + Review the type-check and test results. Fix any failures. + + Type-check: + {{steps.type-check.output}} + + Test run: + {{steps.run-tests.output}} + + If both show PASSED / all tests pass, output: FIX_DONE:none + + Otherwise: + - For TypeScript errors: fix the relevant source files + - For failing tests: fix the tests or the implementation + - Do NOT change the intended behavior — only fix syntax/type/mock issues + - Do NOT weaken or remove test assertions + + Files you may edit: + - packages/sdk/src/workflows/types.ts + - packages/sdk/src/workflows/runner.ts + - packages/sdk/src/workflows/validator.ts + - packages/sdk/src/__tests__/post-action.test.ts + verification: + type: exit_code + maxIterations: 2 + + # ── Phase 9: Final test run ──────────────────────────────────────────── + + - name: final-tests + type: deterministic + dependsOn: [fix-if-broken] + command: >- + cd packages/sdk && { npx vitest run 2>&1; echo "EXIT:$?"; } | tail -60 + captureOutput: true + failOnError: false + + # ── Phase 10: Capture diff for review ────────────────────────────────── + + - name: capture-diff + type: deterministic + dependsOn: [final-tests] + command: > + git diff packages/sdk/src/workflows/types.ts + packages/sdk/src/workflows/runner.ts + packages/sdk/src/workflows/validator.ts + packages/sdk/src/workflows/schema.json + packages/sdk/src/__tests__/post-action.test.ts + captureOutput: true + failOnError: false + + # ── Phase 11: Code review ────────────────────────────────────────────── + + - name: review + type: agent + agent: reviewer + dependsOn: [capture-diff, final-tests] + task: | + Review the postAction hook implementation for issue #500. + + Final test result: + {{steps.final-tests.output}} + + Diff: + {{steps.capture-diff.output}} + + Review checklist: + 1. PostAction and PostActionWebhook interfaces match the spec from issue #500 + 2. WorkflowStep correctly includes the optional postAction field + 3. executePostAction handles both shell commands and webhooks + 4. Interpolation replaces {{step.output}}, {{step.name}}, {{run.id}} correctly + 5. failAction: 'warn' logs but does not abort; failAction: 'fail' throws + 6. Shell commands use execSync with a timeout — no unbounded execution + 7. Webhook uses fetch with proper error handling for non-ok responses + 8. No security issues: command injection via interpolated values? + 9. Validator correctly rejects postAction with no command or webhook + 10. Tests cover all happy and error paths + 11. No regressions to existing workflow behavior (steps without postAction) + 12. schema.json updated to match the TypeScript types + verification: + type: exit_code + +errorHandling: + strategy: continue + maxRetries: 1 + retryDelayMs: 3000 + notifyChannel: post-action-hook + +state: + backend: memory + ttlMs: 7200000 # 2 hours + +trajectories: + enabled: true + autoDecisions: true From 3b8a0bdab169945d7ce7e6bbb67bace28726dbf9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 11:55:37 +0100 Subject: [PATCH 2/5] fix: address PR #527 review feedback on post-action-hook workflow - Change onError from 'continue' to 'skip' (valid enum value) - Change swarm pattern from 'pipeline' to 'dag' (steps run in parallel) - Replace fragile awk-based code extraction with simple cat commands - Add command injection security note to implementer task - Add per-step timeoutMs values (5 min default, 10 min for implementer) Co-Authored-By: Claude Opus 4.6 --- relay.post-action-hook.yaml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/relay.post-action-hook.yaml b/relay.post-action-hook.yaml index 9e9aefacf..0fc1407ee 100644 --- a/relay.post-action-hook.yaml +++ b/relay.post-action-hook.yaml @@ -10,7 +10,7 @@ description: > final tests -> capture diff -> review. swarm: - pattern: pipeline + pattern: dag maxConcurrency: 3 timeoutMs: 2400000 # 40 min channel: post-action-hook @@ -61,7 +61,7 @@ agents: workflows: - name: default description: 'Add postAction hook to workflow steps with shell command and webhook support.' - onError: continue + onError: skip preflight: - command: test -f packages/sdk/src/workflows/types.ts @@ -82,25 +82,19 @@ workflows: - name: read-runner-spawn type: deterministic - command: > - awk '/private async spawnAndWait/,/^ \}$/{print NR": "$0}' - packages/sdk/src/workflows/runner.ts | head -120 + command: cat packages/sdk/src/workflows/runner.ts captureOutput: true failOnError: false - name: read-runner-interpolate type: deterministic - command: > - awk '/private interpolateStepTask/,/^ \}$/{print NR": "$0}' - packages/sdk/src/workflows/runner.ts | head -60 + command: cat packages/sdk/src/workflows/runner.ts captureOutput: true failOnError: false - name: read-runner-deterministic type: deterministic - command: > - grep -n "deterministic\|execSync\|child_process\|captureOutput" - packages/sdk/src/workflows/runner.ts | head -30 + command: cat packages/sdk/src/workflows/runner.ts captureOutput: true failOnError: false @@ -128,6 +122,7 @@ workflows: type: agent agent: types-author dependsOn: [read-types, read-schema] + timeoutMs: 300000 # 5 min task: | Add the PostAction interface and update WorkflowStep in two files. @@ -189,6 +184,7 @@ workflows: type: agent agent: implementer dependsOn: [add-types, read-runner-spawn, read-runner-interpolate, read-runner-deterministic] + timeoutMs: 600000 # 10 min task: | Implement postAction execution in packages/sdk/src/workflows/runner.ts. @@ -297,6 +293,12 @@ workflows: Make sure to import the PostAction type if needed. Use the existing import from './types.js' which already imports WorkflowStep. + SECURITY NOTE: Interpolated values ({{step.output}}, {{step.name}}, {{run.id}}) + are inserted into shell commands via string replacement. Sanitize and escape + these values before shell execution to prevent command injection. Consider + using environment variables or shell-escaped strings instead of direct + interpolation into the command string. + Only modify packages/sdk/src/workflows/runner.ts. verification: type: exit_code @@ -307,6 +309,7 @@ workflows: type: agent agent: validator-author dependsOn: [add-types, read-validator] + timeoutMs: 300000 # 5 min task: | Update the YAML validator to accept and validate postAction fields. @@ -340,6 +343,7 @@ workflows: type: agent agent: test-writer dependsOn: [implement-runner, update-validator, read-existing-tests] + timeoutMs: 300000 # 5 min task: | Write unit tests for the postAction feature. Create a NEW test file: packages/sdk/src/__tests__/post-action.test.ts @@ -421,6 +425,7 @@ workflows: type: agent agent: fixer dependsOn: [run-tests, type-check] + timeoutMs: 300000 # 5 min task: | Review the type-check and test results. Fix any failures. @@ -477,6 +482,7 @@ workflows: type: agent agent: reviewer dependsOn: [capture-diff, final-tests] + timeoutMs: 300000 # 5 min task: | Review the postAction hook implementation for issue #500. From f807d5bd039060f23d901baeeef60219b87e4e12 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 13:41:38 +0100 Subject: [PATCH 3/5] fix(workflows): preserve exit codes through pipes in post-action-hook workflow The type-check step piped tsc output through tail, which swallowed tsc's exit code (tail always exits 0), causing TYPE_CHECK_PASSED to always be printed regardless of actual type errors. Applied the same fix to run-tests and final-tests steps for consistency: capture command output and exit code in variables before piping through tail. Co-Authored-By: Claude Opus 4.6 --- relay.post-action-hook.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/relay.post-action-hook.yaml b/relay.post-action-hook.yaml index 0fc1407ee..c3666b534 100644 --- a/relay.post-action-hook.yaml +++ b/relay.post-action-hook.yaml @@ -404,8 +404,9 @@ workflows: dependsOn: [implement-runner, update-validator, write-tests] command: > cd packages/sdk && - npx tsc --noEmit 2>&1 | tail -30 && - echo "TYPE_CHECK_PASSED" || echo "TYPE_CHECK_FAILED" + TSC_OUT=$(npx tsc --noEmit 2>&1); TSC_EXIT=$?; + echo "$TSC_OUT" | tail -30; + if [ $TSC_EXIT -eq 0 ]; then echo "TYPE_CHECK_PASSED"; else echo "TYPE_CHECK_FAILED"; fi captureOutput: true failOnError: false @@ -415,7 +416,10 @@ workflows: type: deterministic dependsOn: [type-check] command: >- - cd packages/sdk && { npx vitest run 2>&1; echo "EXIT:$?"; } | tail -80 + cd packages/sdk && + TEST_OUT=$(npx vitest run 2>&1); TEST_EXIT=$?; + echo "$TEST_OUT" | tail -80; + echo "EXIT:$TEST_EXIT" captureOutput: true failOnError: false @@ -458,7 +462,10 @@ workflows: type: deterministic dependsOn: [fix-if-broken] command: >- - cd packages/sdk && { npx vitest run 2>&1; echo "EXIT:$?"; } | tail -60 + cd packages/sdk && + TEST_OUT=$(npx vitest run 2>&1); TEST_EXIT=$?; + echo "$TEST_OUT" | tail -60; + echo "EXIT:$TEST_EXIT" captureOutput: true failOnError: false From 07f54885a497b237931a82b6e667073a32100a16 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 13:54:36 +0100 Subject: [PATCH 4/5] fix(workflow): include untracked files in capture diff --- relay.post-action-hook.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/relay.post-action-hook.yaml b/relay.post-action-hook.yaml index c3666b534..39133a1df 100644 --- a/relay.post-action-hook.yaml +++ b/relay.post-action-hook.yaml @@ -475,7 +475,9 @@ workflows: type: deterministic dependsOn: [final-tests] command: > - git diff packages/sdk/src/workflows/types.ts + git add -N packages/sdk/src/__tests__/post-action.test.ts 2>/dev/null; + git diff HEAD -- + packages/sdk/src/workflows/types.ts packages/sdk/src/workflows/runner.ts packages/sdk/src/workflows/validator.ts packages/sdk/src/workflows/schema.json From 25f81dc7521424f1d493f46e6a5869e2110dd08d Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 16 Mar 2026 06:51:15 +0100 Subject: [PATCH 5/5] refactor: fix workflow against best practices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All agents now use preset: worker/reviewer (non-interactive) - Replaced all output_contains verification with exit_code (tokens appeared in task text → double-occurrence problem) - Removed duplicate deterministic reads (read-runner-spawn, read-runner-interpolate, read-runner-deterministic all catted the same file) — replaced with targeted grep/sed extracts - Shortened task prompts from 60+ to ~15 lines - Reduced from 11 phases to 3 (context → impl → verify/fix) - Added file verification gates between implementation waves - Removed coordination/barriers/state config (unnecessary overhead) - Channel renamed to wf-post-action (skill says use wf- prefix) - Removed per-step timeoutMs (skill says use global timeout only) - Fixed read-runner steps to extract relevant sections instead of catting the entire 6000-line runner.ts 3 times --- relay.post-action-hook.yaml | 539 +++++++++++------------------------- 1 file changed, 169 insertions(+), 370 deletions(-) diff --git a/relay.post-action-hook.yaml b/relay.post-action-hook.yaml index 39133a1df..d592ed9c7 100644 --- a/relay.post-action-hook.yaml +++ b/relay.post-action-hook.yaml @@ -5,15 +5,13 @@ description: > postAction supports shell commands and HTTP webhooks that run after a step completes successfully, with configurable failAction (fail vs warn). - Pipeline: read context -> add types -> implement runner logic -> update - validator -> write tests -> type-check -> run tests -> fix if broken -> - final tests -> capture diff -> review. + 3 phases: context reads → parallel implementation → verify + fix. swarm: pattern: dag - maxConcurrency: 3 - timeoutMs: 2400000 # 40 min - channel: post-action-hook + maxConcurrency: 4 + timeoutMs: 2400000 + channel: wf-post-action agents: - name: types-author @@ -26,42 +24,42 @@ agents: - name: implementer cli: codex preset: worker - role: 'Implements postAction execution logic in runner.ts (shell + HTTP + interpolation).' + role: 'Implements postAction execution logic in runner.ts.' constraints: model: o4-mini - name: validator-author cli: codex preset: worker - role: 'Updates the YAML validator to accept and validate postAction fields.' + role: 'Updates YAML validator to accept postAction fields.' constraints: model: o4-mini - name: test-writer cli: codex preset: worker - role: 'Writes unit tests for postAction execution: shell, webhook, interpolation, failAction.' + role: 'Writes unit tests for postAction execution.' constraints: model: o4-mini - name: fixer cli: codex preset: worker - role: 'Fixes TypeScript errors or failing tests found during verification.' + role: 'Fixes TypeScript errors or failing tests.' constraints: model: o4-mini - name: reviewer cli: claude preset: reviewer - role: 'Reviews the full diff for correctness, security, edge cases, and backwards compatibility.' + role: 'Reviews full diff for correctness, security, and backwards compatibility.' constraints: model: sonnet workflows: - name: default description: 'Add postAction hook to workflow steps with shell command and webhook support.' - onError: skip + onError: retry preflight: - command: test -f packages/sdk/src/workflows/types.ts @@ -72,59 +70,62 @@ workflows: description: 'Workflow validator file exists' steps: - # ── Phase 1: Read existing code for context ──────────────────────────── + # ════════════════════════════════════════════════════════════════════════ + # PHASE 1: Context reads + # ════════════════════════════════════════════════════════════════════════ - name: read-types type: deterministic command: cat packages/sdk/src/workflows/types.ts captureOutput: true - failOnError: false - - - name: read-runner-spawn - type: deterministic - command: cat packages/sdk/src/workflows/runner.ts - captureOutput: true - failOnError: false - - - name: read-runner-interpolate - type: deterministic - command: cat packages/sdk/src/workflows/runner.ts - captureOutput: true - failOnError: false - - name: read-runner-deterministic + - name: read-schema type: deterministic - command: cat packages/sdk/src/workflows/runner.ts + command: cat packages/sdk/src/workflows/schema.json captureOutput: true - failOnError: false - name: read-validator type: deterministic command: cat packages/sdk/src/workflows/validator.ts captureOutput: true - failOnError: false - - name: read-schema + - name: read-runner-step-exec type: deterministic - command: cat packages/sdk/src/workflows/schema.json + command: | + echo "=== Step completion sites (where postAction should be called) ===" + grep -n "status.*completed\|step.*complete\|captureStepTerminalEvidence" packages/sdk/src/workflows/runner.ts | head -20 + echo "" + echo "=== Deterministic step execution ===" + sed -n '/private.*executeDeterministic\|private.*runDeterministic/,+40p' packages/sdk/src/workflows/runner.ts | head -50 + echo "" + echo "=== Existing interpolation patterns ===" + grep -n -B2 -A5 'interpolat\|replace.*steps\.' packages/sdk/src/workflows/runner.ts | head -30 + echo "" + echo "=== Imports ===" + head -40 packages/sdk/src/workflows/runner.ts captureOutput: true - failOnError: false - name: read-existing-tests type: deterministic - command: cat packages/sdk/src/__tests__/workflow-runner.test.ts + command: | + echo "=== Test file structure ===" + head -80 packages/sdk/src/__tests__/workflow-runner.test.ts + echo "" + echo "=== Mock patterns ===" + grep -n -B2 -A5 'vi.mock\|vi.fn\|mockResolvedValue' packages/sdk/src/__tests__/workflow-runner.test.ts | head -40 captureOutput: true - failOnError: false - # ── Phase 2: Add type definitions ────────────────────────────────────── + # ════════════════════════════════════════════════════════════════════════ + # PHASE 2: Implementation (2 waves) + # ════════════════════════════════════════════════════════════════════════ + + # ── Wave A: types + schema + validator (parallel, independent) ───────── - name: add-types - type: agent agent: types-author dependsOn: [read-types, read-schema] - timeoutMs: 300000 # 5 min task: | - Add the PostAction interface and update WorkflowStep in two files. + Add PostAction types to two files. CURRENT types.ts: {{steps.read-types.output}} @@ -132,401 +133,199 @@ workflows: CURRENT schema.json: {{steps.read-schema.output}} - ── Task 1: Update packages/sdk/src/workflows/types.ts ── - - Add this interface BEFORE the WorkflowStep interface: - - ```typescript - /** Webhook configuration for a postAction. */ - export interface PostActionWebhook { - /** URL to call. Supports {{step.output}}, {{step.name}}, {{run.id}} interpolation. */ - url: string; - /** HTTP method. Default: POST. */ - method?: 'POST' | 'PUT' | 'PATCH'; - /** Additional headers to send. */ - headers?: Record; - /** Request body. Supports {{step.output}}, {{step.name}}, {{run.id}} interpolation. */ - body?: string; - } - - /** Action to run after a step completes successfully. */ - export interface PostAction { - /** Shell command to execute. */ - command?: string; - /** HTTP webhook to call. */ - webhook?: PostActionWebhook; - /** Behavior on failure: 'fail' aborts the workflow, 'warn' logs and continues. Default: 'warn'. */ - failAction?: 'fail' | 'warn'; - } - ``` - - Then add this field to the WorkflowStep interface, after the `captureOutput` field: - - ```typescript - // ── Post-action hook ──────────────────────────────────────────────────── - /** Optional action to run after the step completes successfully. */ - postAction?: PostAction; - ``` - - ── Task 2: Update packages/sdk/src/workflows/schema.json ── - - Add "postAction" to the step properties in the JSON schema. Add definitions - for PostAction and PostActionWebhook objects matching the TypeScript types. - The postAction property should be optional (do NOT add it to "required"). - - Only modify these two files. Do not change anything else. - verification: - type: exit_code - - # ── Phase 3: Implement runner logic ──────────────────────────────────── + In types.ts, add before WorkflowStep interface: + - PostActionWebhook interface: url (string), method? ('POST'|'PUT'|'PATCH'), headers? (Record), body? (string) + - PostAction interface: command? (string), webhook? (PostActionWebhook), failAction? ('fail'|'warn') + - Add `postAction?: PostAction` to WorkflowStep after captureOutput - - name: implement-runner - type: agent - agent: implementer - dependsOn: [add-types, read-runner-spawn, read-runner-interpolate, read-runner-deterministic] - timeoutMs: 600000 # 10 min - task: | - Implement postAction execution in packages/sdk/src/workflows/runner.ts. + In schema.json, add matching postAction property (optional) to step definition. - CONTEXT — spawnAndWait method: - {{steps.read-runner-spawn.output}} - - CONTEXT — interpolation method: - {{steps.read-runner-interpolate.output}} - - CONTEXT — deterministic/exec patterns: - {{steps.read-runner-deterministic.output}} - - ── What to implement ── - - Add a private method `executePostAction` to the WorkflowRunner class: - - ```typescript - /** - * Execute a step's postAction hook (shell command and/or webhook). - * Called after a step completes successfully. - */ - private async executePostAction( - step: WorkflowStep, - stepOutput: string, - runId: string, - ): Promise { - const postAction = step.postAction; - if (!postAction) return; - - const failAction = postAction.failAction ?? 'warn'; - - // Interpolate postAction-specific variables - const interpolatePostAction = (template: string): string => { - return template - .replace(/\{\{step\.output\}\}/g, stepOutput) - .replace(/\{\{step\.name\}\}/g, step.name) - .replace(/\{\{run\.id\}\}/g, runId); - }; - - // Execute shell command if present - if (postAction.command) { - try { - const resolvedCmd = interpolatePostAction(postAction.command); - this.log(`[${step.name}] Running postAction command: ${resolvedCmd}`); - const { execSync } = await import('node:child_process'); - execSync(resolvedCmd, { - timeout: step.timeoutMs ?? 30_000, - stdio: 'pipe', - cwd: process.cwd(), - }); - this.log(`[${step.name}] postAction command completed`); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - if (failAction === 'fail') { - throw new Error(`postAction command failed for step "${step.name}": ${msg}`); - } - this.log(`[${step.name}] postAction command warning: ${msg}`); - } - } - - // Execute webhook if present - if (postAction.webhook) { - try { - const webhook = postAction.webhook; - const url = interpolatePostAction(webhook.url); - const method = webhook.method ?? 'POST'; - const body = webhook.body ? interpolatePostAction(webhook.body) : undefined; - const headers: Record = { - 'Content-Type': 'application/json', - ...(webhook.headers ?? {}), - }; - - this.log(`[${step.name}] Running postAction webhook: ${method} ${url}`); - const response = await fetch(url, { method, headers, body }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - this.log(`[${step.name}] postAction webhook completed (${response.status})`); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - if (failAction === 'fail') { - throw new Error(`postAction webhook failed for step "${step.name}": ${msg}`); - } - this.log(`[${step.name}] postAction webhook warning: ${msg}`); - } - } - } - ``` - - Then call this method in two places: - - 1. In the agent step execution path — after the step completes successfully - (after verification passes, before updating the step status to 'completed'), - find where the step output is captured and add: - ```typescript - await this.executePostAction(step, output, runId); - ``` - - 2. In the deterministic step execution path — after the command runs successfully - and output is captured, add the same call: - ```typescript - await this.executePostAction(step, output, runId); - ``` - - Make sure to import the PostAction type if needed. Use the existing import - from './types.js' which already imports WorkflowStep. - - SECURITY NOTE: Interpolated values ({{step.output}}, {{step.name}}, {{run.id}}) - are inserted into shell commands via string replacement. Sanitize and escape - these values before shell execution to prevent command injection. Consider - using environment variables or shell-escaped strings instead of direct - interpolation into the command string. - - Only modify packages/sdk/src/workflows/runner.ts. + Write both files to disk. verification: type: exit_code - # ── Phase 4: Update validator ────────────────────────────────────────── - - name: update-validator - type: agent agent: validator-author - dependsOn: [add-types, read-validator] - timeoutMs: 300000 # 5 min + dependsOn: [read-validator, add-types] task: | - Update the YAML validator to accept and validate postAction fields. + Update packages/sdk/src/workflows/validator.ts to validate postAction. - CURRENT validator.ts: + CURRENT: {{steps.read-validator.output}} - ── What to implement ── - - In packages/sdk/src/workflows/validator.ts, add validation for the - postAction field on workflow steps: - - 1. If a step has a `postAction` field, validate that: - - At least one of `command` or `webhook` is present - - If `webhook` is present, `url` is a non-empty string - - If `webhook.method` is present, it is one of 'POST', 'PUT', 'PATCH' - - If `failAction` is present, it is one of 'fail', 'warn' + Validate that: + - At least one of command or webhook is present + - webhook.url is non-empty string when webhook exists + - webhook.method is POST/PUT/PATCH when present + - failAction is 'fail' or 'warn' when present + - Warn (not error) when both command and webhook are defined - 2. Add warnings (not errors) for: - - postAction with both command and webhook (valid but worth noting) - - Follow the existing validation patterns in the file. Use the same - error/warning collection approach already in use. - - Only modify packages/sdk/src/workflows/validator.ts. + Follow existing validation patterns. Write file to disk. verification: type: exit_code - # ── Phase 5: Write tests ─────────────────────────────────────────────── + # ── Wave A gate ──────────────────────────────────────────────────────── - - name: write-tests - type: agent - agent: test-writer - dependsOn: [implement-runner, update-validator, read-existing-tests] - timeoutMs: 300000 # 5 min - task: | - Write unit tests for the postAction feature. Create a NEW test file: - packages/sdk/src/__tests__/post-action.test.ts - - EXISTING test patterns (for reference on mock setup): - {{steps.read-existing-tests.output}} - - ── Tests to write ── - - Use the same mock infrastructure as workflow-runner.test.ts (mock fetch, - mock Relaycast SDK, mock DB adapter, WorkflowRunner import). + - name: verify-types + type: deterministic + dependsOn: [add-types, update-validator] + command: | + errors=0 + grep -q "PostAction" packages/sdk/src/workflows/types.ts || { echo "FAIL: types.ts missing PostAction"; errors=$((errors+1)); } + grep -q "PostActionWebhook" packages/sdk/src/workflows/types.ts || { echo "FAIL: types.ts missing PostActionWebhook"; errors=$((errors+1)); } + grep -q "postAction" packages/sdk/src/workflows/types.ts || { echo "FAIL: types.ts missing postAction field"; errors=$((errors+1)); } + grep -q "postAction" packages/sdk/src/workflows/schema.json || { echo "FAIL: schema.json missing postAction"; errors=$((errors+1)); } + grep -q "postAction\|post_action" packages/sdk/src/workflows/validator.ts || { echo "FAIL: validator not updated"; errors=$((errors+1)); } + [ $errors -gt 0 ] && exit 1 + echo "Types wave verified" + captureOutput: true + failOnError: true - Create these test cases in a describe('postAction hook') block: + # ── Wave B: runner implementation + tests (parallel, after types) ────── - 1. 'executes shell command after deterministic step completes' - - Config with a deterministic step that has postAction.command - - Verify the command runs (mock execSync) - - Step should succeed + - name: implement-runner + agent: implementer + dependsOn: [read-runner-step-exec, verify-types] + task: | + Implement postAction execution in packages/sdk/src/workflows/runner.ts. - 2. 'executes webhook after agent step completes' - - Config with an agent step that has postAction.webhook - - Verify fetch is called with the correct URL, method, headers, body - - Step should succeed + RUNNER CONTEXT: + {{steps.read-runner-step-exec.output}} - 3. 'interpolates {{step.output}}, {{step.name}}, {{run.id}} in webhook body' - - Verify the interpolation replaces placeholders in URL and body + Add a private `executePostAction(step, stepOutput, runId)` method that: + 1. Returns early if no postAction defined + 2. Interpolates {{step.output}}, {{step.name}}, {{run.id}} in command/webhook strings + 3. Executes shell command via execSync with timeout (30s default) + 4. Executes webhook via fetch with proper error handling + 5. On failure: if failAction='fail' throws, if failAction='warn' (default) logs and continues - 4. 'failAction: warn logs warning and continues on command failure' - - Mock execSync to throw - - Step should still succeed (not fail the workflow) + Call executePostAction after step completes successfully — both agent and deterministic paths. - 5. 'failAction: fail throws and fails the step on command failure' - - Mock execSync to throw - - postAction has failAction: 'fail' - - Step should fail + SECURITY: Shell-escape interpolated values to prevent command injection. - 6. 'failAction: warn logs warning and continues on webhook failure' - - Mock fetch to return a 500 response for the webhook URL - - Step should still succeed + Write file to disk. + verification: + type: exit_code - 7. 'skips postAction when not defined on step' - - A step with no postAction field - - Verify no extra execSync or fetch calls + - name: write-tests + agent: test-writer + dependsOn: [read-existing-tests, verify-types] + task: | + Write tests in a NEW file: packages/sdk/src/__tests__/post-action.test.ts - 8. 'executes both command and webhook when both are defined' - - postAction has both command and webhook - - Verify both execute in order + TEST PATTERNS: + {{steps.read-existing-tests.output}} - Use vitest (describe, it, expect, vi) and the same patterns as the - existing test file. Make sure mocks are properly set up and cleaned up. + Test cases (use vitest — describe, it, expect, vi): + 1. Executes shell command after deterministic step completes + 2. Executes webhook after agent step completes + 3. Interpolates {{step.output}}, {{step.name}}, {{run.id}} in webhook body + 4. failAction: 'warn' logs warning and continues on command failure + 5. failAction: 'fail' throws and fails the step + 6. failAction: 'warn' continues on webhook 500 response + 7. Skips postAction when not defined + 8. Executes both command and webhook when both defined + + Mock execSync and fetch. Follow existing test patterns. + Write file to disk. verification: type: exit_code - # ── Phase 6: Type-check ──────────────────────────────────────────────── + # ════════════════════════════════════════════════════════════════════════ + # PHASE 3: Verification + fix + review + # ════════════════════════════════════════════════════════════════════════ - - name: type-check + - name: verify-impl type: deterministic - dependsOn: [implement-runner, update-validator, write-tests] - command: > - cd packages/sdk && - TSC_OUT=$(npx tsc --noEmit 2>&1); TSC_EXIT=$?; - echo "$TSC_OUT" | tail -30; - if [ $TSC_EXIT -eq 0 ]; then echo "TYPE_CHECK_PASSED"; else echo "TYPE_CHECK_FAILED"; fi + dependsOn: [implement-runner, write-tests] + command: | + errors=0 + grep -q "executePostAction" packages/sdk/src/workflows/runner.ts || { echo "FAIL: runner missing executePostAction"; errors=$((errors+1)); } + grep -q "postAction" packages/sdk/src/workflows/runner.ts || { echo "FAIL: runner doesn't reference postAction"; errors=$((errors+1)); } + test -f packages/sdk/src/__tests__/post-action.test.ts || { echo "FAIL: test file not created"; errors=$((errors+1)); } + [ $errors -gt 0 ] && exit 1 + echo "Implementation verified" captureOutput: true - failOnError: false + failOnError: true - # ── Phase 7: Run tests ───────────────────────────────────────────────── + - name: typecheck + type: deterministic + dependsOn: [verify-impl] + command: | + cd packages/sdk && npx tsc --noEmit 2>&1 | tail -30 + captureOutput: true + failOnError: false - name: run-tests type: deterministic - dependsOn: [type-check] - command: >- - cd packages/sdk && - TEST_OUT=$(npx vitest run 2>&1); TEST_EXIT=$?; - echo "$TEST_OUT" | tail -80; - echo "EXIT:$TEST_EXIT" + dependsOn: [typecheck] + command: | + cd packages/sdk && npx vitest run 2>&1 | tail -60 captureOutput: true failOnError: false - # ── Phase 8: Fix failures ────────────────────────────────────────────── - - - name: fix-if-broken - type: agent + - name: fix-failures agent: fixer - dependsOn: [run-tests, type-check] - timeoutMs: 300000 # 5 min + dependsOn: [run-tests, typecheck] task: | - Review the type-check and test results. Fix any failures. + Fix any type or test failures. - Type-check: - {{steps.type-check.output}} + Typecheck: + {{steps.typecheck.output}} - Test run: + Tests: {{steps.run-tests.output}} - If both show PASSED / all tests pass, output: FIX_DONE:none - - Otherwise: - - For TypeScript errors: fix the relevant source files - - For failing tests: fix the tests or the implementation - - Do NOT change the intended behavior — only fix syntax/type/mock issues - - Do NOT weaken or remove test assertions - - Files you may edit: - - packages/sdk/src/workflows/types.ts - - packages/sdk/src/workflows/runner.ts - - packages/sdk/src/workflows/validator.ts - - packages/sdk/src/__tests__/post-action.test.ts + If no failures, just confirm all clear. + Otherwise fix the issues in the relevant files and re-run to verify. + Do not weaken or remove test assertions. + Write files to disk. verification: type: exit_code - maxIterations: 2 - - # ── Phase 9: Final test run ──────────────────────────────────────────── + retries: 2 - name: final-tests type: deterministic - dependsOn: [fix-if-broken] - command: >- - cd packages/sdk && - TEST_OUT=$(npx vitest run 2>&1); TEST_EXIT=$?; - echo "$TEST_OUT" | tail -60; - echo "EXIT:$TEST_EXIT" + dependsOn: [fix-failures] + command: | + cd packages/sdk && npx vitest run 2>&1 | tail -40 + exit_code=$? + [ $exit_code -eq 0 ] && echo "ALL_TESTS_PASSED" || echo "TESTS_FAILED" + exit $exit_code captureOutput: true - failOnError: false - - # ── Phase 10: Capture diff for review ────────────────────────────────── + failOnError: true - name: capture-diff type: deterministic dependsOn: [final-tests] - command: > - git add -N packages/sdk/src/__tests__/post-action.test.ts 2>/dev/null; - git diff HEAD -- - packages/sdk/src/workflows/types.ts - packages/sdk/src/workflows/runner.ts - packages/sdk/src/workflows/validator.ts - packages/sdk/src/workflows/schema.json - packages/sdk/src/__tests__/post-action.test.ts + command: | + git add -N packages/sdk/src/__tests__/post-action.test.ts 2>/dev/null + git diff HEAD -- \ + packages/sdk/src/workflows/types.ts \ + packages/sdk/src/workflows/runner.ts \ + packages/sdk/src/workflows/validator.ts \ + packages/sdk/src/workflows/schema.json \ + packages/sdk/src/__tests__/post-action.test.ts captureOutput: true - failOnError: false - - # ── Phase 11: Code review ────────────────────────────────────────────── - name: review - type: agent agent: reviewer dependsOn: [capture-diff, final-tests] - timeoutMs: 300000 # 5 min task: | Review the postAction hook implementation for issue #500. - Final test result: - {{steps.final-tests.output}} - - Diff: - {{steps.capture-diff.output}} - - Review checklist: - 1. PostAction and PostActionWebhook interfaces match the spec from issue #500 - 2. WorkflowStep correctly includes the optional postAction field - 3. executePostAction handles both shell commands and webhooks - 4. Interpolation replaces {{step.output}}, {{step.name}}, {{run.id}} correctly - 5. failAction: 'warn' logs but does not abort; failAction: 'fail' throws - 6. Shell commands use execSync with a timeout — no unbounded execution - 7. Webhook uses fetch with proper error handling for non-ok responses - 8. No security issues: command injection via interpolated values? - 9. Validator correctly rejects postAction with no command or webhook - 10. Tests cover all happy and error paths - 11. No regressions to existing workflow behavior (steps without postAction) - 12. schema.json updated to match the TypeScript types + Tests: {{steps.final-tests.output}} + + Diff: {{steps.capture-diff.output}} + + Check: PostAction/PostActionWebhook types, executePostAction implementation, + interpolation correctness, failAction handling, command injection prevention, + validator rules, test coverage, no regressions, schema.json consistency. verification: type: exit_code errorHandling: - strategy: continue - maxRetries: 1 - retryDelayMs: 3000 - notifyChannel: post-action-hook - -state: - backend: memory - ttlMs: 7200000 # 2 hours - -trajectories: - enabled: true - autoDecisions: true + strategy: retry + maxRetries: 2 + retryDelayMs: 5000 + notifyChannel: wf-post-action