diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cbe74c614..4b7842e46 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -642,6 +642,7 @@ jobs: # `dependencies` whenever a new internal dep is added. (cd packages/config && npm pack --ignore-scripts --pack-destination "$TARBALLS") (cd packages/github-primitive && npm pack --ignore-scripts --pack-destination "$TARBALLS") + (cd packages/slack-primitive && npm pack --ignore-scripts --pack-destination "$TARBALLS") (cd packages/workflow-types && npm pack --ignore-scripts --pack-destination "$TARBALLS") ls -lh "$TARBALLS" @@ -658,11 +659,12 @@ jobs: BROKER_TGZ=$(ls "$TARBALLS"/agent-relay-broker-${{ matrix.platform }}-*.tgz | head -n1) CONFIG_TGZ=$(ls "$TARBALLS"/agent-relay-config-*.tgz | head -n1) GITHUB_PRIMITIVE_TGZ=$(ls "$TARBALLS"/agent-relay-github-primitive-*.tgz | head -n1) + SLACK_PRIMITIVE_TGZ=$(ls "$TARBALLS"/agent-relay-slack-primitive-*.tgz | head -n1) WORKFLOW_TYPES_TGZ=$(ls "$TARBALLS"/agent-relay-workflow-types-*.tgz | head -n1) - echo "Installing $SDK_TGZ + $BROKER_TGZ + $CONFIG_TGZ + $GITHUB_PRIMITIVE_TGZ + $WORKFLOW_TYPES_TGZ" + echo "Installing $SDK_TGZ + $BROKER_TGZ + $CONFIG_TGZ + $GITHUB_PRIMITIVE_TGZ + $SLACK_PRIMITIVE_TGZ + $WORKFLOW_TYPES_TGZ" npm install --ignore-scripts --no-audit --no-fund \ "$SDK_TGZ" "$BROKER_TGZ" "$CONFIG_TGZ" \ - "$GITHUB_PRIMITIVE_TGZ" "$WORKFLOW_TYPES_TGZ" + "$GITHUB_PRIMITIVE_TGZ" "$SLACK_PRIMITIVE_TGZ" "$WORKFLOW_TYPES_TGZ" ls node_modules/@agent-relay/ - name: Resolver smoke — getBrokerBinaryPath() @@ -714,13 +716,14 @@ jobs: SDK_TGZ=$(ls "$TARBALLS"/agent-relay-sdk-*.tgz | head -n1) CONFIG_TGZ=$(ls "$TARBALLS"/agent-relay-config-*.tgz | head -n1) GITHUB_PRIMITIVE_TGZ=$(ls "$TARBALLS"/agent-relay-github-primitive-*.tgz | head -n1) + SLACK_PRIMITIVE_TGZ=$(ls "$TARBALLS"/agent-relay-slack-primitive-*.tgz | head -n1) WORKFLOW_TYPES_TGZ=$(ls "$TARBALLS"/agent-relay-workflow-types-*.tgz | head -n1) # Install SDK + every internal required dep whose bumped version # isn't on the registry yet, but skip the broker optional deps # entirely. The resolver should return null and spawn() should # throw the clear error. npm install --ignore-scripts --no-audit --no-fund --no-optional \ - "$SDK_TGZ" "$CONFIG_TGZ" "$GITHUB_PRIMITIVE_TGZ" "$WORKFLOW_TYPES_TGZ" + "$SDK_TGZ" "$CONFIG_TGZ" "$GITHUB_PRIMITIVE_TGZ" "$SLACK_PRIMITIVE_TGZ" "$WORKFLOW_TYPES_TGZ" node --input-type=module -e " import { AgentRelayClient } from '@agent-relay/sdk'; try { @@ -756,6 +759,7 @@ jobs: package: - config - github-primitive + - slack-primitive - workflow-types steps: @@ -1250,42 +1254,11 @@ jobs: - name: Update npm for OIDC support run: npm install -g npm@latest - - name: Dry run SDK internal deps - if: github.event.inputs.dry_run == 'true' - run: | - set -euo pipefail - for package in config github-primitive workflow-types; do - echo "Dry run - would publish @agent-relay/${package}" - (cd "packages/${package}" && npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts) - done - - name: Dry run check if: github.event.inputs.dry_run == 'true' working-directory: packages/sdk run: npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts - - name: Publish SDK internal deps to NPM - if: github.event.inputs.dry_run != 'true' - run: | - set -euo pipefail - VERSION="${{ needs.build.outputs.new_version }}" - - publish_if_missing() { - local package="$1" - local name="@agent-relay/${package}" - if npm view "${name}@${VERSION}" version >/dev/null 2>&1; then - echo "✓ ${name}@${VERSION} is already published" - return - fi - - echo "Publishing ${name}@${VERSION}" - (cd "packages/${package}" && npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts) - } - - publish_if_missing config - publish_if_missing workflow-types - publish_if_missing github-primitive - - name: Publish SDK to NPM if: github.event.inputs.dry_run != 'true' working-directory: packages/sdk diff --git a/.github/workflows/verify-publish-sdk.yml b/.github/workflows/verify-publish-sdk.yml index dc07a9eaa..d78ee790d 100644 --- a/.github/workflows/verify-publish-sdk.yml +++ b/.github/workflows/verify-publish-sdk.yml @@ -100,6 +100,7 @@ jobs: "$SPEC" "@agent-relay/config@$VERSION" "@agent-relay/github-primitive@$VERSION" + "@agent-relay/slack-primitive@$VERSION" "@agent-relay/workflow-types@$VERSION" ) diff --git a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json index 2c336b1c1..95d1a7927 100644 --- a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json +++ b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json @@ -313,16 +313,16 @@ "\"update-tests\" completed → - Modified `src/cli/commands/messaging.test.ts` with the three requested test cases.", "\"implement-fix\" completed → Artifact produced: updated `src/cli/commands/messaging.ts`.", "\"fix-build\" completed → Build confirmed clean. Task required no action.", - "\"fix-unit-tests\" completed → Updated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.", + "\"fix-unit-tests\" completed → Updated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.", "\"fix-regressions\" completed → No action taken. The provided full `vitest` run already passes: `50/50` test files and `711/711` tests. No regressions d", "\"update-tests\" completed → - No other files were edited.", "\"fix-build\" completed → Status: no action taken.\n\nThe provided build output ends with `BUILD_EXIT:0`, which indicates the TypeScript build succe", "\"fix-unit-tests\" completed → - Recorded and completed the active `trail` trajectory for this fix", - "\"implement-fixes\" completed → Completed the two requested fixes in [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src", + "\"implement-fixes\" completed → Completed the two requested fixes in [src/cli/commands/messaging.ts](/src", "\"plan\" completed → >7u╭─────────────────────────────────────────────────────╮│ >_ OpenAI Codex (v0.116.0) ││ ", "\"fix-worker-3-step\" completed → - Artifacts produced: none.", "\"fix-worker-2-step\" completed → - No files outside the assigned group were modified.", - "\"fix-worker-1-step\" completed → **Completed**\n- Fixed [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-16b32ead/sr", + "\"fix-worker-1-step\" completed → **Completed**\n- Fixed [src/cli/commands/messaging.ts](/.msd-autofix-16b32ead/sr", "\"plan\" completed → -- INSERT -- ⏵⏵ bypass permissions on (shift+tab to cycle) · PR #708", "\"fix-config\" completed → CHANGES_COMPLETE", "\"fix-sdk\" completed → CHANGES_COMPLETE", diff --git a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md index 28bc485b3..4d550da3f 100644 --- a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md +++ b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md @@ -9,7 +9,7 @@ This batch of trajectories covers a high-volume period (Apr 10 – May 8, 2026) | Question | Decision | Impact | | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | How to forward cloud run resume/start-from flags through the stack? | Forward --resume/--start-from/--previous-run-id from agent-relay CLI through the Cloud API into sandbox SDK env vars | Implemented in src/cli/commands/cloud.ts and packages/cloud/src/types.ts; route + worker payloads + OpenAPI schema all updated; unblocks cloud-side resume. | -| How should the broker mcp-args subcommand obtain its agent token? | Add --register flag that mints its own `at_live_*` token via `register_agent_token`; `agentToken` becomes nullable | src/cli_mcp_args.rs + src/main.rs + docs/reference-cli.md changes; review verdict APPROVE — diff scoped only to flag wiring, no authority functions touched. | +| How should the broker mcp-args subcommand obtain its agent token? | Add --register flag that mints its own `at_live_*` token via `register_agent_token`; `agentToken` becomes nullable | src/cli_mcp_args.rs + src/main.rs + docs/reference-cli.md changes; review verdict APPROVE — diff scoped only to flag wiring, no authority functions touched. | | Where should TypeScript build resolution be pinned to avoid drift? | Pin TypeScript 5.7.3 exact in devDependencies and route every package build through `npx -p typescript@5.7.3 tsc` | Updated package.json across acp-bridge, memory, trajectory, cloud, config, sdk, gateway, hooks, openclaw, policy, telemetry, user-directory. | | How to handle agent-trajectories@0.5.5 narrower exported types breaking SDK build during publish? | Localize compatibility casts at the agent-trajectories adapter boundary; keep workflow metadata shape intact | SDK builds clean under publish-style fresh npm install; package-validation CI now runs full root build to match `npm run build` used at publish. | | How to recover from CloudFront 409 CNAMEAlreadyExists on production deploy? | Switch production SST alias from agentrelay.net to origin.agentrelay.net | sst diff confirmed certificate + CloudFront issued for origin.agentrelay.net; stale agentrelay.net validation resources removed; live apex untouched. | @@ -24,7 +24,7 @@ This batch of trajectories covers a high-volume period (Apr 10 – May 8, 2026) - **Step completion signaled by `Verification passed` + exit=0; legacy `STEP_COMPLETE` marker still observed**: Orchestrator's completion-marker detector accepts either; verification-based is preferred because it ties to actual command exit. (scope: All workflow steps run by orchestrator (workflow-runner).) - **Tests-first ordering inside DAG: write/update test step must precede or run parallel to implement step before verify-impl**: The abandoned fix-inbox-agent-flag run failed at verify-impl because tests weren't yet aligned with the implementation; reordering to update-tests + implement-fix in parallel succeeded. (scope: All DAG workflows that gate on verify-impl.) - **Pin TypeScript exactly (`5.7.3`) and invoke via `npx -p typescript@5.7.3 tsc` in package build scripts**: Publish strips package-lock; ranged versions and bare tsc cause clean-checkout drift. Both pinning the dep and the invocation are required. (scope: Every workspace package's package.json under packages/.) -- **Web-only changes gate the heavy core test, package validation, rust CI, and node compatibility workflows**: Web-only PRs were running unrelated relay/SDK/Rust suites and burning CI minutes. (scope: .github/workflows/\*.yml top-level path filters.) +- **Web-only changes gate the heavy core test, package validation, rust CI, and node compatibility workflows**: Web-only PRs were running unrelated relay/SDK/Rust suites in GitHub Actions and burning CI minutes. (scope: .github/workflows/\*.yml top-level path filters.) - **Autofix swarm: lead writes .msd/autofix-plan.json with up-to-N file-ownership-disjoint groups; workers dispatched by index**: Conflict-free parallel application; workers with no assigned group cleanly no-op. (scope: autofix-swarm-\* workflows triggered by pr_review or review_comment sources.) - **Apply autofix-swarm fixes in the parent git worktree (repo root), not the .relay/workspace cwd copy**: The cwd copy under .relay/workspace is not the git worktree; fixes there never reach the feature branch / PR. (scope: Verifier step in autofix-swarm sessions.) - **In CLI test harnesses using Commander, call `configureOutput({ writeOut, writeErr })` to suppress built-in stderr/stdout**: Missing-argument coverage tests should assert exit behavior without polluting test stderr. (scope: src/cli/commands/messaging.test.ts and other Commander-driven CLI test harnesses.) diff --git a/.trajectories/completed/2026-04/traj_05xg7j388bc4.json b/.trajectories/completed/2026-04/traj_05xg7j388bc4.json index 22cfd015f..9da4aa935 100644 --- a/.trajectories/completed/2026-04/traj_05xg7j388bc4.json +++ b/.trajectories/completed/2026-04/traj_05xg7j388bc4.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "e3853a983feca165c17142f740487172dced606e", diff --git a/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json b/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json index c318fe5db..e24f8cc2a 100644 --- a/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json +++ b/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "6358106e6814adee7ee1d11f8d258ee11c4bed99", diff --git a/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json b/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json index 2bd07ba35..302a35480 100644 --- a/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json +++ b/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json @@ -66,7 +66,7 @@ { "ts": 1776106058085, "type": "completion-evidence", - "content": "\"update-tests\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0; signals=0, Updated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts:430) only., OpenAI Codex v0.116.0 (research preview), Verification passed, **[update-tests] Output:**; channel=**[update-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts:4; files=modified:src/cli/commands/messaging.test.ts; exit=0)", + "content": "\"update-tests\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0; signals=0, Updated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts:430) only., OpenAI Codex v0.116.0 (research preview), Verification passed, **[update-tests] Output:**; channel=**[update-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts:4; files=modified:src/cli/commands/messaging.test.ts; exit=0)", "raw": { "stepName": "update-tests", "completionMode": "verification", @@ -75,13 +75,13 @@ "summary": "5 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0", "signals": [ "0", - "Updated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts:430) only.", + "Updated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts:430) only.", "OpenAI Codex v0.116.0 (research preview)", "Verification passed", "**[update-tests] Output:**" ], "channelPosts": [ - "**[update-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts:4" + "**[update-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts:4" ], "files": ["modified:src/cli/commands/messaging.test.ts"], "exitCode": 0 diff --git a/.trajectories/completed/2026-04/traj_222ha5671idc.json b/.trajectories/completed/2026-04/traj_222ha5671idc.json index 7a256f658..a52c71fe4 100644 --- a/.trajectories/completed/2026-04/traj_222ha5671idc.json +++ b/.trajectories/completed/2026-04/traj_222ha5671idc.json @@ -400,6 +400,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-04/traj_3b3p1z4y7qlo.json b/.trajectories/completed/2026-04/traj_3b3p1z4y7qlo.json index 1d1fa44f3..e5b545e87 100644 --- a/.trajectories/completed/2026-04/traj_3b3p1z4y7qlo.json +++ b/.trajectories/completed/2026-04/traj_3b3p1z4y7qlo.json @@ -144,6 +144,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json b/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json index e2eff1d89..05d5e0aa4 100644 --- a/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json +++ b/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "8e7f0121fd092f78884836682d71aef6e52998a4", diff --git a/.trajectories/completed/2026-04/traj_530xmbfeljyb.json b/.trajectories/completed/2026-04/traj_530xmbfeljyb.json index 0c1a3ffbb..494b9cab0 100644 --- a/.trajectories/completed/2026-04/traj_530xmbfeljyb.json +++ b/.trajectories/completed/2026-04/traj_530xmbfeljyb.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "e3853a983feca165c17142f740487172dced606e", diff --git a/.trajectories/completed/2026-04/traj_703m7sqyq89t.json b/.trajectories/completed/2026-04/traj_703m7sqyq89t.json index 4d04ebe8e..76c93e21b 100644 --- a/.trajectories/completed/2026-04/traj_703m7sqyq89t.json +++ b/.trajectories/completed/2026-04/traj_703m7sqyq89t.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "6358106e6814adee7ee1d11f8d258ee11c4bed99", diff --git a/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json b/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json index 682bc1a66..eb2390261 100644 --- a/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json +++ b/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "e3853a983feca165c17142f740487172dced606e", diff --git a/.trajectories/completed/2026-04/traj_9tt55is74dq5.json b/.trajectories/completed/2026-04/traj_9tt55is74dq5.json index 4b1cd540f..965b297e5 100644 --- a/.trajectories/completed/2026-04/traj_9tt55is74dq5.json +++ b/.trajectories/completed/2026-04/traj_9tt55is74dq5.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "d283ddd26980fa2f9adac2ab793c6c2322d25049", diff --git a/.trajectories/completed/2026-04/traj_abjovknvcijv.json b/.trajectories/completed/2026-04/traj_abjovknvcijv.json index 7bdb37b2d..259bbde22 100644 --- a/.trajectories/completed/2026-04/traj_abjovknvcijv.json +++ b/.trajectories/completed/2026-04/traj_abjovknvcijv.json @@ -10,7 +10,7 @@ "chapters": [], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "6358106e6814adee7ee1d11f8d258ee11c4bed99", diff --git a/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json b/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json index df0165175..8bcdb00ee 100644 --- a/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json +++ b/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "e3853a983feca165c17142f740487172dced606e", diff --git a/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json b/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json index fa283ffaf..f6939ecee 100644 --- a/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json +++ b/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "6358106e6814adee7ee1d11f8d258ee11c4bed99", diff --git a/.trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json b/.trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json index a8cbce10a..5c97fd196 100644 --- a/.trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json +++ b/.trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json @@ -124,7 +124,7 @@ { "ts": 1776110030609, "type": "completion-evidence", - "content": "\"update-tests\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 2 file change(s), exit=0; signals=0, **Changed**, OpenAI Codex v0.116.0 (research preview), Verification passed, **[update-tests] Output:**; channel=**[update-tests] Output:**\n```\n**Changed**\nUpdated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messagi; files=modified:src/cli/commands/messaging.test.ts, modified:src/cli/commands/messaging.ts; exit=0)", + "content": "\"update-tests\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 2 file change(s), exit=0; signals=0, **Changed**, OpenAI Codex v0.116.0 (research preview), Verification passed, **[update-tests] Output:**; channel=**[update-tests] Output:**\n```\n**Changed**\nUpdated [src/cli/commands/messaging.test.ts](/src/cli/commands/messagi; files=modified:src/cli/commands/messaging.test.ts, modified:src/cli/commands/messaging.ts; exit=0)", "raw": { "stepName": "update-tests", "completionMode": "verification", @@ -139,7 +139,7 @@ "**[update-tests] Output:**" ], "channelPosts": [ - "**[update-tests] Output:**\n```\n**Changed**\nUpdated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messagi" + "**[update-tests] Output:**\n```\n**Changed**\nUpdated [src/cli/commands/messaging.test.ts](/src/cli/commands/messagi" ], "files": [ "modified:src/cli/commands/messaging.test.ts", @@ -153,7 +153,7 @@ { "ts": 1776110030610, "type": "finding", - "content": "\"update-tests\" completed → **Changed**\n\nUpdated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/comman", + "content": "\"update-tests\" completed → **Changed**\n\nUpdated [src/cli/commands/messaging.test.ts](/src/cli/comman", "significance": "medium" }, { @@ -179,7 +179,7 @@ { "ts": 1776110031372, "type": "finding", - "content": "\"implement-dm-history\" completed → Updated [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.ts) o", + "content": "\"implement-dm-history\" completed → Updated [src/cli/commands/messaging.ts](/src/cli/commands/messaging.ts) o", "significance": "medium" } ] @@ -335,7 +335,7 @@ { "ts": 1776110225619, "type": "completion-evidence", - "content": "\"fix-regressions\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), exit=0; signals=0, **Result**, OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-regressions] Output:**; channel=**[fix-regressions] Output:**\n```\n**Result**\n- Ran `npx vitest run` in `/Users/khaliqgant/Projects/AgentWorkforce/relay`.\n- Full suite passed: `50` test files, ; exit=0)", + "content": "\"fix-regressions\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), exit=0; signals=0, **Result**, OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-regressions] Output:**; channel=**[fix-regressions] Output:**\n```\n**Result**\n- Ran `npx vitest run` in ``.\n- Full suite passed: `50` test files, ; exit=0)", "raw": { "stepName": "fix-regressions", "completionMode": "verification", @@ -350,7 +350,7 @@ "**[fix-regressions] Output:**" ], "channelPosts": [ - "**[fix-regressions] Output:**\n```\n**Result**\n- Ran `npx vitest run` in `/Users/khaliqgant/Projects/AgentWorkforce/relay`.\n- Full suite passed: `50` test files, " + "**[fix-regressions] Output:**\n```\n**Result**\n- Ran `npx vitest run` in ``.\n- Full suite passed: `50` test files, " ], "exitCode": 0 } @@ -396,6 +396,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json b/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json index 2d99eda7d..f3a3cd407 100644 --- a/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json +++ b/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-16b32ead", + "projectId": "/.msd-autofix-16b32ead", "tags": [], "_trace": { "startRef": "5fb602a448d5eceabbb3380837a1b0d9eec2ac5f", diff --git a/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json b/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json index 6f0305e06..e84c39502 100644 --- a/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json +++ b/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "6358106e6814adee7ee1d11f8d258ee11c4bed99", diff --git a/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json b/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json index 90e529056..153ed8606 100644 --- a/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json +++ b/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "32f7a699308b9fa3788a13319989f0c2b249f84d", diff --git a/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json b/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json index 28910a4d4..4de7fa3ef 100644 --- a/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json +++ b/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "6358106e6814adee7ee1d11f8d258ee11c4bed99", diff --git a/.trajectories/completed/2026-04/traj_qb54w47qwod6.json b/.trajectories/completed/2026-04/traj_qb54w47qwod6.json index d6922a7ad..6d14f729e 100644 --- a/.trajectories/completed/2026-04/traj_qb54w47qwod6.json +++ b/.trajectories/completed/2026-04/traj_qb54w47qwod6.json @@ -150,7 +150,7 @@ { "ts": 1776111497514, "type": "completion-evidence", - "content": "\"implement-fix\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 2 file change(s), exit=0; signals=0, **Result**, OpenAI Codex v0.116.0 (research preview), Verification passed, **[implement-fix] Output:**; channel=**[implement-fix] Output:**\n```\n**Result**\nUpdated [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.ts; files=modified:src/cli/commands/messaging.test.ts, modified:src/cli/commands/messaging.ts; exit=0)", + "content": "\"implement-fix\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 2 file change(s), exit=0; signals=0, **Result**, OpenAI Codex v0.116.0 (research preview), Verification passed, **[implement-fix] Output:**; channel=**[implement-fix] Output:**\n```\n**Result**\nUpdated [src/cli/commands/messaging.ts](/src/cli/commands/messaging.ts; files=modified:src/cli/commands/messaging.test.ts, modified:src/cli/commands/messaging.ts; exit=0)", "raw": { "stepName": "implement-fix", "completionMode": "verification", @@ -165,7 +165,7 @@ "**[implement-fix] Output:**" ], "channelPosts": [ - "**[implement-fix] Output:**\n```\n**Result**\nUpdated [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.ts" + "**[implement-fix] Output:**\n```\n**Result**\nUpdated [src/cli/commands/messaging.ts](/src/cli/commands/messaging.ts" ], "files": [ "modified:src/cli/commands/messaging.test.ts", @@ -304,7 +304,7 @@ { "ts": 1776111851445, "type": "completion-evidence", - "content": "\"fix-unit-tests\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0; signals=0, Updated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts): in `createHarness`, the test `Command` now uses `configureOutput({ writeOut, writeErr })` to suppress Commander’s built-in stderr/stdout during the in, OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-unit-tests] Output:**; channel=**[fix-unit-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts; files=modified:src/cli/commands/messaging.test.ts; exit=0)", + "content": "\"fix-unit-tests\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0; signals=0, Updated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts): in `createHarness`, the test `Command` now uses `configureOutput({ writeOut, writeErr })` to suppress Commander’s built-in stderr/stdout during the in, OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-unit-tests] Output:**; channel=**[fix-unit-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts; files=modified:src/cli/commands/messaging.test.ts; exit=0)", "raw": { "stepName": "fix-unit-tests", "completionMode": "verification", @@ -313,13 +313,13 @@ "summary": "5 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0", "signals": [ "0", - "Updated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts): in `createHarness`, the test `Command` now uses `configureOutput({ writeOut, writeErr })` to suppress Commander’s built-in stderr/stdout during the in", + "Updated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts): in `createHarness`, the test `Command` now uses `configureOutput({ writeOut, writeErr })` to suppress Commander’s built-in stderr/stdout during the in", "OpenAI Codex v0.116.0 (research preview)", "Verification passed", "**[fix-unit-tests] Output:**" ], "channelPosts": [ - "**[fix-unit-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.test.ts" + "**[fix-unit-tests] Output:**\n```\nUpdated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.test.ts" ], "files": ["modified:src/cli/commands/messaging.test.ts"], "exitCode": 0 @@ -330,7 +330,7 @@ { "ts": 1776111851445, "type": "finding", - "content": "\"fix-unit-tests\" completed → Updated [src/cli/commands/messaging.test.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src/cli/commands/messaging.", + "content": "\"fix-unit-tests\" completed → Updated [src/cli/commands/messaging.test.ts](/src/cli/commands/messaging.", "significance": "medium" } ] @@ -399,6 +399,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json b/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json index 32aa71b79..e98ee43b6 100644 --- a/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json +++ b/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json @@ -49,7 +49,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/will/Projects/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "8e7f0121fd092f78884836682d71aef6e52998a4", diff --git a/.trajectories/completed/2026-04/traj_tjadoebpscps.json b/.trajectories/completed/2026-04/traj_tjadoebpscps.json index 7b23e51c4..f65637faf 100644 --- a/.trajectories/completed/2026-04/traj_tjadoebpscps.json +++ b/.trajectories/completed/2026-04/traj_tjadoebpscps.json @@ -64,6 +64,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json b/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json index 7246659bb..4c6f0f00c 100644 --- a/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json +++ b/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json @@ -38,7 +38,7 @@ ], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "e3853a983feca165c17142f740487172dced606e", diff --git a/.trajectories/completed/2026-04/traj_ui5omrgz819d.json b/.trajectories/completed/2026-04/traj_ui5omrgz819d.json index eae2fa9cb..7bedbe736 100644 --- a/.trajectories/completed/2026-04/traj_ui5omrgz819d.json +++ b/.trajectories/completed/2026-04/traj_ui5omrgz819d.json @@ -294,6 +294,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json b/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json index 66a4e967b..51a1424af 100644 --- a/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json +++ b/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json @@ -10,7 +10,7 @@ "chapters": [], "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [], "_trace": { "startRef": "d283ddd26980fa2f9adac2ab793c6c2322d25049", diff --git a/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json b/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json index e2aaec804..66026a45b 100644 --- a/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json +++ b/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json @@ -266,7 +266,7 @@ { "ts": 1776073582644, "type": "completion-evidence", - "content": "\"fix-worker-1-step\" verification-based completion — Verification passed (6 signal(s), 1 relevant channel post(s), 3 file change(s), exit=0; signals=**Completed**, OpenAI Codex v0.116.0 (research preview), OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-worker-1-step] Output:**, **[fix-worker-1-step] Output:**; channel=**[fix-worker-1-step] Output:**\n```\n**Completed**\n- Fixed [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-16b32ead/src/cl; files=modified:src/cli/commands/messaging.ts, created:workflows/fix-history-inbox-v2.js, modified:workflows/fix-history-inbox-v2.ts; exit=0)", + "content": "\"fix-worker-1-step\" verification-based completion — Verification passed (6 signal(s), 1 relevant channel post(s), 3 file change(s), exit=0; signals=**Completed**, OpenAI Codex v0.116.0 (research preview), OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-worker-1-step] Output:**, **[fix-worker-1-step] Output:**; channel=**[fix-worker-1-step] Output:**\n```\n**Completed**\n- Fixed [src/cli/commands/messaging.ts](/.msd-autofix-16b32ead/src/cl; files=modified:src/cli/commands/messaging.ts, created:workflows/fix-history-inbox-v2.js, modified:workflows/fix-history-inbox-v2.ts; exit=0)", "raw": { "stepName": "fix-worker-1-step", "completionMode": "verification", @@ -282,7 +282,7 @@ "**[fix-worker-1-step] Output:**" ], "channelPosts": [ - "**[fix-worker-1-step] Output:**\n```\n**Completed**\n- Fixed [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-16b32ead/src/cl" + "**[fix-worker-1-step] Output:**\n```\n**Completed**\n- Fixed [src/cli/commands/messaging.ts](/.msd-autofix-16b32ead/src/cl" ], "files": [ "modified:src/cli/commands/messaging.ts", @@ -297,7 +297,7 @@ { "ts": 1776073582644, "type": "finding", - "content": "\"fix-worker-1-step\" completed → **Completed**\n- Fixed [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-16b32ead/sr", + "content": "\"fix-worker-1-step\" completed → **Completed**\n- Fixed [src/cli/commands/messaging.ts](/.msd-autofix-16b32ead/sr", "significance": "medium" } ] diff --git a/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json index fc923eab3..b8c5beff6 100644 --- a/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json +++ b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json @@ -186,7 +186,7 @@ "question": "Handled PR 747 feedback in tracked parent worktree", "chosen": "Handled PR 747 feedback in tracked parent worktree", "alternatives": [], - "reasoning": "The cwd copy under .relay/workspace is not the git worktree; fixes must be applied to /Users/khaliqgant/Projects/AgentWorkforce/relay so the feature branch and PR contain them." + "reasoning": "The cwd copy under .relay/workspace is not the git worktree; fixes must be applied to so the feature branch and PR contain them." }, "significance": "high" }, diff --git a/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.md b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.md index 28347d18d..bea5cfdd5 100644 --- a/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.md +++ b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.md @@ -31,7 +31,7 @@ merged ### Handled PR 747 feedback in tracked parent worktree - **Chose:** Handled PR 747 feedback in tracked parent worktree -- **Reasoning:** The cwd copy under .relay/workspace is not the git worktree; fixes must be applied to /Users/khaliqgant/Projects/AgentWorkforce/relay so the feature branch and PR contain them. +- **Reasoning:** The cwd copy under .relay/workspace is not the git worktree; fixes must be applied to so the feature branch and PR contain them. --- diff --git a/.trajectories/completed/2026-05/traj_2tqxnib25omk.json b/.trajectories/completed/2026-05/traj_2tqxnib25omk.json index c039fd263..2d15c315a 100644 --- a/.trajectories/completed/2026-05/traj_2tqxnib25omk.json +++ b/.trajectories/completed/2026-05/traj_2tqxnib25omk.json @@ -44,7 +44,7 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows", + "projectId": "", "tags": [], "_trace": { "startRef": "65ce978ce0fb0b306b11df66c534dc167a3630be", diff --git a/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json b/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json index d74b0ba68..38be8333e 100644 --- a/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json +++ b/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json @@ -283,6 +283,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-05/traj_60qc24ufr96g.json b/.trajectories/completed/2026-05/traj_60qc24ufr96g.json index 5c7f342b2..1ba4e823d 100644 --- a/.trajectories/completed/2026-05/traj_60qc24ufr96g.json +++ b/.trajectories/completed/2026-05/traj_60qc24ufr96g.json @@ -44,7 +44,7 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows", + "projectId": "", "tags": [], "_trace": { "startRef": "10f1bfb80e177fba0ca80c6be4cb4e5320344306", diff --git a/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.json b/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.json new file mode 100644 index 000000000..f8e9af2fa --- /dev/null +++ b/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.json @@ -0,0 +1,142 @@ +{ + "id": "traj_6ujzpx82gqs9", + "version": 1, + "task": { + "title": "ricky-slack-primitive-implementation-workflow-status-r-workflow", + "description": "# Slack Primitive — Implementation Workflow\n\n**Status**: Ready\n**Date**: 2026-05-08\n**Design spec**: [`specs/slack-primitive.md`](./slack-primitive.md)\n**Runtime**: local\n\nThis is the implementation prompt for ricky. The full design lives in `specs/slack-primitive.md`. This file exists so ricky has an unambiguous, local-only generation target without having to disambiguate the design doc's runtime-selection discussion.\n\n## Goal\n\nImplement the `packages/slack-primitive` package as described in the design spec. Mirror the layout of `packages/github-primitive` 1:1.\n\n## Scope (Phase A of the design spec)\n\nPhase A only — postMessage + resolveUser + resolveChannel, with the local Web API runtime. Do not implement askQuestion, the Nango proxy transport, or interactive Block Kit forms in this pass.\n\nConcretely:\n\n1. Create `packages/slack-primitive/` with `src/index.ts`, `src/types.ts`, `src/client.ts`, `src/workflow-step.ts`, `src/local-runtime.ts`, `src/adapter.ts`, and `src/actions/{post-message,resolve-user,resolve-channel}.ts`.\n2. Wire `SLACK_BOT_TOKEN` env-var auth in `local-runtime.ts`. Throw `SlackPostBackError('auth_token_missing')` if absent.\n3. Implement `createSlackStep` with `action: 'postMessage'`, supporting `channel`, `text`, `threadTs`, `mentions`, `unfurl`, and `{{steps.X.output.path}}` templating.\n4. Mention resolution: `@email@example.com` → `users.lookupByEmail`; bare handle `@khaliq` → user-cache lookup; raw user IDs pass through. Unresolved mentions are a soft error (logged on step output, message still posts).\n5. Channel resolution: `#name` → `conversations.list` + match; channel IDs pass through.\n6. Add an example workflow at `packages/slack-primitive/examples/notify-on-pr.ts` that posts a one-line PR-opened announcement (paired with `github-primitive`'s `createPR` step).\n7. Add unit tests in `packages/slack-primitive/src/__tests__/` covering: token-missing error, channel name resolution, mention resolution success and soft-fail, `{{steps.X.output}}` templating substitution.\n\n## Constraints\n\n- Runtime: local only. Do not generate the alternate-runtime adapter, the Nango proxy code, or the fallback-transport code in this pass — those land in later phases described in the design spec.\n- Use `@slack/web-api` as the underlying SDK.\n- TypeScript ES modules, follow the conventions in `.claude/rules/typescript.md`.\n- Match the public-API shape of `packages/github-primitive` so a developer who learned one can read the other in five minutes.\n- Do not modify `packages/github-primitive`. Do not modify the design spec.\n\n## Acceptance gates\n\n1. `pnpm -F slack-primitive build` passes.\n2. `pnpm -F slack-primitive test` passes with the unit tests above green.\n3. `examples/notify-on-pr.ts` type-checks against the rest of the SDK.\n4. A workflow that imports `createSlackStep` and posts to a real channel succeeds when `SLACK_BOT_TOKEN` is set and the bot is invited to the channel. (Manual smoke test — document the steps in `examples/README.md`.)\n\n## Out of scope\n\n- askQuestion (Phase B in the design spec).\n- The alternate-runtime adapter and its transports (Phase A's second half + Phase C in the design spec).\n- Interactive Block Kit, addReaction, updateMessage, replyToThread (Phase C).\n- Workflow runner schema changes for askQuestion audit trail (tracked in issue #825).", + "source": { + "system": "workflow-runner", + "id": "d81727f7b43c235969aa737b" + } + }, + "status": "completed", + "startedAt": "2026-05-08T16:06:54.844Z", + "completedAt": "2026-05-08T16:18:16.119Z", + "agents": [ + { + "name": "orchestrator", + "role": "workflow-runner", + "joinedAt": "2026-05-08T16:06:54.844Z" + }, + { + "name": "lead-claude", + "role": "specialist", + "joinedAt": "2026-05-08T16:07:04.139Z" + }, + { + "name": "impl-primary-codex", + "role": "specialist", + "joinedAt": "2026-05-08T16:08:51.238Z" + } + ], + "chapters": [ + { + "id": "chap_qhpc3hdibz16", + "title": "Planning", + "agentName": "orchestrator", + "startedAt": "2026-05-08T16:06:54.844Z", + "endedAt": "2026-05-08T16:07:04.141Z", + "events": [ + { + "ts": 1778256414845, + "type": "note", + "content": "Purpose: # Slack Primitive — Implementation Workflow\n\n**Status**: Ready\n**Date**: 2026-05-08\n**Design spec**: [`specs/slack-primitive.md`](./slack-primitive.md)\n**Runtime**: local\n\nThis is the implementation prompt for ricky. The full design lives in `specs/slack-primitive.md`. This file exists so ricky has an unambiguous, local-only generation target without having to disambiguate the design doc's runtime-selection discussion.\n\n## Goal\n\nImplement the `packages/slack-primitive` package as described in the design spec. Mirror the layout of `packages/github-primitive` 1:1.\n\n## Scope (Phase A of the design spec)\n\nPhase A only — postMessage + resolveUser + resolveChannel, with the local Web API runtime. Do not implement askQuestion, the Nango proxy transport, or interactive Block Kit forms in this pass.\n\nConcretely:\n\n1. Create `packages/slack-primitive/` with `src/index.ts`, `src/types.ts`, `src/client.ts`, `src/workflow-step.ts`, `src/local-runtime.ts`, `src/adapter.ts`, and `src/actions/{post-message,resolve-user,resolve-channel}.ts`.\n2. Wire `SLACK_BOT_TOKEN` env-var auth in `local-runtime.ts`. Throw `SlackPostBackError('auth_token_missing')` if absent.\n3. Implement `createSlackStep` with `action: 'postMessage'`, supporting `channel`, `text`, `threadTs`, `mentions`, `unfurl`, and `{{steps.X.output.path}}` templating.\n4. Mention resolution: `@email@example.com` → `users.lookupByEmail`; bare handle `@khaliq` → user-cache lookup; raw user IDs pass through. Unresolved mentions are a soft error (logged on step output, message still posts).\n5. Channel resolution: `#name` → `conversations.list` + match; channel IDs pass through.\n6. Add an example workflow at `packages/slack-primitive/examples/notify-on-pr.ts` that posts a one-line PR-opened announcement (paired with `github-primitive`'s `createPR` step).\n7. Add unit tests in `packages/slack-primitive/src/__tests__/` covering: token-missing error, channel name resolution, mention resolution success and soft-fail, `{{steps.X.output}}` templating substitution.\n\n## Constraints\n\n- Runtime: local only. Do not generate the alternate-runtime adapter, the Nango proxy code, or the fallback-transport code in this pass — those land in later phases described in the design spec.\n- Use `@slack/web-api` as the underlying SDK.\n- TypeScript ES modules, follow the conventions in `.claude/rules/typescript.md`.\n- Match the public-API shape of `packages/github-primitive` so a developer who learned one can read the other in five minutes.\n- Do not modify `packages/github-primitive`. Do not modify the design spec.\n\n## Acceptance gates\n\n1. `pnpm -F slack-primitive build` passes.\n2. `pnpm -F slack-primitive test` passes with the unit tests above green.\n3. `examples/notify-on-pr.ts` type-checks against the rest of the SDK.\n4. A workflow that imports `createSlackStep` and posts to a real channel succeeds when `SLACK_BOT_TOKEN` is set and the bot is invited to the channel. (Manual smoke test — document the steps in `examples/README.md`.)\n\n## Out of scope\n\n- askQuestion (Phase B in the design spec).\n- The alternate-runtime adapter and its transports (Phase A's second half + Phase C in the design spec).\n- Interactive Block Kit, addReaction, updateMessage, replyToThread (Phase C).\n- Workflow runner schema changes for askQuestion audit trail (tracked in issue #825)." + }, + { + "ts": 1778256414845, + "type": "note", + "content": "Approach: 23-step dag workflow — Parsed 23 steps, 22 dependent steps, DAG validated, no cycles" + } + ] + }, + { + "id": "chap_hs80xb02xy5q", + "title": "Execution: lead-plan", + "agentName": "lead-claude", + "startedAt": "2026-05-08T16:07:04.141Z", + "endedAt": "2026-05-08T16:08:51.240Z", + "events": [ + { + "ts": 1778256424142, + "type": "note", + "content": "\"lead-plan\": Plan the workflow execution from the packaged context files", + "raw": { + "agent": "lead-claude" + } + }, + { + "ts": 1778256529665, + "type": "completion-evidence", + "content": "\"lead-plan\" verification-based completion — Verification passed (4 signal(s), 1 relevant channel post(s), 2 file change(s), exit=0; signals=0, Wrote `lead-plan.md` with the required headings (Non-goals, Routing contract, Implementation contract) plus Deliverables and Verification gates per the lead-plan-instructions, and ended with the `GENERATION_LEAD_PLAN_READY` sentinel., GENERATION_LEAD_PLAN_READY, **[lead-plan] Output:**; channel=**[lead-plan] Output:**\n```\nWrote `lead-plan.md` with the required headings (Non-goals, Routing contract, Implementation contract) plus Deliverables and Verific; files=modified:.claude/settings.json, created:.workflow-artifacts/generated/slack-primitive-implementation-workflow-status-r/lead-plan.md; exit=0)", + "raw": { + "stepName": "lead-plan", + "completionMode": "verification", + "reason": "Verification passed", + "evidence": { + "summary": "4 signal(s), 1 relevant channel post(s), 2 file change(s), exit=0", + "signals": [ + "0", + "Wrote `lead-plan.md` with the required headings (Non-goals, Routing contract, Implementation contract) plus Deliverables and Verification gates per the lead-plan-instructions, and ended with the `GENERATION_LEAD_PLAN_READY` sentinel.", + "GENERATION_LEAD_PLAN_READY", + "**[lead-plan] Output:**" + ], + "channelPosts": [ + "**[lead-plan] Output:**\n```\nWrote `lead-plan.md` with the required headings (Non-goals, Routing contract, Implementation contract) plus Deliverables and Verific" + ], + "files": [ + "modified:.claude/settings.json", + "created:.workflow-artifacts/generated/slack-primitive-implementation-workflow-status-r/lead-plan.md" + ], + "exitCode": 0 + } + }, + "significance": "medium" + }, + { + "ts": 1778256529665, + "type": "finding", + "content": "\"lead-plan\" completed → Wrote `lead-plan.md` with the required headings (Non-goals, Routing contract, Implementation contract) plus Deliverables", + "significance": "medium" + } + ] + }, + { + "id": "chap_9t8ry2qhgdc5", + "title": "Execution: implement-artifact", + "agentName": "impl-primary-codex", + "startedAt": "2026-05-08T16:08:51.240Z", + "endedAt": "2026-05-08T16:18:16.119Z", + "events": [ + { + "ts": 1778256531240, + "type": "note", + "content": "\"implement-artifact\": Implement the requested code-writing workflow slice", + "raw": { + "agent": "impl-primary-codex" + } + }, + { + "ts": 1778256652311, + "type": "decision", + "content": "Implement Slack primitive as a local-only package with a thin WebClient adapter: Implement Slack primitive as a local-only package with a thin WebClient adapter", + "raw": { + "question": "Implement Slack primitive as a local-only package with a thin WebClient adapter", + "chosen": "Implement Slack primitive as a local-only package with a thin WebClient adapter", + "alternatives": [], + "reasoning": "The Phase A contract excludes alternate runtimes and Nango, so mirroring github-primitive shape should stop at package/API conventions while keeping routing local via SLACK_BOT_TOKEN." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Implemented Phase A slack-primitive package with local Slack Web API runtime, postMessage/createSlackStep workflow integration, channel and mention resolution, unit tests, example workflow, smoke-test docs, and output manifest.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "tags": [] +} diff --git a/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.md b/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.md new file mode 100644 index 000000000..a2207a45c --- /dev/null +++ b/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.md @@ -0,0 +1,42 @@ +# Trajectory: ricky-slack-primitive-implementation-workflow-status-r-workflow + +> **Status:** ✅ Completed +> **Task:** d81727f7b43c235969aa737b +> **Confidence:** 90% +> **Started:** May 8, 2026 at 06:06 PM +> **Completed:** May 8, 2026 at 06:18 PM + +--- + +## Summary + +Implemented Phase A slack-primitive package with local Slack Web API runtime, postMessage/createSlackStep workflow integration, channel and mention resolution, unit tests, example workflow, smoke-test docs, and output manifest. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Implement Slack primitive as a local-only package with a thin WebClient adapter + +- **Chose:** Implement Slack primitive as a local-only package with a thin WebClient adapter +- **Reasoning:** The Phase A contract excludes alternate runtimes and Nango, so mirroring github-primitive shape should stop at package/API conventions while keeping routing local via SLACK_BOT_TOKEN. + +--- + +## Chapters + +### 1. Planning + +_Agent: orchestrator_ + +### 2. Execution: lead-plan + +_Agent: lead-claude_ + +### 3. Execution: implement-artifact + +_Agent: impl-primary-codex_ + +- Implement Slack primitive as a local-only package with a thin WebClient adapter: Implement Slack primitive as a local-only package with a thin WebClient adapter diff --git a/.trajectories/completed/2026-05/traj_itgr2w8qs3xn.json b/.trajectories/completed/2026-05/traj_itgr2w8qs3xn.json index 9910680bf..3b60f82c9 100644 --- a/.trajectories/completed/2026-05/traj_itgr2w8qs3xn.json +++ b/.trajectories/completed/2026-05/traj_itgr2w8qs3xn.json @@ -44,7 +44,7 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows", + "projectId": "", "tags": [], "_trace": { "startRef": "b2f4cbb8d1117cf0be5ff50d47bfb1d6471c68a0", diff --git a/.trajectories/completed/2026-05/traj_k7njijv51iq4.json b/.trajectories/completed/2026-05/traj_k7njijv51iq4.json new file mode 100644 index 000000000..04349d9a8 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_k7njijv51iq4.json @@ -0,0 +1,151 @@ +{ + "id": "traj_k7njijv51iq4", + "version": 1, + "task": { + "title": "ricky-slack-primitive-implementation-workflow-status-r-workflow", + "description": "# Slack Primitive — Implementation Workflow\n\n**Status**: Ready\n**Date**: 2026-05-08\n**Design spec**: [`specs/slack-primitive.md`](./slack-primitive.md)\n**Runtime**: local\n\nThis is the implementation prompt for ricky. The full design lives in `specs/slack-primitive.md`. This file exists so ricky has an unambiguous, local-only generation target without having to disambiguate the design doc's runtime-selection discussion.\n\n## Goal\n\nImplement the `packages/slack-primitive` package as described in the design spec. Mirror the layout of `packages/github-primitive` 1:1.\n\n## Scope (Phase A of the design spec)\n\nPhase A only — postMessage + resolveUser + resolveChannel, with the local Web API runtime. Do not implement askQuestion, the Nango proxy transport, or interactive Block Kit forms in this pass.\n\nConcretely:\n\n1. Create `packages/slack-primitive/` with `src/index.ts`, `src/types.ts`, `src/client.ts`, `src/workflow-step.ts`, `src/local-runtime.ts`, `src/adapter.ts`, and `src/actions/{post-message,resolve-user,resolve-channel}.ts`.\n2. Wire `SLACK_BOT_TOKEN` env-var auth in `local-runtime.ts`. Throw `SlackPostBackError('auth_token_missing')` if absent.\n3. Implement `createSlackStep` with `action: 'postMessage'`, supporting `channel`, `text`, `threadTs`, `mentions`, `unfurl`, and `{{steps.X.output.path}}` templating.\n4. Mention resolution: `@email@example.com` → `users.lookupByEmail`; bare handle `@khaliq` → user-cache lookup; raw user IDs pass through. Unresolved mentions are a soft error (logged on step output, message still posts).\n5. Channel resolution: `#name` → `conversations.list` + match; channel IDs pass through.\n6. Add an example workflow at `packages/slack-primitive/examples/notify-on-pr.ts` that posts a one-line PR-opened announcement (paired with `github-primitive`'s `createPR` step).\n7. Add unit tests in `packages/slack-primitive/src/__tests__/` covering: token-missing error, channel name resolution, mention resolution success and soft-fail, `{{steps.plan.output}}` templating substitution.\n\n## Constraints\n\n- Runtime: local only. Do not generate the alternate-runtime adapter, the Nango proxy code, or the fallback-transport code in this pass — those land in later phases described in the design spec.\n- Use `@slack/web-api` as the underlying SDK.\n- TypeScript ES modules, follow the conventions in `.claude/rules/typescript.md`.\n- Match the public-API shape of `packages/github-primitive` so a developer who learned one can read the other in five minutes.\n- Do not modify `packages/github-primitive`. Do not modify the design spec.\n\n## Acceptance gates\n\n1. `pnpm -F slack-primitive build` passes.\n2. `pnpm -F slack-primitive test` passes with the unit tests above green.\n3. `examples/notify-on-pr.ts` type-checks against the rest of the SDK.\n4. A workflow that imports `createSlackStep` and posts to a real channel succeeds when `SLACK_BOT_TOKEN` is set and the bot is invited to the channel. (Manual smoke test — document the steps in `examples/README.md`.)\n\n## Out of scope\n\n- askQuestion (Phase B in the design spec).\n- The alternate-runtime adapter and its transports (Phase A's second half + Phase C in the design spec).\n- Interactive Block Kit, addReaction, updateMessage, replyToThread (Phase C).\n- Workflow runner schema changes for askQuestion audit trail (tracked in issue #825).", + "source": { + "system": "workflow-runner", + "id": "eb1b5c00e46de5823ea2438a" + } + }, + "status": "completed", + "startedAt": "2026-05-08T16:18:55.300Z", + "completedAt": "2026-05-08T16:26:03.266Z", + "agents": [ + { + "name": "orchestrator", + "role": "workflow-runner", + "joinedAt": "2026-05-08T16:18:55.300Z" + }, + { + "name": "lead-claude", + "role": "specialist", + "joinedAt": "2026-05-08T16:19:01.725Z" + }, + { + "name": "impl-primary-codex", + "role": "specialist", + "joinedAt": "2026-05-08T16:20:57.219Z" + } + ], + "chapters": [ + { + "id": "chap_smpieai8c7vl", + "title": "Planning", + "agentName": "orchestrator", + "startedAt": "2026-05-08T16:18:55.300Z", + "endedAt": "2026-05-08T16:19:01.726Z", + "events": [ + { + "ts": 1778257135300, + "type": "note", + "content": "Purpose: # Slack Primitive — Implementation Workflow\n\n**Status**: Ready\n**Date**: 2026-05-08\n**Design spec**: [`specs/slack-primitive.md`](./slack-primitive.md)\n**Runtime**: local\n\nThis is the implementation prompt for ricky. The full design lives in `specs/slack-primitive.md`. This file exists so ricky has an unambiguous, local-only generation target without having to disambiguate the design doc's runtime-selection discussion.\n\n## Goal\n\nImplement the `packages/slack-primitive` package as described in the design spec. Mirror the layout of `packages/github-primitive` 1:1.\n\n## Scope (Phase A of the design spec)\n\nPhase A only — postMessage + resolveUser + resolveChannel, with the local Web API runtime. Do not implement askQuestion, the Nango proxy transport, or interactive Block Kit forms in this pass.\n\nConcretely:\n\n1. Create `packages/slack-primitive/` with `src/index.ts`, `src/types.ts`, `src/client.ts`, `src/workflow-step.ts`, `src/local-runtime.ts`, `src/adapter.ts`, and `src/actions/{post-message,resolve-user,resolve-channel}.ts`.\n2. Wire `SLACK_BOT_TOKEN` env-var auth in `local-runtime.ts`. Throw `SlackPostBackError('auth_token_missing')` if absent.\n3. Implement `createSlackStep` with `action: 'postMessage'`, supporting `channel`, `text`, `threadTs`, `mentions`, `unfurl`, and `{{steps.X.output.path}}` templating.\n4. Mention resolution: `@email@example.com` → `users.lookupByEmail`; bare handle `@khaliq` → user-cache lookup; raw user IDs pass through. Unresolved mentions are a soft error (logged on step output, message still posts).\n5. Channel resolution: `#name` → `conversations.list` + match; channel IDs pass through.\n6. Add an example workflow at `packages/slack-primitive/examples/notify-on-pr.ts` that posts a one-line PR-opened announcement (paired with `github-primitive`'s `createPR` step).\n7. Add unit tests in `packages/slack-primitive/src/__tests__/` covering: token-missing error, channel name resolution, mention resolution success and soft-fail, `{{steps.plan.output}}` templating substitution.\n\n## Constraints\n\n- Runtime: local only. Do not generate the alternate-runtime adapter, the Nango proxy code, or the fallback-transport code in this pass — those land in later phases described in the design spec.\n- Use `@slack/web-api` as the underlying SDK.\n- TypeScript ES modules, follow the conventions in `.claude/rules/typescript.md`.\n- Match the public-API shape of `packages/github-primitive` so a developer who learned one can read the other in five minutes.\n- Do not modify `packages/github-primitive`. Do not modify the design spec.\n\n## Acceptance gates\n\n1. `pnpm -F slack-primitive build` passes.\n2. `pnpm -F slack-primitive test` passes with the unit tests above green.\n3. `examples/notify-on-pr.ts` type-checks against the rest of the SDK.\n4. A workflow that imports `createSlackStep` and posts to a real channel succeeds when `SLACK_BOT_TOKEN` is set and the bot is invited to the channel. (Manual smoke test — document the steps in `examples/README.md`.)\n\n## Out of scope\n\n- askQuestion (Phase B in the design spec).\n- The alternate-runtime adapter and its transports (Phase A's second half + Phase C in the design spec).\n- Interactive Block Kit, addReaction, updateMessage, replyToThread (Phase C).\n- Workflow runner schema changes for askQuestion audit trail (tracked in issue #825)." + }, + { + "ts": 1778257135300, + "type": "note", + "content": "Approach: 23-step dag workflow — Parsed 23 steps, 22 dependent steps, DAG validated, no cycles" + } + ] + }, + { + "id": "chap_7cdfm6x2dydd", + "title": "Execution: lead-plan", + "agentName": "lead-claude", + "startedAt": "2026-05-08T16:19:01.726Z", + "endedAt": "2026-05-08T16:20:57.222Z", + "events": [ + { + "ts": 1778257141726, + "type": "note", + "content": "\"lead-plan\": Plan the workflow execution from the packaged context files", + "raw": { + "agent": "lead-claude" + } + }, + { + "ts": 1778257255707, + "type": "completion-evidence", + "content": "\"lead-plan\" verification-based completion — Verification passed (4 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0; signals=0, The lead plan file already exists with content matching all required sections (Non-goals, Routing contract, Implementation contract, Deliverables, Verification gates) and ends with `GENERATION_LEAD_PLAN_READY`. The content is well-aligned with the packaged context: it correctly c, GENERATION_LEAD_PLAN_READY, **[lead-plan] Output:**; channel=**[lead-plan] Output:**\n```\ne content is well-aligned with the packaged context: it correctly cites the acceptance contract, non-goals, deliverables, verificati; files=modified:.claude/settings.json; exit=0)", + "raw": { + "stepName": "lead-plan", + "completionMode": "verification", + "reason": "Verification passed", + "evidence": { + "summary": "4 signal(s), 1 relevant channel post(s), 1 file change(s), exit=0", + "signals": [ + "0", + "The lead plan file already exists with content matching all required sections (Non-goals, Routing contract, Implementation contract, Deliverables, Verification gates) and ends with `GENERATION_LEAD_PLAN_READY`. The content is well-aligned with the packaged context: it correctly c", + "GENERATION_LEAD_PLAN_READY", + "**[lead-plan] Output:**" + ], + "channelPosts": [ + "**[lead-plan] Output:**\n```\ne content is well-aligned with the packaged context: it correctly cites the acceptance contract, non-goals, deliverables, verificati" + ], + "files": ["modified:.claude/settings.json"], + "exitCode": 0 + } + }, + "significance": "medium" + }, + { + "ts": 1778257255707, + "type": "finding", + "content": "\"lead-plan\" completed → The lead plan file already exists with content matching all required sections (Non-goals, Routing contract, Implementati", + "significance": "medium" + } + ] + }, + { + "id": "chap_iif0hj0i7lfs", + "title": "Execution: implement-artifact", + "agentName": "impl-primary-codex", + "startedAt": "2026-05-08T16:20:57.222Z", + "endedAt": "2026-05-08T16:26:03.266Z", + "events": [ + { + "ts": 1778257257222, + "type": "note", + "content": "\"implement-artifact\": Implement the requested code-writing workflow slice", + "raw": { + "agent": "impl-primary-codex" + } + }, + { + "ts": 1778257399777, + "type": "decision", + "content": "Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor: Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor", + "raw": { + "question": "Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor", + "chosen": "Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor", + "alternatives": [], + "reasoning": "The Phase A contract forbids alternate Slack runtimes, while the example includes both createPR and postMessage steps and needs deterministic local execution routing for each integration." + }, + "significance": "high" + }, + { + "ts": 1778257563193, + "type": "reflection", + "content": "Slack primitive package implemented and gates passed with npm workspace equivalents; pnpm command is blocked by repository packageManager enforcement.", + "raw": { + "focalPoints": ["implementation", "verification", "routing"], + "adjustments": "Example now uses an explicit composite executor for GitHub and Slack integrations.", + "confidence": 0.9 + }, + "significance": "high", + "tags": ["focal:implementation", "focal:verification", "focal:routing", "confidence:0.9"] + } + ] + } + ], + "retrospective": { + "summary": "Implemented Phase A packages/slack-primitive with local Slack Web API runtime, postMessage/resolveUser/resolveChannel actions, workflow step helper, tests, example workflow, smoke docs, and output manifest.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "tags": [] +} diff --git a/.trajectories/completed/2026-05/traj_k7njijv51iq4.md b/.trajectories/completed/2026-05/traj_k7njijv51iq4.md new file mode 100644 index 000000000..ba4bd67fc --- /dev/null +++ b/.trajectories/completed/2026-05/traj_k7njijv51iq4.md @@ -0,0 +1,43 @@ +# Trajectory: ricky-slack-primitive-implementation-workflow-status-r-workflow + +> **Status:** ✅ Completed +> **Task:** eb1b5c00e46de5823ea2438a +> **Confidence:** 90% +> **Started:** May 8, 2026 at 06:18 PM +> **Completed:** May 8, 2026 at 06:26 PM + +--- + +## Summary + +Implemented Phase A packages/slack-primitive with local Slack Web API runtime, postMessage/resolveUser/resolveChannel actions, workflow step helper, tests, example workflow, smoke docs, and output manifest. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor + +- **Chose:** Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor +- **Reasoning:** The Phase A contract forbids alternate Slack runtimes, while the example includes both createPR and postMessage steps and needs deterministic local execution routing for each integration. + +--- + +## Chapters + +### 1. Planning + +_Agent: orchestrator_ + +### 2. Execution: lead-plan + +_Agent: lead-claude_ + +### 3. Execution: implement-artifact + +_Agent: impl-primary-codex_ + +- Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor: Kept Slack primitive local-only and fixed the example by routing Slack and GitHub integration steps explicitly through a composite executor +- Slack primitive package implemented and gates passed with npm workspace equivalents; pnpm command is blocked by repository packageManager enforcement. diff --git a/.trajectories/completed/2026-05/traj_lieyyspidhfj.json b/.trajectories/completed/2026-05/traj_lieyyspidhfj.json new file mode 100644 index 000000000..8329155da --- /dev/null +++ b/.trajectories/completed/2026-05/traj_lieyyspidhfj.json @@ -0,0 +1,123 @@ +{ + "id": "traj_lieyyspidhfj", + "version": 1, + "task": { + "title": "Fix PR 823 conflicts checks and comments" + }, + "status": "completed", + "startedAt": "2026-05-09T08:37:17.563Z", + "completedAt": "2026-05-09T08:47:54.686Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-09T08:41:56.118Z" + } + ], + "chapters": [ + { + "id": "chap_ok0epc3ob9m0", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-09T08:41:56.118Z", + "endedAt": "2026-05-09T08:47:54.686Z", + "events": [ + { + "ts": 1778316116119, + "type": "decision", + "content": "Break SDK and Slack primitive package cycle: Break SDK and Slack primitive package cycle", + "raw": { + "question": "Break SDK and Slack primitive package cycle", + "chosen": "Break SDK and Slack primitive package cycle", + "alternatives": [], + "reasoning": "CI failed because turbo detected @agent-relay/sdk and @agent-relay/slack-primitive as cyclic build dependencies; the SDK can depend on Slack primitive, but Slack primitive examples can resolve SDK from the workspace without declaring it as a devDependency." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Fixed PR #823 conflicts by merging origin/main, addressed review comments for Slack primitive behavior and workflow publishing, broke the SDK/Slack primitive turbo cycle, added targeted tests, and verified package builds and Slack checks.", + "approach": "Standard approach", + "confidence": 0.9 + }, + "commits": ["6fd439f1", "bdd8bab7", "2e3d8e9f", "557fca0d", "622ec7c7", "03c174e5", "4814ee3d"], + "filesChanged": [ + ".github/workflows/publish.yml", + ".github/workflows/workflow-reliability.yml", + ".trajectories/active/traj_lieyyspidhfj.json", + ".trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md", + ".trajectories/completed/2026-05/traj_34b1u84b19gz.json", + ".trajectories/completed/2026-05/traj_34b1u84b19gz.md", + ".trajectories/completed/2026-05/traj_6ujzpx82gqs9.json", + ".trajectories/completed/2026-05/traj_6ujzpx82gqs9.md", + ".trajectories/completed/2026-05/traj_bdrlknyl8twj.json", + ".trajectories/completed/2026-05/traj_bdrlknyl8twj.md", + ".trajectories/completed/2026-05/traj_k7njijv51iq4.json", + ".trajectories/completed/2026-05/traj_k7njijv51iq4.md", + ".trajectories/completed/2026-05/traj_tavtex0db4b0.json", + ".trajectories/completed/2026-05/traj_tavtex0db4b0.md", + ".trajectories/index.json", + "CHANGELOG.md", + "package-lock.json", + "package.json", + "packages/acp-bridge/package.json", + "packages/brand/package.json", + "packages/broker-darwin-arm64/package.json", + "packages/broker-darwin-x64/package.json", + "packages/broker-linux-arm64/package.json", + "packages/broker-linux-x64/package.json", + "packages/broker-win32-x64/package.json", + "packages/browser-primitive/package.json", + "packages/cloud/package.json", + "packages/config/package.json", + "packages/credential-proxy/package.json", + "packages/gateway/package.json", + "packages/github-primitive/package.json", + "packages/hooks/package.json", + "packages/memory/package.json", + "packages/openclaw/package.json", + "packages/personas/package.json", + "packages/policy/package.json", + "packages/sdk-py/pyproject.toml", + "packages/sdk/package.json", + "packages/sdk/src/client.ts", + "packages/sdk/src/workflows/README.md", + "packages/sdk/src/workflows/__tests__/workflow-reliability-contract.test.ts", + "packages/sdk/src/workflows/__tests__/workflow-reliability-e2e.test.ts", + "packages/sdk/src/workflows/builder.ts", + "packages/sdk/src/workflows/runner.ts", + "packages/sdk/src/workflows/schema.json", + "packages/sdk/src/workflows/types.ts", + "packages/sdk/tsconfig.build.json", + "packages/sdk/tsconfig.json", + "packages/slack-primitive/examples/README.md", + "packages/slack-primitive/package.json", + "packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts", + "packages/slack-primitive/src/__tests__/post-message.test.ts", + "packages/slack-primitive/src/__tests__/runtime-selection.test.ts", + "packages/slack-primitive/src/__tests__/workflow-step.test.ts", + "packages/slack-primitive/src/actions/post-message.ts", + "packages/slack-primitive/src/actions/resolve-user.ts", + "packages/slack-primitive/src/adapter.ts", + "packages/slack-primitive/src/cloud-relay-runtime.ts", + "packages/slack-primitive/src/noop-runtime.ts", + "packages/slack-primitive/src/types.ts", + "packages/slack-primitive/src/workflow-step.ts", + "packages/telemetry/package.json", + "packages/trajectory/package.json", + "packages/user-directory/package.json", + "packages/utils/package.json", + "packages/workflow-types/package.json", + "src/cli/lib/broker-lifecycle.test.ts", + "src/cli/lib/broker-lifecycle.ts" + ], + "projectId": "", + "tags": [], + "_trace": { + "startRef": "3fccf893a70f4840e3a4b4ea5aa116919e7b2835", + "endRef": "6fd439f19b16b065b8200c3239f86d4dcd8c72df", + "traceId": "cd53f059-915c-4add-bd4e-b929b2dd9ab0" + } +} diff --git a/.trajectories/completed/2026-05/traj_lieyyspidhfj.md b/.trajectories/completed/2026-05/traj_lieyyspidhfj.md new file mode 100644 index 000000000..1259f9ad1 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_lieyyspidhfj.md @@ -0,0 +1,40 @@ +# Trajectory: Fix PR 823 conflicts checks and comments + +> **Status:** ✅ Completed +> **Confidence:** 90% +> **Started:** May 9, 2026 at 10:37 AM +> **Completed:** May 9, 2026 at 10:47 AM + +--- + +## Summary + +Fixed PR #823 conflicts by merging origin/main, addressed review comments for Slack primitive behavior and workflow publishing, broke the SDK/Slack primitive turbo cycle, added targeted tests, and verified package builds and Slack checks. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Break SDK and Slack primitive package cycle + +- **Chose:** Break SDK and Slack primitive package cycle +- **Reasoning:** CI failed because turbo detected @agent-relay/sdk and @agent-relay/slack-primitive as cyclic build dependencies; the SDK can depend on Slack primitive, but Slack primitive examples can resolve SDK from the workspace without declaring it as a devDependency. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Break SDK and Slack primitive package cycle: Break SDK and Slack primitive package cycle + +--- + +## Artifacts + +**Commits:** 6fd439f1, bdd8bab7, 2e3d8e9f, 557fca0d, 622ec7c7, 03c174e5, 4814ee3d +**Files changed:** 68 diff --git a/.trajectories/completed/2026-05/traj_lieyyspidhfj.trace.json b/.trajectories/completed/2026-05/traj_lieyyspidhfj.trace.json new file mode 100644 index 000000000..2e6f3260b --- /dev/null +++ b/.trajectories/completed/2026-05/traj_lieyyspidhfj.trace.json @@ -0,0 +1,1694 @@ +{ + "version": "1.0.0", + "id": "cd53f059-915c-4add-bd4e-b929b2dd9ab0", + "timestamp": "2026-05-09T08:47:54.774Z", + "trajectory": "traj_lieyyspidhfj", + "files": [ + { + "path": ".github/workflows/publish.yml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1254, + "end_line": 1264, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".github/workflows/workflow-reliability.yml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 54, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/active/traj_lieyyspidhfj.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 46, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 9, + "end_line": 15, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 24, + "end_line": 30, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_34b1u84b19gz.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 25, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_34b1u84b19gz.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 14, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_6ujzpx82gqs9.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 142, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_6ujzpx82gqs9.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 42, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_bdrlknyl8twj.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_bdrlknyl8twj.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 31, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_k7njijv51iq4.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 151, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_k7njijv51iq4.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 43, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_tavtex0db4b0.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 53, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/completed/2026-05/traj_tavtex0db4b0.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 33, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": ".trajectories/index.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 47, + "end_line": 53, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 55, + "end_line": 61, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 63, + "end_line": 69, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 71, + "end_line": 77, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 79, + "end_line": 85, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 87, + "end_line": 93, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 95, + "end_line": 101, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 103, + "end_line": 109, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 111, + "end_line": 117, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 119, + "end_line": 132, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 134, + "end_line": 140, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 142, + "end_line": 148, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 150, + "end_line": 156, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 158, + "end_line": 164, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 166, + "end_line": 172, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 174, + "end_line": 180, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 182, + "end_line": 188, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 190, + "end_line": 196, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 198, + "end_line": 211, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 213, + "end_line": 219, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 221, + "end_line": 227, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 229, + "end_line": 235, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 237, + "end_line": 243, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 245, + "end_line": 251, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 282, + "end_line": 328, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "CHANGELOG.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 38, + "end_line": 55, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "package-lock.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 12, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 18, + "end_line": 31, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15593, + "end_line": 15602, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15612, + "end_line": 15649, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15657, + "end_line": 15665, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15675, + "end_line": 15681, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15687, + "end_line": 15693, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15698, + "end_line": 15706, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15709, + "end_line": 15717, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15721, + "end_line": 15731, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15734, + "end_line": 15742, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 15745, + "end_line": 15755, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16514, + "end_line": 16527, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16530, + "end_line": 16541, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16555, + "end_line": 16568, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16600, + "end_line": 16612, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16614, + "end_line": 16620, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16649, + "end_line": 16657, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16660, + "end_line": 16668, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16671, + "end_line": 16679, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 16683, + "end_line": 16689, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 130, + "end_line": 143, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/acp-bridge/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 46, + "end_line": 52, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/brand/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/broker-darwin-arm64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/broker-darwin-x64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/broker-linux-arm64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/broker-linux-x64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/broker-win32-x64/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/browser-primitive/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 38, + "end_line": 44, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/cloud/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 23, + "end_line": 29, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/config/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/credential-proxy/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/gateway/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 23, + "end_line": 29, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/github-primitive/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 32, + "end_line": 38, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/hooks/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 37, + "end_line": 45, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/memory/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/openclaw/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 29, + "end_line": 35, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/personas/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/policy/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk-py/pyproject.toml", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 4, + "end_line": 10, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 149, + "end_line": 158, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 167, + "end_line": 180, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/client.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 269, + "end_line": 313, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/README.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 371, + "end_line": 377, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/__tests__/workflow-reliability-contract.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 16, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 65, + "end_line": 129, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 444, + "end_line": 621, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/__tests__/workflow-reliability-e2e.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 248, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/builder.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 98, + "end_line": 105, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 375, + "end_line": 399, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 450, + "end_line": 457, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/runner.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 397, + "end_line": 424, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 2097, + "end_line": 2131, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 2557, + "end_line": 2566, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 2841, + "end_line": 2849, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 2852, + "end_line": 2858, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 2863, + "end_line": 2869, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 2885, + "end_line": 2893, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 2972, + "end_line": 2978, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 3005, + "end_line": 3011, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 3731, + "end_line": 3737, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 3962, + "end_line": 3968, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 4109, + "end_line": 4213, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 4479, + "end_line": 4485, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 4507, + "end_line": 4517, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 4554, + "end_line": 4573, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 4715, + "end_line": 4735, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 4911, + "end_line": 4919, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 5415, + "end_line": 5452, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/schema.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 932, + "end_line": 938, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/src/workflows/types.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 489, + "end_line": 495, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/tsconfig.build.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 9, + "end_line": 17, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 23, + "end_line": 27, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/sdk/tsconfig.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 7, + "end_line": 18, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/examples/README.md", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 4, + "end_line": 14, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 30, + "end_line": 40, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 8, + "end_line": 20, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 40, + "end_line": 54, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 89, + "end_line": 95, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 129, + "end_line": 135, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/__tests__/post-message.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 4, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 7, + "end_line": 16, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 64, + "end_line": 90, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/__tests__/runtime-selection.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 8, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 55, + "end_line": 61, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 70, + "end_line": 97, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/__tests__/workflow-step.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 55, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/actions/post-message.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 19, + "end_line": 30, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/actions/resolve-user.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 5, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 18, + "end_line": 28, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 47, + "end_line": 86, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/adapter.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 33, + "end_line": 42, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 63, + "end_line": 74, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 110, + "end_line": 119, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 166, + "end_line": 172, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/cloud-relay-runtime.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 152, + "end_line": 159, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/noop-runtime.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 49, + "end_line": 70, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/types.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 110, + "end_line": 116, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 208, + "end_line": 218, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/slack-primitive/src/workflow-step.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 32, + "end_line": 39, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 84, + "end_line": 93, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 190, + "end_line": 200, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 203, + "end_line": 220, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 227, + "end_line": 232, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 321, + "end_line": 331, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 413, + "end_line": 418, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 456, + "end_line": 469, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/telemetry/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/trajectory/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/user-directory/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 22, + "end_line": 28, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/utils/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 111, + "end_line": 117, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "packages/workflow-types/package.json", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 6, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "src/cli/lib/broker-lifecycle.test.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 98, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + }, + { + "path": "src/cli/lib/broker-lifecycle.ts", + "conversations": [ + { + "contributor": { + "type": "ai" + }, + "ranges": [ + { + "start_line": 2, + "end_line": 12, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 88, + "end_line": 171, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + }, + { + "start_line": 1206, + "end_line": 1223, + "revision": "6fd439f19b16b065b8200c3239f86d4dcd8c72df" + } + ] + } + ] + } + ] +} diff --git a/.trajectories/completed/2026-05/traj_m7mpv7j8n78h.json b/.trajectories/completed/2026-05/traj_m7mpv7j8n78h.json index d9a1178a7..746a5e832 100644 --- a/.trajectories/completed/2026-05/traj_m7mpv7j8n78h.json +++ b/.trajectories/completed/2026-05/traj_m7mpv7j8n78h.json @@ -79,7 +79,7 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows", + "projectId": "", "tags": [], "_trace": { "startRef": "31aa3d2dc1895ce1887d1096f8c4903aa893d4c0", diff --git a/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json b/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json index 0b0240408..68f2888c1 100644 --- a/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json +++ b/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json @@ -409,6 +409,6 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "projectId": "", "tags": [] } diff --git a/.trajectories/completed/2026-05/traj_tavtex0db4b0.json b/.trajectories/completed/2026-05/traj_tavtex0db4b0.json new file mode 100644 index 000000000..e08d30de6 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_tavtex0db4b0.json @@ -0,0 +1,53 @@ +{ + "id": "traj_tavtex0db4b0", + "version": 1, + "task": { + "title": "Make workflow failures repairable by agents" + }, + "status": "completed", + "startedAt": "2026-05-08T14:34:19.969Z", + "completedAt": "2026-05-08T14:44:45.719Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-05-08T14:37:00.948Z" + } + ], + "chapters": [ + { + "id": "chap_wbxbbqc2c9eo", + "title": "Work", + "agentName": "default", + "startedAt": "2026-05-08T14:37:00.948Z", + "endedAt": "2026-05-08T14:44:45.719Z", + "events": [ + { + "ts": 1778251020952, + "type": "decision", + "content": "Use a separate git worktree for the workflow repair runtime change: Use a separate git worktree for the workflow repair runtime change", + "raw": { + "question": "Use a separate git worktree for the workflow repair runtime change", + "chosen": "Use a separate git worktree for the workflow repair runtime change", + "alternatives": [], + "reasoning": "The user explicitly requested worktree isolation after the branch was created; keeping the dirty base checkout untouched avoids mixing this implementation with existing local edits." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Moved implementation into worktree codex/repairable-workflow-failures. Added bounded deterministic gate repair in the workflow runner so failed deterministic checks can be fixed by a workflow agent before retrying.", + "approach": "Standard approach", + "confidence": 0.82 + }, + "commits": [], + "filesChanged": [], + "projectId": "", + "tags": [], + "_trace": { + "startRef": "b2f4cbb8d1117cf0be5ff50d47bfb1d6471c68a0", + "endRef": "b2f4cbb8d1117cf0be5ff50d47bfb1d6471c68a0" + } +} diff --git a/.trajectories/completed/2026-05/traj_tavtex0db4b0.md b/.trajectories/completed/2026-05/traj_tavtex0db4b0.md new file mode 100644 index 000000000..fcfad293f --- /dev/null +++ b/.trajectories/completed/2026-05/traj_tavtex0db4b0.md @@ -0,0 +1,33 @@ +# Trajectory: Make workflow failures repairable by agents + +> **Status:** ✅ Completed +> **Confidence:** 82% +> **Started:** May 8, 2026 at 04:34 PM +> **Completed:** May 8, 2026 at 04:44 PM + +--- + +## Summary + +Moved implementation into worktree codex/repairable-workflow-failures. Added bounded deterministic gate repair in the workflow runner so failed deterministic checks can be fixed by a workflow agent before retrying. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Use a separate git worktree for the workflow repair runtime change + +- **Chose:** Use a separate git worktree for the workflow repair runtime change +- **Reasoning:** The user explicitly requested worktree isolation after the branch was created; keeping the dirty base checkout untouched avoids mixing this implementation with existing local edits. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Use a separate git worktree for the workflow repair runtime change: Use a separate git worktree for the workflow repair runtime change diff --git a/.trajectories/completed/2026-05/traj_vkozdglobkyg.json b/.trajectories/completed/2026-05/traj_vkozdglobkyg.json index f81509f10..018fd0e19 100644 --- a/.trajectories/completed/2026-05/traj_vkozdglobkyg.json +++ b/.trajectories/completed/2026-05/traj_vkozdglobkyg.json @@ -44,7 +44,7 @@ }, "commits": [], "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows", + "projectId": "", "tags": [], "_trace": { "startRef": "3679aec8ad1108172a404e55fb31b616ac3a246f", diff --git a/.trajectories/completed/traj_1776105620545_9dcebb3d.json b/.trajectories/completed/traj_1776105620545_9dcebb3d.json index 73c7a7ef9..41ae4d159 100644 --- a/.trajectories/completed/traj_1776105620545_9dcebb3d.json +++ b/.trajectories/completed/traj_1776105620545_9dcebb3d.json @@ -76,7 +76,7 @@ { "ts": 1776105728928, "type": "finding", - "content": "\"implement-fixes\" completed → Completed the two requested fixes in [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/relay/src", + "content": "\"implement-fixes\" completed → Completed the two requested fixes in [src/cli/commands/messaging.ts](/src", "significance": "medium" }, { diff --git a/.trajectories/index.json b/.trajectories/index.json index ea541b519..6fe16bd4f 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,13 +1,13 @@ { "version": 1, - "lastUpdated": "2026-05-08T18:33:55.701Z", + "lastUpdated": "2026-05-09T08:47:54.843Z", "trajectories": { "traj_1775914133873_35667beb": { "title": "fix-sdk-build-resolution-workflow", "status": "completed", "startedAt": "2026-04-11T13:28:53.873Z", "completedAt": "2026-05-08T13:33:48.161Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1775914133873_35667beb.json", + "path": ".trajectories/completed/2026-05/traj_1775914133873_35667beb.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776073106646_1839be2d": { @@ -15,7 +15,7 @@ "status": "completed", "startedAt": "2026-04-13T09:38:26.646Z", "completedAt": "2026-05-08T13:33:45.944Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json", + "path": ".trajectories/completed/2026-05/traj_1776073106646_1839be2d.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776113772922_bc92f121": { @@ -23,7 +23,7 @@ "status": "completed", "startedAt": "2026-04-13T20:56:12.922Z", "completedAt": "2026-05-08T13:33:43.489Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json", + "path": ".trajectories/completed/2026-05/traj_1776113772922_bc92f121.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_3b3p1z4y7qlo": { @@ -31,7 +31,7 @@ "status": "completed", "startedAt": "2026-04-20T13:16:22.009Z", "completedAt": "2026-05-08T13:33:40.636Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json", + "path": ".trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_o9cx33xn5u39": { @@ -39,7 +39,7 @@ "status": "completed", "startedAt": "2026-04-20T15:06:23.387Z", "completedAt": "2026-05-08T13:33:35.341Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json", + "path": ".trajectories/completed/2026-05/traj_o9cx33xn5u39.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_0t92gxaz6igh": { @@ -47,7 +47,7 @@ "status": "completed", "startedAt": "2026-04-10T16:29:40.674Z", "completedAt": "2026-04-10T16:32:14.544Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json", + "path": ".trajectories/completed/2026-04/traj_0t92gxaz6igh.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776105620545_9dcebb3d": { @@ -55,7 +55,7 @@ "status": "completed", "startedAt": "2026-04-13T18:40:20.545Z", "completedAt": "2026-04-13T18:41:52.831Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_1776105620545_9dcebb3d.json", + "path": ".trajectories/completed/2026-04/traj_1776105620545_9dcebb3d.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776105988184_29f1270c": { @@ -63,7 +63,7 @@ "status": "completed", "startedAt": "2026-04-13T18:46:28.184Z", "completedAt": "2026-04-13T20:23:54.308Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json", + "path": ".trajectories/completed/2026-04/traj_1776105988184_29f1270c.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_222ha5671idc": { @@ -71,7 +71,7 @@ "status": "completed", "startedAt": "2026-04-15T21:32:51.980Z", "completedAt": "2026-04-15T21:45:41.024Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_222ha5671idc.json", + "path": ".trajectories/completed/2026-04/traj_222ha5671idc.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_4zqhfqw7g28l": { @@ -79,7 +79,7 @@ "status": "completed", "startedAt": "2026-04-10T17:48:33.502Z", "completedAt": "2026-04-10T17:49:14.485Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json", + "path": ".trajectories/completed/2026-04/traj_4zqhfqw7g28l.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_703m7sqyq89t": { @@ -87,7 +87,7 @@ "status": "completed", "startedAt": "2026-04-10T16:33:10.601Z", "completedAt": "2026-04-10T16:35:33.660Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_703m7sqyq89t.json", + "path": ".trajectories/completed/2026-04/traj_703m7sqyq89t.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_9tt55is74dq5": { @@ -95,7 +95,7 @@ "status": "completed", "startedAt": "2026-04-11T13:34:46.304Z", "completedAt": "2026-04-11T13:35:22.677Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_9tt55is74dq5.json", + "path": ".trajectories/completed/2026-04/traj_9tt55is74dq5.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_abjovknvcijv": { @@ -103,7 +103,7 @@ "status": "completed", "startedAt": "2026-04-10T16:08:30.070Z", "completedAt": "2026-04-10T16:11:08.673Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_abjovknvcijv.json", + "path": ".trajectories/completed/2026-04/traj_abjovknvcijv.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_d48czxmgx4ac": { @@ -111,7 +111,7 @@ "status": "completed", "startedAt": "2026-04-10T16:12:27.477Z", "completedAt": "2026-04-10T16:13:14.348Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json", + "path": ".trajectories/completed/2026-04/traj_d48czxmgx4ac.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_dw8ihhdb8ip7": { @@ -119,14 +119,14 @@ "status": "abandoned", "startedAt": "2026-04-13T19:51:57.984Z", "completedAt": "2026-04-13T19:57:27.195Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json" + "path": ".trajectories/completed/2026-04/traj_dw8ihhdb8ip7.json" }, "traj_e5i62wdjx0jd": { "title": "Plan autofix finding groups", "status": "completed", "startedAt": "2026-04-13T09:40:42.044Z", "completedAt": "2026-04-13T09:41:07.789Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json", + "path": ".trajectories/completed/2026-04/traj_e5i62wdjx0jd.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_g3muawdq6bsb": { @@ -134,7 +134,7 @@ "status": "completed", "startedAt": "2026-04-10T16:13:19.744Z", "completedAt": "2026-04-10T16:13:43.289Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json", + "path": ".trajectories/completed/2026-04/traj_g3muawdq6bsb.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_mk0t0cgn4ytq": { @@ -142,7 +142,7 @@ "status": "completed", "startedAt": "2026-04-10T15:10:03.877Z", "completedAt": "2026-04-10T15:10:29.410Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json", + "path": ".trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_o8kgzhfu6jth": { @@ -150,7 +150,7 @@ "status": "completed", "startedAt": "2026-04-10T16:07:15.131Z", "completedAt": "2026-04-10T16:07:42.930Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json", + "path": ".trajectories/completed/2026-04/traj_o8kgzhfu6jth.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_qb54w47qwod6": { @@ -158,7 +158,7 @@ "status": "completed", "startedAt": "2026-04-13T20:16:10.459Z", "completedAt": "2026-04-13T20:25:09.219Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_qb54w47qwod6.json", + "path": ".trajectories/completed/2026-04/traj_qb54w47qwod6.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_rs2bt3x0fqba": { @@ -166,7 +166,7 @@ "status": "completed", "startedAt": "2026-04-10T17:50:43.088Z", "completedAt": "2026-04-10T18:00:44.095Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json", + "path": ".trajectories/completed/2026-04/traj_rs2bt3x0fqba.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_tjadoebpscps": { @@ -174,7 +174,7 @@ "status": "completed", "startedAt": "2026-04-13T20:02:27.719Z", "completedAt": "2026-04-13T20:02:35.662Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_tjadoebpscps.json", + "path": ".trajectories/completed/2026-04/traj_tjadoebpscps.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_w0xpsaoxuiyw": { @@ -182,7 +182,7 @@ "status": "completed", "startedAt": "2026-04-11T13:35:52.600Z", "completedAt": "2026-04-11T13:36:48.341Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json", + "path": ".trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1775914296101_a4397efe": { @@ -190,7 +190,7 @@ "status": "completed", "startedAt": "2026-04-11T13:31:36.101Z", "completedAt": "2026-04-11T13:39:53.105Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/traj_1775914296101_a4397efe.json", + "path": ".trajectories/completed/traj_1775914296101_a4397efe.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776024661304_cfc829b9": { @@ -198,14 +198,14 @@ "status": "abandoned", "startedAt": "2026-04-12T20:11:01.304Z", "completedAt": "2026-04-12T20:11:16.381Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/traj_1776024661304_cfc829b9.json" + "path": ".trajectories/completed/traj_1776024661304_cfc829b9.json" }, "traj_05xg7j388bc4": { "title": "Add browser workflow step integration", "status": "completed", "startedAt": "2026-04-10T14:56:33.229Z", "completedAt": "2026-04-10T15:05:14.660Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_05xg7j388bc4.json", + "path": ".trajectories/completed/2026-04/traj_05xg7j388bc4.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_530xmbfeljyb": { @@ -213,7 +213,7 @@ "status": "completed", "startedAt": "2026-04-10T15:16:25.682Z", "completedAt": "2026-04-10T15:25:16.937Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_530xmbfeljyb.json", + "path": ".trajectories/completed/2026-04/traj_530xmbfeljyb.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_8oh4r5km5eic": { @@ -221,7 +221,7 @@ "status": "completed", "startedAt": "2026-04-10T15:26:11.355Z", "completedAt": "2026-04-10T15:33:35.150Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json", + "path": ".trajectories/completed/2026-04/traj_8oh4r5km5eic.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_avmkyoo2s3rt": { @@ -229,7 +229,7 @@ "status": "completed", "startedAt": "2026-04-10T14:42:17.242Z", "completedAt": "2026-04-10T14:55:45.196Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json", + "path": ".trajectories/completed/2026-04/traj_avmkyoo2s3rt.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_tv1x9pamkqad": { @@ -237,7 +237,7 @@ "status": "completed", "startedAt": "2026-04-10T15:34:36.611Z", "completedAt": "2026-04-10T15:42:17.590Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json", + "path": ".trajectories/completed/2026-04/traj_tv1x9pamkqad.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_ui5omrgz819d": { @@ -245,7 +245,7 @@ "status": "completed", "startedAt": "2026-04-27T20:00:33.269Z", "completedAt": "2026-04-27T20:08:46.379Z", - "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_ui5omrgz819d.json", + "path": ".trajectories/completed/2026-04/traj_ui5omrgz819d.json", "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_itgr2w8qs3xn": { @@ -253,49 +253,77 @@ "status": "completed", "startedAt": "2026-05-08T14:44:45.732Z", "completedAt": "2026-05-08T14:44:45.984Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows/.trajectories/completed/2026-05/traj_itgr2w8qs3xn.json" + "path": ".trajectories/completed/2026-05/traj_itgr2w8qs3xn.json" }, "traj_m7mpv7j8n78h": { "title": "Address Relay PR 826 review feedback", "status": "completed", "startedAt": "2026-05-08T15:17:53.113Z", "completedAt": "2026-05-08T15:24:32.409Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows/.trajectories/completed/2026-05/traj_m7mpv7j8n78h.json" + "path": ".trajectories/completed/2026-05/traj_m7mpv7j8n78h.json" }, "traj_2tqxnib25omk": { "title": "Add workflow reliability contract coverage", "status": "completed", "startedAt": "2026-05-08T15:27:50.875Z", "completedAt": "2026-05-08T15:28:02.639Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows/.trajectories/completed/2026-05/traj_2tqxnib25omk.json" + "path": ".trajectories/completed/2026-05/traj_2tqxnib25omk.json" }, "traj_60qc24ufr96g": { "title": "Expand workflow reliability contract matrix", "status": "completed", "startedAt": "2026-05-08T15:40:11.699Z", "completedAt": "2026-05-08T15:40:22.521Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows/.trajectories/completed/2026-05/traj_60qc24ufr96g.json" + "path": ".trajectories/completed/2026-05/traj_60qc24ufr96g.json" }, "traj_vkozdglobkyg": { "title": "Address Relay PR 826 review comments", "status": "completed", "startedAt": "2026-05-08T15:50:35.978Z", "completedAt": "2026-05-08T15:51:38.854Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-repairable-workflows/.trajectories/completed/2026-05/traj_vkozdglobkyg.json" + "path": ".trajectories/completed/2026-05/traj_vkozdglobkyg.json" }, "traj_bdrlknyl8twj": { "title": "Add workflow reliability defaults and E2E matrix", "status": "completed", "startedAt": "2026-05-08T17:54:45.069Z", "completedAt": "2026-05-08T18:05:37.305Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-workflow-reliability-defaults/.trajectories/completed/2026-05/traj_bdrlknyl8twj.json" + "path": ".trajectories/completed/2026-05/traj_bdrlknyl8twj.json" }, "traj_34b1u84b19gz": { "title": "Address PR 827 review feedback", "status": "completed", "startedAt": "2026-05-08T18:29:34.717Z", "completedAt": "2026-05-08T18:33:55.607Z", - "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay-workflow-reliability-defaults/.trajectories/completed/2026-05/traj_34b1u84b19gz.json" + "path": ".trajectories/completed/2026-05/traj_34b1u84b19gz.json" + }, + "traj_6ujzpx82gqs9": { + "title": "ricky-slack-primitive-implementation-workflow-status-r-workflow", + "status": "completed", + "startedAt": "2026-05-08T16:06:54.844Z", + "completedAt": "2026-05-08T16:18:16.119Z", + "path": ".trajectories/completed/2026-05/traj_6ujzpx82gqs9.json" + }, + "traj_k7njijv51iq4": { + "title": "ricky-slack-primitive-implementation-workflow-status-r-workflow", + "status": "completed", + "startedAt": "2026-05-08T16:18:55.300Z", + "completedAt": "2026-05-08T16:26:03.266Z", + "path": ".trajectories/completed/2026-05/traj_k7njijv51iq4.json" + }, + "traj_tavtex0db4b0": { + "title": "Make workflow failures repairable by agents", + "status": "completed", + "startedAt": "2026-05-08T14:34:19.969Z", + "completedAt": "2026-05-08T14:44:45.719Z", + "path": ".trajectories/completed/2026-05/traj_tavtex0db4b0.json" + }, + "traj_lieyyspidhfj": { + "title": "Fix PR 823 conflicts checks and comments", + "status": "completed", + "startedAt": "2026-05-09T08:37:17.563Z", + "completedAt": "2026-05-09T08:47:54.686Z", + "path": ".trajectories/completed/2026-05/traj_lieyyspidhfj.json" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index d9a2a346d..21defb95b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-relay", - "version": "6.0.9", + "version": "6.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-relay", - "version": "6.0.9", + "version": "6.0.12", "bundleDependencies": [ "@relaycast/sdk", "@relayfile/local-mount" @@ -18,14 +18,14 @@ "web" ], "dependencies": { - "@agent-relay/cloud": "6.0.9", - "@agent-relay/config": "6.0.9", - "@agent-relay/hooks": "6.0.9", - "@agent-relay/sdk": "6.0.9", - "@agent-relay/telemetry": "6.0.9", - "@agent-relay/trajectory": "6.0.9", - "@agent-relay/user-directory": "6.0.9", - "@agent-relay/utils": "6.0.9", + "@agent-relay/cloud": "6.0.12", + "@agent-relay/config": "6.0.12", + "@agent-relay/hooks": "6.0.12", + "@agent-relay/sdk": "6.0.12", + "@agent-relay/telemetry": "6.0.12", + "@agent-relay/trajectory": "6.0.12", + "@agent-relay/user-directory": "6.0.12", + "@agent-relay/utils": "6.0.12", "@aws-sdk/client-s3": "3.1020.0", "@modelcontextprotocol/sdk": "^1.0.0", "@relayauth/core": "^0.1.2", @@ -174,6 +174,10 @@ "resolved": "packages/sdk", "link": true }, + "node_modules/@agent-relay/slack-primitive": { + "resolved": "packages/slack-primitive", + "link": true + }, "node_modules/@agent-relay/telemetry": { "resolved": "packages/telemetry", "link": true @@ -1294,7 +1298,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -4362,6 +4365,59 @@ "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", "license": "MIT" }, + "node_modules/@slack/logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", + "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=18" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/types": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.21.1.tgz", + "integrity": "sha512-I8vmSjNYWsaxuWPx6dz4yeh0h7vRBWbgAMK14LEmblbZ404BtrPbXs6jDPx4cYgGf8msDGF4A9opLZBu21FViQ==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.15.2", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.15.2.tgz", + "integrity": "sha512-/m9qVFkiq85Oa/FSQwYIRDa/AO4qNYkDh4sRBK1WqEc2+RyG7w4tbU6rBIwUOcc/TmWOIr24Nraquxg7um5mYw==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4.0.1", + "@slack/types": "^2.21.0", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.15.0", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.4", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/web-api/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/@smithy/chunked-blob-reader": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", @@ -5748,6 +5804,12 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", @@ -9364,6 +9426,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -9550,6 +9618,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -11984,6 +12064,15 @@ "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -12016,6 +12105,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -12990,6 +13120,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -15454,10 +15593,10 @@ }, "packages/acp-bridge": { "name": "@agent-relay/acp-bridge", - "version": "6.0.9", + "version": "6.0.12", "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "6.0.9", + "@agent-relay/sdk": "6.0.12", "@agentclientprotocol/sdk": "^0.12.0" }, "bin": { @@ -15473,38 +15612,38 @@ }, "packages/brand": { "name": "@agent-relay/brand", - "version": "6.0.9" + "version": "6.0.12" }, "packages/broker-darwin-arm64": { "name": "@agent-relay/broker-darwin-arm64", - "version": "6.0.9", + "version": "6.0.12", "license": "MIT" }, "packages/broker-darwin-x64": { "name": "@agent-relay/broker-darwin-x64", - "version": "6.0.9", + "version": "6.0.12", "license": "MIT" }, "packages/broker-linux-arm64": { "name": "@agent-relay/broker-linux-arm64", - "version": "6.0.9", + "version": "6.0.12", "license": "MIT" }, "packages/broker-linux-x64": { "name": "@agent-relay/broker-linux-x64", - "version": "6.0.9", + "version": "6.0.12", "license": "MIT" }, "packages/broker-win32-x64": { "name": "@agent-relay/broker-win32-x64", - "version": "6.0.9", + "version": "6.0.12", "license": "MIT" }, "packages/browser-primitive": { "name": "@agent-relay/browser-primitive", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/sdk": "6.0.9", + "@agent-relay/sdk": "6.0.12", "playwright": "^1.51.1" }, "bin": { @@ -15518,9 +15657,9 @@ }, "packages/cloud": { "name": "@agent-relay/cloud", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/config": "6.0.9", + "@agent-relay/config": "6.0.12", "@aws-sdk/client-s3": "3.1020.0", "ignore": "^7.0.5", "tar": "^7.5.10" @@ -15536,7 +15675,7 @@ }, "packages/config": { "name": "@agent-relay/config", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { "zod": "^3.23.8", "zod-to-json-schema": "^3.23.1" @@ -15548,7 +15687,7 @@ }, "packages/credential-proxy": { "name": "@agent-relay/credential-proxy", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { "hono": "^4.11.4", "jose": "^6.1.3" @@ -15559,9 +15698,9 @@ }, "packages/gateway": { "name": "@agent-relay/gateway", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/sdk": "6.0.9" + "@agent-relay/sdk": "6.0.12" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15570,9 +15709,9 @@ }, "packages/github-primitive": { "name": "@agent-relay/github-primitive", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/workflow-types": "6.0.9" + "@agent-relay/workflow-types": "6.0.12" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15582,11 +15721,11 @@ }, "packages/hooks": { "name": "@agent-relay/hooks", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/config": "6.0.9", - "@agent-relay/sdk": "6.0.9", - "@agent-relay/trajectory": "6.0.9" + "@agent-relay/config": "6.0.12", + "@agent-relay/sdk": "6.0.12", + "@agent-relay/trajectory": "6.0.12" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15595,9 +15734,9 @@ }, "packages/memory": { "name": "@agent-relay/memory", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/hooks": "6.0.9" + "@agent-relay/hooks": "6.0.12" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15606,11 +15745,11 @@ }, "packages/openclaw": { "name": "@agent-relay/openclaw", - "version": "6.0.9", + "version": "6.0.12", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "6.0.9", + "@agent-relay/sdk": "6.0.12", "@relaycast/sdk": "^1.0.0", "ws": "^8.0.0" }, @@ -16375,14 +16514,14 @@ }, "packages/personas": { "name": "@agent-relay/personas", - "version": "6.0.9", + "version": "6.0.12", "license": "MIT" }, "packages/policy": { "name": "@agent-relay/policy", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/config": "6.0.9" + "@agent-relay/config": "6.0.12" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16391,11 +16530,12 @@ }, "packages/sdk": { "name": "@agent-relay/sdk", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/config": "6.0.9", - "@agent-relay/github-primitive": "6.0.9", - "@agent-relay/workflow-types": "6.0.9", + "@agent-relay/config": "6.0.12", + "@agent-relay/github-primitive": "6.0.12", + "@agent-relay/slack-primitive": "6.0.12", + "@agent-relay/workflow-types": "6.0.12", "@agentworkforce/harness-kit": "^0.11.0", "@agentworkforce/workload-router": "^0.11.0", "@relaycast/sdk": "^1.1.0", @@ -16415,14 +16555,14 @@ "@types/ws": "^8.5.10" }, "optionalDependencies": { - "@agent-relay/broker-darwin-arm64": "6.0.9", - "@agent-relay/broker-darwin-x64": "6.0.9", - "@agent-relay/broker-linux-arm64": "6.0.9", - "@agent-relay/broker-linux-x64": "6.0.9", - "@agent-relay/broker-win32-x64": "6.0.9" + "@agent-relay/broker-darwin-arm64": "6.0.12", + "@agent-relay/broker-darwin-x64": "6.0.12", + "@agent-relay/broker-linux-arm64": "6.0.12", + "@agent-relay/broker-linux-x64": "6.0.12", + "@agent-relay/broker-win32-x64": "6.0.12" }, "peerDependencies": { - "@agent-relay/credential-proxy": "6.0.9", + "@agent-relay/credential-proxy": "6.0.12", "@anthropic-ai/claude-agent-sdk": ">=0.1.0", "@google/adk": ">=0.5.0", "@langchain/langgraph": ">=1.2.0", @@ -16458,9 +16598,23 @@ } } }, + "packages/slack-primitive": { + "name": "@agent-relay/slack-primitive", + "version": "6.0.12", + "dependencies": { + "@agent-relay/workflow-types": "6.0.12", + "@slack/web-api": "^7.15.2" + }, + "devDependencies": { + "@agent-relay/github-primitive": "6.0.12", + "@types/node": "^22.19.3", + "typescript": "^5.9.3", + "vitest": "^3.2.4" + } + }, "packages/telemetry": { "name": "@agent-relay/telemetry", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { "posthog-node": "^5.29.2" }, @@ -16495,9 +16649,9 @@ }, "packages/trajectory": { "name": "@agent-relay/trajectory", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/config": "6.0.9" + "@agent-relay/config": "6.0.12" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16506,9 +16660,9 @@ }, "packages/user-directory": { "name": "@agent-relay/user-directory", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/utils": "6.0.9" + "@agent-relay/utils": "6.0.12" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16517,9 +16671,9 @@ }, "packages/utils": { "name": "@agent-relay/utils", - "version": "6.0.9", + "version": "6.0.12", "dependencies": { - "@agent-relay/config": "6.0.9", + "@agent-relay/config": "6.0.12", "compare-versions": "^6.1.1" }, "devDependencies": { @@ -16529,7 +16683,7 @@ }, "packages/workflow-types": { "name": "@agent-relay/workflow-types", - "version": "6.0.9" + "version": "6.0.12" }, "web": { "version": "0.0.1", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c79c06cc8..416256acc 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -111,6 +111,11 @@ "types": "./dist/github.d.ts", "import": "./dist/github.js", "default": "./dist/github.js" + }, + "./slack": { + "types": "./dist/slack.d.ts", + "import": "./dist/slack.js", + "default": "./dist/slack.js" } }, "files": [ @@ -125,7 +130,7 @@ "directory": "packages/sdk" }, "scripts": { - "prebuild": "npm --prefix ../workflow-types run build && npm --prefix ../github-primitive run build && npm --prefix ../config run build", + "prebuild": "npm --prefix ../workflow-types run build && npm --prefix ../github-primitive run build && npm --prefix ../slack-primitive run build && npm --prefix ../config run build", "build": "npx tsc -p tsconfig.build.json", "build:full": "tsc -p tsconfig.json && npm run bundle:binary", "bundle:binary": "node ./scripts/bundle-agent-relay.mjs", @@ -146,6 +151,7 @@ "dependencies": { "@agent-relay/config": "6.0.12", "@agent-relay/github-primitive": "6.0.12", + "@agent-relay/slack-primitive": "6.0.12", "@agent-relay/workflow-types": "6.0.12", "@agentworkforce/harness-kit": "^0.11.0", "@agentworkforce/workload-router": "^0.11.0", diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 70f47b991..0c311a8bc 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -24,3 +24,5 @@ export * from './cli-resolver.js'; export * from './personas.js'; export * as github from './github.js'; export { createGitHubStep, GitHubClient } from './github.js'; +export * as slack from './slack.js'; +export { createSlackStep, SlackClient } from './slack.js'; diff --git a/packages/sdk/src/slack.ts b/packages/sdk/src/slack.ts new file mode 100644 index 000000000..03cce011f --- /dev/null +++ b/packages/sdk/src/slack.ts @@ -0,0 +1,24 @@ +/** + * Bundled Slack workflow primitive. + * + * Re-exports the full surface of `@agent-relay/slack-primitive` so + * workflow authors can import it from the SDK without a separate + * install. Three import shapes are supported: + * + * // 1. Subpath (full surface): + * import { createSlackStep, SlackClient } from '@agent-relay/sdk/slack'; + * + * // 2. Namespaced from root (full surface, avoids collisions): + * import { slack } from '@agent-relay/sdk'; + * slack.createSlackStep(...); + * + * // 3. Direct from root (curated helpers only): + * import { createSlackStep, SlackClient } from '@agent-relay/sdk'; + * + * `createSlackStep` is the one most workflow authors reach for — it + * produces an integration-type `.step(...)` config you drop straight + * into `workflow(...)`. `SlackClient` is the underlying typed client; + * same methods, runnable outside a workflow too. + */ + +export * from '@agent-relay/slack-primitive'; diff --git a/packages/sdk/tsconfig.build.json b/packages/sdk/tsconfig.build.json index f4cab75d0..b07ef52c7 100644 --- a/packages/sdk/tsconfig.build.json +++ b/packages/sdk/tsconfig.build.json @@ -9,7 +9,9 @@ "@agent-relay/config/*": ["../config/dist/*"], "@agent-relay/workflow-types": ["../workflow-types/dist/index.d.ts"], "@agent-relay/github-primitive": ["../github-primitive/dist/index.d.ts"], - "@agent-relay/github-primitive/workflow-step": ["../github-primitive/dist/workflow-step.d.ts"] + "@agent-relay/github-primitive/workflow-step": ["../github-primitive/dist/workflow-step.d.ts"], + "@agent-relay/slack-primitive": ["../slack-primitive/dist/index.d.ts"], + "@agent-relay/slack-primitive/workflow-step": ["../slack-primitive/dist/workflow-step.d.ts"] }, "strict": true, "declaration": true, @@ -21,7 +23,5 @@ "skipLibCheck": true }, "include": ["src/**/*.ts"], - "exclude": [ - "src/__tests__/**" - ] + "exclude": ["src/__tests__/**"] } diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 0ea896180..3323ac367 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -7,12 +7,12 @@ "@agent-relay/config": ["../config/src/index.ts"], "@agent-relay/workflow-types": ["../workflow-types/src/index.ts"], "@agent-relay/github-primitive": ["../github-primitive/src/index.ts"], - "@agent-relay/github-primitive/workflow-step": ["../github-primitive/src/workflow-step.ts"] + "@agent-relay/github-primitive/workflow-step": ["../github-primitive/src/workflow-step.ts"], + "@agent-relay/slack-primitive": ["../slack-primitive/src/index.ts"], + "@agent-relay/slack-primitive/workflow-step": ["../slack-primitive/src/workflow-step.ts"] }, "noEmit": true }, "include": ["src/**/*.ts"], - "exclude": [ - "src/__tests__/**" - ] + "exclude": ["src/__tests__/**"] } diff --git a/packages/slack-primitive/examples/README.md b/packages/slack-primitive/examples/README.md new file mode 100644 index 000000000..b7bc10351 --- /dev/null +++ b/packages/slack-primitive/examples/README.md @@ -0,0 +1,40 @@ +# Slack Primitive Examples + +## Runtime selection + +`SlackClient` / `SlackStepExecutor` picks one of three runtimes automatically based on what's in the environment: + +| Priority | Runtime | Activated by | Transport | +| -------- | ------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `cloud-relay` | `CLOUD_API_TOKEN` + `CLOUD_API_URL` | `POST /api/v1/slack/post-message` on relay-cloud, which uses the workspace's Nango Slack connection (the ricky app). The caller never holds a Slack bot token. | +| 2 | `local` | `SLACK_BOT_TOKEN` | `@slack/web-api` direct to Slack. | +| 3 | `noop` | _(neither)_ | Calls succeed, log a warning, and return a placeholder `ts`. Useful for CI / smoke runs where Slack delivery isn't required. | + +Override with `runtime: 'local' | 'cloud-relay' | 'noop' | 'auto'` in the config. + +> v1 limitation: in `cloud-relay` mode, `resolveUser` and `resolveChannel` throw `unsupported_in_cloud_relay`. Pass Slack user/channel IDs directly. Mention resolution (`@email@example.com`, `@handle`) is local-only. + +## Manual Smoke Test (local runtime) + +Set `SLACK_BOT_TOKEN` to a bot token with `chat:write`, `channels:read`, `groups:read`, `users:read`, and `users:read.email` scopes. Invite the bot to the destination channel and set `SLACK_CHANNEL` to either a channel id or a `#channel-name` reference. + +Run the notification example from `packages/slack-primitive`: + +```bash +SLACK_BOT_TOKEN=xoxb-... SLACK_CHANNEL=#engineering npx tsx examples/notify-on-pr.ts +``` + +The workflow should open the configured GitHub pull request step and then post a one-line Slack announcement containing the pull request URL. Use `GITHUB_REPO`, `GITHUB_BASE_BRANCH`, and `GITHUB_BRANCH_OVERRIDE` to point the GitHub step at a prepared sandbox branch. + +## Manual Smoke Test (cloud-relay runtime) + +Connect Slack on the workspace (one-time, via the cloud dashboard's integrations page). Then point the example at relay-cloud with a CLI api token: + +```bash +CLOUD_API_TOKEN=rk_cli_... \ +CLOUD_API_URL=https://api.agentrelay.com \ +SLACK_CHANNEL=#engineering \ +npx tsx examples/notify-on-pr.ts +``` + +No `SLACK_BOT_TOKEN` is required — the message is posted via the workspace's existing Nango Slack connection. diff --git a/packages/slack-primitive/examples/notify-on-pr.ts b/packages/slack-primitive/examples/notify-on-pr.ts new file mode 100644 index 000000000..e0f757c20 --- /dev/null +++ b/packages/slack-primitive/examples/notify-on-pr.ts @@ -0,0 +1,93 @@ +import { WorkflowRunner, type RelayYamlConfig } from '@agent-relay/sdk/workflows'; +import { GitHubStepExecutor, createGitHubStep } from '@agent-relay/github-primitive/workflow-step'; +import type { AgentDefinition, RunnerStepExecutor, WorkflowStep } from '@agent-relay/workflow-types'; + +import { SlackStepExecutor, createSlackStep } from '../src/workflow-step.js'; + +const repo = process.env.GITHUB_REPO ?? 'AgentWorkforce/scratch'; +const baseBranch = process.env.GITHUB_BASE_BRANCH ?? 'main'; +const branchName = process.env.GITHUB_BRANCH_OVERRIDE ?? `examples/slack-primitive-${Date.now()}`; +const slackChannel = process.env.SLACK_CHANNEL ?? '#engineering'; + +const slackExecutor = new SlackStepExecutor({ + token: process.env.SLACK_BOT_TOKEN, +}); +const githubExecutor = new GitHubStepExecutor(); + +const localExecutor: RunnerStepExecutor = { + executeAgentStep( + _step: WorkflowStep, + _agentDef: AgentDefinition, + _resolvedTask: string, + _timeoutMs?: number + ): Promise { + return Promise.reject(new Error('notify-on-pr only uses integration steps.')); + }, + async executeIntegrationStep( + step: WorkflowStep, + resolvedParams: Record, + context: { workspaceId?: string } + ): Promise<{ output: string; success: boolean }> { + if (step.integration === 'github') { + return githubExecutor.executeIntegrationStep(step, resolvedParams, context); + } + if (step.integration === 'slack') { + return slackExecutor.executeIntegrationStep(step, resolvedParams); + } + return { + success: false, + output: `Unsupported integration "${step.integration ?? 'unknown'}"`, + }; + }, +}; + +const config: RelayYamlConfig = { + version: '1.0', + name: 'notify-on-pr', + description: 'Open a GitHub pull request and announce it in Slack.', + swarm: { pattern: 'pipeline' }, + agents: [], + workflows: [ + { + name: 'notify-on-pr', + steps: [ + createGitHubStep({ + name: 'create-pr', + action: 'createPR', + repo, + params: { + title: `examples: slack primitive notification (${branchName})`, + body: 'Opened by packages/slack-primitive/examples/notify-on-pr.ts.', + base: baseBranch, + head: branchName, + draft: true, + }, + output: { + mode: 'data', + format: 'json', + }, + }), + createSlackStep({ + name: 'announce-pr', + dependsOn: ['create-pr'], + action: 'postMessage', + channel: slackChannel, + text: 'PR opened: {{steps.create-pr.output.htmlUrl}}', + unfurl: true, + output: { + mode: 'summary', + format: 'json', + pretty: true, + }, + }), + ], + }, + ], +}; + +const runner = new WorkflowRunner({ + cwd: process.cwd(), + executor: localExecutor, +}); + +await runner.execute(config); diff --git a/packages/slack-primitive/package.json b/packages/slack-primitive/package.json new file mode 100644 index 000000000..4731a35da --- /dev/null +++ b/packages/slack-primitive/package.json @@ -0,0 +1,50 @@ +{ + "name": "@agent-relay/slack-primitive", + "version": "6.0.12", + "description": "Slack workflow primitive for Agent Relay", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./workflow-step": { + "types": "./dist/workflow-step.d.ts", + "import": "./dist/workflow-step.js", + "default": "./dist/workflow-step.js" + } + }, + "files": [ + "dist", + "examples", + "README.md" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest", + "typecheck:examples": "tsc -p tsconfig.examples.json --noEmit" + }, + "dependencies": { + "@agent-relay/workflow-types": "6.0.12", + "@slack/web-api": "^7.15.2" + }, + "devDependencies": { + "@agent-relay/github-primitive": "6.0.12", + "@types/node": "^22.19.3", + "typescript": "^5.9.3", + "vitest": "^3.2.4" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/relay.git", + "directory": "packages/slack-primitive" + } +} diff --git a/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts b/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts new file mode 100644 index 000000000..78e5ac641 --- /dev/null +++ b/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts @@ -0,0 +1,174 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { SlackCloudRelayClient, type CloudRelayFetch } from '../cloud-relay-runtime.js'; +import { SlackPostBackError } from '../types.js'; + +interface FakeResponseBody { + status?: number; + body: unknown; +} + +function fakeFetch(responses: FakeResponseBody[]): CloudRelayFetch & { + calls: Array<{ url: string; init?: { method?: string; headers?: Record; body?: string } }>; +} { + const calls: Array<{ + url: string; + init?: { method?: string; headers?: Record; body?: string }; + }> = []; + let i = 0; + const fn = (async (url, init) => { + calls.push({ url: String(url), init }); + const next = responses[i] ?? responses[responses.length - 1]; + i += 1; + const status = next.status ?? 200; + return { + ok: status >= 200 && status < 300, + status, + statusText: 'OK', + json: async () => next.body, + }; + }) as CloudRelayFetch; + (fn as unknown as { calls: typeof calls }).calls = calls; + return fn as CloudRelayFetch & { calls: typeof calls }; +} + +const baseConfig = { + env: {}, + cloudApiToken: 'rk_cli_test', + cloudApiUrl: 'https://api.example.com', +}; + +describe('SlackCloudRelayClient', () => { + it('throws auth_token_missing when CLOUD_API_TOKEN is absent', () => { + expect( + () => new SlackCloudRelayClient({ env: {}, cloudApiUrl: 'https://api.example.com' }, fakeFetch([])) + ).toThrow('CLOUD_API_TOKEN'); + }); + + it('throws auth_token_missing when CLOUD_API_URL is absent', () => { + expect(() => new SlackCloudRelayClient({ env: {}, cloudApiToken: 'rk_test' }, fakeFetch([]))).toThrow( + 'CLOUD_API_URL' + ); + }); + + it('posts via cloud-relay endpoint with bearer auth', async () => { + const fetch = fakeFetch([ + { body: { ok: true, ts: '1709876543.123', channel: 'C0123', workspaceId: 'ws_test' } }, + ]); + const client = new SlackCloudRelayClient(baseConfig, fetch); + + const result = await client.postMessage({ + channel: '#general', + text: 'PR opened', + threadTs: '1234.5', + unfurl: false, + }); + + expect(result).toEqual({ + channel: 'C0123', + ts: '1709876543.123', + text: 'PR opened', + resolvedMentions: [], + unresolvedMentions: [], + warnings: [], + }); + + expect(fetch.calls).toHaveLength(1); + expect(fetch.calls[0].url).toBe('https://api.example.com/api/v1/slack/post-message'); + expect(fetch.calls[0].init?.headers).toMatchObject({ + authorization: 'Bearer rk_cli_test', + 'content-type': 'application/json', + }); + expect(JSON.parse(fetch.calls[0].init?.body ?? '{}')).toEqual({ + channel: '#general', + text: 'PR opened', + threadTs: '1234.5', + unfurlLinks: false, + unfurlMedia: false, + }); + }); + + it('records mentions as unresolved with a warning', async () => { + const fetch = fakeFetch([{ body: { ok: true, ts: '1.0', channel: 'C0123', workspaceId: 'ws_test' } }]); + const client = new SlackCloudRelayClient(baseConfig, fetch); + + const result = await client.postMessage({ + channel: '#general', + text: 'cc people', + mentions: ['@khaliq', 'khaliq@example.com'], + }); + + expect(result.unresolvedMentions).toEqual(['@khaliq', 'khaliq@example.com']); + expect(result.warnings).toHaveLength(2); + expect(result.warnings[0]).toMatchObject({ type: 'mention_unresolved' }); + }); + + it('throws SlackPostBackError(rate_limited) on rate-limit response', async () => { + const fetch = fakeFetch([ + { + status: 429, + body: { ok: false, code: 'rate_limited', error: 'channel rate limit exceeded', retryAfterMs: 5000 }, + }, + ]); + const client = new SlackCloudRelayClient(baseConfig, fetch); + + await expect(client.postMessage({ channel: '#general', text: 'hi' })).rejects.toMatchObject({ + code: 'rate_limited', + }); + }); + + it('maps cloud not_connected to SlackPostBackError(not_connected)', async () => { + const fetch = fakeFetch([ + { status: 404, body: { ok: false, code: 'not_connected', error: 'no Slack integration' } }, + ]); + const client = new SlackCloudRelayClient(baseConfig, fetch); + + await expect(client.postMessage({ channel: '#general', text: 'hi' })).rejects.toMatchObject({ + code: 'not_connected', + }); + }); + + it('maps cloud slack_error to SlackPostBackError(slack_api_error)', async () => { + const fetch = fakeFetch([{ body: { ok: false, code: 'slack_error', error: 'channel_not_found' } }]); + const client = new SlackCloudRelayClient(baseConfig, fetch); + + await expect(client.postMessage({ channel: '#bogus', text: 'hi' })).rejects.toMatchObject({ + code: 'slack_api_error', + }); + }); + + it('throws SlackPostBackError(upstream_error) when fetch rejects', async () => { + const fetch = (async () => { + throw new Error('network down'); + }) as unknown as CloudRelayFetch; + const client = new SlackCloudRelayClient(baseConfig, fetch); + + await expect(client.postMessage({ channel: '#general', text: 'hi' })).rejects.toMatchObject({ + code: 'upstream_error', + }); + }); + + it('rejects resolveUser with unsupported_in_cloud_relay', async () => { + const client = new SlackCloudRelayClient(baseConfig, fakeFetch([])); + await expect(client.resolveUser({ mention: '@khaliq' })).rejects.toMatchObject({ + code: 'unsupported_in_cloud_relay', + }); + }); + + it('rejects resolveChannel with unsupported_in_cloud_relay', async () => { + const client = new SlackCloudRelayClient(baseConfig, fakeFetch([])); + await expect(client.resolveChannel({ channel: '#general' })).rejects.toMatchObject({ + code: 'unsupported_in_cloud_relay', + }); + }); + + it('reports cloud-relay runtime', async () => { + const client = new SlackCloudRelayClient(baseConfig, fakeFetch([])); + expect(client.getRuntime()).toBe('cloud-relay'); + await expect(client.isAuthenticated()).resolves.toBe(true); + }); +}); + +// Compile-time guards +void SlackPostBackError; +void vi; diff --git a/packages/slack-primitive/src/__tests__/noop-runtime.test.ts b/packages/slack-primitive/src/__tests__/noop-runtime.test.ts new file mode 100644 index 000000000..53aace884 --- /dev/null +++ b/packages/slack-primitive/src/__tests__/noop-runtime.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { SlackNoopClient } from '../noop-runtime.js'; + +describe('SlackNoopClient', () => { + it('returns a noop ts and logs a warning', async () => { + const logger = vi.fn(); + const client = new SlackNoopClient({ env: {} }, logger); + + const result = await client.postMessage({ + channel: '#general', + text: 'PR opened', + mentions: ['@khaliq'], + }); + + expect(result).toMatchObject({ + channel: '#general', + ts: '0000000000.000000', + text: 'PR opened', + resolvedMentions: [], + unresolvedMentions: ['@khaliq'], + }); + expect(result.warnings).toHaveLength(1); + expect(logger).toHaveBeenCalledTimes(1); + }); + + it('reports noop runtime and unauthenticated', async () => { + const client = new SlackNoopClient({ env: {} }, vi.fn()); + expect(client.getRuntime()).toBe('noop'); + await expect(client.isAuthenticated()).resolves.toBe(false); + }); + + it('throws auth_token_missing on resolveUser/resolveChannel', async () => { + const client = new SlackNoopClient({ env: {} }, vi.fn()); + await expect(client.resolveUser({ mention: '@khaliq' })).rejects.toMatchObject({ + code: 'auth_token_missing', + }); + await expect(client.resolveChannel({ channel: '#general' })).rejects.toMatchObject({ + code: 'auth_token_missing', + }); + }); +}); diff --git a/packages/slack-primitive/src/__tests__/post-message.test.ts b/packages/slack-primitive/src/__tests__/post-message.test.ts new file mode 100644 index 000000000..572ebb217 --- /dev/null +++ b/packages/slack-primitive/src/__tests__/post-message.test.ts @@ -0,0 +1,191 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { postMessage } from '../actions/post-message.js'; +import { resolveChannel } from '../actions/resolve-channel.js'; +import { SlackWebApiClient } from '../local-runtime.js'; +import { SlackPostBackError, type SlackWebApiLike } from '../types.js'; +import { renderSlackTemplates } from '../workflow-step.js'; + +describe('Slack primitive', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('throws auth_token_missing when SLACK_BOT_TOKEN is absent', () => { + expect(() => new SlackWebApiClient({ env: {} })).toThrow(SlackPostBackError); + expect(() => new SlackWebApiClient({ env: {} })).toThrow('auth_token_missing'); + }); + + it('resolves #channel names through conversations.list', async () => { + const slack = createRecordingSlack(); + + await expect(resolveChannel(slack, '#engineering')).resolves.toEqual({ + id: 'CENGINEERING', + name: 'engineering', + }); + expect(slack.calls.conversationsList).toBe(1); + }); + + it('resolves email and handle mentions before posting', async () => { + const slack = createRecordingSlack(); + + const result = await postMessage(slack, { + channel: '#engineering', + text: 'PR opened', + mentions: ['@dev@example.com', '@khaliq'], + }); + + expect(result.resolvedMentions).toEqual([ + { input: '@dev@example.com', userId: 'UEMAIL' }, + { input: '@khaliq', userId: 'UHANDLE' }, + ]); + expect(result.unresolvedMentions).toEqual([]); + expect(slack.lastPost?.text).toBe('<@UEMAIL> <@UHANDLE> PR opened'); + }); + + it('soft-fails unresolved mentions and still posts the message', async () => { + const slack = createRecordingSlack(); + + const result = await postMessage(slack, { + channel: 'CENGINEERING', + text: 'PR opened', + mentions: ['@missing'], + }); + + expect(result.unresolvedMentions).toEqual(['@missing']); + expect(result.warnings).toEqual([ + { + type: 'mention_unresolved', + input: '@missing', + message: 'Slack user not found for handle: @missing', + }, + ]); + expect(slack.lastPost?.channel).toBe('CENGINEERING'); + expect(slack.lastPost?.text).toBe('PR opened'); + }); + + it('uses SLACK_DEFAULT_CHANNEL when channel is omitted', async () => { + vi.stubEnv('SLACK_DEFAULT_CHANNEL', '#engineering'); + const slack = createRecordingSlack(); + + await postMessage(slack, { + text: 'PR opened', + }); + + expect(slack.lastPost?.channel).toBe('CENGINEERING'); + }); + + it('throws a clear error when channel is omitted and no default exists', async () => { + const slack = createRecordingSlack(); + + await expect( + postMessage(slack, { + text: 'PR opened', + }) + ).rejects.toThrow('provide channel or set SLACK_DEFAULT_CHANNEL'); + }); + + it('substitutes {{steps.X.output}} templates by nested path', () => { + const text = renderSlackTemplates('Opened {{steps.create-pr.output.htmlUrl}}', { + steps: { + 'create-pr': { + output: { + htmlUrl: 'https://github.test/octo/repo/pull/7', + }, + }, + }, + }); + + expect(text).toBe('Opened https://github.test/octo/repo/pull/7'); + }); +}); + +interface RecordingSlack extends SlackWebApiLike { + calls: { + conversationsList: number; + usersList: number; + lookupByEmail: number; + postMessage: number; + }; + lastPost?: { + channel: string; + text: string; + }; +} + +function createRecordingSlack(): RecordingSlack { + const slack: RecordingSlack = { + calls: { + conversationsList: 0, + usersList: 0, + lookupByEmail: 0, + postMessage: 0, + }, + conversations: { + async list() { + slack.calls.conversationsList += 1; + return { + ok: true, + channels: [ + { + id: 'CENGINEERING', + name: 'engineering', + }, + ], + }; + }, + }, + users: { + async lookupByEmail({ email }) { + slack.calls.lookupByEmail += 1; + if (email === 'dev@example.com') { + return { + ok: true, + user: { + id: 'UEMAIL', + name: 'dev', + profile: { + email, + }, + }, + }; + } + return { ok: false, error: 'users_not_found' }; + }, + async list() { + slack.calls.usersList += 1; + return { + ok: true, + members: [ + { + id: 'UHANDLE', + name: 'khaliq', + profile: { + displayName: 'khaliq', + }, + }, + ], + }; + }, + }, + chat: { + async postMessage(params) { + slack.calls.postMessage += 1; + slack.lastPost = { + channel: params.channel, + text: params.text, + }; + return { + ok: true, + channel: params.channel, + ts: '1710000000.000001', + message: { + text: params.text, + }, + }; + }, + }, + }; + + return slack; +} diff --git a/packages/slack-primitive/src/__tests__/runtime-selection.test.ts b/packages/slack-primitive/src/__tests__/runtime-selection.test.ts new file mode 100644 index 000000000..11bb46de5 --- /dev/null +++ b/packages/slack-primitive/src/__tests__/runtime-selection.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; + +import { SlackAdapterFactory, normalizeSlackRuntimeConfig } from '../adapter.js'; +import { SlackWebApiClient } from '../local-runtime.js'; +import type { SlackWebApiLike } from '../types.js'; + +describe('SlackAdapterFactory runtime selection', () => { + it("picks 'cloud-relay' when CLOUD_API_TOKEN and CLOUD_API_URL are set, even if SLACK_BOT_TOKEN is also set", () => { + const normalized = normalizeSlackRuntimeConfig({ + env: { + SLACK_BOT_TOKEN: 'xoxb-local', + CLOUD_API_TOKEN: 'rk_cli', + CLOUD_API_URL: 'https://api.example.com', + }, + }); + expect(normalized.runtime).toBe('cloud-relay'); + }); + + it("picks 'local' when only SLACK_BOT_TOKEN is set", () => { + const normalized = normalizeSlackRuntimeConfig({ + env: { SLACK_BOT_TOKEN: 'xoxb-local' }, + }); + expect(normalized.runtime).toBe('local'); + }); + + it("picks 'noop' when no tokens are configured", () => { + const normalized = normalizeSlackRuntimeConfig({ env: {} }); + expect(normalized.runtime).toBe('noop'); + }); + + it("falls back to 'noop' when CLOUD_API_TOKEN is set but CLOUD_API_URL is missing", () => { + const normalized = normalizeSlackRuntimeConfig({ + env: { CLOUD_API_TOKEN: 'rk_cli' }, + }); + expect(normalized.runtime).toBe('noop'); + }); + + it('honors explicit runtime override over env detection', () => { + const normalized = normalizeSlackRuntimeConfig({ + runtime: 'noop', + env: { + SLACK_BOT_TOKEN: 'xoxb-local', + CLOUD_API_TOKEN: 'rk_cli', + CLOUD_API_URL: 'https://api.example.com', + }, + }); + expect(normalized.runtime).toBe('noop'); + }); + + it("treats 'auto' the same as omitting runtime", () => { + const normalized = normalizeSlackRuntimeConfig({ + runtime: 'auto', + env: { SLACK_BOT_TOKEN: 'xoxb-local' }, + }); + expect(normalized.runtime).toBe('local'); + }); + + it('detect() reports availability for all three runtimes', async () => { + const detection = await SlackAdapterFactory.detect({ + env: { + SLACK_BOT_TOKEN: 'xoxb-local', + CLOUD_API_TOKEN: 'rk_cli', + CLOUD_API_URL: 'https://api.example.com', + }, + }); + + expect(detection.runtime).toBe('cloud-relay'); + expect(detection.cloudRelay.available).toBe(true); + expect(detection.local.available).toBe(true); + expect(detection.noop.available).toBe(true); + }); + + it('create() returns a noop adapter when no tokens are configured', async () => { + const adapter = await SlackAdapterFactory.create({ env: {} }); + expect(adapter.getRuntime()).toBe('noop'); + }); + + it('isAuthenticated() returns false when Slack auth probing rejects', async () => { + const slack = { + auth: { + test: async () => { + throw new Error('invalid_auth'); + }, + }, + chat: { postMessage: async () => ({ ok: true }) }, + conversations: { list: async () => ({ ok: true }) }, + users: { + lookupByEmail: async () => ({ ok: false }), + list: async () => ({ ok: true }), + }, + } as unknown as SlackWebApiLike; + + const client = new SlackWebApiClient({ token: 'xoxb-local', env: {} }, slack); + + await expect(client.isAuthenticated()).resolves.toBe(false); + }); +}); diff --git a/packages/slack-primitive/src/__tests__/workflow-step.test.ts b/packages/slack-primitive/src/__tests__/workflow-step.test.ts new file mode 100644 index 000000000..6d1fc0368 --- /dev/null +++ b/packages/slack-primitive/src/__tests__/workflow-step.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; + +import { SlackAction, type SlackActionResult } from '../types.js'; +import { + SlackStepExecutor, + slackStepConfigFromWorkflowStep, + type SlackStepConfig, +} from '../workflow-step.js'; +import type { SlackClient } from '../client.js'; + +describe('SlackStepExecutor', () => { + it('keeps numeric-looking threadTs as a string after workflow param resolution', () => { + const config = slackStepConfigFromWorkflowStep( + { + name: 'announce', + type: 'integration', + integration: 'slack', + action: 'postMessage', + }, + { + text: 'PR opened', + threadTs: '1715273540.123456', + unfurl: 'true', + mentions: '["@dev"]', + } + ); + + expect(config.threadTs).toBe('1715273540.123456'); + expect(config.unfurl).toBe(true); + expect(config.mentions).toEqual(['@dev']); + }); + + it('surfaces the real error for failed default data-mode steps', async () => { + const executor = new SlackStepExecutor(); + const client = { + executeAction: async (): Promise => ({ + success: false, + output: '', + error: 'channel_not_found', + }), + } as unknown as SlackClient; + + const result = await executor.execute( + { + name: 'announce', + action: SlackAction.PostMessage, + channel: '#missing', + text: 'PR opened', + } satisfies SlackStepConfig, + { client } + ); + + expect(result.output).toBe('"channel_not_found"'); + }); +}); diff --git a/packages/slack-primitive/src/actions/post-message.ts b/packages/slack-primitive/src/actions/post-message.ts new file mode 100644 index 000000000..0648a2b9e --- /dev/null +++ b/packages/slack-primitive/src/actions/post-message.ts @@ -0,0 +1,62 @@ +import { resolveChannel } from './resolve-channel.js'; +import { resolveUser } from './resolve-user.js'; +import type { + PostMessageOutput, + PostMessageParams, + SlackResolutionWarning, + SlackResolvedMention, + SlackUserSummary, + SlackWebApiLike, +} from '../types.js'; + +/** + * Resolve Slack references and post a message. + * @param slack - Slack Web API client. + * @param params - Message parameters. + * @returns Posted message metadata and soft mention-resolution warnings. + */ +export async function postMessage( + slack: SlackWebApiLike, + params: PostMessageParams +): Promise { + const channelInput = params.channel ?? process.env.SLACK_DEFAULT_CHANNEL; + if (!channelInput) { + throw new Error('Slack postMessage channel is missing; provide channel or set SLACK_DEFAULT_CHANNEL.'); + } + + const channel = await resolveChannel(slack, channelInput); + const userCache = new Map(); + const resolvedMentions: SlackResolvedMention[] = []; + const warnings: SlackResolutionWarning[] = []; + + for (const mention of params.mentions ?? []) { + try { + resolvedMentions.push(await resolveUser(slack, mention, { cache: userCache })); + } catch (error) { + warnings.push({ + type: 'mention_unresolved', + input: mention, + message: error instanceof Error ? error.message : String(error), + }); + } + } + + const mentionPrefix = resolvedMentions.map((mention) => `<@${mention.userId}>`).join(' '); + const text = mentionPrefix ? `${mentionPrefix} ${params.text}` : params.text; + const response = await slack.chat.postMessage({ + channel: channel.id, + text, + thread_ts: params.threadTs, + unfurl_links: params.unfurl, + unfurl_media: params.unfurl, + }); + + return { + channel: response.channel ?? channel.id, + ts: response.ts ?? '', + text: response.message?.text ?? text, + resolvedMentions, + unresolvedMentions: warnings.map((warning) => warning.input), + warnings, + }; +} diff --git a/packages/slack-primitive/src/actions/resolve-channel.ts b/packages/slack-primitive/src/actions/resolve-channel.ts new file mode 100644 index 000000000..4711c1478 --- /dev/null +++ b/packages/slack-primitive/src/actions/resolve-channel.ts @@ -0,0 +1,38 @@ +import { SlackPostBackError, type SlackChannelSummary, type SlackWebApiLike } from '../types.js'; + +const CHANNEL_ID_PATTERN = /^[CGD][A-Z0-9]{2,}$/; + +/** + * Resolve a Slack channel reference to a channel object. + * @param slack - Slack Web API client. + * @param channel - Raw channel id or #channel-name reference. + * @returns Resolved Slack channel summary. + */ +export async function resolveChannel( + slack: SlackWebApiLike, + channel: string +): Promise { + if (CHANNEL_ID_PATTERN.test(channel)) { + return { id: channel }; + } + + const name = channel.startsWith('#') ? channel.slice(1) : channel; + let cursor: string | undefined; + + do { + const response = await slack.conversations.list({ + cursor, + limit: 200, + types: 'public_channel,private_channel', + }); + + const match = response.channels?.find((candidate) => candidate.name === name); + if (match?.id) { + return match; + } + + cursor = response.response_metadata?.next_cursor || undefined; + } while (cursor); + + throw new SlackPostBackError('channel_not_found', `Slack channel not found: ${channel}`); +} diff --git a/packages/slack-primitive/src/actions/resolve-user.ts b/packages/slack-primitive/src/actions/resolve-user.ts new file mode 100644 index 000000000..d0423d677 --- /dev/null +++ b/packages/slack-primitive/src/actions/resolve-user.ts @@ -0,0 +1,115 @@ +import type { SlackResolvedMention, SlackUserSummary, SlackWebApiLike } from '../types.js'; + +export interface ResolveUserOptions { + cache?: Map; +} + +/** + * Resolve a Slack user mention to a user id. + * @param slack - Slack Web API client. + * @param mention - Raw user id, @email@example.com, or @handle reference. + * @param options - Optional user cache. + * @returns Resolved mention record. + */ +export async function resolveUser( + slack: SlackWebApiLike, + mention: string, + options: ResolveUserOptions = {} +): Promise { + const normalized = mention.startsWith('@') ? mention.slice(1) : mention; + + if (isSlackUserId(normalized)) { + return { input: mention, userId: normalized }; + } + + if (isEmailCandidate(normalized)) { + const response = await slack.users.lookupByEmail({ email: normalized }); + const userId = response.user?.id; + if (!userId) { + throw new Error(`Slack user not found for email: ${mention}`); + } + if (response.user) { + rememberUser(options.cache, response.user); + } + return { input: mention, userId }; + } + + const cache = options.cache ?? new Map(); + let cached = cache.get(normalized.toLowerCase()); + if (!cached) { + await populateUserCache(slack, cache); + cached = cache.get(normalized.toLowerCase()); + } + if (cached?.id) { + return { input: mention, userId: cached.id }; + } + + throw new Error(`Slack user not found for handle: ${mention}`); +} + +function isSlackUserId(value: string): boolean { + if (value.length < 3) return false; + if (value[0] !== 'U' && value[0] !== 'W') return false; + + for (let i = 1; i < value.length; i += 1) { + const code = value.charCodeAt(i); + const isDigit = code >= 48 && code <= 57; + const isUppercase = code >= 65 && code <= 90; + if (!isDigit && !isUppercase) return false; + } + + return true; +} + +function isEmailCandidate(value: string): boolean { + const at = value.indexOf('@'); + if (at <= 0 || at !== value.lastIndexOf('@') || at === value.length - 1) return false; + + const domain = value.slice(at + 1); + const dot = domain.indexOf('.'); + return dot > 0 && dot < domain.length - 1 && !hasAsciiWhitespace(value); +} + +function hasAsciiWhitespace(value: string): boolean { + for (let i = 0; i < value.length; i += 1) { + const code = value.charCodeAt(i); + if (code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32) { + return true; + } + } + + return false; +} + +async function populateUserCache( + slack: SlackWebApiLike, + cache: Map +): Promise { + let cursor: string | undefined; + + do { + const response = await slack.users.list({ cursor, limit: 200 }); + for (const user of response.members ?? []) { + rememberUser(cache, user); + } + cursor = response.response_metadata?.next_cursor || undefined; + } while (cursor); +} + +function rememberUser(cache: Map | undefined, user: SlackUserSummary): void { + if (!cache) return; + for (const key of userKeys(user)) { + cache.set(key.toLowerCase(), user); + } +} + +function userKeys(user: SlackUserSummary): string[] { + return [ + user.id, + user.name, + user.realName, + user.profile?.email, + user.profile?.displayName, + user.profile?.realName, + ].filter((value): value is string => Boolean(value)); +} diff --git a/packages/slack-primitive/src/adapter.ts b/packages/slack-primitive/src/adapter.ts new file mode 100644 index 000000000..8e1c22535 --- /dev/null +++ b/packages/slack-primitive/src/adapter.ts @@ -0,0 +1,247 @@ +import { postMessage as postMessageAction } from './actions/post-message.js'; +import { resolveChannel as resolveChannelAction } from './actions/resolve-channel.js'; +import { resolveUser as resolveUserAction } from './actions/resolve-user.js'; +import { + SlackAction, + SlackClientInterface, + type PostMessageOutput, + type PostMessageParams, + type RequiredSlackRuntimeConfig, + type ResolveChannelParams, + type ResolveUserParams, + type SlackActionName, + type SlackActionOutputMap, + type SlackActionParamsMap, + type SlackActionResult, + type SlackChannelSummary, + type SlackResolvedMention, + type SlackRuntime, + type SlackRuntimeAvailability, + type SlackRuntimeConfig, + type SlackRuntimeDetectionResult, + type SlackRuntimePreference, + type SlackWebApiLike, +} from './types.js'; + +const DEFAULT_TIMEOUT = 30_000; + +export function normalizeSlackRuntimeConfig(config: SlackRuntimeConfig = {}): RequiredSlackRuntimeConfig { + const env = config.env ?? process.env; + const token = nonEmpty(config.token) ?? nonEmpty(env.SLACK_BOT_TOKEN) ?? ''; + const cloudApiToken = nonEmpty(config.cloudApiToken) ?? nonEmpty(env.CLOUD_API_TOKEN) ?? ''; + const cloudApiUrl = nonEmpty(config.cloudApiUrl) ?? nonEmpty(env.CLOUD_API_URL) ?? ''; + + return { + ...config, + runtime: + config.runtime && config.runtime !== 'auto' + ? config.runtime + : selectRuntime({ token, cloudApiToken, cloudApiUrl }), + env, + token, + cloudApiToken, + cloudApiUrl, + timeout: config.timeout ?? DEFAULT_TIMEOUT, + }; +} + +function selectRuntime(input: { token: string; cloudApiToken: string; cloudApiUrl: string }): SlackRuntime { + if (input.cloudApiToken && input.cloudApiUrl) return 'cloud-relay'; + if (input.token) return 'local'; + return 'noop'; +} + +export abstract class BaseSlackAdapter extends SlackClientInterface { + constructor( + config: RequiredSlackRuntimeConfig, + protected readonly slack: SlackWebApiLike + ) { + super(config); + } + + async isAuthenticated(): Promise { + if (!this.slack.auth) { + return Boolean(this.config.token); + } + try { + const response = await this.slack.auth.test(); + return response.ok !== false; + } catch { + return false; + } + } + + executeAction( + action: Name, + params: SlackActionParamsMap[Name] + ): Promise>; + executeAction( + action: SlackAction | SlackActionName, + params?: unknown + ): Promise>; + async executeAction( + action: SlackAction | SlackActionName, + params?: unknown + ): Promise> { + const startedAt = Date.now(); + + try { + const data = (await this.dispatchAction(action, params)) as TOutput; + return { + success: true, + output: stringifyOutput(data), + data, + metadata: { + runtime: this.getRuntime(), + executionTime: Date.now() - startedAt, + }, + }; + } catch (error) { + return { + success: false, + output: '', + error: error instanceof Error ? error.message : String(error), + metadata: { + runtime: this.getRuntime(), + executionTime: Date.now() - startedAt, + }, + }; + } + } + + async postMessage(params: PostMessageParams): Promise { + return postMessageAction(this.slack, { + ...params, + channel: params.channel ?? this.config.env.SLACK_DEFAULT_CHANNEL, + }); + } + + async resolveUser(params: ResolveUserParams): Promise { + return resolveUserAction(this.slack, params.mention); + } + + async resolveChannel(params: ResolveChannelParams): Promise { + return resolveChannelAction(this.slack, params.channel); + } + + private async dispatchAction(action: SlackAction | SlackActionName, params: unknown): Promise { + switch (action) { + case SlackAction.PostMessage: + return this.postMessage(params as PostMessageParams); + case SlackAction.ResolveUser: + return this.resolveUser(params as ResolveUserParams); + case SlackAction.ResolveChannel: + return this.resolveChannel(params as ResolveChannelParams); + default: + throw new Error(`Unsupported Slack action: ${String(action)}`); + } + } +} + +export class SlackAdapterFactory { + static async create(config: SlackRuntimeConfig = {}): Promise { + const normalized = normalizeSlackRuntimeConfig(config); + switch (normalized.runtime) { + case 'cloud-relay': { + const { SlackCloudRelayClient } = await import('./cloud-relay-runtime.js'); + return new SlackCloudRelayClient(normalized); + } + case 'noop': { + const { SlackNoopClient } = await import('./noop-runtime.js'); + return new SlackNoopClient(normalized); + } + case 'local': + default: { + const { SlackWebApiClient } = await import('./local-runtime.js'); + return new SlackWebApiClient(normalized); + } + } + } + + static async detect(config: SlackRuntimeConfig = {}): Promise { + const normalized = normalizeSlackRuntimeConfig(config); + const local = await this.testRuntime('local', normalized); + const cloudRelay = await this.testRuntime('cloud-relay', normalized); + const noop = await this.testRuntime('noop', normalized); + + const requested: SlackRuntimePreference = config.runtime ?? 'auto'; + const selected = normalized.runtime; + const summary = selected === 'cloud-relay' ? cloudRelay : selected === 'noop' ? noop : local; + + return { + runtime: selected, + requestedRuntime: requested, + source: + normalized.token || normalized.cloudApiToken || normalized.cloudApiUrl ? 'config' : 'environment', + available: summary.available, + reason: summary.reason, + checkedAt: new Date().toISOString(), + local, + cloudRelay, + noop, + }; + } + + static async detectRuntime(config: SlackRuntimeConfig = {}): Promise { + return normalizeSlackRuntimeConfig(config).runtime; + } + + static testRuntime( + runtime: SlackRuntime, + config: SlackRuntimeConfig = {} + ): Promise { + const normalized = normalizeSlackRuntimeConfig(config); + switch (runtime) { + case 'cloud-relay': { + const ready = Boolean(normalized.cloudApiToken && normalized.cloudApiUrl); + return Promise.resolve({ + runtime, + available: ready, + authenticated: ready, + reason: ready + ? 'CLOUD_API_TOKEN and CLOUD_API_URL are configured.' + : 'CLOUD_API_TOKEN or CLOUD_API_URL is not configured.', + }); + } + case 'noop': { + return Promise.resolve({ + runtime, + available: true, + authenticated: false, + reason: 'noop runtime is always available.', + }); + } + case 'local': + default: { + const ready = Boolean(normalized.token); + return Promise.resolve({ + runtime, + available: ready, + authenticated: ready, + reason: ready ? 'SLACK_BOT_TOKEN is configured.' : 'SLACK_BOT_TOKEN is not configured.', + }); + } + } + } +} + +export const SlackClientFactory = SlackAdapterFactory; + +export function detectSlackRuntime(config: SlackRuntimeConfig = {}): Promise { + return SlackAdapterFactory.detect(config); +} + +export function createSlackAdapter(config: SlackRuntimeConfig = {}): Promise { + return SlackAdapterFactory.create(config); +} + +function nonEmpty(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function stringifyOutput(value: unknown): string { + if (typeof value === 'string') return value; + if (typeof value === 'undefined') return ''; + return JSON.stringify(value); +} diff --git a/packages/slack-primitive/src/client.ts b/packages/slack-primitive/src/client.ts new file mode 100644 index 000000000..61656c1fd --- /dev/null +++ b/packages/slack-primitive/src/client.ts @@ -0,0 +1,130 @@ +import { SlackAdapterFactory } from './adapter.js'; +import type { + PostMessageOutput, + PostMessageParams, + ResolveChannelParams, + ResolveUserParams, + SlackAction, + SlackActionName, + SlackActionOutputMap, + SlackActionParamsMap, + SlackActionResult, + SlackChannelSummary, + SlackClientInterface, + SlackResolvedMention, + SlackRuntime, + SlackRuntimeConfig, + SlackRuntimeDetectionResult, +} from './types.js'; + +/** + * High-level Slack primitive client. + */ +export class SlackClient { + private readonly adapterPromise: Promise; + + constructor(config: SlackRuntimeConfig = {}) { + this.adapterPromise = SlackAdapterFactory.create(config); + } + + /** + * Create a Slack client and eagerly resolve the local runtime. + * @param config - Slack runtime configuration. + * @returns Configured Slack client. + */ + static async create(config: SlackRuntimeConfig = {}): Promise { + const client = new SlackClient(config); + await client.getAdapter(); + return client; + } + + /** + * Inspect local runtime availability without creating a client. + * @param config - Slack runtime configuration. + * @returns Runtime detection details. + */ + static detect(config: SlackRuntimeConfig = {}): Promise { + return SlackAdapterFactory.detect(config); + } + + /** + * Detect the runtime that will be selected. Phase A always returns local. + * @param config - Slack runtime configuration. + * @returns Selected Slack runtime. + */ + static detectRuntime(config: SlackRuntimeConfig = {}): Promise { + return SlackAdapterFactory.detectRuntime(config); + } + + /** + * Return the selected low-level adapter. + * @returns Slack adapter. + */ + getAdapter(): Promise { + return this.adapterPromise; + } + + /** + * Return the selected runtime. + * @returns Slack runtime. + */ + async getRuntime(): Promise { + return (await this.getAdapter()).getRuntime(); + } + + /** + * Check whether the selected runtime is authenticated. + * @returns True when Slack auth succeeds. + */ + async isAuthenticated(): Promise { + return (await this.getAdapter()).isAuthenticated(); + } + + executeAction( + action: Name, + params: SlackActionParamsMap[Name] + ): Promise>; + executeAction( + action: SlackAction | SlackActionName, + params?: unknown + ): Promise>; + /** + * Execute any registered Slack primitive action by action name. + * @param action - Slack action name. + * @param params - Action parameters. + * @returns Action result. + */ + async executeAction( + action: SlackAction | SlackActionName, + params?: unknown + ): Promise> { + return (await this.getAdapter()).executeAction(action, params); + } + + /** + * Post a Slack message. + * @param params - Message parameters. + * @returns Posted message output. + */ + async postMessage(params: PostMessageParams): Promise { + return (await this.getAdapter()).postMessage(params); + } + + /** + * Resolve a Slack user mention. + * @param params - User resolution parameters. + * @returns Resolved mention. + */ + async resolveUser(params: ResolveUserParams): Promise { + return (await this.getAdapter()).resolveUser(params); + } + + /** + * Resolve a Slack channel reference. + * @param params - Channel resolution parameters. + * @returns Resolved channel. + */ + async resolveChannel(params: ResolveChannelParams): Promise { + return (await this.getAdapter()).resolveChannel(params); + } +} diff --git a/packages/slack-primitive/src/cloud-relay-runtime.ts b/packages/slack-primitive/src/cloud-relay-runtime.ts new file mode 100644 index 000000000..54d9274af --- /dev/null +++ b/packages/slack-primitive/src/cloud-relay-runtime.ts @@ -0,0 +1,235 @@ +import { BaseSlackAdapter, normalizeSlackRuntimeConfig } from './adapter.js'; +import { + SlackPostBackError, + type PostMessageOutput, + type PostMessageParams, + type ResolveChannelParams, + type ResolveUserParams, + type SlackChannelSummary, + type SlackResolutionWarning, + type SlackResolvedMention, + type SlackRuntime, + type SlackRuntimeConfig, + type SlackWebApiLike, +} from './types.js'; + +const POST_MESSAGE_PATH = '/api/v1/slack/post-message'; + +interface CloudRelayPostMessageSuccess { + ok: true; + ts: string; + channel: string; + workspaceId: string; +} + +interface CloudRelayPostMessageError { + ok: false; + error: string; + code: string; + retryAfterMs?: number; +} + +type CloudRelayPostMessageResponse = CloudRelayPostMessageSuccess | CloudRelayPostMessageError; + +export type CloudRelayFetch = ( + input: string | URL, + init?: { method?: string; headers?: Record; body?: string; signal?: AbortSignal } +) => Promise<{ ok: boolean; status: number; statusText: string; json: () => Promise }>; + +/** + * Slack adapter that proxies postMessage through relay-cloud's + * /api/v1/slack/post-message endpoint, which uses the workspace's + * configured Nango Slack connection (the ricky app). + * + * Used when the caller has CLOUD_API_TOKEN + CLOUD_API_URL but no local + * SLACK_BOT_TOKEN. Resolve operations are not supported in this mode — + * Phase A intentionally exposes only postMessage. + */ +export class SlackCloudRelayClient extends BaseSlackAdapter { + private readonly fetchImpl: CloudRelayFetch; + + constructor(config: SlackRuntimeConfig = {}, fetchImpl?: CloudRelayFetch) { + const normalized = normalizeSlackRuntimeConfig(config); + if (!normalized.cloudApiToken) { + throw new SlackPostBackError( + 'auth_token_missing', + 'auth_token_missing: CLOUD_API_TOKEN is required for Slack cloud-relay runtime.' + ); + } + if (!normalized.cloudApiUrl) { + throw new SlackPostBackError( + 'auth_token_missing', + 'auth_token_missing: CLOUD_API_URL is required for Slack cloud-relay runtime.' + ); + } + + super({ ...normalized, runtime: 'cloud-relay' }, createCloudRelaySlackStub()); + + const resolved = fetchImpl ?? (globalThis.fetch as CloudRelayFetch | undefined); + if (!resolved) { + throw new SlackPostBackError( + 'upstream_error', + 'cloud-relay runtime requires a fetch implementation; pass one explicitly or run on Node 18+.' + ); + } + this.fetchImpl = resolved; + } + + getRuntime(): SlackRuntime { + return 'cloud-relay'; + } + + async isAuthenticated(): Promise { + return Boolean(this.config.cloudApiToken && this.config.cloudApiUrl); + } + + async postMessage(params: PostMessageParams): Promise { + if (!params.channel) { + throw new SlackPostBackError('channel_not_found', 'channel is required for postMessage'); + } + if (!params.text) { + throw new SlackPostBackError('slack_api_error', 'text is required for postMessage'); + } + + const baseUrl = trimTrailingSlash(this.config.cloudApiUrl); + const url = `${baseUrl}${POST_MESSAGE_PATH}`; + + const body: Record = { + channel: params.channel, + text: params.text, + }; + if (params.threadTs) body.threadTs = params.threadTs; + if (typeof params.unfurl === 'boolean') { + body.unfurlLinks = params.unfurl; + body.unfurlMedia = params.unfurl; + } + + const controller = typeof AbortController === 'function' ? new AbortController() : null; + const timeoutHandle = + controller && this.config.timeout > 0 + ? setTimeout(() => controller.abort(), this.config.timeout) + : null; + + let response: Awaited>; + try { + response = await this.fetchImpl(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: `Bearer ${this.config.cloudApiToken}`, + }, + body: JSON.stringify(body), + ...(controller ? { signal: controller.signal } : {}), + }); + } catch (error) { + throw new SlackPostBackError( + 'upstream_error', + `cloud-relay request failed: ${error instanceof Error ? error.message : String(error)}`, + { cause: error } + ); + } finally { + if (timeoutHandle) clearTimeout(timeoutHandle); + } + + const payload = (await response.json().catch(() => null)) as CloudRelayPostMessageResponse | null; + + if (!payload) { + throw new SlackPostBackError( + 'upstream_error', + `cloud-relay returned non-JSON response (${response.status} ${response.statusText})` + ); + } + + if (!payload.ok) { + throw mapCloudRelayError(payload); + } + + const warnings: SlackResolutionWarning[] = []; + const unresolvedMentions: string[] = []; + if (params.mentions && params.mentions.length > 0) { + for (const mention of params.mentions) { + unresolvedMentions.push(mention); + warnings.push({ + type: 'mention_unresolved', + input: mention, + message: + 'mention resolution is not supported in cloud-relay runtime; pass user IDs in text directly.', + }); + } + } + + return { + channel: payload.channel, + ts: payload.ts, + text: params.text, + resolvedMentions: [], + unresolvedMentions, + warnings, + }; + } + + async resolveUser(_params: ResolveUserParams): Promise { + throw new SlackPostBackError( + 'unsupported_in_cloud_relay', + 'resolveUser is not supported in cloud-relay runtime (Phase A). Pass a Slack user ID directly.' + ); + } + + async resolveChannel(_params: ResolveChannelParams): Promise { + throw new SlackPostBackError( + 'unsupported_in_cloud_relay', + 'resolveChannel is not supported in cloud-relay runtime (Phase A). Pass a Slack channel ID or #name directly.' + ); + } +} + +/** + * Stub Slack web-api shaped object for the BaseSlackAdapter constructor. + * Cloud-relay overrides every action at the class level, so this stub + * only fires if a future change forgets to override one — in which case + * we want a loud error rather than a silent no-op. + */ +function createCloudRelaySlackStub(): SlackWebApiLike { + const unsupported = (method: string) => async () => { + throw new SlackPostBackError( + 'unsupported_in_cloud_relay', + `${method} is not available in cloud-relay runtime` + ); + }; + + return { + chat: { postMessage: unsupported('chat.postMessage') }, + conversations: { list: unsupported('conversations.list') }, + users: { lookupByEmail: unsupported('users.lookupByEmail') }, + auth: { test: unsupported('auth.test') }, + } as unknown as SlackWebApiLike; +} + +function mapCloudRelayError(payload: CloudRelayPostMessageError): SlackPostBackError { + const code = mapErrorCode(payload.code); + return new SlackPostBackError(code, `${payload.code}: ${payload.error}`, { + cause: payload, + }); +} + +function mapErrorCode(code: string): SlackPostBackError['code'] { + switch (code) { + case 'unauthorized': + return 'unauthorized'; + case 'not_connected': + return 'not_connected'; + case 'rate_limited': + return 'rate_limited'; + case 'slack_error': + return 'slack_api_error'; + case 'bad_request': + return 'slack_api_error'; + case 'upstream_error': + default: + return 'upstream_error'; + } +} + +function trimTrailingSlash(value: string): string { + return value.endsWith('/') ? value.slice(0, -1) : value; +} diff --git a/packages/slack-primitive/src/index.ts b/packages/slack-primitive/src/index.ts new file mode 100644 index 000000000..1165fcf91 --- /dev/null +++ b/packages/slack-primitive/src/index.ts @@ -0,0 +1,10 @@ +export * from './types.js'; +export * from './adapter.js'; +export * from './local-runtime.js'; +export * from './cloud-relay-runtime.js'; +export * from './noop-runtime.js'; +export * from './client.js'; +export * from './workflow-step.js'; +export * from './actions/post-message.js'; +export * from './actions/resolve-user.js'; +export * from './actions/resolve-channel.js'; diff --git a/packages/slack-primitive/src/local-runtime.ts b/packages/slack-primitive/src/local-runtime.ts new file mode 100644 index 000000000..829a33583 --- /dev/null +++ b/packages/slack-primitive/src/local-runtime.ts @@ -0,0 +1,35 @@ +import { WebClient } from '@slack/web-api'; + +import { BaseSlackAdapter, normalizeSlackRuntimeConfig } from './adapter.js'; +import { SlackPostBackError, type SlackRuntime, type SlackRuntimeConfig, type SlackWebApiLike } from './types.js'; + +/** + * Local Slack Web API adapter backed by SLACK_BOT_TOKEN. + */ +export class SlackWebApiClient extends BaseSlackAdapter { + constructor(config: SlackRuntimeConfig = {}, slack?: SlackWebApiLike) { + const normalized = normalizeSlackRuntimeConfig(config); + if (!normalized.token) { + throw new SlackPostBackError( + 'auth_token_missing', + 'auth_token_missing: SLACK_BOT_TOKEN is required for Slack local runtime.' + ); + } + + super(normalized, slack ?? createSlackWebClient(normalized.token, normalized.timeout)); + } + + getRuntime(): SlackRuntime { + return 'local'; + } +} + +/** + * Create a Slack WebClient instance. + * @param token - Slack bot token. + * @param timeout - Request timeout in milliseconds. + * @returns Slack Web API compatible client. + */ +export function createSlackWebClient(token: string, timeout: number): SlackWebApiLike { + return new WebClient(token, { timeout }) as SlackWebApiLike; +} diff --git a/packages/slack-primitive/src/noop-runtime.ts b/packages/slack-primitive/src/noop-runtime.ts new file mode 100644 index 000000000..36daf2251 --- /dev/null +++ b/packages/slack-primitive/src/noop-runtime.ts @@ -0,0 +1,102 @@ +import { BaseSlackAdapter, normalizeSlackRuntimeConfig } from './adapter.js'; +import { + SlackPostBackError, + type PostMessageOutput, + type PostMessageParams, + type ResolveChannelParams, + type ResolveUserParams, + type SlackChannelSummary, + type SlackResolutionWarning, + type SlackResolvedMention, + type SlackRuntime, + type SlackRuntimeConfig, + type SlackWebApiLike, +} from './types.js'; + +export type NoopLogger = (message: string, context: Record) => void; + +const DEFAULT_LOGGER: NoopLogger = (message, context) => { + // eslint-disable-next-line no-console + console.warn(`[slack-primitive:noop] ${message}`, context); +}; + +const NOOP_TS = '0000000000.000000'; + +/** + * Slack adapter that no-ops all actions. + * + * Used when neither SLACK_BOT_TOKEN nor CLOUD_API_TOKEN is configured. + * Lets workflows run end-to-end without hard-failing on missing Slack + * credentials in environments (CI, smoke tests, demos) where Slack + * delivery isn't required. Each call logs a warning so the operator + * can see that messages are being dropped. + */ +export class SlackNoopClient extends BaseSlackAdapter { + private readonly logger: NoopLogger; + + constructor(config: SlackRuntimeConfig = {}, logger?: NoopLogger) { + const normalized = normalizeSlackRuntimeConfig(config); + super({ ...normalized, runtime: 'noop' }, createNoopSlackStub()); + this.logger = logger ?? DEFAULT_LOGGER; + } + + getRuntime(): SlackRuntime { + return 'noop'; + } + + async isAuthenticated(): Promise { + return false; + } + + async postMessage(params: PostMessageParams): Promise { + const channel = params.channel ?? this.config.env.SLACK_DEFAULT_CHANNEL ?? '#noop'; + this.logger('postMessage dropped: no SLACK_BOT_TOKEN and no CLOUD_API_TOKEN configured.', { + channel, + preview: params.text.slice(0, 80), + }); + + const warnings: SlackResolutionWarning[] = [ + { + type: 'mention_unresolved', + input: channel, + message: 'Slack runtime is noop; configure SLACK_BOT_TOKEN or CLOUD_API_TOKEN to deliver messages.', + }, + ]; + + return { + channel, + ts: NOOP_TS, + text: params.text, + resolvedMentions: [], + unresolvedMentions: params.mentions ?? [], + warnings, + }; + } + + async resolveUser(_params: ResolveUserParams): Promise { + throw new SlackPostBackError( + 'auth_token_missing', + 'resolveUser requires SLACK_BOT_TOKEN; current runtime is noop.' + ); + } + + async resolveChannel(_params: ResolveChannelParams): Promise { + throw new SlackPostBackError( + 'auth_token_missing', + 'resolveChannel requires SLACK_BOT_TOKEN; current runtime is noop.' + ); + } +} + +function createNoopSlackStub(): SlackWebApiLike { + const unsupported = (method: string) => async () => { + throw new SlackPostBackError('auth_token_missing', `${method} is not available in noop runtime`); + }; + + return { + chat: { postMessage: unsupported('chat.postMessage') }, + conversations: { list: unsupported('conversations.list') }, + users: { lookupByEmail: unsupported('users.lookupByEmail') }, + auth: { test: unsupported('auth.test') }, + } as unknown as SlackWebApiLike; +} diff --git a/packages/slack-primitive/src/types.ts b/packages/slack-primitive/src/types.ts new file mode 100644 index 000000000..af4fbc6b0 --- /dev/null +++ b/packages/slack-primitive/src/types.ts @@ -0,0 +1,246 @@ +export type SlackRuntime = 'local' | 'cloud-relay' | 'noop'; + +export type SlackRuntimePreference = SlackRuntime | 'auto'; + +export enum SlackAction { + PostMessage = 'postMessage', + ResolveUser = 'resolveUser', + ResolveChannel = 'resolveChannel', +} + +export type SlackActionName = `${SlackAction}`; + +export const SLACK_ACTIONS = Object.values(SlackAction); + +export interface SlackRuntimeConfig { + /** Runtime mode. Defaults to 'auto' (cloud-relay → local → noop priority). */ + runtime?: SlackRuntimePreference; + /** Slack bot token. Defaults to SLACK_BOT_TOKEN. */ + token?: string; + /** Cloud API bearer token used by the cloud-relay runtime. Defaults to CLOUD_API_TOKEN. */ + cloudApiToken?: string; + /** Cloud API base URL used by the cloud-relay runtime. Defaults to CLOUD_API_URL. */ + cloudApiUrl?: string; + /** Environment used for token lookup. Defaults to process.env. */ + env?: Record; + /** Request timeout in milliseconds passed to the Slack WebClient or cloud-relay fetch. */ + timeout?: number; +} + +export interface RequiredSlackRuntimeConfig extends SlackRuntimeConfig { + runtime: SlackRuntime; + env: Record; + token: string; + cloudApiToken: string; + cloudApiUrl: string; + timeout: number; +} + +export interface SlackRuntimeAvailability { + runtime: SlackRuntime; + available: boolean; + authenticated?: boolean; + reason: string; + error?: string; +} + +export interface SlackRuntimeDetectionResult { + runtime: SlackRuntime; + requestedRuntime: SlackRuntimePreference; + source: 'config' | 'environment'; + available: boolean; + reason: string; + checkedAt: string; + local: SlackRuntimeAvailability; + cloudRelay: SlackRuntimeAvailability; + noop: SlackRuntimeAvailability; +} + +export type SlackPostBackErrorCode = + | 'auth_token_missing' + | 'channel_not_found' + | 'slack_api_error' + | 'not_connected' + | 'rate_limited' + | 'upstream_error' + | 'unauthorized' + | 'unsupported_in_cloud_relay'; + +export class SlackPostBackError extends Error { + readonly code: SlackPostBackErrorCode; + readonly cause?: unknown; + + constructor(code: SlackPostBackErrorCode, message?: string, options: { cause?: unknown } = {}) { + super(message ?? code); + this.name = 'SlackPostBackError'; + this.code = code; + this.cause = options.cause; + } +} + +export interface SlackUserSummary { + id: string; + name?: string; + realName?: string; + profile?: { + email?: string; + displayName?: string; + realName?: string; + }; +} + +export interface SlackChannelSummary { + id: string; + name?: string; + isChannel?: boolean; + isGroup?: boolean; + isIm?: boolean; + isPrivate?: boolean; +} + +export interface SlackResolutionWarning { + type: 'mention_unresolved'; + input: string; + message: string; +} + +export interface SlackResolvedMention { + input: string; + userId: string; +} + +export interface PostMessageParams { + channel?: string; + text: string; + threadTs?: string; + mentions?: string[]; + unfurl?: boolean; +} + +export interface ResolveUserParams { + mention: string; +} + +export interface ResolveChannelParams { + channel: string; +} + +export interface PostMessageOutput { + channel: string; + ts: string; + text: string; + resolvedMentions: SlackResolvedMention[]; + unresolvedMentions: string[]; + warnings: SlackResolutionWarning[]; +} + +export interface SlackActionParamsMap { + [SlackAction.PostMessage]: PostMessageParams; + [SlackAction.ResolveUser]: ResolveUserParams; + [SlackAction.ResolveChannel]: ResolveChannelParams; +} + +export interface SlackActionOutputMap { + [SlackAction.PostMessage]: PostMessageOutput; + [SlackAction.ResolveUser]: SlackResolvedMention; + [SlackAction.ResolveChannel]: SlackChannelSummary; +} + +export interface SlackActionResult { + success: boolean; + output: string; + data?: TOutput; + error?: string; + metadata?: { + runtime?: SlackRuntime; + executionTime?: number; + }; +} + +export interface SlackChatPostMessageParams { + channel: string; + text: string; + thread_ts?: string; + unfurl_links?: boolean; + unfurl_media?: boolean; +} + +export interface SlackPostMessageResponse { + ok?: boolean; + channel?: string; + ts?: string; + message?: { + text?: string; + }; + error?: string; +} + +export interface SlackLookupByEmailResponse { + ok?: boolean; + user?: SlackUserSummary; + error?: string; +} + +export interface SlackUsersListResponse { + ok?: boolean; + members?: SlackUserSummary[]; + response_metadata?: { + next_cursor?: string; + }; + error?: string; +} + +export interface SlackConversationsListResponse { + ok?: boolean; + channels?: SlackChannelSummary[]; + response_metadata?: { + next_cursor?: string; + }; + error?: string; +} + +export interface SlackWebApiLike { + chat: { + postMessage(params: SlackChatPostMessageParams): Promise; + }; + users: { + lookupByEmail(params: { email: string }): Promise; + list(params?: { cursor?: string; limit?: number }): Promise; + }; + conversations: { + list(params?: { + cursor?: string; + limit?: number; + types?: string; + }): Promise; + }; + auth?: { + test(): Promise<{ ok?: boolean; error?: string }>; + }; +} + +export abstract class SlackClientInterface { + protected readonly config: RequiredSlackRuntimeConfig; + + constructor(config: RequiredSlackRuntimeConfig) { + this.config = config; + } + + getRuntimeConfig(): RequiredSlackRuntimeConfig { + return this.config; + } + + abstract getRuntime(): SlackRuntime; + abstract isAuthenticated(): Promise; + abstract executeAction( + action: Name, + params: SlackActionParamsMap[Name] + ): Promise>; + abstract executeAction( + action: SlackAction | SlackActionName, + params?: unknown + ): Promise>; + abstract postMessage(params: PostMessageParams): Promise; + abstract resolveUser(params: ResolveUserParams): Promise; + abstract resolveChannel(params: ResolveChannelParams): Promise; +} diff --git a/packages/slack-primitive/src/workflow-step.ts b/packages/slack-primitive/src/workflow-step.ts new file mode 100644 index 000000000..df8dfd118 --- /dev/null +++ b/packages/slack-primitive/src/workflow-step.ts @@ -0,0 +1,469 @@ +import type { RunnerStepExecutor, WorkflowStep } from '@agent-relay/workflow-types'; + +import { SlackClient } from './client.js'; +import { + SlackAction, + SLACK_ACTIONS, + type PostMessageParams, + type SlackActionResult, + type SlackRuntimeConfig, +} from './types.js'; + +export type SlackStepOutputMode = 'data' | 'result' | 'summary' | 'raw' | 'none'; +export type SlackStepOutputFormat = 'json' | 'text'; + +export interface SlackStepOutputConfig { + /** Which action result becomes the workflow step output. Defaults to "data". */ + mode?: SlackStepOutputMode; + /** Emit JSON for structured chaining or text for simple downstream interpolation. Defaults to "json". */ + format?: SlackStepOutputFormat; + /** Select a nested field from the projected output, e.g. "ts" or "data.channel". */ + path?: string; + /** Include adapter metadata such as runtime and timing in JSON output. Defaults false. */ + includeMetadata?: boolean; + /** Pretty-print JSON output. Defaults false. */ + pretty?: boolean; +} + +export interface SlackStepConfig { + /** Unique step name within the workflow. */ + name: string; + /** Dependencies in the Relay workflow DAG. */ + dependsOn?: string[]; + /** Slack action to execute. Phase A supports postMessage. */ + action: 'postMessage'; + /** Slack channel id or #channel-name reference. Falls back to SLACK_DEFAULT_CHANNEL when omitted. */ + channel?: string; + /** Message text. Values may include workflow templates such as {{steps.plan.output.title}}. */ + text: string; + /** Optional parent message timestamp for threaded delivery. */ + threadTs?: string; + /** User mentions to prefix when resolved. Unresolved mentions are soft warnings in output. */ + mentions?: string[]; + /** Slack unfurl setting for links and media. */ + unfurl?: boolean; + /** Runtime settings for the local Slack Web API runtime. */ + config?: SlackRuntimeConfig; + /** Controls the string captured as {{steps..output}}. */ + output?: SlackStepOutputConfig; + /** Workflow step timeout in milliseconds. */ + timeoutMs?: number; + /** Number of retry attempts when the workflow runner retries this integration step. */ + retries?: number; +} + +export interface SlackStepExecutionContext { + workspaceId?: string; + client?: SlackClient; + config?: SlackRuntimeConfig; +} + +export interface SlackStepExecutionResult { + success: boolean; + output: string; + result: SlackActionResult; + error?: string; +} + +export interface SlackIntegrationStepResult { + output: string; + success: boolean; +} + +type ResolvedParams = Record; + +const SLACK_INTEGRATION = 'slack'; +const RESERVED_PARAM_KEYS = new Set(['action', 'config', 'slackConfig', 'output', 'params']); + +/** + * Create a Relay integration step for posting a Slack message. + * @param config - Slack step configuration. + * @returns Workflow integration step. + */ +export function createSlackStep(config: SlackStepConfig): WorkflowStep { + validateSlackStepConfig(config); + + const params: Record = { + text: config.text, + }; + + if (config.channel !== undefined) params.channel = config.channel; + if (config.threadTs !== undefined) params.threadTs = config.threadTs; + if (config.mentions !== undefined) params.mentions = JSON.stringify(config.mentions); + if (config.unfurl !== undefined) params.unfurl = String(config.unfurl); + if (config.config !== undefined) params.config = JSON.stringify(config.config); + if (config.output !== undefined) params.output = JSON.stringify(config.output); + + const step: WorkflowStep = { + name: config.name, + type: 'integration', + integration: SLACK_INTEGRATION, + action: config.action, + params, + }; + + if (config.dependsOn !== undefined) step.dependsOn = config.dependsOn; + if (config.timeoutMs !== undefined) step.timeoutMs = config.timeoutMs; + if (config.retries !== undefined) step.retries = config.retries; + + return step; +} + +export class SlackStepExecutor implements RunnerStepExecutor { + constructor(private readonly options: SlackRuntimeConfig = {}) {} + + async executeAgentStep(): Promise { + throw new Error('SlackStepExecutor only executes Slack integration steps.'); + } + + async execute( + config: SlackStepConfig, + context: SlackStepExecutionContext = {} + ): Promise> { + validateSlackStepConfig(config); + + const runtimeConfig = mergeRuntimeConfig(this.options, context.config, config.config); + const client = context.client ?? new SlackClient(runtimeConfig); + const params = buildActionParams(config); + const result = await client.executeAction(SlackAction.PostMessage, params); + const output = formatStepOutput(config, result); + + return { + success: result.success, + output, + result, + error: result.error, + }; + } + + async executeIntegrationStep( + step: WorkflowStep, + resolvedParams: Record + ): Promise { + if (step.integration !== SLACK_INTEGRATION) { + return { + success: false, + output: `SlackStepExecutor only handles "${SLACK_INTEGRATION}" integration steps`, + }; + } + + try { + const config = slackStepConfigFromWorkflowStep(step, resolvedParams); + const result = await this.execute(config); + + return { + success: result.success, + output: result.success ? result.output : result.output || result.error || 'Slack step failed', + }; + } catch (error) { + return { + success: false, + output: error instanceof Error ? error.message : String(error), + }; + } + } +} + +/** + * Rebuild a Slack step config from resolved workflow params. + * @param step - Workflow step. + * @param resolvedParams - Params after workflow templating. + * @returns Slack step configuration. + */ +export function slackStepConfigFromWorkflowStep( + step: WorkflowStep, + resolvedParams: Record +): SlackStepConfig { + const params = normalizeResolvedParams(resolvedParams); + const action = step.action; + + if (action !== SlackAction.PostMessage) { + throw new Error(`Slack step "${step.name}" requires action "postMessage"`); + } + + const config = + readJsonParam(params.config ?? params.slackConfig, 'config') ?? undefined; + const output = readJsonParam(params.output, 'output') ?? undefined; + const actionParams = readActionParams(params); + + return { + name: step.name, + dependsOn: step.dependsOn, + action: SlackAction.PostMessage, + channel: readOptionalString(actionParams.channel), + text: readRequiredString(actionParams.text, 'text'), + threadTs: readOptionalString(actionParams.threadTs), + mentions: readStringArray(actionParams.mentions), + unfurl: readOptionalBoolean(actionParams.unfurl, 'unfurl'), + config, + output, + timeoutMs: step.timeoutMs, + retries: step.retries, + }; +} + +export function renderSlackTemplates(value: string, data: Record): string { + return value.replace( + /\{\{\s*steps\.([A-Za-z0-9_-]+)\.output(?:\.([A-Za-z0-9_.-]+))?\s*\}\}/g, + (_match, step, path) => { + const stepData = data.steps; + if (!isRecord(stepData)) return ''; + const entry = stepData[String(step)]; + if (!isRecord(entry)) return ''; + const output = entry.output; + const resolved = typeof path === 'string' && path.length > 0 ? resolvePath(output, path) : output; + return projectionToText(resolved); + } + ); +} + +function validateSlackStepConfig(config: SlackStepConfig): void { + if (!config.name) { + throw new Error('Slack step requires a non-empty name'); + } + if (!SLACK_ACTIONS.includes(config.action as SlackAction)) { + throw new Error(`Slack step "${config.name}" uses unsupported action "${config.action}"`); + } + if (config.action !== SlackAction.PostMessage) { + throw new Error(`Slack step "${config.name}" requires action "postMessage"`); + } + if (typeof config.text !== 'string' || config.text.length === 0) { + throw new Error(`Slack step "${config.name}" requires message text`); + } +} + +function buildActionParams(config: SlackStepConfig): PostMessageParams { + return { + channel: config.channel, + text: config.text, + threadTs: config.threadTs, + mentions: config.mentions, + unfurl: config.unfurl, + }; +} + +function readActionParams(params: ResolvedParams): Record { + const serializedParams = params.params; + if (serializedParams !== undefined) { + const parsed = readJsonParam>(serializedParams, 'params'); + if (parsed === undefined) return {}; + if (!isRecord(parsed)) { + throw new Error('Slack step params.params must be a JSON object'); + } + return parsed; + } + + const actionParams: Record = {}; + for (const [key, value] of Object.entries(params)) { + if (RESERVED_PARAM_KEYS.has(key)) continue; + actionParams[key] = value; + } + + return actionParams; +} + +function mergeRuntimeConfig(...configs: Array): SlackRuntimeConfig { + const merged: SlackRuntimeConfig = {}; + + for (const config of configs) { + if (!config) continue; + const { env, ...flatConfig } = config; + Object.assign(merged, flatConfig); + if (env) { + merged.env = { + ...merged.env, + ...env, + }; + } + } + + return merged; +} + +function formatStepOutput(config: SlackStepConfig, result: SlackActionResult): string { + const outputConfig = config.output ?? {}; + const mode = outputConfig.mode ?? 'data'; + const format = outputConfig.format ?? 'json'; + + if (mode === 'none') { + return ''; + } + + let projection = buildOutputProjection(mode, result, outputConfig); + + if (outputConfig.path) { + projection = resolvePath(projection, outputConfig.path); + } + + if (format === 'text') { + return projectionToText(projection); + } + + return JSON.stringify(projection, undefined, outputConfig.pretty ? 2 : undefined); +} + +function buildOutputProjection( + mode: SlackStepOutputMode, + result: SlackActionResult, + outputConfig: SlackStepOutputConfig +): unknown { + if (mode === 'raw') return result.output; + if (mode === 'summary') { + return withOptionalMetadata(summarizeResult(result), result, outputConfig); + } + if (mode === 'result') { + const projected: Record = { + success: result.success, + output: result.output, + }; + if (result.data !== undefined) projected.data = result.data; + if (result.error !== undefined) projected.error = result.error; + return withOptionalMetadata(projected, result, outputConfig); + } + + return withOptionalMetadata( + result.data ?? (result.output ? result.output : (result.error ?? null)), + result, + outputConfig + ); +} + +function summarizeResult(result: SlackActionResult): Record { + if (!result.success) { + return { + success: false, + error: result.error ?? 'Slack action failed', + }; + } + + if (isRecord(result.data)) { + return { + success: true, + channel: result.data.channel, + ts: result.data.ts, + unresolvedMentions: result.data.unresolvedMentions, + }; + } + + return { + success: true, + value: result.data ?? result.output, + }; +} + +function withOptionalMetadata( + value: unknown, + result: SlackActionResult, + outputConfig: SlackStepOutputConfig +): unknown { + if (!outputConfig.includeMetadata || result.metadata === undefined) { + return value; + } + + return { + value, + metadata: result.metadata, + }; +} + +function projectionToText(value: unknown): string { + if (typeof value === 'string') return value; + if (value === null || value === undefined) return ''; + if (Array.isArray(value)) return value.map((entry) => projectionToText(entry)).join('\n'); + if (isRecord(value)) { + if ('output' in value) return projectionToText(value.output); + if ('value' in value) return projectionToText(value.value); + if ('text' in value) return projectionToText(value.text); + if ('ts' in value) return projectionToText(value.ts); + if ('channel' in value) return projectionToText(value.channel); + } + return JSON.stringify(value); +} + +function resolvePath(value: unknown, path: string): unknown { + if (!path) return value; + + let current = value; + for (const segment of path.split('.')) { + if (Array.isArray(current) && /^\d+$/.test(segment)) { + current = current[Number(segment)]; + continue; + } + if (isRecord(current)) { + current = current[segment]; + continue; + } + return undefined; + } + + return current; +} + +function normalizeResolvedParams(params: Record): ResolvedParams { + const normalized: ResolvedParams = {}; + for (const [key, value] of Object.entries(params)) { + normalized[key] = coerceScalar(value); + } + return normalized; +} + +function coerceScalar(value: unknown): unknown { + if (typeof value !== 'string') { + return value; + } + + const trimmed = value.trim(); + if ( + (trimmed.startsWith('{') && trimmed.endsWith('}')) || + (trimmed.startsWith('[') && trimmed.endsWith(']')) || + (trimmed.startsWith('"') && trimmed.endsWith('"')) + ) { + try { + return JSON.parse(trimmed) as unknown; + } catch { + return value; + } + } + + return value; +} + +function readJsonParam(value: unknown, name: string): T | undefined { + if (value === undefined) return undefined; + if (typeof value !== 'string') return value as T; + + try { + return JSON.parse(value) as T; + } catch (error) { + throw new Error( + `Slack step params.${name} must be valid JSON: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +function readRequiredString(value: unknown, name: string): string { + if (typeof value === 'string' && value.length > 0) return value; + throw new Error(`Slack step requires ${name}`); +} + +function readOptionalString(value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined; +} + +function readStringArray(value: unknown): string[] | undefined { + if (value === undefined) return undefined; + if (Array.isArray(value) && value.every((item) => typeof item === 'string')) return value; + throw new Error('Slack step mentions must be a string array'); +} + +function readOptionalBoolean(value: unknown, name: string): boolean | undefined { + if (value === undefined) return undefined; + if (typeof value === 'boolean') return value; + if (value === 'true') return true; + if (value === 'false') return false; + throw new Error(`Slack step ${name} must be a boolean`); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/slack-primitive/tsconfig.examples.json b/packages/slack-primitive/tsconfig.examples.json new file mode 100644 index 000000000..d72a4c760 --- /dev/null +++ b/packages/slack-primitive/tsconfig.examples.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, + "include": ["examples/**/*", "src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/packages/slack-primitive/tsconfig.json b/packages/slack-primitive/tsconfig.json new file mode 100644 index 000000000..222999fef --- /dev/null +++ b/packages/slack-primitive/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["node"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/slack-primitive/vitest.config.ts b/packages/slack-primitive/vitest.config.ts new file mode 100644 index 000000000..e5c178309 --- /dev/null +++ b/packages/slack-primitive/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + setupFiles: ['../../test/vitest.setup.ts'], + include: ['src/**/*.test.ts'], + exclude: ['node_modules', 'dist'], + }, +}); diff --git a/specs/slack-primitive-impl.md b/specs/slack-primitive-impl.md new file mode 100644 index 000000000..a07cb4b17 --- /dev/null +++ b/specs/slack-primitive-impl.md @@ -0,0 +1,67 @@ +# Slack Primitive — Implementation Workflow + +**Status**: Ready +**Date**: 2026-05-08 +**Design spec**: [`specs/slack-primitive.md`](./slack-primitive.md) +**Runtime**: local + +This is the implementation prompt for ricky. The full design lives in `specs/slack-primitive.md`. This file exists so ricky has an unambiguous, local-only generation target without having to disambiguate the design doc's runtime-selection discussion. + +## Goal + +Implement the `packages/slack-primitive` package as described in the design spec. Mirror the layout of `packages/github-primitive` 1:1. + +## Files to create + +Target files (bare paths so the spec parser picks them up as `targetFiles`): + +- packages/slack-primitive/package.json +- packages/slack-primitive/tsconfig.json +- packages/slack-primitive/src/index.ts +- packages/slack-primitive/src/types.ts +- packages/slack-primitive/src/client.ts +- packages/slack-primitive/src/workflow-step.ts +- packages/slack-primitive/src/local-runtime.ts +- packages/slack-primitive/src/adapter.ts +- packages/slack-primitive/src/actions/post-message.ts +- packages/slack-primitive/src/actions/resolve-user.ts +- packages/slack-primitive/src/actions/resolve-channel.ts +- packages/slack-primitive/src/__tests__/post-message.test.ts +- packages/slack-primitive/examples/notify-on-pr.ts +- packages/slack-primitive/examples/README.md + +## Scope (Phase A of the design spec) + +Phase A only — postMessage + resolveUser + resolveChannel, with the local Web API runtime. Do not implement askQuestion, the Nango proxy transport, or interactive Block Kit forms in this pass. + +Concretely: + +1. Create `packages/slack-primitive/` with `src/index.ts`, `src/types.ts`, `src/client.ts`, `src/workflow-step.ts`, `src/local-runtime.ts`, `src/adapter.ts`, and `src/actions/{post-message,resolve-user,resolve-channel}.ts`. +2. Wire `SLACK_BOT_TOKEN` env-var auth in `local-runtime.ts`. Throw `SlackPostBackError('auth_token_missing')` if absent. +3. Implement `createSlackStep` with `action: 'postMessage'`, supporting `channel`, `text`, `threadTs`, `mentions`, `unfurl`, and `{{steps.X.output.path}}` templating. +4. Mention resolution: `@email@example.com` → `users.lookupByEmail`; bare handle `@khaliq` → user-cache lookup; raw user IDs pass through. Unresolved mentions are a soft error (logged on step output, message still posts). +5. Channel resolution: `#name` → `conversations.list` + match; channel IDs pass through. +6. Add an example workflow at `packages/slack-primitive/examples/notify-on-pr.ts` that posts a one-line PR-opened announcement (paired with `github-primitive`'s `createPR` step). +7. Add unit tests in `packages/slack-primitive/src/__tests__/` covering: token-missing error, channel name resolution, mention resolution success and soft-fail, `{{steps.X.output}}` templating substitution. + +## Constraints + +- Runtime: local only. Do not generate the alternate-runtime adapter, the Nango proxy code, or the fallback-transport code in this pass — those land in later phases described in the design spec. +- Use `@slack/web-api` as the underlying SDK. +- TypeScript ES modules, follow the conventions in `.claude/rules/typescript.md`. +- Match the public-API shape of `packages/github-primitive` so a developer who learned one can read the other in five minutes. +- Do not modify `packages/github-primitive`. Do not modify the design spec. + +## Acceptance gates + +1. `pnpm -F slack-primitive build` passes. +2. `pnpm -F slack-primitive test` passes with the unit tests above green. +3. `examples/notify-on-pr.ts` type-checks against the rest of the SDK. +4. A workflow that imports `createSlackStep` and posts to a real channel succeeds when `SLACK_BOT_TOKEN` is set and the bot is invited to the channel. (Manual smoke test — document the steps in `examples/README.md`.) + +## Out of scope + +- askQuestion (Phase B in the design spec). +- The alternate-runtime adapter and its transports (Phase A's second half + Phase C in the design spec). +- Interactive Block Kit, addReaction, updateMessage, replyToThread (Phase C). +- Workflow runner schema changes for askQuestion audit trail (tracked in issue #825). diff --git a/specs/slack-primitive.md b/specs/slack-primitive.md index 0ef62f2f3..732a3f0d8 100644 --- a/specs/slack-primitive.md +++ b/specs/slack-primitive.md @@ -78,7 +78,7 @@ createSlackStep({ name: 'announce-pr', action: 'postMessage', params: { - channel: '#wf-feature', // or channel id + channel?: '#wf-feature', // or channel id; optional — see "Default channel resolution" below text: 'PR opened: {{steps.open-pr.output.html_url}}', threadTs?: string, // reply into a thread mentions?: string[], // ['@khaliq', 'U02ABC123', 'khaliq@agent-relay.com'] @@ -94,6 +94,7 @@ Notes: - **Mentions are resolved before send.** `@khaliq` is looked up via `users.lookupByEmail` or the user-cache; if not found, the message still posts but a typed `SlackPostBackError(unknown_mention)` is logged on the step output. This is the same "fail soft on cosmetic errors, fail hard on real errors" pattern as github-primitive. - **Templating uses the existing `{{steps.X.output.path}}` chain.** No special Slack-specific templating syntax. - **Channel may be a name (`#wf-feature`) or ID.** Names are resolved at step time. +- **Channel is optional for `postMessage`.** If omitted, cloud-runtime calls sage's existing notify-channel resolver (`/api/internal/proactive/notify-channel`), which falls back: configured workspace default → `#general` → first joined channel (alphabetical). Local-runtime falls back to the `SLACK_DEFAULT_CHANNEL` env var; if that's also unset, validation fails. Reusing sage's resolver keeps "where do agent messages go" configured in one place per workspace. (Follow-up: factor the resolver into a shared cloud package once slack-primitive is the second consumer — tracked separately.) ### 4.3 `askQuestion` — the load-bearing verb @@ -102,11 +103,11 @@ createSlackStep({ name: 'confirm-account', action: 'askQuestion', params: { - channel: '#wf-feature', + channel: '#wf-feature', // required for askQuestion (no default fallback — be deliberate about where you block on humans) text: 'I found two AWS accounts that match `prod-*`. Which one should I deploy to?\n • acct-1234 (us-east-1, last modified 2 weeks ago)\n • acct-5678 (us-west-2, last modified yesterday)\nReply with `1` or `2`.', // How long to wait before failing the step - timeoutSeconds: 1800, // required: caller must set explicitly + timeoutSeconds: 1800, // required: caller must set explicitly (1800 = 30 min) // Who is allowed to answer. Default: anyone in the channel. allowedReplyFrom?: string[], // ['@khaliq'] @@ -140,15 +141,18 @@ Semantics: - Step succeeds with the parsed reply as output. 4. On timeout: step fails with a typed `SlackPostBackError(human_no_response, timeoutSeconds)` so the workflow's `onError` handler can decide whether to retry, escalate again, or hard-fail. 5. The primitive **never** falls back to a default answer. Silence is failure. +6. The primitive emits the full question/answer pair on the step's output record. **Durable persistence (for post-mortems) is the workflow runner's responsibility**, not the primitive's — see issue #825. #### Why `askQuestion` is the hard part Posting is trivial. Waiting on a human is the load-bearing piece. It introduces three constraints the rest of the SDK doesn't have: - **Workflows must be allowed to block on external input.** The runner already supports long-running steps (verification gates, sandbox bootstraps), so this is reusing existing plumbing — not inventing new lifecycle. -- **The step must be resumable.** If the workflow crashes between posting the question and receiving the answer, the resumed run must find the existing question (by run-id-tagged metadata in the message) and continue waiting from there, not re-ask. Implementation: stash `(questionTs, runId, stepName)` in the workflow run record before the polling loop starts; on resume, look up the row and rejoin the poll. +- **The step must be resumable, and idempotent across retries.** If the workflow crashes between posting the question and receiving the answer — or if the step retries via `retries: N` — the resumed/retried attempt must find the existing question and rejoin the poll, not re-ask. Implementation: stash `(questionTs, runId, stepName)` in the workflow run record before the polling loop starts, and embed `(runId, stepName)` in the posted message's metadata. The dedup key is `(runId, stepName)` — retries within the same run reuse the same question; different runs are independent even if they share a step name; attempt number is **not** part of the key. - **The channel's history must include the question.** This means cloud-runtime cannot use private DMs (the bot can't read DM history without `im:history` scope and that scope is rarely granted). `askQuestion` against a DM throws at validation time. +> **v1 limitation:** `askQuestion` only supports public/private channels, not DMs. To ask a single person privately, create a private channel containing just that person and the bot. DM support may land in v2 if real demand appears. + ### 4.4 `replyToThread`, `updateMessage`, `addReaction` These are utility verbs that exist so post/ask flows can be cleaned up: @@ -271,15 +275,7 @@ const token = config.token ?? process.env.SLACK_BOT_TOKEN; If neither is set and we're in `auto` mode, `local` is _not_ selected; `auto` falls through to `cloud`. The detection chain is the same as github-primitive's. -## 8. Open questions - -- **DM support.** Should `askQuestion` to a DM be supported when `im:history` is granted? Probably yes, gated on scope check. Defer to v2. -- **Slack Connect / shared channels.** The primitive should treat shared channels exactly like internal ones — the bot just needs to be invited. Need to verify Nango's Slack provider exposes them correctly. -- **Audit trail.** Cloud should write every `askQuestion` exchange to the workflow run record so post-mortems can see what the agent asked and how the human answered. This is straightforward but needs schema work; out of scope for the primitive itself. -- **Default channel resolution.** If the workflow doesn't specify a channel, should the primitive default to the workspace's "wf-default" channel? I think no — the workflow author should be explicit. But cloud could surface the default as `Resource.SlackDefaultChannel.value` for convenience. -- **Question idempotency on retry.** When a step retries (e.g. `retries: 2`), the second attempt should _not_ re-ask. The primitive should check the channel for an existing question with the same `(runId, stepName)` tag and resume waiting. Mentioned above under resumability — calling out here as the same mechanism. - -## 9. Acceptance criteria for v1 +## 8. Acceptance criteria for v1 The primitive ships when: @@ -290,7 +286,7 @@ The primitive ships when: 5. Cloud-runtime auth uses the workspace's existing Slack Nango connection — no new SST resource bindings, no new env vars beyond what github-primitive already added. 6. The `writing-agent-relay-workflows` skill has two new recipes: **Announce + Done** and **Ask Before You Guess**. -## 10. Phasing +## 9. Phasing | Phase | Scope | | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |