From 75f4d4f2e5461e7daed3eeb2058f566f72a0322b Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 5 May 2026 16:10:24 +0200 Subject: [PATCH 1/9] =?UTF-8?q?spec(slack-primitive):=20design=20for=20wor?= =?UTF-8?q?kflow=20=E2=86=94=20human=20messaging=20via=20local=20+=20cloud?= =?UTF-8?q?=20adapters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the github-primitive's adapter shape so the same workflow file runs locally (gh CLI / Slack token) and in cloud (Nango). Two flagship verbs: - postMessage — fire-and-forget human notification (PR opened, workflow done, etc.). Resolves @-mentions and #channel names at step time. - askQuestion — block the workflow on a human reply. Configurable timeout, replyMatch (regex/choice/any), allowedReplyFrom, and Block Kit interactive forms. Resumable across sandbox restarts via run-record metadata so retries don't re-ask. Captures the cultural change the primitive enables: agents should ask for clarification when blocked rather than guess. Includes two recipes ("Announce + Done", "Ask Before You Guess") that the writing-agent-relay-workflows skill will pick up when v1 ships. Cloud-runtime auth reuses the workspace's existing Nango Slack connection — no new SST resource bindings. Slack tokens don't rotate per-call, so the proxy form (nango.proxy({ endpoint: '/chat.postMessage' })) is the right shape — avoids the get-token-vs-proxy confusion the github-primitive had to work through. Phasing: A — postMessage + resolvers B — askQuestion (blocking, resumable) C — Block Kit interactive forms + utility verbs Co-Authored-By: Claude Opus 4.7 (1M context) --- specs/slack-primitive.md | 301 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 specs/slack-primitive.md diff --git a/specs/slack-primitive.md b/specs/slack-primitive.md new file mode 100644 index 000000000..4cb9ff2bb --- /dev/null +++ b/specs/slack-primitive.md @@ -0,0 +1,301 @@ +# Slack Primitive — Design Spec + +**Status**: Draft +**Date**: 2026-05-05 +**Author**: design session (human + Claude) +**Related**: `packages/github-primitive` (precedent), `skills/writing-agent-relay-workflows` (recipe #4 — Escalation) + +--- + +## 1. Why this primitive + +Workflows already produce _code_ — Phase C push-back lands the diff, the github-primitive opens the PR. What workflows can't yet do well is **talk to a human in the loop**: + +- Tell the human something happened ("PR #451 opened, here's the diff"). +- Ask the human a question and **wait for the answer** ("Is this the right account ID? I see two candidates."). +- Surface a blocker ("Auth failed, I need someone to re-auth this connection."). + +Today the answer is "post in a Slack-bridged relay channel and hope the bridge is up." That works in a sandbox where someone is watching. It does not work for cloud runs that the operator has walked away from. **The Slack primitive turns Slack into a first-class transport for workflow ↔ human communication**, with the same local/cloud adapter shape as the github-primitive, so the same workflow file works on a laptop and in `agent-relay cloud run`. + +This spec defines the API, runtime selection, and the two flagship verbs: + +1. **`postMessage`** — fire-and-forget human notification. +2. **`askQuestion`** — block the workflow on a human reply. + +Plus the cultural change it's meant to enable: **agents should ask for clarification when blocked rather than hallucinate a fix**. + +## 2. What we're not building (yet) + +- A general-purpose Slack bot. The primitive is **outbound from the workflow**: it posts and it waits-for-reply. Inbound message classification, slash commands, app home views, etc., are out of scope. +- Channel/user provisioning. The workflow assumes the channel and the bot user already exist. +- Threaded conversations beyond a single round-trip. `askQuestion` reads exactly one reply (configurable: first reply, first reply by a specific user, first reply matching a regex). Multi-turn dialogue with the same agent goes through the existing relay channel primitive. + +## 3. Runtime selection (mirrors github-primitive) + +```ts +type SlackRuntimePreference = 'local' | 'cloud' | 'auto'; +``` + +| Runtime | Transport | Auth source | When chosen | +| ------------------ | --------------------------- | -------------------------------- | ------------------------------------------------------ | +| `local` | Slack Web API directly | `SLACK_BOT_TOKEN` env or config | Operator running `agent-relay run` from a laptop | +| `cloud` | Nango → Slack workspace App | Nango connection (per-workspace) | `agent-relay cloud run`, workspace has Slack connected | +| `cloud` (fallback) | Relay-cloud Slack proxy | Workspace bearer token | `agent-relay cloud run`, no Nango Slack connection | +| `auto` | Detects the above in order | — | Default | + +Same as github-primitive: **the workflow author writes one file**. `runtime: 'auto'` does the right thing on a laptop and in cloud. + +### Auth resolution (cloud path) + +Cloud's lambda already wires `Resource.NangoSecretKey.value` and resolves `(workspaceId, provider) → connectionId`. The Slack primitive's `cloud-runtime` reuses that resolver — no new resource binding. + +For Slack, we expect the connection to be a **bot user OAuth token** (xoxb-_), not user-token (xoxp-_). Posting and reading replies both work with `chat:write`, `channels:history`, and `groups:history` scopes. The primitive validates scopes on first call and throws a typed error early if they're missing. + +## 4. Public API + +The shape is the same as github-primitive: a `SlackClient` for direct calls, a `SlackStepExecutor` + `createSlackStep` for declarative use inside `workflow(...)`. Most workflows use the step form. + +### 4.1 Action enum + +```ts +export enum SlackAction { + PostMessage = 'postMessage', + AskQuestion = 'askQuestion', + UpdateMessage = 'updateMessage', + AddReaction = 'addReaction', + ReplyToThread = 'replyToThread', + ResolveUser = 'resolveUser', // email/handle -> user id + ResolveChannel = 'resolveChannel', // name -> channel id +} +``` + +The first two are the load-bearing ones. The rest exist to make the first two pleasant (e.g. `ResolveUser` so you can write `@khaliq` instead of `U02ABC123` in workflow source). + +### 4.2 `postMessage` + +```ts +createSlackStep({ + name: 'announce-pr', + action: 'postMessage', + params: { + channel: '#wf-feature', // or channel id + text: 'PR opened: {{steps.open-pr.output.html_url}}', + threadTs?: string, // reply into a thread + mentions?: string[], // ['@khaliq', 'U02ABC123', 'khaliq@agent-relay.com'] + blocks?: SlackBlock[], // optional rich blocks + unfurl?: boolean, // default true + }, + output: { mode: 'data', path: 'ts' }, // message timestamp for follow-ups +}) +``` + +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. + +### 4.3 `askQuestion` — the load-bearing verb + +```ts +createSlackStep({ + name: 'confirm-account', + action: 'askQuestion', + params: { + channel: '#wf-feature', + 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, // 30 min default; required to set explicitly + + // Who is allowed to answer. Default: anyone in the channel. + allowedReplyFrom?: string[], // ['@khaliq'] + + // What constitutes a valid reply. Default: any non-empty text. + replyMatch?: + | { type: 'regex'; pattern: string } + | { type: 'choice'; choices: string[] } // exact match against one of these + | { type: 'any' }, + + // Optional: a structured form via Slack Block Kit. When set, the + // primitive renders a button group / select / etc. and the + // step output is the chosen value, not the raw reply text. + interactive?: SlackInteractiveSpec, + }, + output: { + mode: 'data', + // The step output is the parsed answer: + // { reply: string, replierUserId: string, replyTs: string, + // matchedChoice?: string, matchedGroups?: string[] } + }, +}) +``` + +Semantics: + +1. The primitive posts the question, with the workflow run id appended in small text so a human can find the source run. +2. It begins polling `conversations.history` (cloud) or subscribing via Slack Events API webhook (when configured) for replies in the channel after the question's `ts`. **No global event listener** — each `askQuestion` step polls its own scope, then unsubscribes. This is important: workflows must not interfere with each other. +3. On a reply that matches `replyMatch` from a user in `allowedReplyFrom`: + - Reaction `:eyes:` added to the question (so the human sees their answer was registered). + - 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. + +#### 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 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. + +### 4.4 `replyToThread`, `updateMessage`, `addReaction` + +These are utility verbs that exist so post/ask flows can be cleaned up: + +- `replyToThread` — post into the thread of a prior message (e.g. announce intermediate progress on a long workflow). +- `updateMessage` — edit a posted message (e.g. update a "running…" message to "done ✅" with the PR link). +- `addReaction` — `:white_check_mark:` on the question once the workflow's downstream succeeded; `:x:` on failure. + +## 5. Two recipes the skill should encourage + +These go into `skills/writing-agent-relay-workflows/SKILL.md` as new chat-native coordination recipes the moment the primitive ships. + +### 5.1 Announce + Done (post-result notification) + +```ts +.step(createSlackStep({ + name: 'notify-pr', + dependsOn: ['open-pr'], + action: 'postMessage', + params: { + channel: '#eng-cloud', + text: 'Workflow `{{workflow.name}}` opened {{steps.open-pr.output.html_url}}.', + mentions: ['@khaliq'], + }, +}), { executor: slack }) +``` + +Pair with the github-primitive's `createPR` step. Whenever a workflow ships a PR, post a one-liner in a channel humans actually watch. This is what closes the loop — without it, PRs created by cloud workflows live in a tab no one opens. + +### 5.2 Ask Before You Guess (clarification) + +```ts +.step('plan', { + agent: 'lead', + task: `... investigate the schema ... + +If the migration is ambiguous in any of these ways, do NOT guess and do NOT +pick one heuristically: + - the column to drop has data in production + - two tables both look like candidates for the FK target + - the index name conflicts with an existing one in a sibling repo + +Use the slack primitive to ask the human: + + await slack.askQuestion({ + channel: '#wf-migration', + text: 'I see two candidates for the FK target. Which one?', + timeoutSeconds: 1800, + replyMatch: { type: 'choice', choices: ['users', 'accounts'] }, + }); + +Resume only after you get an answer. Do not exit. Do not pick a default. +`, +}) +``` + +The cultural rule the skill should make explicit: **guessing is worse than asking.** Agents should be told, in their task strings, to escalate via Slack when they hit ambiguity in: + +- account/credential choice +- destructive operations (drops, deletes, force-pushes) +- scope conflicts ("the spec says X but the existing code does Y") +- upstream dependencies that look stale or broken + +The agent posts the question, waits, and resumes from the answer. The workflow remains deterministic from the runner's point of view — only the _content_ of one step's output is human-supplied. + +## 6. Failure modes & error codes + +```ts +type SlackPostBackErrorCode = + | 'auth_token_missing' // local: no SLACK_BOT_TOKEN; cloud: no Nango connection + | 'auth_token_invalid' // 401 from Slack — token revoked or wrong env + | 'missing_scope' // bot lacks chat:write / channels:history / etc. + | 'channel_not_found' // name didn't resolve, or bot not invited + | 'unknown_mention' // @-mention couldn't be resolved (soft error, logged) + | 'human_no_response' // askQuestion hit timeoutSeconds + | 'reply_did_not_match' // got a reply but replyMatch rejected it + | 'reply_from_unauthorized_user' + | 'rate_limited' // 429, with retry-after honored automatically + | 'slack_api_error'; // catch-all, includes upstream message +``` + +These match the github-primitive's error-code shape so workflow `onError` handlers can discriminate on `err.code` consistently across primitives. + +## 7. Implementation outline + +``` +packages/slack-primitive/ + src/ + index.ts // public exports + types.ts // SlackAction, SlackRuntimeConfig, SlackPostBackError + client.ts // SlackClient — direct API + workflow-step.ts // SlackStepExecutor + createSlackStep + local-runtime.ts // Web API via @slack/web-api + cloud-runtime.ts // Nango proxy + relay-cloud fallback + adapter.ts // runtime detection + selection + actions/ + post-message.ts + ask-question.ts + reply-to-thread.ts + update-message.ts + resolve-user.ts + resolve-channel.ts + __tests__/ + examples/ + end-to-end-ask-question.ts + notify-on-pr.ts +``` + +Keep it 1:1 with `packages/github-primitive` so anyone who learned one can read the other in five minutes. + +### Cloud-runtime token sourcing + +The cloud runtime calls Nango via `nango.proxy({ providerConfigKey, connectionId, method: 'POST', endpoint: '/chat.postMessage', data: {...} })`. Slack accepts both bot-token (xoxb-\*) and user-token; the connection must be configured for bot-token in the Nango Slack integration. Unlike github-app, there's no "give me a token to use directly" semantic — Slack tokens don't rotate per-call — so the proxy form is the right shape here. (This avoids the `nango.getToken(..., true)` confusion the github-primitive had to work through.) + +### Local-runtime token sourcing + +```ts +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 + +The primitive ships when: + +1. The same workflow file runs unmodified in `agent-relay run` (local) and `agent-relay cloud run` (cloud), posting a Slack message to the configured channel in both. +2. `askQuestion` blocks the workflow for at least 30 minutes, surfaces a reply matching the configured rule, and the parsed reply is available as `{{steps.X.output.reply}}` to downstream steps. +3. Workflow resume after a sandbox restart picks up an in-flight `askQuestion` from the message metadata rather than re-asking. +4. Mismatched scopes throw `missing_scope` at first call with a hint listing the missing scopes. +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 + +| Phase | Scope | +| ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **A** | `postMessage` + `resolveUser` + `resolveChannel`; local + cloud-Nango runtimes; example workflow that posts a PR-opened notification. | +| **B** | `askQuestion` with `replyMatch: { type: 'any' \| 'choice' }`; resumability via run-record metadata; example workflow that asks "deploy to prod?" and gates a deploy step on the answer. | +| **C** | `interactive` Block Kit forms; `addReaction`, `updateMessage`, `replyToThread`; relay-cloud fallback transport; skill-doc update with the two recipes. | + +A and B together are the v1 shipped surface — they're what unblocks the "agent should ask rather than guess" cultural change. C is polish that makes the primitive pleasant to use in production workflows. From 24d1180c17a89a29f67b484c0c6a34ff4ff13724 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 8 May 2026 15:37:08 +0200 Subject: [PATCH 2/9] trajectories cleanup --- .../compact_j5u7qhaw4q6a_2026-05-08.json | 482 ++++++++++++++++++ .../compact_j5u7qhaw4q6a_2026-05-08.md | 54 ++ .../2026-05}/traj_1775914133873_35667beb.json | 14 +- .../2026-05/traj_1775914133873_35667beb.md | 23 + .../2026-05}/traj_1776073106646_1839be2d.json | 74 +-- .../2026-05/traj_1776073106646_1839be2d.md | 63 +++ .../2026-05}/traj_1776113772922_bc92f121.json | 9 +- .../2026-05/traj_1776113772922_bc92f121.md | 66 +++ .../2026-05}/traj_3b3p1z4y7qlo.json | 9 +- .../completed/2026-05/traj_3b3p1z4y7qlo.md | 43 ++ .../2026-05}/traj_o9cx33xn5u39.json | 9 +- .../completed/2026-05/traj_o9cx33xn5u39.md | 67 +++ .trajectories/index.json | 104 ++-- 13 files changed, 945 insertions(+), 72 deletions(-) create mode 100644 .trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json create mode 100644 .trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md rename .trajectories/{active => completed/2026-05}/traj_1775914133873_35667beb.json (77%) create mode 100644 .trajectories/completed/2026-05/traj_1775914133873_35667beb.md rename .trajectories/{active => completed/2026-05}/traj_1776073106646_1839be2d.json (92%) create mode 100644 .trajectories/completed/2026-05/traj_1776073106646_1839be2d.md rename .trajectories/{active => completed/2026-05}/traj_1776113772922_bc92f121.json (97%) create mode 100644 .trajectories/completed/2026-05/traj_1776113772922_bc92f121.md rename .trajectories/{active => completed/2026-05}/traj_3b3p1z4y7qlo.json (98%) create mode 100644 .trajectories/completed/2026-05/traj_3b3p1z4y7qlo.md rename .trajectories/{active => completed/2026-05}/traj_o9cx33xn5u39.json (98%) create mode 100644 .trajectories/completed/2026-05/traj_o9cx33xn5u39.md diff --git a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json new file mode 100644 index 000000000..3c82be704 --- /dev/null +++ b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json @@ -0,0 +1,482 @@ +{ + "id": "compact_j5u7qhaw4q6a", + "version": 1, + "type": "compacted", + "compactedAt": "2026-05-08T13:33:56.731Z", + "sourceTrajectories": [ + "traj_ui5omrgz819d", + "traj_o9cx33xn5u39", + "traj_3b3p1z4y7qlo", + "traj_222ha5671idc", + "traj_1776113772922_bc92f121", + "traj_qb54w47qwod6", + "traj_tjadoebpscps", + "traj_1776105988184_29f1270c", + "traj_1776105620545_9dcebb3d", + "traj_e5i62wdjx0jd", + "traj_1776073106646_1839be2d", + "traj_w0xpsaoxuiyw", + "traj_9tt55is74dq5", + "traj_1775914296101_a4397efe", + "traj_1775914133873_35667beb", + "traj_rs2bt3x0fqba", + "traj_4zqhfqw7g28l", + "traj_703m7sqyq89t", + "traj_0t92gxaz6igh", + "traj_g3muawdq6bsb", + "traj_d48czxmgx4ac", + "traj_abjovknvcijv", + "traj_o8kgzhfu6jth", + "traj_tv1x9pamkqad", + "traj_8oh4r5km5eic", + "traj_530xmbfeljyb", + "traj_mk0t0cgn4ytq", + "traj_05xg7j388bc4", + "traj_avmkyoo2s3rt" + ], + "dateRange": { + "start": "2026-04-10T14:42:17.242Z", + "end": "2026-05-08T13:33:48.161Z" + }, + "summary": { + "totalDecisions": 31, + "totalEvents": 213, + "uniqueAgents": [ + "orchestrator", + "impl-codex", + "review-claude", + "planner", + "implementer", + "reviewer", + "impl", + "tester", + "fixer", + "fix-worker-1", + "verifier", + "default", + "lead", + "fix-worker-2", + "fix-worker-3", + "impl-1", + "impl-2", + "impl-3", + "impl-4" + ] + }, + "decisionGroups": [ + { + "category": "testing", + "decisions": [ + { + "question": "Muted Commander output in messaging CLI tests", + "chosen": "Muted Commander output in messaging CLI tests", + "reasoning": "The missing-argument coverage test should assert exit behavior without polluting stderr; configuring the test Command output keeps the suite clean while preserving runtime CLI behavior.", + "fromTrajectory": "traj_1776105988184_29f1270c" + }, + { + "question": "Whether to skip read-tests", + "chosen": "skip", + "reasoning": "Upstream dependency \"verify-impl\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip update-tests", + "chosen": "skip", + "reasoning": "Upstream dependency \"read-tests\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip verify-tests", + "chosen": "skip", + "reasoning": "Upstream dependency \"update-tests\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip build", + "chosen": "skip", + "reasoning": "Upstream dependency \"verify-tests\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip run-unit-tests", + "chosen": "skip", + "reasoning": "Upstream dependency \"fix-build\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip fix-unit-tests", + "chosen": "skip", + "reasoning": "Upstream dependency \"run-unit-tests\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip run-unit-tests-gate", + "chosen": "skip", + "reasoning": "Upstream dependency \"fix-unit-tests\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip e2e-validate", + "chosen": "skip", + "reasoning": "Upstream dependency \"run-unit-tests-gate\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip regression-tests", + "chosen": "skip", + "reasoning": "Upstream dependency \"e2e-validate\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Whether to skip fix-regressions", + "chosen": "skip", + "reasoning": "Upstream dependency \"regression-tests\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Preserved both sides of the trajectory merge by combining events and keeping the completed-file form", + "chosen": "Preserved both sides of the trajectory merge by combining events and keeping the completed-file form", + "reasoning": "Your branch still contained recent UI and preview-routing decisions on the old active trajectory, while main had already abandoned that trajectory and added new CI hardening notes. The safe resolution was to keep all recorded events, move the entry to the completed path, and advance its completion time to the latest preserved event.", + "fromTrajectory": "traj_mk0t0cgn4ytq" + } + ] + }, + { + "category": "other", + "decisions": [ + { + "question": "Handled PR 747 feedback in tracked parent worktree", + "chosen": "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.", + "fromTrajectory": "traj_1776113772922_bc92f121" + }, + { + "question": "Whether to skip commit", + "chosen": "skip", + "reasoning": "Upstream dependency \"fix-regressions\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Grouped findings by file ownership into two conflict-free buckets", + "chosen": "Grouped findings by file ownership into two conflict-free buckets", + "reasoning": "All findings on messaging.ts must stay together, all findings on fix-history-inbox-v2.ts must stay together, and both groups remain under the six-finding limit for parallel assignment.", + "fromTrajectory": "traj_e5i62wdjx0jd" + }, + { + "question": "Switch production SST alias to orgin.agentrelay.net to avoid the existing apex alias conflict", + "chosen": "Switch production SST alias to orgin.agentrelay.net to avoid the existing apex alias conflict", + "reasoning": "The current production deployment path in the new AWS account cannot claim agentrelay.net because the live site is still attached elsewhere. Moving the new stack to orgin.agentrelay.net allows a no-downtime deploy while the main domain remains untouched.", + "fromTrajectory": "traj_rs2bt3x0fqba" + }, + { + "question": "Collapsed the mobile docs sidebar into the hamburger menu instead of rendering it above the content rail", + "chosen": "Collapsed the mobile docs sidebar into the hamburger menu instead of rendering it above the content rail", + "reasoning": "On small screens the left rail consumes vertical space and duplicates navigation. Passing a mobile-only DocsNav into SiteNav keeps the docs tree available from the existing hamburger while preserving the desktop sidebar unchanged.", + "fromTrajectory": "traj_0t92gxaz6igh" + }, + { + "question": "Added /cloud to the footer company links", + "chosen": "Added /cloud to the footer company links", + "reasoning": "Cloud is a first-class site destination and fits the footer's secondary navigation without adding new layout complexity.", + "fromTrajectory": "traj_o8kgzhfu6jth" + }, + { + "question": "Implemented Browser primitive as SDK integration step", + "chosen": "Implemented Browser primitive as SDK integration step", + "reasoning": "The SDK runner already delegates type: integration steps through executeIntegrationStep, so the Browser primitive can plug in without changing DAG scheduling or runner internals.", + "fromTrajectory": "traj_05xg7j388bc4" + } + ] + }, + { + "category": "tooling", + "decisions": [ + { + "question": "Aligned publish-resolution CI check with publish build command", + "chosen": "Aligned publish-resolution CI check with publish build command", + "reasoning": "Codex review noted the new job used build:packages while publish runs npm run build. The CI guard now runs the full root build so root CLI/TypeScript failures are caught before merge.", + "fromTrajectory": "traj_1776113772922_bc92f121" + }, + { + "question": "Whether to skip fix-build", + "chosen": "skip", + "reasoning": "Upstream dependency \"build\" failed", + "fromTrajectory": "traj_1776105620545_9dcebb3d" + }, + { + "question": "Pinned package build resolution to TypeScript 5.7.3 via exact devDependency and npx-based build script", + "chosen": "Pinned package build resolution to TypeScript 5.7.3 via exact devDependency and npx-based build script", + "reasoning": "These packages were using a ranged TypeScript version and bare tsc, which allows resolution drift across environments. Pinning both the dependency and the build invocation makes package builds deterministic.", + "fromTrajectory": "traj_9tt55is74dq5" + }, + { + "question": "Identified deploy failure as CloudFront CNAME conflict on production web domain", + "chosen": "Identified deploy failure as CloudFront CNAME conflict on production web domain", + "reasoning": "GitHub Actions logs show sst deploy failed in WebCdnDistribution creation with AWS CloudFront 409 CNAMEAlreadyExists after the Next.js build completed successfully.", + "fromTrajectory": "traj_4zqhfqw7g28l" + }, + { + "question": "Stopped docs and blog content loaders from binding to build-machine absolute paths, and explicitly traced MDX content into the Next server bundle", + "chosen": "Stopped docs and blog content loaders from binding to build-machine absolute paths, and explicitly traced MDX content into the Next server bundle", + "reasoning": "The production stack showed a read against /home/runner/work/.../web/content/docs/introduction.mdx, which means the docs loader baked the GitHub runner path into the server chunk via import.meta.url. Resolving content from runtime cwd candidates fixes the path, and tracing content/docs plus content/blog ensures those source files are present in the deployed artifact.", + "fromTrajectory": "traj_703m7sqyq89t" + } + ] + }, + { + "category": "architecture", + "decisions": [ + { + "question": "Mirrored browser primitive workflow-step pattern for GitHub", + "chosen": "Mirrored browser primitive workflow-step pattern for GitHub", + "reasoning": "The SDK already routes integration steps through RunnerStepExecutor; serializing params/config/output preserves workflow template interpolation and keeps GitHub primitive behavior local to the primitive package.", + "fromTrajectory": "traj_tv1x9pamkqad" + }, + { + "question": "Implemented GitHub actions as adapter-driven REST helpers", + "chosen": "Implemented GitHub actions as adapter-driven REST helpers", + "reasoning": "The existing local and cloud runtimes already share a request(method,path,options) abstraction, so action modules can work across gh CLI and Nango without runtime-specific branches.", + "fromTrajectory": "traj_8oh4r5km5eic" + }, + { + "question": "Implemented GitHub primitive as base adapter layer with action stubs", + "chosen": "Implemented GitHub primitive as base adapter layer with action stubs", + "reasoning": "The requested step creates client/runtime infrastructure only; action-specific methods should exist on the contract but throw until the next implementation phase.", + "fromTrajectory": "traj_530xmbfeljyb" + }, + { + "question": "Implemented browser primitive as a nested workspace package", + "chosen": "Implemented browser primitive as a nested workspace package", + "reasoning": "The design document defines packages/primitives/browser as an independent primitive, so the implementation needs package metadata, a workspace entry, and package build coverage in addition to source files.", + "fromTrajectory": "traj_avmkyoo2s3rt" + } + ] + }, + { + "category": "database", + "decisions": [ + { + "question": "Localized workflow trajectory compatibility casts", + "chosen": "Localized workflow trajectory compatibility casts", + "reasoning": "Publish removes package-lock and resolves agent-trajectories@0.5.5, whose exported type aliases are narrower than its runtime schema. Keeping workflow metadata and casting only at the agent-trajectories adapter boundary preserves persisted trajectory shape while compiling against both 0.5.4 and 0.5.5.", + "fromTrajectory": "traj_1776113772922_bc92f121" + } + ] + }, + { + "category": "api", + "decisions": [ + { + "question": "Reversed the dark footer gradient so the brighter Relay blue sits on the top edge", + "chosen": "Reversed the dark footer gradient so the brighter Relay blue sits on the top edge", + "reasoning": "The footer background is driven by a single dark-mode token, so flipping the gradient stop order achieves the requested visual change without affecting the footer layout or light theme.", + "fromTrajectory": "traj_g3muawdq6bsb" + } + ] + }, + { + "category": "security", + "decisions": [ + { + "question": "Shifted dark footer styling to the existing Relay blue gradient", + "chosen": "Shifted dark footer styling to the existing Relay blue gradient", + "reasoning": "The footer tokens lived in brand.css, so the cleanest change was to swap the dark footer background from near-black to the same blue range already used elsewhere in the dark theme and tint the divider line to match.", + "fromTrajectory": "traj_d48czxmgx4ac" + } + ] + } + ], + "keyLearnings": [], + "keyFindings": [ + "\"implement-cloud-api-bootstrap\" completed → >7u›Write tests for @filename gpt-5.5 high · Context 100% left · ~/Projects/AgentWorkforce/relay · gpt-5.5 …\r\n╭────────", + "\"implement-relay-cli-client\" completed → >7u›Write tests for @filename gpt-5.5 high · Context 100% left · ~/Projects/AgentWorkforce/relay · gpt-5.5 …\r\n╭────────", + "\"fix-loop\" completed → >7u\r\n╭──────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.124.0) │\r\n│ ", + "\"final-review\" completed → Full review written to `.workflow-artifacts/cloud-run-start-from/final-review.md`.", + "\"plan\" completed → ✻ Crunched fo 1m 4s \r❯", + "\"implement\" completed → >7u\r\n╭─────────────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.121.0) │\r\n│", + "\"fix-cargo-check\" completed → >7u\r\n╭─────────────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.121.0) │\r\n│", + "\"fix-mcp-args-tests\" completed → >7u\r\n╭─────────────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.121.0) │\r\n│", + "\"fix-regressions\" completed → >7u\r\n╭─────────────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.121.0) │\r\n│", + "\"review\" completed → ✽", + "\"plan\" completed → ✻ Baked for 1m 58s \r❯", + "\"implement\" completed → >7u\r\n╭─────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.121.0) │\r\n│ ", + "\"fix-compile-errors\" completed → >7u\r\n╭─────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.121.0) │\r\n│ ", + "\"fix-test-failures\" completed → >7u\r\n╭─────────────────────────────────────────────╮\r\n│ >_ OpenAI Codex (v0.121.0) │\r\n│ ", + "\"review\" completed → ✢", + "\"edit-build-bun\" completed → EDIT_DONE", + "\"write-live-integration-test\" completed → LIVE_TEST_WRITTEN", + "\"fix-live-test\" completed → LIVE_GREEN", + "\"fix-typecheck\" completed → TYPECHECK_OK", + "\"fix-unit-regressions\" completed → UNIT_GREEN", + "\"review-diff\" completed → **REVIEW_OK**", + "\"fix-worker-1-step\" completed → Both fixes look correct. Here's the summary:\n\n---\n\n## Fix Summary\n\n### Finding 1: Duplicate `bundledDependencies`/`bundl", + "\"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-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", + "\"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", + "\"plan\" completed → -- INSERT -- ⏵⏵ bypass permissions on (shift+tab to cycle) · PR #708", + "\"fix-config\" completed → CHANGES_COMPLETE", + "\"fix-sdk\" completed → CHANGES_COMPLETE", + "\"fix-remaining-a\" completed → CHANGES_COMPLETE", + "\"fix-remaining-b\" completed → CHANGES_COMPLETE", + "\"review-results\" completed → REVIEW_COMPLETE" + ], + "filesAffected": [], + "commits": [], + "narrative": "This batch of trajectories covers a high-volume period (Apr 10 – May 8, 2026) of mixed work on the relay monorepo: CLI/broker feature additions (cloud run --resume/--start-from/--previous-run-id, mcp-args subcommand with --register flag, history/inbox --from/--agent fixes), build infrastructure hardening (TypeScript 5.7.3 pinning across 10+ packages, web-only CI gating, SDK build-resolution casts for agent-trajectories@0.5.5), web/docs frontend polish (mobile docs sidebar into hamburger, dark footer gradient, /cloud footer link, MDX path resolution fix), and primitive package construction (Browser and GitHub primitives with shared adapter pattern across gh CLI / Nango runtimes). A staging deploy regression was diagnosed as a CloudFront 409 CNAMEAlreadyExists conflict and worked around by switching the production SST alias to orgin.agentrelay.net. Most work ran through DAG-based workflow orchestrators (planner → implementer → verify gates → fix-loop → reviewer → PR), with codex as the implementer and claude as planner/reviewer. Two autofix-swarm sessions ran lead-architect plans that grouped findings by file ownership (messaging.ts vs fix-history-inbox-v2.ts) into non-overlapping parallel buckets — fix-worker-3 correctly no-op'd when its assigned group index didn't exist. One fix-inbox-agent-flag run was abandoned at verify-impl after implement-fixes succeeded, cascading 12 skipped downstream steps; a re-run with the tester-first ordering succeeded. Key reusable artifacts: PLAN.md / IMPL_SUMMARY.md / REVIEW.md repo-root convention per workflow, .workflow-artifacts// for per-run outputs, .msd/autofix-plan.json for swarm group assignment, and a verification-passed exit=0 signal as the canonical step-completion marker.", + "decisions": [ + { + "question": "How to forward cloud run resume/start-from flags through the stack?", + "chosen": "Forward --resume/--start-from/--previous-run-id from agent-relay CLI through the Cloud API into sandbox SDK env vars", + "reasoning": "Keeps the resume contract a thin pass-through — CLI parses, Cloud API bootstraps, sandbox SDK reads env. Avoids a new schema and matches existing launcher env propagation.", + "impact": "Implemented in src/cli/commands/cloud.ts and packages/cloud/src/types.ts; route + worker payloads + OpenAPI schema all updated; unblocks cloud-side resume." + }, + { + "question": "How should the broker mcp-args subcommand obtain its agent token?", + "chosen": "Add --register flag that mints its own at_live_* token via register_agent_token; agentToken becomes nullable", + "reasoning": "Lets cloud cleanup PR call mcp-args without pre-provisioning a token, while leaving the existing explicit-token path intact.", + "impact": "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." + }, + { + "question": "Where should TypeScript build resolution be pinned to avoid drift?", + "chosen": "Pin TypeScript 5.7.3 exact in devDependencies and route every package build through `npx -p typescript@5.7.3 tsc`", + "reasoning": "Ranged tsc + bare invocation allowed resolution drift across clean checkouts (npm publish strips lockfile). Pinning both dep version and invocation makes builds deterministic.", + "impact": "Updated package.json across acp-bridge, memory, trajectory, cloud, config, sdk, gateway, hooks, openclaw, policy, telemetry, user-directory." + }, + { + "question": "How to handle agent-trajectories@0.5.5 narrower exported types breaking SDK build during publish?", + "chosen": "Localize compatibility casts at the agent-trajectories adapter boundary; keep workflow metadata shape intact", + "reasoning": "The 0.5.5 export aliases are narrower than the runtime schema. Casting only at the adapter boundary preserves persisted trajectory shape and compiles against both 0.5.4 and 0.5.5.", + "impact": "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." + }, + { + "question": "How to recover from CloudFront 409 CNAMEAlreadyExists on production deploy?", + "chosen": "Switch production SST alias from agentrelay.net to orgin.agentrelay.net", + "reasoning": "Live apex site is still attached to the prior AWS account, so the new account cannot claim the CNAME. Moving to a sibling hostname allows zero-downtime cutover.", + "impact": "sst diff confirmed certificate + CloudFront issued for orgin.agentrelay.net; stale agentrelay.net validation resources removed; live apex untouched." + }, + { + "question": "How to fix production docs loader reading /home/runner/... paths?", + "chosen": "Resolve docs/blog content from runtime cwd candidates instead of import.meta.url; explicitly trace content/docs and content/blog into Next server bundle", + "reasoning": "import.meta.url baked the GitHub-runner build path into the server chunk. Runtime resolution + outputFileTracing ensures MDX is loaded from the deployed filesystem.", + "impact": "Production stops 404'ing on docs; build still hits unrelated intermittent prerender flake." + }, + { + "question": "How to structure the Browser and GitHub primitives?", + "chosen": "Independent nested workspace packages under packages/primitives// with shared adapter base (gh CLI / Nango / relay-cloud runtimes) and integration-step entry", + "reasoning": "SDK runner already routes type:integration steps through executeIntegrationStep, so primitives plug in without changing DAG scheduling. Adapter pattern lets actions share request(method,path,options) across runtimes.", + "impact": "Browser primitive (Playwright-backed) and GitHub primitive (REST helpers) both shipped with workflow-step examples and package build coverage." + }, + { + "question": "How to group autofix findings for parallel workers?", + "chosen": "Group by file ownership: all messaging.ts findings in one bucket, all fix-history-inbox-v2.ts in another, both under 6-finding limit", + "reasoning": "Same-file findings would race on edits; ownership grouping makes parallel buckets conflict-free.", + "impact": "Two swarm sessions completed cleanly; fix-worker-3 correctly returned no-op when assigned a non-existent group index." + } + ], + "conventions": [ + { + "pattern": "Workflow lifecycle: planner writes PLAN.md → codex implementer writes IMPL_SUMMARY.md → verify gates + test-fix-rerun loop → reviewer writes REVIEW.md at repo root → PR", + "rationale": "Three named artifacts let each role read prior context without re-deriving; reviewer compares diff against PLAN.md + IMPL_SUMMARY.md as ground truth.", + "scope": "All multi-step DAG workflows in this repo (mcp-args, cloud-run-start-from, sdk-build-resolution)." + }, + { + "pattern": "Per-run outputs land in .workflow-artifacts//.md (e.g. .workflow-artifacts/cloud-run-start-from/final-review.md)", + "rationale": "Keeps repo root clean of per-run debris while preserving completion evidence for the orchestrator to detect.", + "scope": "Workflow runner steps that produce review or fix-loop summaries." + }, + { + "pattern": "Step completion signaled by `Verification passed` + exit=0; legacy `STEP_COMPLETE` marker still observed", + "rationale": "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)." + }, + { + "pattern": "Tests-first ordering inside DAG: write/update test step must precede or run parallel to implement step before verify-impl", + "rationale": "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." + }, + { + "pattern": "Pin TypeScript exactly (`5.7.3`) and invoke via `npx -p typescript@5.7.3 tsc` in package build scripts", + "rationale": "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/." + }, + { + "pattern": "Web-only changes gate the heavy core test, package validation, rust CI, and node compatibility workflows", + "rationale": "Web-only PRs were running unrelated relay/SDK/Rust suites and burning CI minutes.", + "scope": ".github/workflows/*.yml top-level path filters." + }, + { + "pattern": "Autofix swarm: lead writes .msd/autofix-plan.json with up-to-N file-ownership-disjoint groups; workers dispatched by index", + "rationale": "Conflict-free parallel application; workers with no assigned group cleanly no-op.", + "scope": "autofix-swarm-* workflows triggered by pr_review or review_comment sources." + }, + { + "pattern": "Apply autofix-swarm fixes in the parent worktree at /Users/khaliqgant/Projects/AgentWorkforce/relay, not the .relay/workspace cwd copy", + "rationale": "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." + }, + { + "pattern": "In CLI test harnesses using Commander, call `configureOutput({ writeOut, writeErr })` to suppress built-in stderr/stdout", + "rationale": "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." + }, + { + "pattern": "Resolve runtime content paths via cwd candidate list, never via `import.meta.url`, in the Next.js web app; add MDX dirs to outputFileTracing", + "rationale": "import.meta.url bakes build-machine absolute paths into deployed chunks.", + "scope": "web/ docs and blog content loaders." + } + ], + "lessons": [ + { + "lesson": "Trajectory completion-evidence detector is noisy — it treats ANSI banners and TUI redraws as signals", + "context": "Many `findings` entries are garbled Codex TUI banners (`>7u╭──╮`) rather than substantive completion summaries.", + "recommendation": "Have specialist agents emit a structured COMPLETION_SUMMARY block (one-line verdict + files changed) so the trajectory captures meaning, not terminal escape sequences." + }, + { + "lesson": "DAG verify-impl failure cascades aggressively — one failed gate skipped 12 downstream steps", + "context": "The abandoned fix-inbox-agent-flag run completed implement-fixes successfully, then verify-impl failed and skipped read-tests, update-tests, build, fix-build, run-unit-tests, e2e-validate, regression-tests, commit, etc.", + "recommendation": "Either order tests-update before/parallel with verify-impl, or make verify-impl a soft gate that records failure but lets test-update run so the next attempt has a clean baseline." + }, + { + "lesson": "npm publish builds resolve types differently than dev installs", + "context": "agent-trajectories@0.5.5 narrower exported types broke the SDK build under publish but not under workspace npm install with the lockfile.", + "recommendation": "Run package-validation CI with `npm run build` (the publish command) and a fresh install, not `build:packages`, so root CLI/TS failures are caught before merge." + }, + { + "lesson": "CloudFront CNAMEs are AWS-account-global — moving infra between accounts cannot reuse the same hostname while the old account still owns it", + "context": "Production deploy run 24255758219 failed with 409 CNAMEAlreadyExists for agentrelay.net even though the AWS account had changed.", + "recommendation": "For multi-account migrations, plan a sibling hostname (orgin.agentrelay.net pattern) for cutover, then DNS-flip the apex once the old distribution is detached." + }, + { + "lesson": "Autofix swarm worktree confusion silently drops fixes", + "context": "Workers were editing files under .relay/workspace// instead of /Users/khaliqgant/Projects/AgentWorkforce/relay/, so changes never landed on the feature branch.", + "recommendation": "Verifier step must explicitly cd into the tracked parent worktree before re-applying or validating; document the cwd contract in the swarm worker prompt." + }, + { + "lesson": "`import.meta.url` is unsafe for content path resolution in serverless Next deployments", + "context": "Production docs loader was reading /home/runner/work/.../web/content/docs/introduction.mdx — the GitHub Actions runner path baked into the server chunk.", + "recommendation": "Always resolve content from runtime cwd candidates and add the content directories to Next outputFileTracing so the files ship in the deployed bundle." + }, + { + "lesson": "Long workflow `completed` durations (430h+) reflect orchestrator session bookkeeping, not real work", + "context": "Multiple sessions show 432h or 648h durations while the actual review step finished in minutes; the long tail is the workflow-runner waiting for trajectory finalization.", + "recommendation": "Use real wall-clock from first-step → last-step completion-evidence to gauge work effort; treat the orchestrator-level `completed` timestamp as session-close, not work-end." + } + ], + "openQuestions": [ + "Should the workflow runner finalize the trajectory immediately after the last verification rather than holding the session open for hours/days, or is the long tail intentional?", + "Verifier sessions in two autofix-swarm trajectories appear to have run for ~600 hours before completion — was the verifier blocked, or is this a session-close-on-merge artifact?", + "The mcp-args --register flag mints at_live_* tokens via register_agent_token; is there a documented rotation/cleanup story when the cloud cleanup PR retires the temporary token?", + "Web build still hits intermittent prerender failures after compile and type-check (noted in three sessions); is there a tracking issue, and should the workflow gate treat it as soft-fail?", + "After the orgin.agentrelay.net cutover, is there a plan and date to reclaim the apex agentrelay.net CNAME from the prior AWS account?" + ] +} diff --git a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md new file mode 100644 index 000000000..e596ada55 --- /dev/null +++ b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md @@ -0,0 +1,54 @@ +# Trajectory Compaction: 2026-04-10 - 2026-05-08 + +## Summary + +This batch of trajectories covers a high-volume period (Apr 10 – May 8, 2026) of mixed work on the relay monorepo: CLI/broker feature additions (cloud run --resume/--start-from/--previous-run-id, mcp-args subcommand with --register flag, history/inbox --from/--agent fixes), build infrastructure hardening (TypeScript 5.7.3 pinning across 10+ packages, web-only CI gating, SDK build-resolution casts for agent-trajectories@0.5.5), web/docs frontend polish (mobile docs sidebar into hamburger, dark footer gradient, /cloud footer link, MDX path resolution fix), and primitive package construction (Browser and GitHub primitives with shared adapter pattern across gh CLI / Nango runtimes). A staging deploy regression was diagnosed as a CloudFront 409 CNAMEAlreadyExists conflict and worked around by switching the production SST alias to orgin.agentrelay.net. Most work ran through DAG-based workflow orchestrators (planner → implementer → verify gates → fix-loop → reviewer → PR), with codex as the implementer and claude as planner/reviewer. Two autofix-swarm sessions ran lead-architect plans that grouped findings by file ownership (messaging.ts vs fix-history-inbox-v2.ts) into non-overlapping parallel buckets — fix-worker-3 correctly no-op'd when its assigned group index didn't exist. One fix-inbox-agent-flag run was abandoned at verify-impl after implement-fixes succeeded, cascading 12 skipped downstream steps; a re-run with the tester-first ordering succeeded. Key reusable artifacts: PLAN.md / IMPL_SUMMARY.md / REVIEW.md repo-root convention per workflow, .workflow-artifacts// for per-run outputs, .msd/autofix-plan.json for swarm group assignment, and a verification-passed exit=0 signal as the canonical step-completion marker. + +## Key Decisions (8) + +| 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. | +| 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 orgin.agentrelay.net | sst diff confirmed certificate + CloudFront issued for orgin.agentrelay.net; stale agentrelay.net validation resources removed; live apex untouched. | +| How to fix production docs loader reading /home/runner/... paths? | Resolve docs/blog content from runtime cwd candidates instead of import.meta.url; explicitly trace content/docs and content/blog into Next server bundle | Production stops 404'ing on docs; build still hits unrelated intermittent prerender flake. | +| How to structure the Browser and GitHub primitives? | Independent nested workspace packages under packages/primitives// with shared adapter base (gh CLI / Nango / relay-cloud runtimes) and integration-step entry | Browser primitive (Playwright-backed) and GitHub primitive (REST helpers) both shipped with workflow-step examples and package build coverage. | +| How to group autofix findings for parallel workers? | Group by file ownership: all messaging.ts findings in one bucket, all fix-history-inbox-v2.ts in another, both under 6-finding limit | Two swarm sessions completed cleanly; fix-worker-3 correctly returned no-op when assigned a non-existent group index. | + +## Conventions Established + +- **Workflow lifecycle: planner writes PLAN.md → codex implementer writes IMPL_SUMMARY.md → verify gates + test-fix-rerun loop → reviewer writes REVIEW.md at repo root → PR**: Three named artifacts let each role read prior context without re-deriving; reviewer compares diff against PLAN.md + IMPL_SUMMARY.md as ground truth. (scope: All multi-step DAG workflows in this repo (mcp-args, cloud-run-start-from, sdk-build-resolution).) +- **Per-run outputs land in .workflow-artifacts//.md (e.g. .workflow-artifacts/cloud-run-start-from/final-review.md)**: Keeps repo root clean of per-run debris while preserving completion evidence for the orchestrator to detect. (scope: Workflow runner steps that produce review or fix-loop summaries.) +- **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.) +- **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 worktree at /Users/khaliqgant/Projects/AgentWorkforce/relay, 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.) +- **Resolve runtime content paths via cwd candidate list, never via `import.meta.url`, in the Next.js web app; add MDX dirs to outputFileTracing**: import.meta.url bakes build-machine absolute paths into deployed chunks. (scope: web/ docs and blog content loaders.) + +## Lessons Learned + +- Trajectory completion-evidence detector is noisy — it treats ANSI banners and TUI redraws as signals (Many `findings` entries are garbled Codex TUI banners (`>7u╭──╮`) rather than substantive completion summaries.) - Have specialist agents emit a structured COMPLETION_SUMMARY block (one-line verdict + files changed) so the trajectory captures meaning, not terminal escape sequences. +- DAG verify-impl failure cascades aggressively — one failed gate skipped 12 downstream steps (The abandoned fix-inbox-agent-flag run completed implement-fixes successfully, then verify-impl failed and skipped read-tests, update-tests, build, fix-build, run-unit-tests, e2e-validate, regression-tests, commit, etc.) - Either order tests-update before/parallel with verify-impl, or make verify-impl a soft gate that records failure but lets test-update run so the next attempt has a clean baseline. +- npm publish builds resolve types differently than dev installs (agent-trajectories@0.5.5 narrower exported types broke the SDK build under publish but not under workspace npm install with the lockfile.) - Run package-validation CI with `npm run build` (the publish command) and a fresh install, not `build:packages`, so root CLI/TS failures are caught before merge. +- CloudFront CNAMEs are AWS-account-global — moving infra between accounts cannot reuse the same hostname while the old account still owns it (Production deploy run 24255758219 failed with 409 CNAMEAlreadyExists for agentrelay.net even though the AWS account had changed.) - For multi-account migrations, plan a sibling hostname (orgin.agentrelay.net pattern) for cutover, then DNS-flip the apex once the old distribution is detached. +- Autofix swarm worktree confusion silently drops fixes (Workers were editing files under .relay/workspace// instead of /Users/khaliqgant/Projects/AgentWorkforce/relay/, so changes never landed on the feature branch.) - Verifier step must explicitly cd into the tracked parent worktree before re-applying or validating; document the cwd contract in the swarm worker prompt. +- `import.meta.url` is unsafe for content path resolution in serverless Next deployments (Production docs loader was reading /home/runner/work/.../web/content/docs/introduction.mdx — the GitHub Actions runner path baked into the server chunk.) - Always resolve content from runtime cwd candidates and add the content directories to Next outputFileTracing so the files ship in the deployed bundle. +- Long workflow `completed` durations (430h+) reflect orchestrator session bookkeeping, not real work (Multiple sessions show 432h or 648h durations while the actual review step finished in minutes; the long tail is the workflow-runner waiting for trajectory finalization.) - Use real wall-clock from first-step → last-step completion-evidence to gauge work effort; treat the orchestrator-level `completed` timestamp as session-close, not work-end. + +## Open Questions + +- Should the workflow runner finalize the trajectory immediately after the last verification rather than holding the session open for hours/days, or is the long tail intentional? +- Verifier sessions in two autofix-swarm trajectories appear to have run for ~600 hours before completion — was the verifier blocked, or is this a session-close-on-merge artifact? +- The mcp-args --register flag mints at*live*\* tokens via register_agent_token; is there a documented rotation/cleanup story when the cloud cleanup PR retires the temporary token? +- Web build still hits intermittent prerender failures after compile and type-check (noted in three sessions); is there a tracking issue, and should the workflow gate treat it as soft-fail? +- After the orgin.agentrelay.net cutover, is there a plan and date to reclaim the apex agentrelay.net CNAME from the prior AWS account? + +## Stats + +- Sessions: 29, Agents: orchestrator, impl-codex, review-claude, planner, implementer, reviewer, impl, tester, fixer, fix-worker-1, verifier, default, lead, fix-worker-2, fix-worker-3, impl-1, impl-2, impl-3, impl-4, Files: 0, Commits: 0 +- Date range: 2026-04-10T14:42:17.242Z - 2026-05-08T13:33:48.161Z diff --git a/.trajectories/active/traj_1775914133873_35667beb.json b/.trajectories/completed/2026-05/traj_1775914133873_35667beb.json similarity index 77% rename from .trajectories/active/traj_1775914133873_35667beb.json rename to .trajectories/completed/2026-05/traj_1775914133873_35667beb.json index 9b37648c1..875c1b76d 100644 --- a/.trajectories/active/traj_1775914133873_35667beb.json +++ b/.trajectories/completed/2026-05/traj_1775914133873_35667beb.json @@ -8,8 +8,9 @@ "id": "9915b02beabe8e11e3101c6c" } }, - "status": "active", + "status": "completed", "startedAt": "2026-04-11T13:28:53.873Z", + "completedAt": "2026-05-08T13:33:48.161Z", "agents": [ { "name": "orchestrator", @@ -23,6 +24,7 @@ "title": "Planning", "agentName": "orchestrator", "startedAt": "2026-04-11T13:28:53.873Z", + "endedAt": "2026-05-08T13:33:48.161Z", "events": [ { "ts": 1775914133873, @@ -36,5 +38,13 @@ } ] } - ] + ], + "retrospective": { + "summary": "merged", + "approach": "Standard approach", + "confidence": 0.8 + }, + "commits": [], + "filesChanged": [], + "tags": [] } diff --git a/.trajectories/completed/2026-05/traj_1775914133873_35667beb.md b/.trajectories/completed/2026-05/traj_1775914133873_35667beb.md new file mode 100644 index 000000000..c2b164c22 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_1775914133873_35667beb.md @@ -0,0 +1,23 @@ +# Trajectory: fix-sdk-build-resolution-workflow + +> **Status:** ✅ Completed +> **Task:** 9915b02beabe8e11e3101c6c +> **Confidence:** 80% +> **Started:** April 11, 2026 at 03:28 PM +> **Completed:** May 8, 2026 at 03:33 PM + +--- + +## Summary + +merged + +**Approach:** Standard approach + +--- + +## Chapters + +### 1. Planning + +_Agent: orchestrator_ diff --git a/.trajectories/active/traj_1776073106646_1839be2d.json b/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json similarity index 92% rename from .trajectories/active/traj_1776073106646_1839be2d.json rename to .trajectories/completed/2026-05/traj_1776073106646_1839be2d.json index 0eb524091..e2aaec804 100644 --- a/.trajectories/active/traj_1776073106646_1839be2d.json +++ b/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json @@ -8,8 +8,9 @@ "id": "4b4d6459ebd2cb7a584fc785" } }, - "status": "active", + "status": "completed", "startedAt": "2026-04-13T09:38:26.646Z", + "completedAt": "2026-05-08T13:33:45.944Z", "agents": [ { "name": "orchestrator", @@ -48,6 +49,7 @@ "title": "Planning", "agentName": "orchestrator", "startedAt": "2026-04-13T09:38:26.646Z", + "endedAt": "2026-04-13T09:38:30.198Z", "events": [ { "ts": 1776073106646, @@ -59,41 +61,41 @@ "type": "note", "content": "Approach: 13-step dag workflow — Parsed 13 steps, 2 parallel tracks, 11 dependent steps, DAG validated, no cycles" } - ], - "endedAt": "2026-04-13T09:38:30.198Z" + ] }, { "id": "ch_1fbc8bc5", "title": "Execution: init-msd-dir, read-context", "agentName": "orchestrator", "startedAt": "2026-04-13T09:38:30.198Z", - "events": [], - "endedAt": "2026-04-13T09:38:30.310Z" + "endedAt": "2026-04-13T09:38:30.310Z", + "events": [] }, { "id": "ch_dfab3787", "title": "Convergence: init-msd-dir + read-context", "agentName": "orchestrator", "startedAt": "2026-04-13T09:38:30.310Z", + "endedAt": "2026-04-13T09:38:30.336Z", "events": [ { "ts": 1776073110312, "type": "reflection", "content": "init-msd-dir + read-context resolved. 2/2 steps completed. All steps completed on first attempt. Unblocking: write-findings, plan.", - "significance": "high", "raw": { "confidence": 0.75, "focalPoints": ["init-msd-dir: completed", "read-context: completed"] - } + }, + "significance": "high" } - ], - "endedAt": "2026-04-13T09:38:30.336Z" + ] }, { "id": "ch_7648cc0d", "title": "Execution: plan", "agentName": "lead", "startedAt": "2026-04-13T09:38:30.337Z", + "endedAt": "2026-04-13T09:41:25.802Z", "events": [ { "ts": 1776073110337, @@ -107,7 +109,6 @@ "ts": 1776073285714, "type": "completion-marker", "content": "\"plan\" marker-based completion — Legacy STEP_COMPLETE marker observed (6 signal(s), 1 relevant channel post(s), 1 file change(s); signals=COMPLETE, COMPLETE, >7u╭─────────────────────────────────────────────────────╮│ >_ OpenAI Codex (v0.116.0) ││ ││ model: loading /model to change ││ directory: ~/…/AgentWorkforce/.msd-autofix-16b32ead │╰─, Verification passed, COMPLETE, **[plan] Output:**; channel=**[plan] Output:**\n```\nhen I’ll notify the broker and retire the worker.◦WoorrkkiinWng5Wogorrk•kiinngg◦6•WWoorrkkiin◦Wng7Wog0orrk · 1 background terminal runnin; files=created:.msd/autofix-plan.json)", - "significance": "medium", "raw": { "stepName": "plan", "completionMode": "marker", @@ -127,7 +128,8 @@ ], "files": ["created:.msd/autofix-plan.json"] } - } + }, + "significance": "medium" }, { "ts": 1776073285714, @@ -135,22 +137,22 @@ "content": "\"plan\" completed → >7u╭─────────────────────────────────────────────────────╮│ >_ OpenAI Codex (v0.116.0) ││ ", "significance": "medium" } - ], - "endedAt": "2026-04-13T09:41:25.802Z" + ] }, { "id": "ch_e9e4a127", "title": "Execution: fix-worker-1-step, fix-worker-2-step, fix-worker-3-step", "agentName": "orchestrator", "startedAt": "2026-04-13T09:41:25.802Z", - "events": [], - "endedAt": "2026-04-13T09:41:25.803Z" + "endedAt": "2026-04-13T09:41:25.803Z", + "events": [] }, { "id": "ch_0396f73a", "title": "Execution: fix-worker-1-step", "agentName": "fix-worker-1", "startedAt": "2026-04-13T09:41:25.803Z", + "endedAt": "2026-04-13T09:41:25.803Z", "events": [ { "ts": 1776073285803, @@ -160,14 +162,14 @@ "agent": "fix-worker-1" } } - ], - "endedAt": "2026-04-13T09:41:25.803Z" + ] }, { "id": "ch_c581dc17", "title": "Execution: fix-worker-3-step", "agentName": "fix-worker-3", "startedAt": "2026-04-13T09:41:25.803Z", + "endedAt": "2026-04-13T09:41:25.803Z", "events": [ { "ts": 1776073285803, @@ -177,14 +179,14 @@ "agent": "fix-worker-3" } } - ], - "endedAt": "2026-04-13T09:41:25.803Z" + ] }, { "id": "ch_5d394f7d", "title": "Execution: fix-worker-2-step", "agentName": "fix-worker-2", "startedAt": "2026-04-13T09:41:25.803Z", + "endedAt": "2026-04-13T09:46:22.646Z", "events": [ { "ts": 1776073285803, @@ -198,7 +200,6 @@ "ts": 1776073295920, "type": "completion-evidence", "content": "\"fix-worker-3-step\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), exit=0; signals=0, No assigned work: plan defines groups at indexes `0` and `1` only, so group index `2` does not exist., OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-worker-3-step] Output:**; channel=**[fix-worker-3-step] Output:**\n```\nNo assigned work: plan defines groups at indexes `0` and `1` only, so group index `2` does not exist.\nActions taken:\n- Per i; exit=0)", - "significance": "medium", "raw": { "stepName": "fix-worker-3-step", "completionMode": "verification", @@ -217,7 +218,8 @@ ], "exitCode": 0 } - } + }, + "significance": "medium" }, { "ts": 1776073295920, @@ -229,7 +231,6 @@ "ts": 1776073537947, "type": "completion-evidence", "content": "\"fix-worker-2-step\" verification-based completion — Verification passed (5 signal(s), 1 relevant channel post(s), 3 file change(s), exit=0; signals=0, Completed work for group index `1` (`group-2`)., OpenAI Codex v0.116.0 (research preview), Verification passed, **[fix-worker-2-step] Output:**; channel=**[fix-worker-2-step] Output:**\n```\nCompleted work for group index `1` (`group-2`).\n**Files changed**\n- `workflows/fix-history-inbox-v2.ts`\n**What changed**\n- R; files=modified:src/cli/commands/messaging.ts, created:workflows/fix-history-inbox-v2.js, modified:workflows/fix-history-inbox-v2.ts; exit=0)", - "significance": "medium", "raw": { "stepName": "fix-worker-2-step", "completionMode": "verification", @@ -253,7 +254,8 @@ ], "exitCode": 0 } - } + }, + "significance": "medium" }, { "ts": 1776073537947, @@ -265,7 +267,6 @@ "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)", - "significance": "medium", "raw": { "stepName": "fix-worker-1-step", "completionMode": "verification", @@ -290,7 +291,8 @@ ], "exitCode": 0 } - } + }, + "significance": "medium" }, { "ts": 1776073582644, @@ -298,20 +300,19 @@ "content": "\"fix-worker-1-step\" completed → **Completed**\n- Fixed [src/cli/commands/messaging.ts](/Users/khaliqgant/Projects/AgentWorkforce/.msd-autofix-16b32ead/sr", "significance": "medium" } - ], - "endedAt": "2026-04-13T09:46:22.646Z" + ] }, { "id": "ch_631d4b5d", "title": "Convergence: fix-worker-1-step + fix-worker-2-step + fix-worker-3-step", "agentName": "orchestrator", "startedAt": "2026-04-13T09:46:22.646Z", + "endedAt": "2026-04-13T09:46:23.250Z", "events": [ { "ts": 1776073582646, "type": "reflection", "content": "fix-worker-1-step + fix-worker-2-step + fix-worker-3-step resolved. 3/3 steps completed. All steps completed on first attempt. Unblocking: check-files.", - "significance": "high", "raw": { "confidence": 1, "focalPoints": [ @@ -319,16 +320,17 @@ "fix-worker-2-step: completed", "fix-worker-3-step: completed" ] - } + }, + "significance": "high" } - ], - "endedAt": "2026-04-13T09:46:23.250Z" + ] }, { "id": "ch_4a34d7cd", "title": "Execution: verify-and-finalize", "agentName": "verifier", "startedAt": "2026-04-13T09:46:23.250Z", + "endedAt": "2026-05-08T13:33:45.944Z", "events": [ { "ts": 1776073583250, @@ -340,5 +342,13 @@ } ] } - ] + ], + "retrospective": { + "summary": "merged", + "approach": "Standard approach", + "confidence": 0.8 + }, + "commits": [], + "filesChanged": [], + "tags": [] } diff --git a/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.md b/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.md new file mode 100644 index 000000000..30f70b5f3 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.md @@ -0,0 +1,63 @@ +# Trajectory: autofix-swarm-Agentworkforce-relay-workflow + +> **Status:** ✅ Completed +> **Task:** 4b4d6459ebd2cb7a584fc785 +> **Confidence:** 80% +> **Started:** April 13, 2026 at 11:38 AM +> **Completed:** May 8, 2026 at 03:33 PM + +--- + +## Summary + +merged + +**Approach:** Standard approach + +--- + +## Chapters + +### 1. Planning + +_Agent: orchestrator_ + +### 2. Execution: init-msd-dir, read-context + +_Agent: orchestrator_ + +### 3. Convergence: init-msd-dir + read-context + +_Agent: orchestrator_ + +- init-msd-dir + read-context resolved. 2/2 steps completed. All steps completed on first attempt. Unblocking: write-findings, plan. + +### 4. Execution: plan + +_Agent: lead_ + +### 5. Execution: fix-worker-1-step, fix-worker-2-step, fix-worker-3-step + +_Agent: orchestrator_ + +### 6. Execution: fix-worker-1-step + +_Agent: fix-worker-1_ + +### 7. Execution: fix-worker-3-step + +_Agent: fix-worker-3_ + +### 8. Execution: fix-worker-2-step + +_Agent: fix-worker-2_ + +### 9. Convergence: fix-worker-1-step + fix-worker-2-step + fix-worker-3-step + +_Agent: orchestrator_ + +- fix-worker-1-step + fix-worker-2-step + fix-worker-3-step resolved. 3/3 steps completed. All steps completed on first attempt. Unblocking: check-files. + +### 10. Execution: verify-and-finalize + +_Agent: verifier_ diff --git a/.trajectories/active/traj_1776113772922_bc92f121.json b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json similarity index 97% rename from .trajectories/active/traj_1776113772922_bc92f121.json rename to .trajectories/completed/2026-05/traj_1776113772922_bc92f121.json index 956738e18..fc923eab3 100644 --- a/.trajectories/active/traj_1776113772922_bc92f121.json +++ b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json @@ -8,8 +8,9 @@ "id": "e22e8009a30d97412bef928d" } }, - "status": "active", + "status": "completed", "startedAt": "2026-04-13T20:56:12.922Z", + "completedAt": "2026-05-08T13:33:43.489Z", "agents": [ { "name": "orchestrator", @@ -132,6 +133,7 @@ "title": "Execution: verify-and-finalize", "agentName": "verifier", "startedAt": "2026-04-13T20:58:30.129Z", + "endedAt": "2026-05-08T13:33:43.489Z", "events": [ { "ts": 1776113910129, @@ -202,6 +204,11 @@ ] } ], + "retrospective": { + "summary": "merged", + "approach": "Standard approach", + "confidence": 0.8 + }, "commits": [], "filesChanged": [], "tags": [] diff --git a/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.md b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.md new file mode 100644 index 000000000..28347d18d --- /dev/null +++ b/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.md @@ -0,0 +1,66 @@ +# Trajectory: autofix-swarm-Agentworkforce-relay-workflow + +> **Status:** ✅ Completed +> **Task:** e22e8009a30d97412bef928d +> **Confidence:** 80% +> **Started:** April 13, 2026 at 10:56 PM +> **Completed:** May 8, 2026 at 03:33 PM + +--- + +## Summary + +merged + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Localized workflow trajectory compatibility casts + +- **Chose:** Localized workflow trajectory compatibility casts +- **Reasoning:** Publish removes package-lock and resolves agent-trajectories@0.5.5, whose exported type aliases are narrower than its runtime schema. Keeping workflow metadata and casting only at the agent-trajectories adapter boundary preserves persisted trajectory shape while compiling against both 0.5.4 and 0.5.5. + +### Aligned publish-resolution CI check with publish build command + +- **Chose:** Aligned publish-resolution CI check with publish build command +- **Reasoning:** Codex review noted the new job used build:packages while publish runs npm run build. The CI guard now runs the full root build so root CLI/TypeScript failures are caught before merge. + +### 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. + +--- + +## Chapters + +### 1. Planning + +_Agent: orchestrator_ + +### 2. Execution: init-msd-dir, read-context + +_Agent: orchestrator_ + +### 3. Convergence: init-msd-dir + read-context + +_Agent: orchestrator_ + +- init-msd-dir + read-context resolved. 2/2 steps completed. All steps completed on first attempt. Unblocking: fast-plan. + +### 4. Execution: fix-worker-1-step + +_Agent: fix-worker-1_ + +### 5. Execution: verify-and-finalize + +_Agent: verifier_ + +- Localized workflow trajectory compatibility casts: Localized workflow trajectory compatibility casts +- SDK trajectory build failure fixed and package-validation now includes a publish-style fresh npm install build. Verified sdk build with lifecycle scripts, sdk build against agent-trajectories@0.5.5 declarations, and full package build. +- Aligned publish-resolution CI check with publish build command: Aligned publish-resolution CI check with publish build command +- Handled PR 747 feedback in tracked parent worktree: Handled PR 747 feedback in tracked parent worktree +- PR 747 feedback fixes verified with SDK build, package build, full root build, and focused ProcessBackend executor tests diff --git a/.trajectories/active/traj_3b3p1z4y7qlo.json b/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json similarity index 98% rename from .trajectories/active/traj_3b3p1z4y7qlo.json rename to .trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json index 6b4334240..d74b0ba68 100644 --- a/.trajectories/active/traj_3b3p1z4y7qlo.json +++ b/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json @@ -9,8 +9,9 @@ "id": "7b3cd6bed0179c78ce57fa38" } }, - "status": "active", + "status": "completed", "startedAt": "2026-04-20T13:16:22.009Z", + "completedAt": "2026-05-08T13:33:40.636Z", "agents": [ { "name": "orchestrator", @@ -233,6 +234,7 @@ "title": "Execution: review", "agentName": "reviewer", "startedAt": "2026-04-20T13:30:02.551Z", + "endedAt": "2026-05-08T13:33:40.636Z", "events": [ { "ts": 1776691802551, @@ -274,6 +276,11 @@ ] } ], + "retrospective": { + "summary": "merged", + "approach": "Standard approach", + "confidence": 0.8 + }, "commits": [], "filesChanged": [], "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", diff --git a/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.md b/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.md new file mode 100644 index 000000000..650283449 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.md @@ -0,0 +1,43 @@ +# Trajectory: add-mcp-args-subcommand-workflow + +> **Status:** ✅ Completed +> **Task:** 7b3cd6bed0179c78ce57fa38 +> **Confidence:** 80% +> **Started:** April 20, 2026 at 03:16 PM +> **Completed:** May 8, 2026 at 03:33 PM + +--- + +## Summary + +merged + +**Approach:** Standard approach + +--- + +## Chapters + +### 1. Planning + +_Agent: orchestrator_ + +### 2. Execution: plan + +_Agent: planner_ + +### 3. Execution: implement + +_Agent: implementer_ + +### 4. Execution: fix-compile-errors + +_Agent: implementer_ + +### 5. Execution: fix-test-failures + +_Agent: implementer_ + +### 6. Execution: review + +_Agent: reviewer_ diff --git a/.trajectories/active/traj_o9cx33xn5u39.json b/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json similarity index 98% rename from .trajectories/active/traj_o9cx33xn5u39.json rename to .trajectories/completed/2026-05/traj_o9cx33xn5u39.json index 9e03fc2c8..6cd2750b3 100644 --- a/.trajectories/active/traj_o9cx33xn5u39.json +++ b/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json @@ -9,8 +9,9 @@ "id": "af395e548879905e314fe615" } }, - "status": "active", + "status": "completed", "startedAt": "2026-04-20T15:06:23.387Z", + "completedAt": "2026-05-08T13:33:35.341Z", "agents": [ { "name": "orchestrator", @@ -363,6 +364,7 @@ "title": "Execution: review", "agentName": "reviewer", "startedAt": "2026-04-20T15:19:38.968Z", + "endedAt": "2026-05-08T13:33:35.341Z", "events": [ { "ts": 1776698378968, @@ -400,6 +402,11 @@ ] } ], + "retrospective": { + "summary": "merged", + "approach": "Standard approach", + "confidence": 0.8 + }, "commits": [], "filesChanged": [], "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", diff --git a/.trajectories/completed/2026-05/traj_o9cx33xn5u39.md b/.trajectories/completed/2026-05/traj_o9cx33xn5u39.md new file mode 100644 index 000000000..64f279e01 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_o9cx33xn5u39.md @@ -0,0 +1,67 @@ +# Trajectory: add-mcp-args-register-flag-workflow + +> **Status:** ✅ Completed +> **Task:** af395e548879905e314fe615 +> **Confidence:** 80% +> **Started:** April 20, 2026 at 05:06 PM +> **Completed:** May 8, 2026 at 03:33 PM + +--- + +## Summary + +merged + +**Approach:** Standard approach + +--- + +## Chapters + +### 1. Planning + +_Agent: orchestrator_ + +### 2. Execution: read-cli-mcp-args, read-mcp-args-command-def, read-register-agent-token + +_Agent: orchestrator_ + +### 3. Convergence: read-cli-mcp-args + read-mcp-args-command-def + read-register-agent-token + +_Agent: orchestrator_ + +- read-cli-mcp-args + read-mcp-args-command-def + read-register-agent-token resolved. 3/3 steps completed. All steps completed on first attempt. Unblocking: plan. + +### 4. Execution: plan + +_Agent: planner_ + +### 5. Execution: implement + +_Agent: implementer_ + +### 6. Execution: verify-register-flag-added, verify-authority-functions-untouched + +_Agent: orchestrator_ + +### 7. Convergence: verify-register-flag-added + verify-authority-functions-untouched + +_Agent: orchestrator_ + +- verify-register-flag-added + verify-authority-functions-untouched resolved. 2/2 steps completed. All steps completed on first attempt. Unblocking: cargo-check. + +### 8. Execution: fix-cargo-check + +_Agent: implementer_ + +### 9. Execution: fix-mcp-args-tests + +_Agent: implementer_ + +### 10. Execution: fix-regressions + +_Agent: implementer_ + +### 11. Execution: review + +_Agent: reviewer_ diff --git a/.trajectories/index.json b/.trajectories/index.json index 6d6c7fb87..f4aea8091 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,99 +1,118 @@ { "version": 1, - "lastUpdated": "2026-04-27T20:08:46.387Z", + "lastUpdated": "2026-05-08T13:35:35.850Z", "trajectories": { "traj_1775914133873_35667beb": { "title": "fix-sdk-build-resolution-workflow", - "status": "active", + "status": "completed", "startedAt": "2026-04-11T13:28:53.873Z", - "path": "/home/runner/work/relay/relay/.trajectories/active/traj_1775914133873_35667beb.json" + "completedAt": "2026-05-08T13:33:48.161Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1775914133873_35667beb.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776073106646_1839be2d": { "title": "autofix-swarm-Agentworkforce-relay-workflow", - "status": "active", + "status": "completed", "startedAt": "2026-04-13T09:38:26.646Z", - "path": "/home/runner/work/relay/relay/.trajectories/active/traj_1776073106646_1839be2d.json" + "completedAt": "2026-05-08T13:33:45.944Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1776073106646_1839be2d.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776113772922_bc92f121": { "title": "autofix-swarm-Agentworkforce-relay-workflow", - "status": "active", + "status": "completed", "startedAt": "2026-04-13T20:56:12.922Z", - "path": "/home/runner/work/relay/relay/.trajectories/active/traj_1776113772922_bc92f121.json" + "completedAt": "2026-05-08T13:33:43.489Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_1776113772922_bc92f121.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_3b3p1z4y7qlo": { "title": "add-mcp-args-subcommand-workflow", - "status": "active", + "status": "completed", "startedAt": "2026-04-20T13:16:22.009Z", - "path": "/home/runner/work/relay/relay/.trajectories/active/traj_3b3p1z4y7qlo.json" + "completedAt": "2026-05-08T13:33:40.636Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_3b3p1z4y7qlo.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_o9cx33xn5u39": { "title": "add-mcp-args-register-flag-workflow", - "status": "active", + "status": "completed", "startedAt": "2026-04-20T15:06:23.387Z", - "path": "/home/runner/work/relay/relay/.trajectories/active/traj_o9cx33xn5u39.json" + "completedAt": "2026-05-08T13:33:35.341Z", + "path": "/Users/khaliqgant/Projects/AgentWorkforce/relay/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_0t92gxaz6igh": { "title": "Move docs sidebar into the mobile hamburger menu", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_0t92gxaz6igh.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776105620545_9dcebb3d": { "title": "fix-inbox-agent-flag-workflow", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_1776105620545_9dcebb3d.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776105988184_29f1270c": { "title": "fix-inbox-agent-flag-workflow", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_1776105988184_29f1270c.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_222ha5671idc": { "title": "validate-cloud-connect-e2e-workflow", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_222ha5671idc.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_4zqhfqw7g28l": { "title": "Investigate GitHub Actions failure for run 24255758219 job 70826792063", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_4zqhfqw7g28l.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_703m7sqyq89t": { "title": "Fix production docs loader using build-machine absolute MDX paths", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_703m7sqyq89t.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_9tt55is74dq5": { "title": "Pin TypeScript build resolution for acp-bridge, memory, trajectory, and cloud", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_9tt55is74dq5.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_abjovknvcijv": { "title": "Scope CI so web-only changes do not run unrelated relay and SDK tests", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_abjovknvcijv.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_d48czxmgx4ac": { "title": "Shift dark-mode footer from black to Relay blue", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_d48czxmgx4ac.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_dw8ihhdb8ip7": { "title": "fix-dm-history-workflow", @@ -107,63 +126,72 @@ "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_e5i62wdjx0jd.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_g3muawdq6bsb": { "title": "Invert dark footer gradient so the lighter blue is at the top", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_g3muawdq6bsb.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_mk0t0cgn4ytq": { "title": "Merge origin/main into better-nav and resolve trajectory conflicts", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_mk0t0cgn4ytq.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_o8kgzhfu6jth": { "title": "Add /cloud link to footer", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_o8kgzhfu6jth.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_qb54w47qwod6": { "title": "fix-history-from-workflow", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_qb54w47qwod6.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_rs2bt3x0fqba": { "title": "Compare failed web deploy to prior deploys and identify no-downtime fix", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_rs2bt3x0fqba.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_tjadoebpscps": { "title": "fix-dm-history-workflow", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_tjadoebpscps.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_w0xpsaoxuiyw": { "title": "Pin TypeScript compiler version and build invocation in selected packages", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_w0xpsaoxuiyw.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1775914296101_a4397efe": { "title": "fix-sdk-build-resolution-workflow", "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": "/home/runner/work/relay/relay/.trajectories/completed/traj_1775914296101_a4397efe.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_1776024661304_cfc829b9": { "title": "fix-workflow-resume-elegant-workflow", @@ -177,42 +205,48 @@ "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_05xg7j388bc4.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_530xmbfeljyb": { "title": "Implement GitHub primitive adapter base layer", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_530xmbfeljyb.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_8oh4r5km5eic": { "title": "Implement GitHub primitive actions", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_8oh4r5km5eic.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_avmkyoo2s3rt": { "title": "Implement Browser primitive client", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_avmkyoo2s3rt.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_tv1x9pamkqad": { "title": "Add GitHub primitive workflow step integration", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_tv1x9pamkqad.json", + "compactedInto": "compact_j5u7qhaw4q6a" }, "traj_ui5omrgz819d": { "title": "cloud-run-start-from-workflow", "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": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_ui5omrgz819d.json", + "compactedInto": "compact_j5u7qhaw4q6a" } } } From b2f4cbb8d1117cf0be5ff50d47bfb1d6471c68a0 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 8 May 2026 15:42:26 +0200 Subject: [PATCH 3/9] feat(sdk): re-export github primitive from root entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes the bundled `@agent-relay/github-primitive` from the root `@agent-relay/sdk` so workflow authors no longer need the subpath import. Two new shapes added alongside the existing `/github` subpath: - `import { github } from '@agent-relay/sdk'` — full namespaced surface, no collision risk with other root exports. - `import { createGitHubStep, GitHubClient } from '@agent-relay/sdk'` — curated helpers for the common workflow-author path. Avoided a flat `export *` because the primitive ships ~40 generic-named action helpers (createFile, readFile, getUser, errorMessage, ...) that would pollute the root namespace. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/sdk/src/github.ts | 10 +++++++++- packages/sdk/src/index.ts | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/github.ts b/packages/sdk/src/github.ts index 46e6fc73d..fd6e88bda 100644 --- a/packages/sdk/src/github.ts +++ b/packages/sdk/src/github.ts @@ -3,10 +3,18 @@ * * Re-exports the full surface of `@agent-relay/github-primitive` so * workflow authors can import it from the SDK without a separate - * install: + * install. Three import shapes are supported: * + * // 1. Subpath (full surface): * import { createGitHubStep, GitHubClient } from '@agent-relay/sdk/github'; * + * // 2. Namespaced from root (full surface, avoids collisions): + * import { github } from '@agent-relay/sdk'; + * github.createGitHubStep(...); + * + * // 3. Direct from root (curated helpers only): + * import { createGitHubStep, GitHubClient } from '@agent-relay/sdk'; + * * `createGitHubStep` is the one most workflow authors reach for — it * produces an integration-type `.step(...)` config you drop straight * into `workflow(...)`. `GitHubClient` is the underlying typed client; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 079d65db5..6213d58cd 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -22,3 +22,5 @@ export * from './spawn-from-env.js'; export * from './cli-registry.js'; export * from './cli-resolver.js'; export * from './personas.js'; +export * as github from './github.js'; +export { createGitHubStep, GitHubClient } from '@agent-relay/github-primitive'; From c997b219ef51607db16dc368ea7f4b7a38ed7114 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 8 May 2026 17:05:14 +0200 Subject: [PATCH 4/9] docs(specs): resolve open questions in slack-primitive spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walked through §8 open questions and folded answers into the body: - DM support deferred to v2 (limitation noted in §4.3) - Slack Connect verification moved to Phase A implementation work - Audit-trail persistence assigned to the runner; tracked as #825 - channel optional for postMessage (reuses sage notify-channel resolver), required for askQuestion - Retry idempotency keyed on (runId, stepName); folded into §4.3 resumability Co-Authored-By: Claude Opus 4.7 (1M context) --- specs/slack-primitive.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/specs/slack-primitive.md b/specs/slack-primitive.md index 4cb9ff2bb..ea753bf2d 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,7 +103,7 @@ 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 @@ -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 | | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | From f08cc19be12b1737e33b9b33d491143cca54ee62 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 8 May 2026 17:50:39 +0200 Subject: [PATCH 5/9] chore: address PR #823 review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit flagged privacy/portability issues on the trajectory artifacts and two minor formatting issues on the Slack primitive spec. - Sanitize machine-specific paths in .trajectories/* — replace /Users/khaliqgant/... and /Users/will/... with / placeholders. Path fields in .trajectories/index.json normalized to repo-relative paths so lookup works across environments. - specs/slack-primitive.md: wrap token-prefix examples in backticks (xoxb-* / xoxp-*) so markdown doesn't eat the asterisks. - specs/slack-primitive.md: add 'text' language tag to the package-tree fenced code block so markdownlint MD040 stops complaining. Skipped: the "use GitHub casing" comment on the compacted .md — it triggered on the literal '.github/workflows/' directory path, not the product name. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../compact_j5u7qhaw4q6a_2026-05-08.json | 12 +++++------ .../compact_j5u7qhaw4q6a_2026-05-08.md | 4 ++-- .../completed/2026-04/traj_05xg7j388bc4.json | 2 +- .../completed/2026-04/traj_0t92gxaz6igh.json | 2 +- .../2026-04/traj_1776105988184_29f1270c.json | 6 +++--- .../completed/2026-04/traj_222ha5671idc.json | 2 +- .../completed/2026-04/traj_3b3p1z4y7qlo.json | 2 +- .../completed/2026-04/traj_4zqhfqw7g28l.json | 2 +- .../completed/2026-04/traj_530xmbfeljyb.json | 2 +- .../completed/2026-04/traj_703m7sqyq89t.json | 2 +- .../completed/2026-04/traj_8oh4r5km5eic.json | 2 +- .../completed/2026-04/traj_9tt55is74dq5.json | 2 +- .../completed/2026-04/traj_abjovknvcijv.json | 2 +- .../completed/2026-04/traj_avmkyoo2s3rt.json | 2 +- .../completed/2026-04/traj_d48czxmgx4ac.json | 2 +- .../completed/2026-04/traj_dw8ihhdb8ip7.json | 14 ++++++------- .../completed/2026-04/traj_e5i62wdjx0jd.json | 2 +- .../completed/2026-04/traj_g3muawdq6bsb.json | 2 +- .../completed/2026-04/traj_mk0t0cgn4ytq.json | 2 +- .../completed/2026-04/traj_o8kgzhfu6jth.json | 2 +- .../completed/2026-04/traj_qb54w47qwod6.json | 14 ++++++------- .../completed/2026-04/traj_rs2bt3x0fqba.json | 2 +- .../completed/2026-04/traj_tjadoebpscps.json | 2 +- .../completed/2026-04/traj_tv1x9pamkqad.json | 2 +- .../completed/2026-04/traj_ui5omrgz819d.json | 2 +- .../completed/2026-04/traj_w0xpsaoxuiyw.json | 2 +- .../2026-05/traj_1776073106646_1839be2d.json | 6 +++--- .../2026-05/traj_1776113772922_bc92f121.json | 2 +- .../2026-05/traj_1776113772922_bc92f121.md | 2 +- .../completed/2026-05/traj_3b3p1z4y7qlo.json | 2 +- .../completed/2026-05/traj_o9cx33xn5u39.json | 2 +- .../traj_1776105620545_9dcebb3d.json | 2 +- .trajectories/index.json | 21 ++++++++++++------- specs/slack-primitive.md | 4 ++-- 34 files changed, 70 insertions(+), 63 deletions(-) diff --git a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json index 3c82be704..544fafb9a 100644 --- a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json +++ b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.json @@ -147,7 +147,7 @@ { "question": "Handled PR 747 feedback in tracked parent worktree", "chosen": "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.", "fromTrajectory": "traj_1776113772922_bc92f121" }, { @@ -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", @@ -420,7 +420,7 @@ "scope": "autofix-swarm-* workflows triggered by pr_review or review_comment sources." }, { - "pattern": "Apply autofix-swarm fixes in the parent worktree at /Users/khaliqgant/Projects/AgentWorkforce/relay, not the .relay/workspace cwd copy", + "pattern": "Apply autofix-swarm fixes in the parent worktree at , not the .relay/workspace cwd copy", "rationale": "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." }, @@ -458,7 +458,7 @@ }, { "lesson": "Autofix swarm worktree confusion silently drops fixes", - "context": "Workers were editing files under .relay/workspace// instead of /Users/khaliqgant/Projects/AgentWorkforce/relay/, so changes never landed on the feature branch.", + "context": "Workers were editing files under .relay/workspace// instead of /, so changes never landed on the feature branch.", "recommendation": "Verifier step must explicitly cd into the tracked parent worktree before re-applying or validating; document the cwd contract in the swarm worker prompt." }, { diff --git a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md index e596ada55..dea9640c0 100644 --- a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md +++ b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md @@ -26,7 +26,7 @@ This batch of trajectories covers a high-volume period (Apr 10 – May 8, 2026) - **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.) - **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 worktree at /Users/khaliqgant/Projects/AgentWorkforce/relay, 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.) +- **Apply autofix-swarm fixes in the parent worktree at , 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.) - **Resolve runtime content paths via cwd candidate list, never via `import.meta.url`, in the Next.js web app; add MDX dirs to outputFileTracing**: import.meta.url bakes build-machine absolute paths into deployed chunks. (scope: web/ docs and blog content loaders.) @@ -36,7 +36,7 @@ This batch of trajectories covers a high-volume period (Apr 10 – May 8, 2026) - DAG verify-impl failure cascades aggressively — one failed gate skipped 12 downstream steps (The abandoned fix-inbox-agent-flag run completed implement-fixes successfully, then verify-impl failed and skipped read-tests, update-tests, build, fix-build, run-unit-tests, e2e-validate, regression-tests, commit, etc.) - Either order tests-update before/parallel with verify-impl, or make verify-impl a soft gate that records failure but lets test-update run so the next attempt has a clean baseline. - npm publish builds resolve types differently than dev installs (agent-trajectories@0.5.5 narrower exported types broke the SDK build under publish but not under workspace npm install with the lockfile.) - Run package-validation CI with `npm run build` (the publish command) and a fresh install, not `build:packages`, so root CLI/TS failures are caught before merge. - CloudFront CNAMEs are AWS-account-global — moving infra between accounts cannot reuse the same hostname while the old account still owns it (Production deploy run 24255758219 failed with 409 CNAMEAlreadyExists for agentrelay.net even though the AWS account had changed.) - For multi-account migrations, plan a sibling hostname (orgin.agentrelay.net pattern) for cutover, then DNS-flip the apex once the old distribution is detached. -- Autofix swarm worktree confusion silently drops fixes (Workers were editing files under .relay/workspace// instead of /Users/khaliqgant/Projects/AgentWorkforce/relay/, so changes never landed on the feature branch.) - Verifier step must explicitly cd into the tracked parent worktree before re-applying or validating; document the cwd contract in the swarm worker prompt. +- Autofix swarm worktree confusion silently drops fixes (Workers were editing files under .relay/workspace// instead of /, so changes never landed on the feature branch.) - Verifier step must explicitly cd into the tracked parent worktree before re-applying or validating; document the cwd contract in the swarm worker prompt. - `import.meta.url` is unsafe for content path resolution in serverless Next deployments (Production docs loader was reading /home/runner/work/.../web/content/docs/introduction.mdx — the GitHub Actions runner path baked into the server chunk.) - Always resolve content from runtime cwd candidates and add the content directories to Next outputFileTracing so the files ship in the deployed bundle. - Long workflow `completed` durations (430h+) reflect orchestrator session bookkeeping, not real work (Multiple sessions show 432h or 648h durations while the actual review step finished in minutes; the long tail is the workflow-runner waiting for trajectory finalization.) - Use real wall-clock from first-step → last-step completion-evidence to gauge work effort; treat the orchestrator-level `completed` timestamp as session-close, not work-end. 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_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_o9cx33xn5u39.json b/.trajectories/completed/2026-05/traj_o9cx33xn5u39.json index 6cd2750b3..b9d38dbdb 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/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 f4aea8091..1b28d9bfa 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,13 +1,13 @@ { "version": 1, - "lastUpdated": "2026-05-08T13:35:35.850Z", + "lastUpdated": "2026-05-08T14:44:45.859Z", "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": { @@ -247,6 +247,13 @@ "completedAt": "2026-04-27T20:08:46.379Z", "path": "/home/runner/work/relay/relay/.trajectories/completed/2026-04/traj_ui5omrgz819d.json", "compactedInto": "compact_j5u7qhaw4q6a" + }, + "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" } } -} +} \ No newline at end of file diff --git a/specs/slack-primitive.md b/specs/slack-primitive.md index ea753bf2d..2718d2d0e 100644 --- a/specs/slack-primitive.md +++ b/specs/slack-primitive.md @@ -49,7 +49,7 @@ Same as github-primitive: **the workflow author writes one file**. `runtime: 'au Cloud's lambda already wires `Resource.NangoSecretKey.value` and resolves `(workspaceId, provider) → connectionId`. The Slack primitive's `cloud-runtime` reuses that resolver — no new resource binding. -For Slack, we expect the connection to be a **bot user OAuth token** (xoxb-_), not user-token (xoxp-_). Posting and reading replies both work with `chat:write`, `channels:history`, and `groups:history` scopes. The primitive validates scopes on first call and throws a typed error early if they're missing. +For Slack, we expect the connection to be a **bot user OAuth token** (`xoxb-*`), not user-token (`xoxp-*`). Posting and reading replies both work with `chat:write`, `channels:history`, and `groups:history` scopes. The primitive validates scopes on first call and throws a typed error early if they're missing. ## 4. Public API @@ -238,7 +238,7 @@ These match the github-primitive's error-code shape so workflow `onError` handle ## 7. Implementation outline -``` +```text packages/slack-primitive/ src/ index.ts // public exports From b02ff1de5492c354fae23878530fec0779a14486 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 8 May 2026 19:39:47 +0200 Subject: [PATCH 6/9] feat(slack-primitive): Phase A implementation (postMessage + resolvers) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements packages/slack-primitive following the design spec at specs/slack-primitive.md and the implementation prompt at specs/slack-primitive-impl.md. Phase A only — local Web API runtime, postMessage + resolveUser + resolveChannel. - Local runtime via @slack/web-api, SLACK_BOT_TOKEN env auth. - createSlackStep workflow helper with postMessage action. - Mention resolution: emails via users.lookupByEmail, handles via user-cache, raw IDs pass through. Unresolved mentions logged as soft error on step output. - Channel name resolution via conversations.list; IDs pass through. - Example workflow + smoke-test docs in examples/. - Unit tests cover token-missing, channel resolution, mention success and soft-fail, and templating substitution. Out of scope: askQuestion, alternate-runtime adapter, Block Kit and utility verbs, runner schema for askQuestion audit trail (#825). Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 270 +++++++--- packages/slack-primitive/examples/README.md | 13 + .../slack-primitive/examples/notify-on-pr.ts | 93 ++++ packages/slack-primitive/package.json | 51 ++ .../src/__tests__/post-message.test.ts | 166 +++++++ .../src/actions/post-message.ts | 57 +++ .../src/actions/resolve-channel.ts | 38 ++ .../src/actions/resolve-user.ts | 84 ++++ packages/slack-primitive/src/adapter.ts | 178 +++++++ packages/slack-primitive/src/client.ts | 130 +++++ packages/slack-primitive/src/index.ts | 8 + packages/slack-primitive/src/local-runtime.ts | 35 ++ packages/slack-primitive/src/types.ts | 226 +++++++++ packages/slack-primitive/src/workflow-step.ts | 461 ++++++++++++++++++ .../slack-primitive/tsconfig.examples.json | 9 + packages/slack-primitive/tsconfig.json | 22 + packages/slack-primitive/vitest.config.ts | 11 + specs/slack-primitive-impl.md | 67 +++ 18 files changed, 1861 insertions(+), 58 deletions(-) create mode 100644 packages/slack-primitive/examples/README.md create mode 100644 packages/slack-primitive/examples/notify-on-pr.ts create mode 100644 packages/slack-primitive/package.json create mode 100644 packages/slack-primitive/src/__tests__/post-message.test.ts create mode 100644 packages/slack-primitive/src/actions/post-message.ts create mode 100644 packages/slack-primitive/src/actions/resolve-channel.ts create mode 100644 packages/slack-primitive/src/actions/resolve-user.ts create mode 100644 packages/slack-primitive/src/adapter.ts create mode 100644 packages/slack-primitive/src/client.ts create mode 100644 packages/slack-primitive/src/index.ts create mode 100644 packages/slack-primitive/src/local-runtime.ts create mode 100644 packages/slack-primitive/src/types.ts create mode 100644 packages/slack-primitive/src/workflow-step.ts create mode 100644 packages/slack-primitive/tsconfig.examples.json create mode 100644 packages/slack-primitive/tsconfig.json create mode 100644 packages/slack-primitive/vitest.config.ts create mode 100644 specs/slack-primitive-impl.md diff --git a/package-lock.json b/package-lock.json index d9a2a346d..0a9a4d8a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-relay", - "version": "6.0.9", + "version": "6.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-relay", - "version": "6.0.9", + "version": "6.0.11", "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.11", + "@agent-relay/config": "6.0.11", + "@agent-relay/hooks": "6.0.11", + "@agent-relay/sdk": "6.0.11", + "@agent-relay/telemetry": "6.0.11", + "@agent-relay/trajectory": "6.0.11", + "@agent-relay/user-directory": "6.0.11", + "@agent-relay/utils": "6.0.11", "@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.11", "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "6.0.9", + "@agent-relay/sdk": "6.0.11", "@agentclientprotocol/sdk": "^0.12.0" }, "bin": { @@ -15473,38 +15612,38 @@ }, "packages/brand": { "name": "@agent-relay/brand", - "version": "6.0.9" + "version": "6.0.11" }, "packages/broker-darwin-arm64": { "name": "@agent-relay/broker-darwin-arm64", - "version": "6.0.9", + "version": "6.0.11", "license": "MIT" }, "packages/broker-darwin-x64": { "name": "@agent-relay/broker-darwin-x64", - "version": "6.0.9", + "version": "6.0.11", "license": "MIT" }, "packages/broker-linux-arm64": { "name": "@agent-relay/broker-linux-arm64", - "version": "6.0.9", + "version": "6.0.11", "license": "MIT" }, "packages/broker-linux-x64": { "name": "@agent-relay/broker-linux-x64", - "version": "6.0.9", + "version": "6.0.11", "license": "MIT" }, "packages/broker-win32-x64": { "name": "@agent-relay/broker-win32-x64", - "version": "6.0.9", + "version": "6.0.11", "license": "MIT" }, "packages/browser-primitive": { "name": "@agent-relay/browser-primitive", - "version": "6.0.9", + "version": "6.0.11", "dependencies": { - "@agent-relay/sdk": "6.0.9", + "@agent-relay/sdk": "6.0.11", "playwright": "^1.51.1" }, "bin": { @@ -15518,9 +15657,9 @@ }, "packages/cloud": { "name": "@agent-relay/cloud", - "version": "6.0.9", + "version": "6.0.11", "dependencies": { - "@agent-relay/config": "6.0.9", + "@agent-relay/config": "6.0.11", "@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.11", "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.11", "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.11", "dependencies": { - "@agent-relay/sdk": "6.0.9" + "@agent-relay/sdk": "6.0.11" }, "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.11", "dependencies": { - "@agent-relay/workflow-types": "6.0.9" + "@agent-relay/workflow-types": "6.0.11" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15582,11 +15721,11 @@ }, "packages/hooks": { "name": "@agent-relay/hooks", - "version": "6.0.9", + "version": "6.0.11", "dependencies": { - "@agent-relay/config": "6.0.9", - "@agent-relay/sdk": "6.0.9", - "@agent-relay/trajectory": "6.0.9" + "@agent-relay/config": "6.0.11", + "@agent-relay/sdk": "6.0.11", + "@agent-relay/trajectory": "6.0.11" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15595,9 +15734,9 @@ }, "packages/memory": { "name": "@agent-relay/memory", - "version": "6.0.9", + "version": "6.0.11", "dependencies": { - "@agent-relay/hooks": "6.0.9" + "@agent-relay/hooks": "6.0.11" }, "devDependencies": { "@types/node": "^22.19.3", @@ -15606,11 +15745,11 @@ }, "packages/openclaw": { "name": "@agent-relay/openclaw", - "version": "6.0.9", + "version": "6.0.11", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "6.0.9", + "@agent-relay/sdk": "6.0.11", "@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.11", "license": "MIT" }, "packages/policy": { "name": "@agent-relay/policy", - "version": "6.0.9", + "version": "6.0.11", "dependencies": { - "@agent-relay/config": "6.0.9" + "@agent-relay/config": "6.0.11" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16391,11 +16530,11 @@ }, "packages/sdk": { "name": "@agent-relay/sdk", - "version": "6.0.9", + "version": "6.0.11", "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.11", + "@agent-relay/github-primitive": "6.0.11", + "@agent-relay/workflow-types": "6.0.11", "@agentworkforce/harness-kit": "^0.11.0", "@agentworkforce/workload-router": "^0.11.0", "@relaycast/sdk": "^1.1.0", @@ -16415,14 +16554,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.11", + "@agent-relay/broker-darwin-x64": "6.0.11", + "@agent-relay/broker-linux-arm64": "6.0.11", + "@agent-relay/broker-linux-x64": "6.0.11", + "@agent-relay/broker-win32-x64": "6.0.11" }, "peerDependencies": { - "@agent-relay/credential-proxy": "6.0.9", + "@agent-relay/credential-proxy": "6.0.11", "@anthropic-ai/claude-agent-sdk": ">=0.1.0", "@google/adk": ">=0.5.0", "@langchain/langgraph": ">=1.2.0", @@ -16458,9 +16597,24 @@ } } }, + "packages/slack-primitive": { + "name": "@agent-relay/slack-primitive", + "version": "6.0.11", + "dependencies": { + "@agent-relay/workflow-types": "6.0.11", + "@slack/web-api": "^7.15.2" + }, + "devDependencies": { + "@agent-relay/github-primitive": "6.0.11", + "@agent-relay/sdk": "6.0.11", + "@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.11", "dependencies": { "posthog-node": "^5.29.2" }, @@ -16495,9 +16649,9 @@ }, "packages/trajectory": { "name": "@agent-relay/trajectory", - "version": "6.0.9", + "version": "6.0.11", "dependencies": { - "@agent-relay/config": "6.0.9" + "@agent-relay/config": "6.0.11" }, "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.11", "dependencies": { - "@agent-relay/utils": "6.0.9" + "@agent-relay/utils": "6.0.11" }, "devDependencies": { "@types/node": "^22.19.3", @@ -16517,9 +16671,9 @@ }, "packages/utils": { "name": "@agent-relay/utils", - "version": "6.0.9", + "version": "6.0.11", "dependencies": { - "@agent-relay/config": "6.0.9", + "@agent-relay/config": "6.0.11", "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.11" }, "web": { "version": "0.0.1", diff --git a/packages/slack-primitive/examples/README.md b/packages/slack-primitive/examples/README.md new file mode 100644 index 000000000..6c3627516 --- /dev/null +++ b/packages/slack-primitive/examples/README.md @@ -0,0 +1,13 @@ +# Slack Primitive Examples + +## Manual Smoke Test + +First, set `SLACK_BOT_TOKEN` to a bot token with `chat:write`, `channels:read`, `groups:read`, `users:read`, and `users:read.email` scopes. Then 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. 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..683dbdc04 --- /dev/null +++ b/packages/slack-primitive/package.json @@ -0,0 +1,51 @@ +{ + "name": "@agent-relay/slack-primitive", + "version": "6.0.11", + "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.11", + "@slack/web-api": "^7.15.2" + }, + "devDependencies": { + "@agent-relay/github-primitive": "6.0.11", + "@agent-relay/sdk": "6.0.11", + "@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__/post-message.test.ts b/packages/slack-primitive/src/__tests__/post-message.test.ts new file mode 100644 index 000000000..2ed9683e4 --- /dev/null +++ b/packages/slack-primitive/src/__tests__/post-message.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, it } 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', () => { + 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('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/actions/post-message.ts b/packages/slack-primitive/src/actions/post-message.ts new file mode 100644 index 000000000..5a038fda3 --- /dev/null +++ b/packages/slack-primitive/src/actions/post-message.ts @@ -0,0 +1,57 @@ +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 channel = await resolveChannel(slack, params.channel); + 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..6764216f1 --- /dev/null +++ b/packages/slack-primitive/src/actions/resolve-user.ts @@ -0,0 +1,84 @@ +import type { SlackResolvedMention, SlackUserSummary, SlackWebApiLike } from '../types.js'; + +const USER_ID_PATTERN = /^[UW][A-Z0-9]{2,}$/; +const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +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 (USER_ID_PATTERN.test(normalized)) { + return { input: mention, userId: normalized }; + } + + if (EMAIL_PATTERN.test(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}`); +} + +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..3810b8f87 --- /dev/null +++ b/packages/slack-primitive/src/adapter.ts @@ -0,0 +1,178 @@ +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 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) ?? ''; + + return { + ...config, + runtime: 'local', + env, + token, + timeout: config.timeout ?? DEFAULT_TIMEOUT, + }; +} + +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); + } + const response = await this.slack.auth.test(); + return response.ok !== 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); + } + + 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 { SlackWebApiClient } = await import('./local-runtime.js'); + return new SlackWebApiClient(config); + } + + static async detect(config: SlackRuntimeConfig = {}): Promise { + const normalized = normalizeSlackRuntimeConfig(config); + const local = await this.testRuntime('local', normalized); + + return { + runtime: 'local', + requestedRuntime: 'local', + source: normalized.token ? 'config' : 'environment', + available: local.available, + reason: local.reason, + checkedAt: new Date().toISOString(), + local, + }; + } + + static detectRuntime(_config: SlackRuntimeConfig = {}): Promise { + return Promise.resolve('local'); + } + + static testRuntime( + runtime: SlackRuntime, + config: SlackRuntimeConfig = {} + ): Promise { + const normalized = normalizeSlackRuntimeConfig(config); + return Promise.resolve({ + runtime, + available: Boolean(normalized.token), + authenticated: Boolean(normalized.token), + reason: normalized.token ? '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/index.ts b/packages/slack-primitive/src/index.ts new file mode 100644 index 000000000..70c904e35 --- /dev/null +++ b/packages/slack-primitive/src/index.ts @@ -0,0 +1,8 @@ +export * from './types.js'; +export * from './adapter.js'; +export * from './local-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/types.ts b/packages/slack-primitive/src/types.ts new file mode 100644 index 000000000..cc1f044ff --- /dev/null +++ b/packages/slack-primitive/src/types.ts @@ -0,0 +1,226 @@ +export type SlackRuntime = 'local'; + +export type SlackRuntimePreference = SlackRuntime; + +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. Phase A supports only the local Web API runtime. */ + runtime?: SlackRuntimePreference; + /** Slack bot token. Defaults to SLACK_BOT_TOKEN. */ + token?: string; + /** Environment used for token lookup. Defaults to process.env. */ + env?: Record; + /** Request timeout in milliseconds passed to the Slack WebClient. */ + timeout?: number; +} + +export interface RequiredSlackRuntimeConfig extends SlackRuntimeConfig { + runtime: SlackRuntime; + env: Record; + token: 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; +} + +export type SlackPostBackErrorCode = 'auth_token_missing' | 'channel_not_found' | 'slack_api_error'; + +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..f7b49824b --- /dev/null +++ b/packages/slack-primitive/src/workflow-step.ts @@ -0,0 +1,461 @@ +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. */ + 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 = { + channel: config.channel, + text: config.text, + }; + + 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: readRequiredString(actionParams.channel, 'channel'), + text: readRequiredString(actionParams.text, 'text'), + threadTs: readOptionalString(actionParams.threadTs), + mentions: readStringArray(actionParams.mentions), + unfurl: typeof actionParams.unfurl === 'boolean' ? actionParams.unfurl : undefined, + 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 (!config.channel) { + throw new Error(`Slack step "${config.name}" requires a channel`); + } + 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 : 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 === 'true') return true; + if (trimmed === 'false') return false; + if (trimmed === 'null') return null; + if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmed)) return Number(trimmed); + 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 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). From 3fccf893a70f4840e3a4b4ea5aa116919e7b2835 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Fri, 8 May 2026 20:26:16 +0200 Subject: [PATCH 7/9] feat(slack-primitive): cloud-relay + noop runtimes, SDK re-export, publish wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase A primitive previously hard-failed when SLACK_BOT_TOKEN wasn't set. That breaks the realistic CLI flow: most users don't have a bot token locally — they have a relay session and a workspace that's already connected to Slack via ricky's Nango app. This adds two new runtimes alongside `local`: - `cloud-relay`: posts via relay-cloud's POST /api/v1/slack/post-message (cloud PR #493). Activated when CLOUD_API_TOKEN + CLOUD_API_URL are set. The cloud endpoint uses the workspace's existing Nango Slack connection — no per-user bot token needed. - `noop`: postMessage logs a warning and returns a placeholder ts. Activated when no tokens are set. Lets workflows run end-to-end in CI / smoke environments without hard-failing on missing Slack creds. Selection priority: cloud-relay → local → noop. Override with `runtime: 'local' | 'cloud-relay' | 'noop' | 'auto'`. In cloud-relay mode, resolveUser/resolveChannel throw `unsupported_in_cloud_relay` (Phase A intentionally exposes only postMessage). Mention resolution is local-only; cloud-relay surfaces unresolved mentions as warnings on the step output. Also wires slack-primitive as an SDK internal dep, mirroring the github primitive shape: - packages/sdk/package.json: dep + ./slack subpath export - packages/sdk/src/slack.ts: re-exports the full surface - packages/sdk/src/index.ts: `export * as slack` + curated `{ createSlackStep, SlackClient }` from the root - .github/workflows/publish.yml: pack + install in smoke build, publish-sdk-internal-deps matrix, dry-run loop, publish_if_missing chain - .github/workflows/verify-publish-sdk.yml: package-availability check Tests: 22 new vitest cases (cloud-relay 11, noop 3, runtime-selection 8) covering auth requirements, success path with thread_ts/unfurl forwarding, mention warnings, error mapping (rate_limited / not_connected / slack_error / upstream_error), resolve rejection, and the auto-detect priority order. Related: AgentWorkforce/cloud#493 (the cloud-side endpoint). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/publish.yml | 13 +- .github/workflows/verify-publish-sdk.yml | 1 + packages/sdk/package.json | 8 +- packages/sdk/src/index.ts | 2 + packages/sdk/src/slack.ts | 24 ++ packages/slack-primitive/examples/README.md | 31 ++- .../src/__tests__/cloud-relay-runtime.test.ts | 173 +++++++++++++ .../src/__tests__/noop-runtime.test.ts | 42 ++++ .../src/__tests__/runtime-selection.test.ts | 75 ++++++ packages/slack-primitive/src/adapter.ts | 96 +++++-- .../src/cloud-relay-runtime.ts | 234 ++++++++++++++++++ packages/slack-primitive/src/index.ts | 2 + packages/slack-primitive/src/noop-runtime.ts | 101 ++++++++ packages/slack-primitive/src/types.ts | 30 ++- 14 files changed, 802 insertions(+), 30 deletions(-) create mode 100644 packages/sdk/src/slack.ts create mode 100644 packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts create mode 100644 packages/slack-primitive/src/__tests__/noop-runtime.test.ts create mode 100644 packages/slack-primitive/src/__tests__/runtime-selection.test.ts create mode 100644 packages/slack-primitive/src/cloud-relay-runtime.ts create mode 100644 packages/slack-primitive/src/noop-runtime.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cbe74c614..b710e6bc0 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: @@ -1254,7 +1258,7 @@ jobs: if: github.event.inputs.dry_run == 'true' run: | set -euo pipefail - for package in config github-primitive workflow-types; do + for package in config github-primitive slack-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 @@ -1285,6 +1289,7 @@ jobs: publish_if_missing config publish_if_missing workflow-types publish_if_missing github-primitive + publish_if_missing slack-primitive - name: Publish SDK to NPM if: github.event.inputs.dry_run != 'true' 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/packages/sdk/package.json b/packages/sdk/package.json index 9e5553afe..8af4fa190 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.11", "@agent-relay/github-primitive": "6.0.11", + "@agent-relay/slack-primitive": "6.0.11", "@agent-relay/workflow-types": "6.0.11", "@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/slack-primitive/examples/README.md b/packages/slack-primitive/examples/README.md index 6c3627516..911f2e12e 100644 --- a/packages/slack-primitive/examples/README.md +++ b/packages/slack-primitive/examples/README.md @@ -1,8 +1,22 @@ # Slack Primitive Examples -## Manual Smoke Test +## Runtime selection -First, set `SLACK_BOT_TOKEN` to a bot token with `chat:write`, `channels:read`, `groups:read`, `users:read`, and `users:read.email` scopes. Then invite the bot to the destination channel and set `SLACK_CHANNEL` to either a channel id or a `#channel-name` reference. +`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`: @@ -11,3 +25,16 @@ SLACK_BOT_TOKEN=xoxb-... SLACK_CHANNEL=#engineering npx tsx examples/notify-on-p ``` 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/src/__tests__/cloud-relay-runtime.test.ts b/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts new file mode 100644 index 000000000..7fc9a2411 --- /dev/null +++ b/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts @@ -0,0 +1,173 @@ +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__/runtime-selection.test.ts b/packages/slack-primitive/src/__tests__/runtime-selection.test.ts new file mode 100644 index 000000000..62d2842cb --- /dev/null +++ b/packages/slack-primitive/src/__tests__/runtime-selection.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; + +import { SlackAdapterFactory, normalizeSlackRuntimeConfig } from '../adapter.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'); + }); +}); diff --git a/packages/slack-primitive/src/adapter.ts b/packages/slack-primitive/src/adapter.ts index 3810b8f87..ea3c82d1f 100644 --- a/packages/slack-primitive/src/adapter.ts +++ b/packages/slack-primitive/src/adapter.ts @@ -19,6 +19,7 @@ import { type SlackRuntimeAvailability, type SlackRuntimeConfig, type SlackRuntimeDetectionResult, + type SlackRuntimePreference, type SlackWebApiLike, } from './types.js'; @@ -27,16 +28,26 @@ 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: 'local', + 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, @@ -119,27 +130,55 @@ export abstract class BaseSlackAdapter extends SlackClientInterface { export class SlackAdapterFactory { static async create(config: SlackRuntimeConfig = {}): Promise { - const { SlackWebApiClient } = await import('./local-runtime.js'); - return new SlackWebApiClient(config); + 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: 'local', - requestedRuntime: 'local', - source: normalized.token ? 'config' : 'environment', - available: local.available, - reason: local.reason, + 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 detectRuntime(_config: SlackRuntimeConfig = {}): Promise { - return Promise.resolve('local'); + static async detectRuntime(config: SlackRuntimeConfig = {}): Promise { + return normalizeSlackRuntimeConfig(config).runtime; } static testRuntime( @@ -147,12 +186,37 @@ export class SlackAdapterFactory { config: SlackRuntimeConfig = {} ): Promise { const normalized = normalizeSlackRuntimeConfig(config); - return Promise.resolve({ - runtime, - available: Boolean(normalized.token), - authenticated: Boolean(normalized.token), - reason: normalized.token ? 'SLACK_BOT_TOKEN is configured.' : 'SLACK_BOT_TOKEN is not configured.', - }); + 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.', + }); + } + } } } 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..5ae3a088b --- /dev/null +++ b/packages/slack-primitive/src/cloud-relay-runtime.ts @@ -0,0 +1,234 @@ +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 index 70c904e35..1165fcf91 100644 --- a/packages/slack-primitive/src/index.ts +++ b/packages/slack-primitive/src/index.ts @@ -1,6 +1,8 @@ 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'; diff --git a/packages/slack-primitive/src/noop-runtime.ts b/packages/slack-primitive/src/noop-runtime.ts new file mode 100644 index 000000000..0689fa5ad --- /dev/null +++ b/packages/slack-primitive/src/noop-runtime.ts @@ -0,0 +1,101 @@ +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 { + this.logger('postMessage dropped: no SLACK_BOT_TOKEN and no CLOUD_API_TOKEN configured.', { + channel: params.channel, + preview: params.text.slice(0, 80), + }); + + const warnings: SlackResolutionWarning[] = [ + { + type: 'mention_unresolved', + input: params.channel, + message: 'Slack runtime is noop; configure SLACK_BOT_TOKEN or CLOUD_API_TOKEN to deliver messages.', + }, + ]; + + return { + channel: params.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 index cc1f044ff..3b7fa2344 100644 --- a/packages/slack-primitive/src/types.ts +++ b/packages/slack-primitive/src/types.ts @@ -1,6 +1,6 @@ -export type SlackRuntime = 'local'; +export type SlackRuntime = 'local' | 'cloud-relay' | 'noop'; -export type SlackRuntimePreference = SlackRuntime; +export type SlackRuntimePreference = SlackRuntime | 'auto'; export enum SlackAction { PostMessage = 'postMessage', @@ -13,13 +13,17 @@ export type SlackActionName = `${SlackAction}`; export const SLACK_ACTIONS = Object.values(SlackAction); export interface SlackRuntimeConfig { - /** Runtime mode. Phase A supports only the local Web API runtime. */ + /** 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. */ + /** Request timeout in milliseconds passed to the Slack WebClient or cloud-relay fetch. */ timeout?: number; } @@ -27,6 +31,8 @@ export interface RequiredSlackRuntimeConfig extends SlackRuntimeConfig { runtime: SlackRuntime; env: Record; token: string; + cloudApiToken: string; + cloudApiUrl: string; timeout: number; } @@ -46,9 +52,19 @@ export interface SlackRuntimeDetectionResult { reason: string; checkedAt: string; local: SlackRuntimeAvailability; -} - -export type SlackPostBackErrorCode = 'auth_token_missing' | 'channel_not_found' | 'slack_api_error'; + 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; From bdd8bab72bdb0c99c3779f0649e3ce31a7821213 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sat, 9 May 2026 10:45:54 +0200 Subject: [PATCH 8/9] fix(slack-primitive): address PR checks and comments --- .github/workflows/publish.yml | 32 ---- .trajectories/active/traj_lieyyspidhfj.json | 46 +++++ .../compact_j5u7qhaw4q6a_2026-05-08.md | 2 +- .../completed/2026-05/traj_6ujzpx82gqs9.json | 142 +++++++++++++++ .../completed/2026-05/traj_6ujzpx82gqs9.md | 38 ++++ .../completed/2026-05/traj_k7njijv51iq4.json | 162 ++++++++++++++++++ .../completed/2026-05/traj_k7njijv51iq4.md | 39 +++++ .../completed/2026-05/traj_tavtex0db4b0.json | 53 ++++++ .../completed/2026-05/traj_tavtex0db4b0.md | 31 ++++ .trajectories/index.json | 97 ++++++++--- package-lock.json | 2 +- packages/sdk/tsconfig.build.json | 4 +- packages/sdk/tsconfig.json | 4 +- packages/slack-primitive/examples/README.md | 10 +- packages/slack-primitive/package.json | 1 - .../src/__tests__/cloud-relay-runtime.test.ts | 29 ++-- .../src/__tests__/post-message.test.ts | 27 ++- .../src/__tests__/runtime-selection.test.ts | 26 ++- .../src/__tests__/workflow-step.test.ts | 55 ++++++ .../src/actions/post-message.ts | 7 +- .../src/actions/resolve-user.ts | 41 ++++- packages/slack-primitive/src/adapter.ts | 25 +-- .../src/cloud-relay-runtime.ts | 3 +- packages/slack-primitive/src/noop-runtime.ts | 7 +- packages/slack-primitive/src/types.ts | 8 +- packages/slack-primitive/src/workflow-step.ts | 31 ++-- 26 files changed, 801 insertions(+), 121 deletions(-) create mode 100644 .trajectories/active/traj_lieyyspidhfj.json create mode 100644 .trajectories/completed/2026-05/traj_6ujzpx82gqs9.json create mode 100644 .trajectories/completed/2026-05/traj_6ujzpx82gqs9.md create mode 100644 .trajectories/completed/2026-05/traj_k7njijv51iq4.json create mode 100644 .trajectories/completed/2026-05/traj_k7njijv51iq4.md create mode 100644 .trajectories/completed/2026-05/traj_tavtex0db4b0.json create mode 100644 .trajectories/completed/2026-05/traj_tavtex0db4b0.md create mode 100644 packages/slack-primitive/src/__tests__/workflow-step.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b710e6bc0..4b7842e46 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1254,43 +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 slack-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 - publish_if_missing slack-primitive - - name: Publish SDK to NPM if: github.event.inputs.dry_run != 'true' working-directory: packages/sdk diff --git a/.trajectories/active/traj_lieyyspidhfj.json b/.trajectories/active/traj_lieyyspidhfj.json new file mode 100644 index 000000000..31b11c49e --- /dev/null +++ b/.trajectories/active/traj_lieyyspidhfj.json @@ -0,0 +1,46 @@ +{ + "id": "traj_lieyyspidhfj", + "version": 1, + "task": { + "title": "Fix PR 823 conflicts checks and comments" + }, + "status": "active", + "startedAt": "2026-05-09T08:37:17.563Z", + "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", + "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" + } + ] + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "3fccf893a70f4840e3a4b4ea5aa116919e7b2835", + "endRef": "3fccf893a70f4840e3a4b4ea5aa116919e7b2835" + } +} \ No newline at end of file diff --git a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md index 28bc485b3..2df36f7d2 100644 --- a/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md +++ b/.trajectories/compacted/compact_j5u7qhaw4q6a_2026-05-08.md @@ -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-05/traj_6ujzpx82gqs9.json b/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.json new file mode 100644 index 000000000..dc8f2087a --- /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": [] +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.md b/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.md new file mode 100644 index 000000000..1e36455e1 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_6ujzpx82gqs9.md @@ -0,0 +1,38 @@ +# 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_k7njijv51iq4.json b/.trajectories/completed/2026-05/traj_k7njijv51iq4.json new file mode 100644 index 000000000..94aaca715 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_k7njijv51iq4.json @@ -0,0 +1,162 @@ +{ + "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": [] +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_k7njijv51iq4.md b/.trajectories/completed/2026-05/traj_k7njijv51iq4.md new file mode 100644 index 000000000..df9ccc344 --- /dev/null +++ b/.trajectories/completed/2026-05/traj_k7njijv51iq4.md @@ -0,0 +1,39 @@ +# 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_tavtex0db4b0.json b/.trajectories/completed/2026-05/traj_tavtex0db4b0.json new file mode 100644 index 000000000..b443b4e35 --- /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" + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-05/traj_tavtex0db4b0.md b/.trajectories/completed/2026-05/traj_tavtex0db4b0.md new file mode 100644 index 000000000..519e0903f --- /dev/null +++ b/.trajectories/completed/2026-05/traj_tavtex0db4b0.md @@ -0,0 +1,31 @@ +# 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/index.json b/.trajectories/index.json index 8834beb56..67fe1d757 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-08T15:51:38.996Z", + "lastUpdated": "2026-05-09T08:41:56.120Z", "trajectories": { "traj_1775914133873_35667beb": { "title": "fix-sdk-build-resolution-workflow", @@ -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": { @@ -282,6 +282,47 @@ "startedAt": "2026-05-08T15:50:35.978Z", "completedAt": "2026-05-08T15:51:38.854Z", "path": ".trajectories/completed/2026-05/traj_vkozdglobkyg.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": "active", + "startedAt": "2026-05-09T08:37:17.563Z", + "path": ".trajectories/active/traj_lieyyspidhfj.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": ".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": ".trajectories/completed/2026-05/traj_34b1u84b19gz.json" } } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 0a9a4d8a5..fd8973ce2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16534,6 +16534,7 @@ "dependencies": { "@agent-relay/config": "6.0.11", "@agent-relay/github-primitive": "6.0.11", + "@agent-relay/slack-primitive": "6.0.11", "@agent-relay/workflow-types": "6.0.11", "@agentworkforce/harness-kit": "^0.11.0", "@agentworkforce/workload-router": "^0.11.0", @@ -16606,7 +16607,6 @@ }, "devDependencies": { "@agent-relay/github-primitive": "6.0.11", - "@agent-relay/sdk": "6.0.11", "@types/node": "^22.19.3", "typescript": "^5.9.3", "vitest": "^3.2.4" diff --git a/packages/sdk/tsconfig.build.json b/packages/sdk/tsconfig.build.json index f4cab75d0..bf5a4e50c 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, diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 0ea896180..4717a00af 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -7,7 +7,9 @@ "@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 }, diff --git a/packages/slack-primitive/examples/README.md b/packages/slack-primitive/examples/README.md index 911f2e12e..b7bc10351 100644 --- a/packages/slack-primitive/examples/README.md +++ b/packages/slack-primitive/examples/README.md @@ -4,11 +4,11 @@ `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. | +| 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. diff --git a/packages/slack-primitive/package.json b/packages/slack-primitive/package.json index 683dbdc04..1d964c63d 100644 --- a/packages/slack-primitive/package.json +++ b/packages/slack-primitive/package.json @@ -35,7 +35,6 @@ }, "devDependencies": { "@agent-relay/github-primitive": "6.0.11", - "@agent-relay/sdk": "6.0.11", "@types/node": "^22.19.3", "typescript": "^5.9.3", "vitest": "^3.2.4" diff --git a/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts b/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts index 7fc9a2411..243fc2168 100644 --- a/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts +++ b/packages/slack-primitive/src/__tests__/cloud-relay-runtime.test.ts @@ -8,8 +8,15 @@ interface FakeResponseBody { 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 } }> = []; +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 }); @@ -35,15 +42,15 @@ const baseConfig = { describe('SlackCloudRelayClient', () => { it('throws auth_token_missing when CLOUD_API_TOKEN is absent', () => { - expect(() => - new SlackCloudRelayClient({ env: {}, cloudApiUrl: 'https://api.example.com' }, fakeFetch([])) + 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'); + expect(() => new SlackCloudRelayClient({ env: {}, cloudApiToken: 'rk_test' }, fakeFetch([]))).toThrow( + 'CLOUD_API_URL' + ); }); it('posts via cloud-relay endpoint with bearer auth', async () => { @@ -84,9 +91,7 @@ describe('SlackCloudRelayClient', () => { }); it('records mentions as unresolved with a warning', async () => { - const fetch = fakeFetch([ - { body: { ok: true, ts: '1.0', channel: 'C0123', workspaceId: 'ws_test' } }, - ]); + 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({ @@ -126,9 +131,7 @@ describe('SlackCloudRelayClient', () => { }); 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 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({ diff --git a/packages/slack-primitive/src/__tests__/post-message.test.ts b/packages/slack-primitive/src/__tests__/post-message.test.ts index 2ed9683e4..572ebb217 100644 --- a/packages/slack-primitive/src/__tests__/post-message.test.ts +++ b/packages/slack-primitive/src/__tests__/post-message.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { postMessage } from '../actions/post-message.js'; import { resolveChannel } from '../actions/resolve-channel.js'; @@ -7,6 +7,10 @@ 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'); @@ -60,6 +64,27 @@ describe('Slack primitive', () => { 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: { diff --git a/packages/slack-primitive/src/__tests__/runtime-selection.test.ts b/packages/slack-primitive/src/__tests__/runtime-selection.test.ts index 62d2842cb..11bb46de5 100644 --- a/packages/slack-primitive/src/__tests__/runtime-selection.test.ts +++ b/packages/slack-primitive/src/__tests__/runtime-selection.test.ts @@ -1,6 +1,8 @@ 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", () => { @@ -53,7 +55,7 @@ describe('SlackAdapterFactory runtime selection', () => { expect(normalized.runtime).toBe('local'); }); - it("detect() reports availability for all three runtimes", async () => { + it('detect() reports availability for all three runtimes', async () => { const detection = await SlackAdapterFactory.detect({ env: { SLACK_BOT_TOKEN: 'xoxb-local', @@ -68,8 +70,28 @@ describe('SlackAdapterFactory runtime selection', () => { expect(detection.noop.available).toBe(true); }); - it("create() returns a noop adapter when no tokens are configured", async () => { + 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 index 5a038fda3..0648a2b9e 100644 --- a/packages/slack-primitive/src/actions/post-message.ts +++ b/packages/slack-primitive/src/actions/post-message.ts @@ -19,7 +19,12 @@ export async function postMessage( slack: SlackWebApiLike, params: PostMessageParams ): Promise { - const channel = await resolveChannel(slack, params.channel); + 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[] = []; diff --git a/packages/slack-primitive/src/actions/resolve-user.ts b/packages/slack-primitive/src/actions/resolve-user.ts index 6764216f1..d0423d677 100644 --- a/packages/slack-primitive/src/actions/resolve-user.ts +++ b/packages/slack-primitive/src/actions/resolve-user.ts @@ -1,8 +1,5 @@ import type { SlackResolvedMention, SlackUserSummary, SlackWebApiLike } from '../types.js'; -const USER_ID_PATTERN = /^[UW][A-Z0-9]{2,}$/; -const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - export interface ResolveUserOptions { cache?: Map; } @@ -21,11 +18,11 @@ export async function resolveUser( ): Promise { const normalized = mention.startsWith('@') ? mention.slice(1) : mention; - if (USER_ID_PATTERN.test(normalized)) { + if (isSlackUserId(normalized)) { return { input: mention, userId: normalized }; } - if (EMAIL_PATTERN.test(normalized)) { + if (isEmailCandidate(normalized)) { const response = await slack.users.lookupByEmail({ email: normalized }); const userId = response.user?.id; if (!userId) { @@ -50,6 +47,40 @@ export async function resolveUser( 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 diff --git a/packages/slack-primitive/src/adapter.ts b/packages/slack-primitive/src/adapter.ts index ea3c82d1f..8e1c22535 100644 --- a/packages/slack-primitive/src/adapter.ts +++ b/packages/slack-primitive/src/adapter.ts @@ -33,7 +33,10 @@ export function normalizeSlackRuntimeConfig(config: SlackRuntimeConfig = {}): Re return { ...config, - runtime: config.runtime && config.runtime !== 'auto' ? config.runtime : selectRuntime({ token, cloudApiToken, cloudApiUrl }), + runtime: + config.runtime && config.runtime !== 'auto' + ? config.runtime + : selectRuntime({ token, cloudApiToken, cloudApiUrl }), env, token, cloudApiToken, @@ -60,8 +63,12 @@ export abstract class BaseSlackAdapter extends SlackClientInterface { if (!this.slack.auth) { return Boolean(this.config.token); } - const response = await this.slack.auth.test(); - return response.ok !== false; + try { + const response = await this.slack.auth.test(); + return response.ok !== false; + } catch { + return false; + } } executeAction( @@ -103,7 +110,10 @@ export abstract class BaseSlackAdapter extends SlackClientInterface { } async postMessage(params: PostMessageParams): Promise { - return postMessageAction(this.slack, params); + return postMessageAction(this.slack, { + ...params, + channel: params.channel ?? this.config.env.SLACK_DEFAULT_CHANNEL, + }); } async resolveUser(params: ResolveUserParams): Promise { @@ -156,12 +166,7 @@ export class SlackAdapterFactory { const requested: SlackRuntimePreference = config.runtime ?? 'auto'; const selected = normalized.runtime; - const summary = - selected === 'cloud-relay' - ? cloudRelay - : selected === 'noop' - ? noop - : local; + const summary = selected === 'cloud-relay' ? cloudRelay : selected === 'noop' ? noop : local; return { runtime: selected, diff --git a/packages/slack-primitive/src/cloud-relay-runtime.ts b/packages/slack-primitive/src/cloud-relay-runtime.ts index 5ae3a088b..54d9274af 100644 --- a/packages/slack-primitive/src/cloud-relay-runtime.ts +++ b/packages/slack-primitive/src/cloud-relay-runtime.ts @@ -152,7 +152,8 @@ export class SlackCloudRelayClient extends BaseSlackAdapter { warnings.push({ type: 'mention_unresolved', input: mention, - message: 'mention resolution is not supported in cloud-relay runtime; pass user IDs in text directly.', + message: + 'mention resolution is not supported in cloud-relay runtime; pass user IDs in text directly.', }); } } diff --git a/packages/slack-primitive/src/noop-runtime.ts b/packages/slack-primitive/src/noop-runtime.ts index 0689fa5ad..36daf2251 100644 --- a/packages/slack-primitive/src/noop-runtime.ts +++ b/packages/slack-primitive/src/noop-runtime.ts @@ -49,21 +49,22 @@ export class SlackNoopClient extends BaseSlackAdapter { } 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: params.channel, + channel, preview: params.text.slice(0, 80), }); const warnings: SlackResolutionWarning[] = [ { type: 'mention_unresolved', - input: params.channel, + input: channel, message: 'Slack runtime is noop; configure SLACK_BOT_TOKEN or CLOUD_API_TOKEN to deliver messages.', }, ]; return { - channel: params.channel, + channel, ts: NOOP_TS, text: params.text, resolvedMentions: [], diff --git a/packages/slack-primitive/src/types.ts b/packages/slack-primitive/src/types.ts index 3b7fa2344..af4fbc6b0 100644 --- a/packages/slack-primitive/src/types.ts +++ b/packages/slack-primitive/src/types.ts @@ -110,7 +110,7 @@ export interface SlackResolvedMention { } export interface PostMessageParams { - channel: string; + channel?: string; text: string; threadTs?: string; mentions?: string[]; @@ -208,7 +208,11 @@ export interface SlackWebApiLike { list(params?: { cursor?: string; limit?: number }): Promise; }; conversations: { - list(params?: { cursor?: string; limit?: number; types?: string }): Promise; + list(params?: { + cursor?: string; + limit?: number; + types?: string; + }): Promise; }; auth?: { test(): Promise<{ ok?: boolean; error?: string }>; diff --git a/packages/slack-primitive/src/workflow-step.ts b/packages/slack-primitive/src/workflow-step.ts index f7b49824b..285e3e61c 100644 --- a/packages/slack-primitive/src/workflow-step.ts +++ b/packages/slack-primitive/src/workflow-step.ts @@ -32,8 +32,8 @@ export interface SlackStepConfig { dependsOn?: string[]; /** Slack action to execute. Phase A supports postMessage. */ action: 'postMessage'; - /** Slack channel id or #channel-name reference. */ - channel: string; + /** 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. */ @@ -84,10 +84,10 @@ export function createSlackStep(config: SlackStepConfig): WorkflowStep { validateSlackStepConfig(config); const params: Record = { - channel: config.channel, 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); @@ -190,11 +190,11 @@ export function slackStepConfigFromWorkflowStep( name: step.name, dependsOn: step.dependsOn, action: SlackAction.PostMessage, - channel: readRequiredString(actionParams.channel, 'channel'), + channel: readOptionalString(actionParams.channel), text: readRequiredString(actionParams.text, 'text'), threadTs: readOptionalString(actionParams.threadTs), mentions: readStringArray(actionParams.mentions), - unfurl: typeof actionParams.unfurl === 'boolean' ? actionParams.unfurl : undefined, + unfurl: readOptionalBoolean(actionParams.unfurl, 'unfurl'), config, output, timeoutMs: step.timeoutMs, @@ -224,9 +224,6 @@ function validateSlackStepConfig(config: SlackStepConfig): void { if (config.action !== SlackAction.PostMessage) { throw new Error(`Slack step "${config.name}" requires action "postMessage"`); } - if (!config.channel) { - throw new Error(`Slack step "${config.name}" requires a channel`); - } if (typeof config.text !== 'string' || config.text.length === 0) { throw new Error(`Slack step "${config.name}" requires message text`); } @@ -321,7 +318,11 @@ function buildOutputProjection( return withOptionalMetadata(projected, result, outputConfig); } - return withOptionalMetadata(result.data ?? (result.output ? result.output : null), result, outputConfig); + return withOptionalMetadata( + result.data ?? (result.output ? result.output : result.error ?? null), + result, + outputConfig + ); } function summarizeResult(result: SlackActionResult): Record { @@ -409,10 +410,6 @@ function coerceScalar(value: unknown): unknown { } const trimmed = value.trim(); - if (trimmed === 'true') return true; - if (trimmed === 'false') return false; - if (trimmed === 'null') return null; - if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmed)) return Number(trimmed); if ( (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')) || @@ -456,6 +453,14 @@ function readStringArray(value: unknown): string[] | undefined { 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); } From f02e3dcc462e42e8ec1ed35378fc5abd8653441a Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sat, 9 May 2026 10:48:33 +0200 Subject: [PATCH 9/9] chore: complete PR 823 fix trajectory --- .trajectories/active/traj_lieyyspidhfj.json | 46 - .../completed/2026-05/traj_lieyyspidhfj.json | 123 ++ .../completed/2026-05/traj_lieyyspidhfj.md | 40 + .../2026-05/traj_lieyyspidhfj.trace.json | 1694 +++++++++++++++++ .trajectories/index.json | 7 +- 5 files changed, 1861 insertions(+), 49 deletions(-) delete mode 100644 .trajectories/active/traj_lieyyspidhfj.json create mode 100644 .trajectories/completed/2026-05/traj_lieyyspidhfj.json create mode 100644 .trajectories/completed/2026-05/traj_lieyyspidhfj.md create mode 100644 .trajectories/completed/2026-05/traj_lieyyspidhfj.trace.json diff --git a/.trajectories/active/traj_lieyyspidhfj.json b/.trajectories/active/traj_lieyyspidhfj.json deleted file mode 100644 index d526412cf..000000000 --- a/.trajectories/active/traj_lieyyspidhfj.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "id": "traj_lieyyspidhfj", - "version": 1, - "task": { - "title": "Fix PR 823 conflicts checks and comments" - }, - "status": "active", - "startedAt": "2026-05-09T08:37:17.563Z", - "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", - "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" - } - ] - } - ], - "commits": [], - "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/AgentWorkforce/relay", - "tags": [], - "_trace": { - "startRef": "3fccf893a70f4840e3a4b4ea5aa116919e7b2835", - "endRef": "3fccf893a70f4840e3a4b4ea5aa116919e7b2835" - } -} 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/index.json b/.trajectories/index.json index 43e2170e2..6fe16bd4f 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-05-09T08:46:32.497Z", + "lastUpdated": "2026-05-09T08:47:54.843Z", "trajectories": { "traj_1775914133873_35667beb": { "title": "fix-sdk-build-resolution-workflow", @@ -320,9 +320,10 @@ }, "traj_lieyyspidhfj": { "title": "Fix PR 823 conflicts checks and comments", - "status": "active", + "status": "completed", "startedAt": "2026-05-09T08:37:17.563Z", - "path": ".trajectories/active/traj_lieyyspidhfj.json" + "completedAt": "2026-05-09T08:47:54.686Z", + "path": ".trajectories/completed/2026-05/traj_lieyyspidhfj.json" } } }