This guide captures the exact sequence that worked for validating OpenTag against real GitHub and Slack services, plus the failure modes we actually hit along the way.
Use this document when you want to prove that OpenTag works beyond local unit tests:
- real GitHub App webhook delivery
- real Slack Events API delivery
- real Lark card reply and patch delivery
- real Linear workspace comment and issue update delivery
- real dispatcher and daemon execution
- real callback messages posted back to the source thread
- real local Claude Code execution
- protocol metrics that show callback noise and artifact flow
For the consolidated runner that wraps the local protocol smoke cases and the live GitHub/Slack/Lark/Linear scripts, see Live E2E Smoke Harness.
A smoke test is complete when all of these are true:
- A real GitHub comment or Slack app mention reaches the matching ingress service.
- The dispatcher creates a run and records audit events.
- The local daemon claims the run and executes the configured executor.
- The final result is posted back to the original GitHub thread or Slack thread.
- Metrics show low human callback noise relative to audit events.
Use this script when you need to prove the real Linear hosted-relay callback and
issue-update path. It registers a temporary Linear relay installation for
token/project-target storage and submits signed Linear webhook payloads locally
to the fixed hosted OAuth App webhook path (/linear/oauth/webhooks by
default), so it does not need ngrok, but it does call the real Linear GraphQL
API for commentCreate and issueUpdate.
OPENTAG_LINEAR_SMOKE_TOKEN='Bearer <linear-oauth-app-actor-access-token>' \
OPENTAG_LINEAR_SMOKE_ISSUE=<linear-issue-key-or-url-or-uuid> \
corepack pnpm smoke:live -- --case linear-workspace-liveBy default, the smoke verifies that the token resolves to a Linear OAuth app
actor (viewer.app=true) and reports that evidence in oauthActor. This keeps
the live validation aligned with the hosted OAuth App / actor=app install
path. Set OPENTAG_LINEAR_SMOKE_ALLOW_NON_APP_TOKEN=true only when intentionally
running an API-key compatibility smoke.
Expected evidence:
- signed
/linear/oauth/webhookspayload creates an OpenTag run through the hosted OAuth webhook ingress and routes byorganizationId oauthActor.appActorVerifiedistruefor the default OAuth App smoke token- acknowledgement/final callback creates or updates a real Linear issue comment
singleStatusComment.reusedExternalMessageIdandsingleStatusComment.graphql.statusCommentUpdateVerifiedaretrue, proving the status lifecycle updated one Linear comment instead of creating comment noise- signed
apply 1payload creates an approval and apply plan - Linear direct apply executes a real
issueUpdate - metadata discovery queries teams, users, workflow states, and labels, then
reports generated mapping domains and value counts in
metadataDiscovery - optional
OPENTAG_LINEAR_SMOKE_AGENT_SESSION_IDvalidates nativeAgentSessionEventcreatedandprompteddelivery throughagentSessionUpdateandagentActivityCreate - optional Agent Session output includes
agentSessionSmoke.promptedwith the queued follow-up id, the active run it queued behind,followUpStatus: "promoted", and the promoted follow-up run id linearGraphqlEvidence.requiredOperationsistruefor the required Linear GraphQL paths, with operation counts included in the JSON output- the JSON output reports
ok: trueandapplyOutcomeCounts.applied: 1
OPENTAG_LINEAR_SMOKE_ISSUE_ID remains supported for older local scripts, but
new runs should prefer OPENTAG_LINEAR_SMOKE_ISSUE because it accepts the
human-readable Linear issue key as well as the model UUID or issue URL.
- Node 22.14 or newer
- pnpm 9.x
- A clean local checkout for any repository you want to bind
pnpm install
The examples below assume:
- dispatcher on
http://localhost:3031 - GitHub Probot ingress on
http://localhost:3000 - Slack Events ingress on
http://localhost:3040 - a pairing token of
dev_pairing_token
Use this script when you need to prove that OpenTag can send one Lark status
card and then patch that same message for the final callback. It exercises the
real Lark API through lark-cli, but keeps dispatcher state in memory and does
not require a long-running daemon.
Prerequisites:
lark-cliis installed and available onPATH, orOPENTAG_LARK_CLIpoints to the binary.lark-cli auth statusreports the bot identity as ready and the user identity as ready, or at least provides a cached useropenIdfor bot-to-user seed messages.- The bot identity can send a message to the current user.
Run:
NODE_OPTIONS='--conditions=development' \
packages/dispatcher/node_modules/.bin/tsx \
scripts/dev/run-lark-message-patch-live-test.tsBy default the script creates a bot-to-user seed message, creates an in-memory Lark-shaped run, sends the acknowledgement card as a Lark reply, and completes the run so the final callback patches the acknowledgement card in place.
Optional inputs:
OPENTAG_LARK_LIVE_REPO=amplifthq/opentag-test
OPENTAG_LARK_LIVE_CHAT_ID=<existing-chat-id>
OPENTAG_LARK_LIVE_SOURCE_MESSAGE_ID=<existing-source-message-id>Set both OPENTAG_LARK_LIVE_CHAT_ID and
OPENTAG_LARK_LIVE_SOURCE_MESSAGE_ID to reuse an existing source message. Set
neither to let the script create a private seed message.
Expected evidence:
ok: truecreatedSeedMessageshows whether the script created the source message.- The acknowledgement delivery has
returnedExternal: true. - The final delivery has
hasExternal: trueand returns the same redacted message id, proving that the final callback patched the first Lark card instead of posting a second final card.
The script intentionally prints only a redacted summary. It must not print app secrets, tokens, full message ids, raw request bodies, binding metadata payloads, or local workspace paths. Its generated idempotency keys are deliberately short because Lark rejects overlong keys with field validation errors.
Create a local runner config that binds the repository you want to test:
{
"runnerId": "runner_local",
"dispatcherUrl": "http://localhost:3031",
"pairingToken": "dev_pairing_token",
"pollIntervalMs": 5000,
"heartbeatIntervalMs": 15000,
"repositories": [
{
"provider": "github",
"owner": "amplifthq",
"repo": "opentag",
"checkoutPath": "/absolute/path/to/opentag",
"defaultExecutor": "echo",
"baseBranch": "main",
"pushRemote": "origin"
}
],
"slackChannels": [
{
"teamId": "T_REAL",
"channelId": "C_REAL",
"repoProvider": "github",
"owner": "amplifthq",
"repo": "opentag"
}
]
}Start with "defaultExecutor": "echo" until the end-to-end callback loop is proven. Switch to "codex" or "claude-code" only after GitHub or Slack replies are working.
For Claude Code, set the repository binding to:
"defaultExecutor": "claude-code"The Claude ACP adapter is resolved from its pinned Registry package through
npx. Complete the normal local Claude login before
starting the daemon. Direct Claude command, model, and permission-mode settings
have been removed. If authentication intentionally comes from
ANTHROPIC_API_KEY, add ANTHROPIC_API_KEY to security.extraSafeEnv so the
ACP child receives it after environment scrubbing.
The fastest real GitHub test does not require a GitHub App webhook. It uses the active gh CLI login to create a GitHub-shaped run directly in the dispatcher, then lets opentagd execute locally and callback to a real issue.
Use an existing issue:
OPENTAG_GH_REPO=amplifthq/opentag-test \
OPENTAG_WORKSPACE_PATH=/absolute/path/to/clean/opentag-test \
OPENTAG_GH_TEST_ISSUE=1 \
scripts/dev/run-gh-claude-local-test.shOr create a temporary issue:
OPENTAG_GH_REPO=amplifthq/opentag-test \
OPENTAG_WORKSPACE_PATH=/absolute/path/to/clean/opentag-test \
OPENTAG_GH_CREATE_ISSUE=true \
scripts/dev/run-gh-claude-local-test.shFor the thread-native PR flow, prefer:
OPENTAG_GH_PREPARE_PR_BRANCH=true \
OPENTAG_GH_APPLY_PR_ACTION=true \
scripts/dev/run-gh-claude-local-test.shUse this when you need to prove the real GitHub repository webhook ingress, not
only the dispatcher-assisted path. The script starts the local CLI stack,
exposes the GitHub webhook listener through ngrok, creates a temporary
repository webhook, posts a real issue comment, replies apply 1, waits for the
PR, and deletes the temporary webhook on exit.
OPENTAG_GH_REPO=amplifthq/opentag-test \
scripts/dev/run-github-webhook-live-test.shExpected evidence:
- GitHub issue comment creates a dispatcher run through
/github/webhooks. - Final GitHub comment renders a ready or mixed-state action receipt and
advertises
apply 1only for actions the dispatcher can currently apply. - Replying
apply 1through GitHub creates anApprovalDecisionandApplyPlan. - Apply executes through GitHub and comments with the created PR URL.
- The temporary repository webhook is deleted after the run.
To prove the missing-apply-credentials path while keeping GitHub callbacks live, run the same script with direct apply disabled:
OPENTAG_GH_REPO=amplifthq/opentag-test \
OPENTAG_GH_LIVE_DISABLE_APPLY_TOKEN=true \
scripts/dev/run-github-webhook-live-test.shExpected evidence for that branch:
- The final GitHub comment renders
### Needs setup. - The comment does not advertise
apply 1as an available direct-apply command. - The script skips the apply reply because the dispatcher has no GitHub apply token.
This path requires gh admin or maintain access for the target repository so
the script can create and delete the temporary webhook.
This lets the daemon commit and push the executor-produced run branch, but it does not open the PR immediately. The final callback should render a create_pull_request suggested action with the PR title, head branch, base branch, changed files, risks, executor conditions, and any real verification the executor reported. Reply in the source thread with:
apply 1
When OPENTAG_GH_APPLY_PR_ACTION=true is set, scripts/dev/run-gh-claude-local-test.sh submits that same apply 1 reply through /v1/thread-actions after the daemon finishes, so the run can produce a complete dogfood trace without manually copying the reply.
Expected result:
- dispatcher records
approval.decision.recorded - dispatcher creates an ApplyPlan for the selected
create_pull_requestintent - GitHub adapter calls
POST /repos/{owner}/{repo}/pulls - source thread receives a final callback containing the pull request URL
- metrics show
approvalDecisionCount: 1,applyPlanCount: 1, andapplyOutcomeCounts.applied: 1
Set OPENTAG_GH_CREATE_PR=true only when you explicitly want the legacy daemon path to commit, push, and create the pull request immediately after executor completion. Without either PR flag, the smoke test validates callback delivery and local execution without opening a PR.
This path has been validated with:
GitHub issue -> dispatcher -> opentagd -> local Claude Code -> commit branch -> push -> PR -> GitHub callback
The thread-native action variant should be traced as:
GitHub issue -> dispatcher -> opentagd -> local Claude Code -> pushed run branch -> final callback with create_pr action -> source-thread apply 1 -> ApprovalDecision -> ApplyPlan -> GitHub PR -> final callback with PR URL
GitHub callbacks update one comment per run in place. The first callback creates the run comment, and later progress/final callbacks patch that same comment instead of creating a new issue comment for every state change.
- Create a GitHub App for local testing.
- Install it to the target repository.
- Use a webhook URL that points to your public tunnel and GitHub webhook path.
Recommended webhook path:
/github/webhooks
Recommended minimum repository permissions:
Issues: Read and writePull requests: ReadMetadata: Read
Recommended subscribed events:
Issue commentPull request review comment
Expose the Probot ingress port:
ngrok http 3000Use the resulting public URL in the GitHub App webhook URL:
https://<ngrok-host>/github/webhooks
Start the dispatcher with a GitHub callback token:
OPENTAG_DATABASE_PATH=opentag.github-test.db \
OPENTAG_PAIRING_TOKEN=dev_pairing_token \
OPENTAG_GITHUB_TOKEN=<github-token> \
PORT=3031 \
apps/dispatcher/node_modules/.bin/tsx apps/dispatcher/src/index.tsRegister the runner and bind the repository:
OPENTAG_CONFIG_PATH=/absolute/path/to/opentag.real.json \
apps/opentagd/node_modules/.bin/tsx apps/opentagd/src/index.ts register-runner
OPENTAG_CONFIG_PATH=/absolute/path/to/opentag.real.json \
apps/opentagd/node_modules/.bin/tsx apps/opentagd/src/index.ts bind-reposStart the daemon:
OPENTAG_CONFIG_PATH=/absolute/path/to/opentag.real.json \
apps/opentagd/node_modules/.bin/tsx apps/opentagd/src/index.ts serveStart Probot from built output:
APP_ID=<github-app-id> \
WEBHOOK_SECRET=<github-webhook-secret> \
PRIVATE_KEY_PATH=/absolute/path/to/github-app.private-key.pem \
PORT=3000 \
WEBHOOK_PATH=/github/webhooks \
OPENTAG_DISPATCHER_URL=http://localhost:3031 \
OPENTAG_DISPATCHER_TOKEN=dev_pairing_token \
OPENTAG_DISPATCHER_OWNS_CALLBACKS=true \
pnpm --filter @opentag/github-probot exec probot run ./dist/index.jsCreate or reuse a real issue, then post:
@opentag investigate this
Expected result:
- Probot receives
issue_comment - dispatcher creates a run
- daemon claims and executes the run
- GitHub thread receives:
- acknowledgement
- progress
- final result
For GitHub App webhook testing, keep OPENTAG_DISPATCHER_OWNS_CALLBACKS=true on the Probot app so the dispatcher owns the run comment lifecycle.
-
Probot setup mode This happened when Probot did not receive
APP_ID,WEBHOOK_SECRET, andPRIVATE_KEY_PATHin the names it expects.GITHUB_APP_IDis not enough by itself. -
Webhook path mismatch The GitHub App must point to the actual Probot webhook path, not just the tunnel root.
-
App installed but repo missing The App installation must explicitly include the repository you are testing.
-
PR push failed because no commit existed Local executors leave file changes in the run branch.
opentagdnow stages and commits changed files before pushing a run branch for PR creation.
The fastest real Slack test does not require a public Events API tunnel. It uses the configured Slack bot token to post a seed message in the bound channel, then creates a Slack-shaped OpenTag run against that thread.
OPENTAG_ENV_FILE=/absolute/path/to/.env.slack-test \
scripts/dev/run-slack-claude-local-test.shBy default this script reads OPENTAG_CONFIG_PATH from the env file, creates a temporary clean checkout for the configured repo, executes local Claude Code, and prints the run metrics plus Slack thread replies.
To validate the thread-native PR action loop from Slack, let the daemon prepare the executor branch and submit apply 1 after the final callback:
OPENTAG_ENV_FILE=/absolute/path/to/.env.slack-test \
OPENTAG_SLACK_APPLY_PR_ACTION=true \
scripts/dev/run-slack-claude-local-test.shOPENTAG_SLACK_APPLY_PR_ACTION=true automatically enables PR branch preparation. The dispatcher must also have a GitHub token available through OPENTAG_GITHUB_TOKEN or an authenticated gh CLI, because the approved create_pull_request intent is applied through the GitHub adapter even though the source thread is Slack.
This path has been validated with:
Slack thread -> dispatcher -> opentagd -> local Claude Code -> Slack final callback + metrics
With OPENTAG_SLACK_APPLY_PR_ACTION=true, the expected action-loop shape is:
Slack thread -> dispatcher -> opentagd -> local Claude Code -> pushed run branch
-> Slack final callback with create_pr action -> apply 1 -> ApprovalDecision
-> ApplyPlan(adapter=github) -> GitHub PR -> Slack final callback with PR URL
Expected Slack thread shape:
- original user/bot seed message
- OpenTag
eyesreaction receipt on the source message - OpenTag final result
- optional
apply 1reply when validating the action loop - optional apply result callback containing the PR URL
- if direct apply is not available, the receipt should show
Needs setuporContinueinstead of presentingApplyas the primary path
Routine acknowledgement and progress stay out of Slack thread replies by default, so they should not produce additional Slack messages.
Required bot scopes:
app_mentions:readchat:writereactions:write
Required bot event:
app_mention
Install the app to the workspace, then collect:
App-Level Tokenfor Socket Mode, orSigning Secretfor Events APIBot User OAuth Token- target
teamId - target
channelId
Invite the bot user into the test channel before sending any mention.
For local README recordings, prefer Socket Mode. It avoids public Request URL drift and sends app mentions plus Block Kit button actions through Slack's WebSocket connection:
OPENTAG_SLACK_APP_TOKEN=xapp-... \
scripts/dev/run-slack-ui-trigger-local-test.shSlack Interactivity still needs to be enabled, but Socket Mode does not require an Interactivity Request URL.
Use Events API only when you intentionally want to validate a public Request URL or hosted ingress. Disable Socket Mode for that specific app setup.
If Socket Mode is still enabled, Slack may give confusing signals in the dashboard and you end up debugging the wrong transport.
Expose the Slack Events ingress port:
cloudflared tunnel --url http://localhost:3040ngrok works too:
ngrok http 3040Use the resulting public URL in Slack Event Subscriptions:
https://<tunnel-host>/slack/events
Do not use http://localhost:3040/slack/events in Slack. Slack validates the Request URL from Slack's servers, so the URL must be public HTTPS and must forward to the local Slack ingress port 3040.
Start the dispatcher with both GitHub and Slack callback tokens when you want both integrations live:
OPENTAG_DATABASE_PATH=opentag.github-test.db \
OPENTAG_PAIRING_TOKEN=dev_pairing_token \
OPENTAG_GITHUB_TOKEN=<github-token> \
OPENTAG_SLACK_BOT_TOKEN=<xoxb-token> \
PORT=3031 \
apps/dispatcher/node_modules/.bin/tsx apps/dispatcher/src/index.tsBind the Slack channel to the repository:
OPENTAG_CONFIG_PATH=/absolute/path/to/opentag.real.json \
apps/opentagd/node_modules/.bin/tsx apps/opentagd/src/index.ts bind-slack-channelsStart Slack ingress:
SLACK_SIGNING_SECRET=<signing-secret> \
OPENTAG_DISPATCHER_URL=http://localhost:3031 \
OPENTAG_DISPATCHER_TOKEN=dev_pairing_token \
PORT=3040 \
apps/slack-events/node_modules/.bin/tsx apps/slack-events/src/index.tsThe daemon process can be the same one already running for GitHub.
In the bound channel, post:
@little_pig investigate this
Expected result:
slack-eventsreceivesapp_mention- dispatcher creates a run
- daemon claims and executes the run
- the Slack thread receives:
- reaction receipt (
eyes) - final result
- reaction receipt (
Routine progress should be visible in dispatcher audit events, not as Slack replies.
-
Request URL pointed at the wrong port We accidentally reused a tunnel that still pointed at
3000for GitHub, while Slack needed3040. -
url_verificationreturned the wrong shape Slack validation required the rawchallengebody. Returning a JSON object caused dashboard verification to fail. The correct behavior now lives inapps/slack-events/src/app.ts. -
Channel binding missing Slack requests were accepted but ignored as
unbound_channeluntil the dispatcher had a Slack channel binding. -
Bot token missing on dispatcher Runs executed successfully, but nothing appeared in Slack because the dispatcher callback sink started without
OPENTAG_SLACK_BOT_TOKEN. -
Replies show up in the thread, not the main channel stream Slack callback delivery uses
thread_ts, so the most reliable place to check for bot replies is the message thread you mentioned the bot in.
Every real smoke test should inspect run metrics:
curl -H "authorization: Bearer $OPENTAG_PAIRING_TOKEN" \
http://localhost:3031/v1/runs/<run-id>/metricsImportant fields:
humanCallbackCountauditEventCountthreadNoiseRatiosuggestedChangesCountapprovalDecisionCountapplyPlanCountchildRunCountapplyOutcomeCounts
For Slack, humanCallbackCount should normally be 1 for a completed run: the final callback. The source receipt is a reaction, and routine progress stays audit-only. A low threadNoiseRatio means OpenTag is recording detail in audit without flooding the human thread.
Use this trace when validating the model action layer against a real GitHub issue or Slack thread. Record concrete IDs and URLs; do not count the run as complete from local logs alone.
| Step | Evidence to capture |
|---|---|
| Mention | Source thread URL and exact mention text |
| Run created | Dispatcher runId, source eventId, and run.created audit event |
| Executor result | Branch name, changed files, executor conditions, optional real verification, and final callback body |
| Suggested action | Rendered create_pull_request action with title, head branch, base branch, risks, executor conditions, and optional real verification |
| Approval/apply reply | Source-thread reply text such as apply 1 and actor identity |
| ApprovalDecision | approval.decision.recorded event and selected intent IDs |
| ApplyPlan | apply_plan.created event, adapter, selected intent IDs, and preflight outcomes |
| Adapter or setup path | PR URL when applied, or a Needs setup / continuation reason when the adapter cannot apply |
| Final callback | Source-thread callback containing the PR URL or child-run continuation context |
| Metrics | approvalDecisionCount, applyPlanCount, childRunCount, and applyOutcomeCounts |
Expected GitHub create-PR success shape:
mention -> run -> executor changed files -> final callback with Ready to apply source-thread action receipt
apply 1 -> approval decision -> apply plan -> POST /pulls -> callback with https://github.com/<owner>/<repo>/pull/<number>
Expected unavailable-apply shape when the branch is not pushed or the adapter cannot apply:
final callback -> Needs setup or Continue -> no primary Apply button
The receipt must explain why direct apply is not possible. A manually typed
continue 1 can still start a follow-up run when a human explicitly chooses to
carry the proposal context forward.
This trace validated both sides of the source-thread receipt state machine
against the real amplifthq/opentag-test repository webhook path.
| Field | Value |
|---|---|
| Source issue | https://github.com/amplifthq/opentag-test/issues/46 |
| Mention comment | https://github.com/amplifthq/opentag-test/issues/46#issuecomment-4837078185 |
| Apply comment | https://github.com/amplifthq/opentag-test/issues/46#issuecomment-4837085850 |
| Run ID | run_afeb0d55-7f40-473e-887d-fb652750881b |
| Created PR | https://github.com/amplifthq/opentag-test/pull/47 |
| Head branch | opentag/run_afeb0d55-7f40-473e-887d-fb652750881b |
| Base branch | main |
Observed behavior:
- The real GitHub issue comment triggered the dispatcher through
/github/webhooks. - The final GitHub callback rendered
### Ready to apply. - The real
apply 1reply created anApprovalDecisionandApplyPlan. - The GitHub adapter created PR #47.
- The apply receipt included
Applied:and the created PR URL.
Observed metrics after apply:
{
"approvalDecisionCount": 1,
"applyPlanCount": 1,
"childRunCount": 0,
"applyOutcomeCounts": {
"applied": 1,
"failed": 0,
"skipped": 0,
"stale": 0,
"unsupported": 0
},
"suggestedChangesCount": 3,
"threadNoiseRatio": 0.2857142857142857
}| Field | Value |
|---|---|
| Source issue | https://github.com/amplifthq/opentag-test/issues/48 |
| Mention comment | https://github.com/amplifthq/opentag-test/issues/48#issuecomment-4837099441 |
| Run ID | run_08c68db8-36d0-44ba-8f06-954ecb40b5bc |
| Head branch | opentag/run_08c68db8-36d0-44ba-8f06-954ecb40b5bc |
Command shape:
OPENTAG_GH_REPO=amplifthq/opentag-test \
OPENTAG_GH_LIVE_DISABLE_APPLY_TOKEN=true \
scripts/dev/run-github-webhook-live-test.shObserved behavior:
- GitHub callbacks still worked because the callback token remained configured.
- Dispatcher direct apply was unavailable because the apply token was disabled.
- The final GitHub callback rendered
### Needs setup. - The receipt did not advertise
apply 1as an available command. - No approval decision, apply plan, child run, or external write was created.
Observed metrics:
{
"approvalDecisionCount": 0,
"applyPlanCount": 0,
"childRunCount": 0,
"applyOutcomeCounts": {
"applied": 0,
"failed": 0,
"skipped": 0,
"stale": 0,
"unsupported": 0
},
"suggestedChangesCount": 3,
"threadNoiseRatio": 0.25
}Repository cleanup:
- Temporary repository webhook
647717865was deleted by the ready-to-apply run. - Temporary repository webhook
647718589was deleted by the missing-apply-token run. - The live script removed temporary config files after each run because they contain local credentials.
This trace validated the repository-webhook path against the real
amplifthq/opentag-test repository. Unlike the CLI-assisted smoke test, both
the initial mention and apply 1 reply entered through the real GitHub
repository webhook listener.
| Field | Value |
|---|---|
| Source issue | https://github.com/amplifthq/opentag-test/issues/44 |
| Mention comment | https://github.com/amplifthq/opentag-test/issues/44#issuecomment-4836858128 |
| Apply comment | https://github.com/amplifthq/opentag-test/issues/44#issuecomment-4836863316 |
| Run ID | run_5384bdb9-9b6e-4639-94f4-2b2055be6e56 |
| Proposal ID | proposal_run_5384bdb9-9b6e-4639-94f4-2b2055be6e56 |
| Applied intent | proposal_run_5384bdb9-9b6e-4639-94f4-2b2055be6e56_create_pr |
| Approval ID | approval_github_comment_4836863316 |
| ApplyPlan ID | apply_ef9146b4f45d |
| Created PR | https://github.com/amplifthq/opentag-test/pull/45 |
| Head branch | opentag/run_5384bdb9-9b6e-4639-94f4-2b2055be6e56 |
| Base branch | main |
Command shape:
OPENTAG_GH_LIVE_KILL_PORTS=true \
scripts/dev/run-github-webhook-live-test.shObserved source-thread callbacks:
- Mention comment:
@opentag run Add one short sentence to README.md saying GitHub can trigger local Claude Code through OpenTag. Keep the change small and do not modify anything else. - Final run callback rendered a source-thread action receipt with
apply 1available for the create-PR action. - Real
apply 1issue comment returnedApplied: Create a pull request for branch opentag/run_5384bdb9-9b6e-4639-94f4-2b2055be6e56.plus the PR URL.
Observed metrics after apply:
{
"approvalDecisionCount": 1,
"applyPlanCount": 1,
"childRunCount": 0,
"applyOutcomeCounts": {
"applied": 1,
"skipped": 0,
"failed": 0,
"stale": 0,
"unsupported": 0
},
"suggestedChangesCount": 3,
"threadNoiseRatio": 0.2857142857142857
}Repository cleanup:
- Temporary repository webhook
647712049was deleted by the script. gh api repos/amplifthq/opentag-test/hooksreturned[]after cleanup.
This trace validated the thread-native PR flow against the real amplifthq/opentag-test repository.
| Field | Value |
|---|---|
| Source issue | https://github.com/amplifthq/opentag-test/issues/20 |
| Run ID | run_gh_claude_1782542192 |
| Proposal ID | proposal_run_gh_claude_1782542192 |
| Applied intent | proposal_run_gh_claude_1782542192_create_pr |
| Approval ID | approval_a1a014f1213d |
| ApplyPlan ID | apply_a7fa7b1514ac |
| Created PR | https://github.com/amplifthq/opentag-test/pull/21 |
| Head branch | opentag/run_gh_claude_1782542192 |
| Base branch | main |
Command shape:
OPENTAG_GH_REPO=amplifthq/opentag-test \
OPENTAG_WORKSPACE_PATH=/tmp/opentag-dogfood-worktree \
OPENTAG_GH_TEST_ISSUE=20 \
OPENTAG_DATABASE_PATH=/tmp/opentag-dogfood-action-loop.db \
OPENTAG_GH_PREPARE_PR_BRANCH=true \
OPENTAG_GH_APPLY_PR_ACTION=true \
scripts/dev/run-gh-claude-local-test.shObserved source-thread callbacks:
- Final run callback rendered Suggested action
1. Create a pull request for branch opentag/run_gh_claude_1782542192. - The rendered action included title
OpenTag run run_gh_claude_1782542192, branchopentag/run_gh_claude_1782542192 -> main, changed fileREADME.md, risk note, and executor conditionisolated branch exists. - The automatic
apply 1thread action returned a final callback withproposal_run_gh_claude_1782542192_create_pr: applied (https://github.com/amplifthq/opentag-test/pull/21).
Observed audit events:
proposal.snapshot.createdapproval.decision.recordedapply_plan.createdapply_plan.executed- final callback delivery for the PR URL
Observed ApplyPlan outcome:
{
"adapter": "github",
"selectedIntentIds": ["proposal_run_gh_claude_1782542192_create_pr"],
"outcomes": [
{
"intentId": "proposal_run_gh_claude_1782542192_create_pr",
"outcome": "applied",
"externalUri": "https://github.com/amplifthq/opentag-test/pull/21"
}
]
}This trace validated the same action loop through a real GitHub App
issue_comment.created webhook delivered to Probot through the public webhook
URL. Unlike the script-assisted trace above, the source-thread apply 1 reply
also entered through the GitHub App webhook.
| Field | Value |
|---|---|
| Source issue | https://github.com/amplifthq/opentag-test/issues/24 |
| Mention comment | https://github.com/amplifthq/opentag-test/issues/24#issuecomment-4816025393 |
| Apply comment | https://github.com/amplifthq/opentag-test/issues/24#issuecomment-4816029160 |
| Run ID | run_1782548134282 |
| Proposal ID | proposal_run_1782548134282 |
| Applied intent | proposal_run_1782548134282_create_pr |
| Approval ID | approval_github_comment_4816029160 |
| ApplyPlan ID | apply_023a7401d573 |
| Created PR | https://github.com/amplifthq/opentag-test/pull/25 |
| Head branch | opentag/run_1782548134282 |
| Base branch | main |
Observed source-thread callbacks:
- Acknowledgement:
OpenTag picked this up. Run: run_1782548134282. - Final run callback rendered Suggested action
1. Create a pull request for branch opentag/run_1782548134282. - The rendered action included title
OpenTag run run_1782548134282, branchopentag/run_1782548134282 -> main, changed fileREADME.md, risk note, and executor conditionisolated branch exists. - The real
apply 1issue comment returned a final callback withproposal_run_1782548134282_create_pr: applied (https://github.com/amplifthq/opentag-test/pull/25).
Observed thread identity:
proposal_run_1782548134282 | run_1782548134282 |
thread_github_amplifthq/opentag-test#24_amplifthq/opentag-test#24 |
primaryAnchor.threadKey=amplifthq/opentag-test#24 |
workItemReference.externalId=amplifthq/opentag-test#24
The issue-scoped thread key matters: source-thread approvals should not share a repo-wide action namespace across unrelated issues.
Observed audit events:
context_packet.generatedrun.createdrun.claimedproposal.snapshot.createdrun.completedapproval.decision.recordedapply_plan.createdapply_plan.executed- final callback delivery for the PR URL
Observed ApprovalDecision:
{
"id": "approval_github_comment_4816029160",
"proposalId": "proposal_run_1782548134282",
"approvedIntentIds": ["proposal_run_1782548134282_create_pr"],
"approvedBy": {
"provider": "github",
"handle": "mingyooagi"
},
"metadata": {
"source": "thread_action",
"rawText": "apply 1",
"callback": {
"provider": "github",
"threadKey": "amplifthq/opentag-test#24"
}
}
}Observed ApplyPlan outcome:
{
"id": "apply_023a7401d573",
"proposalId": "proposal_run_1782548134282",
"approvalDecisionId": "approval_github_comment_4816029160",
"selectedIntentIds": ["proposal_run_1782548134282_create_pr"],
"adapter": "github",
"outcomes": [
{
"intentId": "proposal_run_1782548134282_create_pr",
"outcome": "applied",
"externalUri": "https://github.com/amplifthq/opentag-test/pull/25"
}
]
}Dogfood findings fixed from this trace:
- GitHub issue mentions now use issue-scoped callback thread keys such as
owner/repo#24, matching the thread action replies submitted by Probot. - If preparing the PR action branch fails after the executor finishes, the daemon completes the run with a
needs_humanresult instead of crashing and leaving a stale running lease.
This trace validated the same thread-native PR flow from a real Slack thread into the real amplifthq/opentag-test repository. It also verifies that repo-level create_pull_request intents from Slack are applied through the GitHub adapter instead of being trapped behind the Slack source adapter.
| Field | Value |
|---|---|
| Source thread | Slack T098U4EM971/C0BCC83S54P/1782543299.304939 |
| Run ID | run_slack_claude_real_1782543299 |
| Proposal ID | proposal_run_slack_claude_real_1782543299 |
| Applied intent | proposal_run_slack_claude_real_1782543299_create_pr |
| Approval ID | approval_2d103709b771 |
| ApplyPlan ID | apply_092634c6720a |
| ApplyPlan adapter | github |
| Created PR | https://github.com/amplifthq/opentag-test/pull/22 |
| Head branch | opentag/run_slack_claude_real_1782543299 |
| Base branch | main |
Command shape:
OPENTAG_ENV_FILE=/absolute/path/to/opentag/.env.slack-test \
OPENTAG_DATABASE_PATH=/tmp/opentag-slack-action-loop-3.db \
OPENTAG_SLACK_APPLY_PR_ACTION=true \
scripts/dev/run-slack-claude-local-test.shObserved Slack thread callbacks:
- Final run callback rendered Suggested action
1. Create a pull request for branch opentag/run_slack_claude_real_1782543299. - The rendered action included title
OpenTag run run_slack_claude_real_1782543299, branchopentag/run_slack_claude_real_1782543299 -> main, changed fileREADME.md, risk note, and executor conditionisolated branch exists. - The
apply 1thread action returned a final Slack callback withproposal_run_slack_claude_real_1782543299_create_pr: applied (https://github.com/amplifthq/opentag-test/pull/22).
Observed metrics:
{
"suggestedChangesCount": 3,
"approvalDecisionCount": 1,
"applyPlanCount": 1,
"childRunCount": 0,
"applyOutcomeCounts": { "applied": 1, "skipped": 0, "failed": 0, "stale": 0, "unsupported": 0 }
}Observed audit events:
proposal.snapshot.createdapproval.decision.recordedapply_plan.createdapply_plan.executed- final callback delivery for the PR URL
When something is broken, debug in this order:
- Public tunnel Confirm it points at the right local port.
- Ingress health Confirm the ingress process is listening locally.
- Platform handshake
GitHub webhook delivery or Slack
url_verificationmust succeed first. - Binding Confirm repository binding or Slack channel binding exists in the dispatcher.
- Run creation
Confirm the dispatcher recorded
run.created. - Claim and execution
Confirm the daemon recorded
run.claimed,run.running,run.completed. - Callback token Confirm the dispatcher was started with the right token for the platform being tested.
For real integration work, keep two separate public tunnels or be deliberate about switching one tunnel between GitHub and Slack. Reusing the same public hostname across both ports is possible, but it is an easy way to lose half an hour to the wrong endpoint.
After the individual GitHub and Slack paths work, you can run both ingress services against one local Claude Code daemon:
OPENTAG_CONFIG_PATH=/absolute/path/to/opentag.real.json \
OPENTAG_GITHUB_TOKEN=<github-token> \
OPENTAG_SLACK_BOT_TOKEN=<xoxb-token> \
SLACK_SIGNING_SECRET=<signing-secret> \
APP_ID=<github-app-id> \
WEBHOOK_SECRET=<github-webhook-secret> \
PRIVATE_KEY_PATH=/absolute/path/to/github-app.private-key.pem \
scripts/dev/start-github-slack-claude-test.shThe config file should bind the target repository and Slack channel, and the repository binding should use "defaultExecutor": "claude-code".
Use scripts/dev/run-gh-claude-local-test.sh and scripts/dev/run-slack-claude-local-test.sh first. They are easier to debug because they do not require public webhook tunnels. Use the combined stack once both assisted paths work.