Implementation-ready draft, 2026-06-24
OpenTag is the open mention layer for agents: tag any approved agent from a workspace surface, route the request through scoped permissions and auditable context, and let a local or hosted runner execute the work and report back.
Claude Tag makes agent mentions feel natural inside team collaboration tools. The opportunity for OpenTag is to turn that interaction pattern into an open, vendor-neutral layer:
- Claude Tag brings Claude into Slack.
- OpenTag brings any agent into any workspace.
- Claude Tag is one model, one vendor, one collaboration surface first.
- OpenTag should be protocol-first, executor-neutral, and local-runner friendly.
The first release should move fast enough to ride the conversation while still proving the idea with a real end-to-end task flow.
OpenTag should let a person write something like:
@opentag fix this flaky test
from GitHub, Slack, or a similar workspace surface, then have OpenTag:
- Recognize the tagged agent request.
- Normalize the workspace event into a stable OpenTag event.
- Check actor, target, context, and permission scope.
- Create a run with auditable state.
- Dispatch the run to an approved local or hosted executor.
- Stream progress and final output back to the original surface.
The first release combines a fast open-source launch with one real MVP:
- A public repository with a clear README, design document, and protocol draft.
- A GitHub App MVP powered by Probot at the edge.
- An extremely thin hosted dispatcher for public webhook ingress, run persistence, runner lease claiming, and callback coordination.
- A local runner daemon that can receive or poll for OpenTag runs.
- At least one executor adapter for a coding agent, initially Claude Code, Codex, Hermes, or OpenClaw.
- A callback adapter that posts status and final results back to GitHub.
The first release should not attempt to support every workspace, every agent, every permission model, or every deployment shape.
- No additional workspace apps in the first implementation milestone.
- No custom hosted IDE or chat UI.
- No general-purpose agent framework.
- No dependency on a single executor framework.
- No broad multi-tenant SaaS control plane until the local GitHub flow proves demand.
- No Rust requirement for v0. Keep the default implementation in TypeScript unless packaging, security, or performance constraints prove otherwise.
- No autonomous background execution without an explicit mention, command, or approval boundary.
- Open core, closed surfaces optional: the protocol and local runner path should be usable without depending on a hosted SaaS.
- Protocol at the center: GitHub, Slack, and future surfaces are adapters, not the architecture.
- Local-first execution: the runner can live on the user's machine so repo access, credentials, build tools, and private context stay local.
- Auditable by default: every run records who asked, what context was provided, what permissions were granted, which executor ran, and where the result was posted.
- Small reversible permissions: each tagged run receives a narrow grant instead of inheriting broad ambient authority.
- Adapter neutrality: Claude Code, Codex, Hermes, OpenClaw, Mastra workflows, and custom agents should all be possible executors.
Workspace Surface
GitHub issue comment / PR comment
Slack app mention
|
v
GitHub Ingress App
Probot webhook handler
Slack Ingress App
Events API webhook handler
|
v
OpenTag Core
event normalization
target parsing
policy check
run creation
|
v
Dispatch Layer
thin hosted dispatcher
run queue, leases, audit events, callbacks
|
v
OpenTag Local Daemon
workspace checkout access
executor adapter
progress streaming
|
v
Executor
Claude Code / Codex / Hermes / OpenClaw / custom
|
v
Callback Adapter
GitHub status comment / PR / summary
Slack thread reply
Suggested repository layout:
packages/opentag-core
packages/opentag-client
packages/opentag-dispatcher
packages/opentag-github
packages/opentag-slack
packages/opentag-runner
packages/opentag-store
apps/github-probot
apps/slack-events
apps/dispatcher
apps/opentagd
examples/github-to-codex
examples/github-to-omp
docs/
Owns the stable OpenTag domain model. It must not import Probot, Octokit, Slack SDKs, workspace SDKs, or executor-specific packages.
Responsibilities:
OpenTagEventtypes.OpenTagRunlifecycle.- target parsing rules.
- permission grant model.
- callback route model.
- run status vocabulary.
- serialization and validation.
Owns the HTTP client contract for systems that create, claim, or update runs through a dispatcher.
Responsibilities:
- create dispatcher runs from normalized
OpenTagEventobjects. - register runners and bind repositories or Slack channels.
- claim runs for local or hosted runners.
- report runner heartbeat, progress, running status, and final results.
- validate dispatcher responses at the package boundary.
Owns the embeddable dispatcher application and provider callback sinks.
Responsibilities:
- expose the Hono app factory for services that want to host OpenTag themselves.
- accept normalized OpenTag events from ingress apps.
- persist runs and audit events through
packages/opentag-store. - expose runner pairing and polling endpoints.
- implement lease-based run claiming.
- receive runner status updates.
- coordinate callback delivery through provider adapters.
Owns GitHub-specific translation and callback behavior.
Responsibilities:
- convert GitHub webhook payloads into
OpenTagEvent. - map repository, issue, PR, branch, and comment context.
- post status comments.
- optionally open branches or PRs in later milestones.
Owns Slack-specific translation and callback behavior.
Responsibilities:
- convert Slack
app_mentionpayloads intoOpenTagEvent. - encode and decode Slack thread keys for callback routing.
- map bound channels to repository context.
Owns the Probot GitHub App edge.
Responsibilities:
- receive GitHub webhooks.
- validate installation and event type.
- detect
@opentagmentions. - call
packages/opentag-githubandpackages/opentag-core. - enqueue a run for the dispatcher.
Probot is intentionally an edge dependency only. If future deployment requires Octokit directly, Cloudflare Workers, or a different webhook server, this app can be replaced without changing the core protocol.
Owns the Slack Events API edge.
Responsibilities:
- verify Slack request signatures.
- answer
url_verification. - normalize
app_mentionevents from bound channels. - create dispatcher runs using the same control-plane contract as GitHub ingress.
Owns the runnable Node process for the extremely thin hosted control plane.
Responsibilities:
- read deployment configuration from environment variables.
- compose callback sinks.
- start the
packages/opentag-dispatcherHono app with@hono/node-server.
The dispatcher should stay boring. It is not an agent runtime, workflow engine, hosted IDE, or chat product. Its job is to bridge public workspace events to private/local runners without requiring the user's machine to expose an inbound port.
Owns persistence schemas and repository-style accessors.
Responsibilities:
- run storage.
- audit event storage.
- runner registration storage.
- repository-to-runner binding storage.
- lease claiming primitives.
The v0 store can be SQLite or libSQL with a simple state machine. Avoid Redis, Temporal, Trigger.dev, Inngest, or other durable workflow platforms until the run lifecycle proves it needs them.
Owns the local daemon.
Responsibilities:
- authenticate with the dispatcher.
- poll or receive assigned runs.
- resolve local workspace paths.
- start executor adapters.
- stream logs, status, and final artifacts.
- report completion or failure.
The local daemon is the main product differentiator. It lets OpenTag work with existing local coding environments instead of forcing all execution into a hosted black box.
Owns executor-neutral runner contracts and common lifecycle handling.
Responsibilities:
- executor adapter interface.
- process spawning contract.
- timeout and cancellation.
- status normalization.
- artifact and patch reporting.
- structured final result.
type OpenTagEvent = {
id: string;
source: "github" | "slack" | "lark" | "cli" | "webhook";
sourceEventId: string;
receivedAt: string;
actor: ActorIdentity;
target: AgentTarget;
command: OpenTagCommand;
context: ContextPointer[];
permissions: PermissionGrant[];
callback: CallbackRoute;
metadata: Record<string, unknown>;
};type ActorIdentity = {
provider: "github" | "slack" | "lark";
providerUserId: string;
handle?: string;
displayName?: string;
organizationId?: string;
};type AgentTarget = {
mention: string;
agentId: string;
executorHint?: "claude-code" | "codex" | "hermes" | "openclaw" | "custom";
workspaceHint?: string;
};type OpenTagCommand = {
rawText: string;
intent: "fix" | "review" | "investigate" | "explain" | "run" | "unknown";
args: Record<string, string | boolean | number>;
};type ContextPointer = {
kind:
| "github.repo"
| "github.issue"
| "github.pull_request"
| "github.qkg1.topment"
| "github.qkg1.topmit"
| "file"
| "url"
| "text";
uri: string;
title?: string;
visibility: "public" | "private" | "organization";
};type PermissionGrant = {
scope:
| "repo:read"
| "repo:write"
| "issue:comment"
| "pr:create"
| "pr:update"
| "runner:local"
| "network:restricted";
reason: string;
expiresAt?: string;
};type CallbackRoute = {
provider: "github" | "slack" | "lark" | "webhook";
uri: string;
threadKey?: string;
};type OpenTagRun = {
id: string;
eventId: string;
status:
| "queued"
| "assigned"
| "running"
| "needs_approval"
| "succeeded"
| "failed"
| "cancelled";
assignedRunnerId?: string;
executor?: string;
createdAt: string;
updatedAt: string;
result?: OpenTagRunResult;
};The v0 hosted dispatcher exists because GitHub webhooks need a public endpoint while the executor should remain local-first. It should be as small as possible:
- receive normalized events from
apps/github-probot. - create and persist
OpenTagRunrecords. - let paired local daemons poll for eligible runs.
- lease a run to exactly one daemon at a time.
- accept status/result updates from the daemon.
- call provider callback adapters to post acknowledgements, progress, and final results.
The dispatcher should not inspect local files, hold repository credentials beyond what the GitHub App needs for callbacks, execute agent code, or own workspace-specific business logic.
queued
-> assigned
-> running
-> needs_approval
-> succeeded
-> failed
-> cancelled
Lease fields should be explicit:
type RunLease = {
runId: string;
runnerId: string;
leasedAt: string;
leaseExpiresAt: string;
heartbeatAt?: string;
};If a lease expires, the dispatcher can move the run back to queued or mark it failed depending on retry policy.
Start with four tables:
runs: current run state and normalized event snapshot.run_events: append-only audit events.runners: registered local daemon identities.repo_bindings: provider repo to runner/workspace mapping.
This is enough for a real demo without introducing a workflow platform.
The v0 trigger is an issue comment, pull request review comment, or Slack app mention:
@opentag fix this
@opentag review this PR
@opentag investigate the failing test
<@opentag> fix this
apps/github-probot listens for:
issue_comment.createdpull_request_review_comment.created
The handler ignores events without a configured OpenTag mention.
apps/slack-events listens for:
url_verificationevent_callbackwhereevent.type === "app_mention"
The Slack handler ignores mentions from unbound channels and rejects invalid signatures.
The GitHub adapter extracts:
- repository owner/name.
- issue or pull request number.
- comment URL and body.
- actor login and ID.
- installation ID.
- callback location.
It creates an OpenTagEvent and stores or enqueues it.
The Slack adapter extracts:
- team ID and channel ID.
- source user ID and thread timestamp.
- bound repository owner/name from the channel mapping.
- callback route via
chat.postMessage.
It creates an OpenTagEvent and stores or enqueues it.
For v0, runner assignment can be simple:
- a single local daemon is paired with a single GitHub installation or repo.
- the daemon polls for eligible runs.
- the dispatcher returns one queued run at a time.
- the daemon claims the run before execution.
The local daemon:
- Finds the configured local checkout.
- Creates an isolated worktree or branch for the run.
- Starts the selected executor with a bounded prompt.
- Streams status back to OpenTag.
- Produces a structured final result.
The GitHub callback adapter posts:
- initial acknowledgement: "OpenTag picked this up."
- progress checkpoints for long runs.
- final success/failure summary.
- optional PR link when the executor creates a change.
The Slack callback adapter posts:
- initial acknowledgement in the source thread.
- progress checkpoints in the same thread.
- final success/failure summary.
- optional PR link when one is created.
An executor adapter should hide tool-specific process details behind one interface:
type ExecutorAdapter = {
id: string;
displayName: string;
canRun(input: ExecutorRunInput): Promise<ExecutorReadiness>;
run(input: ExecutorRunInput, sink: ExecutorEventSink): Promise<ExecutorRunResult>;
cancel(runId: string): Promise<void>;
};The first executor adapter can be intentionally narrow. For example:
- run a command-line coding agent in a local checkout.
- pass the normalized task prompt and context.
- capture stdout/stderr and final summary.
- report whether files changed.
The adapter should not decide OpenTag permissions. It receives an already-authorized run and reports what it did.
Use TypeScript across the v0 repository: Probot app, dispatcher, protocol schemas, runner contracts, CLI, and local daemon.
Reasons:
- GitHub, Slack, Probot, Octokit, and most agent SDK ecosystems are already TypeScript-friendly.
- One type system can cover the protocol, adapters, dispatcher, and daemon.
- Zod or a similar schema library can validate runtime payloads and export public JSON Schema.
- A TypeScript daemon is easier to iterate while the runner contract is still changing.
Do not choose Rust as a default implementation language for v0.
Rust may become useful later for:
- a hardened local daemon with stronger process isolation.
- native packaging and auto-update flows.
- high-volume log streaming or patch processing.
- a security-sensitive sandbox boundary.
For the first release, Rust adds cross-language build complexity before the architecture has proven the tag-to-run loop. Keep Rust as a future local-runtime optimization, not a product prerequisite.
Use Probot for the first GitHub App because it is the fastest way to receive GitHub App webhooks and call GitHub APIs from Node.js.
Constraint:
- Probot must stay at the edge. No Probot types in
opentag-core.
Rejected alternative:
- Direct Octokit for v0. Octokit gives more control and may be better for a future custom edge gateway, but it requires more webhook and app-auth wiring before OpenTag has proven the core flow.
Mastra can be useful for workflows, agent abstractions, memory, observability, and model routing, but it should not define the OpenTag protocol.
V0 should only introduce Mastra if it materially reduces implementation time for run orchestration. Otherwise, a lightweight TypeScript orchestrator is preferable until the domain model stabilizes.
Builder.io Agent Native is useful as a reference for shared actions and app-native agent interactions. OpenTag can borrow the idea that actions should be reusable across UI, HTTP, MCP, CLI, and agents.
Do not make Agent Native a required dependency for v0.
Vercel Eve could accelerate a Slack-flavored cloud demo, especially for teams already on Vercel. It should be treated as an optional adapter path, not the core.
Claude Code, Codex, Hermes, and OpenClaw are best treated as executor targets. OpenTag can route a run to one of these executors, but OpenTag should not become a remote-control wrapper for any single agent runtime.
OpenTag needs explicit trust boundaries because mentions can look casual while execution may be powerful.
V0 policy:
- Only configured repositories can trigger runs.
- Only configured actors or teams can invoke write-capable tasks.
- Every event gets an explicit permission grant list.
- Local runner pairing is explicit.
- The runner should reject runs outside configured workspace paths.
- Dangerous operations require either local confirmation or a future approval state.
Important future policy questions:
- Should
@opentag fixbe allowed to push directly, or only open a PR? - Should a reviewer approval be required before write operations?
- Should organization admins define agent allowlists centrally?
- How should local secrets be protected from prompt-injected repo content?
Each run should preserve:
- original source event ID and URL.
- actor identity.
- normalized command.
- context pointers.
- permission grants.
- assigned runner.
- executor adapter.
- start/end timestamps.
- status transitions.
- callback messages posted.
- artifact or PR links.
The audit log is part of the product, not an implementation detail. It is how OpenTag earns trust as an open alternative to opaque chat-agent execution.
The initial pairing model can be simple:
- User installs
opentagd. - User logs in or registers the daemon with a pairing token.
- User maps a GitHub repo to a local checkout path.
- User selects a default executor adapter.
- The daemon starts polling for assigned runs.
Example config:
{
"runnerId": "runner_local_mingyoo_macbook",
"repositories": [
{
"provider": "github",
"owner": "example",
"repo": "demo",
"checkoutPath": "/Users/mingyoo/repos/demo",
"defaultExecutor": "codex"
}
]
}The final result should be structured so each surface can render it consistently:
type OpenTagRunResult = {
conclusion: "success" | "failure" | "cancelled" | "needs_human";
summary: string;
changedFiles?: string[];
createdPullRequestUrl?: string;
artifacts?: { title: string; uri: string }[];
verification?: {
command: string;
outcome: "passed" | "failed" | "not_run";
excerpt?: string;
}[];
nextAction?: string;
};OpenTag should sound concrete, open, and developer-native.
Good copy:
OpenTag is the open mention layer for agents.
Tag Claude Code, Codex, Hermes, OpenClaw, or your own local runner from GitHub or Slack.
Claude Tag brings Claude into Slack. OpenTag brings any agent into any workspace.
Your workspace stays collaborative. Your execution stays under your control.
Avoid:
- "AI teammate platform" as the primary phrase, because it sounds generic.
- "Slack bot clone", because it loses the protocol and local-runner angle.
- "Agent framework", because OpenTag should route agents, not replace them.
Deliverables:
- repository created.
- README with one-liner, comparison, and demo target.
- this design document.
- protocol draft with
OpenTagEvent,OpenTagRun, and callback types.
Success:
- a developer can understand the idea in less than one minute.
- a future contributor can see what to build first.
Deliverables:
- Probot app receives GitHub comments.
@opentagmention detection works.- GitHub event normalizes into
OpenTagEvent. - run is created in a simple store.
- acknowledgement comment is posted.
Success:
- commenting
@opentag investigate thison an allowed issue creates a run and posts an acknowledgement.
Deliverables:
opentagdcan pair with the app.- repo-to-local-path mapping exists.
- daemon polls for queued runs.
- daemon claims and starts an executor adapter.
- status updates are posted back.
Success:
- a local runner receives a GitHub-triggered run and executes a no-op or echo adapter end to end.
Deliverables:
- Claude Code, Codex, Hermes, or OpenClaw adapter.
- worktree or branch isolation.
- final summary capture.
- changed-file detection.
- GitHub final comment.
Success:
@opentag fix thiscan produce a local code change and report what happened.
Deliverables:
- branch creation.
- push or patch export path.
- PR creation.
- verification summary in PR body.
Success:
- a tagged GitHub issue can result in a linked PR.
- First real executor: Codex, with echo retained as the smoke-test executor.
- First public demo: local CLI/HTTP replay, while the Probot GitHub App path remains implemented for real installations.
- Minimum write approval model: write-capable work is constrained to an isolated branch plus pull request; direct writes to the target branch are not part of v0.
- Protocol schema: TypeScript/Zod schemas are the source of truth and
@opentag/coreexports public JSON Schema definitions forOpenTagEvent,OpenTagRun, andOpenTagRunResult. - Orchestration framework: no Mastra dependency in v0; use lightweight TypeScript primitives until workflows justify a dedicated framework.
Use Probot for GitHub ingress, a very thin hosted dispatcher for run persistence and runner leasing, TypeScript for v0 implementation, and the local daemon as the differentiating path.
This gives the project a fast demo without sacrificing the bigger thesis:
OpenTag is not a GitHub bot.
OpenTag is not an agent framework.
OpenTag is the open tag-to-run bridge between collaboration surfaces and agent executors.