From 35215c33c7e1cdd72b9b1c65ddc382b9f7e425ec Mon Sep 17 00:00:00 2001 From: Ming Yoo Date: Mon, 17 Aug 2026 15:17:43 +0800 Subject: [PATCH 1/3] docs(skill): align OpenTag guidance with 0.10.0 Cover hosted Control Plane pairing, governed completion, service operations, and safe Teams setup so the companion skill matches the released CLI. Co-authored-by: Cursor --- docs/platforms/teams.en.md | 15 ++---- packages/cli/test/docs-contract.test.ts | 34 +++++++++++- skills/opentag/SKILL.md | 30 ++++++++--- .../references/completion-governance.md | 48 +++++++++++++++++ skills/opentag/references/control-plane.md | 52 +++++++++++++++++++ skills/opentag/references/teams-setup.md | 49 +++++++++++++++++ 6 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 skills/opentag/references/completion-governance.md create mode 100644 skills/opentag/references/control-plane.md create mode 100644 skills/opentag/references/teams-setup.md diff --git a/docs/platforms/teams.en.md b/docs/platforms/teams.en.md index 03f54131..2d15168c 100644 --- a/docs/platforms/teams.en.md +++ b/docs/platforms/teams.en.md @@ -140,16 +140,11 @@ Tenant ID: # recommended for single-tenant apps Webhook path: /teams/messages # default ``` -For scripted setup, use: - -```bash -opentag setup \ - --platform teams \ - --teams-app-id \ - --teams-app-password \ - --teams-tenant-id \ - --teams-webhook-path /teams/messages -``` +Do not put the client secret in command-line arguments, including +`--teams-app-password`; shell history and process listings can expose it. For +unattended operation, provision `platforms.teams.appPassword` through an +operator-managed ignored config or secret store before starting OpenTag. Keep +scripted command-line flags limited to non-secret values. The setup command saves: diff --git a/packages/cli/test/docs-contract.test.ts b/packages/cli/test/docs-contract.test.ts index 531e11b4..65c9b224 100644 --- a/packages/cli/test/docs-contract.test.ts +++ b/packages/cli/test/docs-contract.test.ts @@ -138,6 +138,9 @@ describe("platform setup docs contract", () => { it("keeps the OpenTag skill aligned with Codex askhuman setup guidance", () => { const skill = repoFile("skills/opentag/SKILL.md"); + expect(skill).toMatch(/^---\nname: opentag\ndescription: Use when /u); + const controlPlane = repoFile("skills/opentag/references/control-plane.md"); + const completion = repoFile("skills/opentag/references/completion-governance.md"); expect(skill).toContain("request_user_input"); expect(skill).toContain("askhuman"); expect(skill).toContain("Codex Plan mode"); @@ -175,7 +178,9 @@ describe("platform setup docs contract", () => { expect(skill).toContain("npm cache metadata exists"); expect(skill).toContain("`npx --offline` or `npm pack --offline`"); expect(skill).toContain("do not claim the CLI is available offline"); - expect(skill).toContain("Platform: Slack, GitHub, GitLab, Linear, Lark / Feishu, Telegram, or Discord"); + expect(skill).toContain( + "Platform: Slack, GitHub, GitLab, Linear, Lark / Feishu, Telegram, Discord, or Microsoft Teams" + ); expect(skill).toContain("Coding agent: Codex, Claude Code, Cursor, OpenCode, Hermes, OpenClaw, or Echo"); expect(skill).toContain("Local project: the current working directory"); expect(skill).toContain("Slack Socket Mode vs Events API"); @@ -189,6 +194,33 @@ describe("platform setup docs contract", () => { expect(skill).toContain("--tenant"); expect(skill).toContain("--lark-setup"); expect(skill).toContain("--binding"); + expect(skill).toContain("references/control-plane.md"); + expect(skill).toContain("references/completion-governance.md"); + expect(skill).toContain("references/teams-setup.md"); + expect(skill).toContain("docs/platforms/teams.en.md"); + expect(skill).toContain("opentag service install"); + expect(skill).toContain("opentag service logs"); + expect(skill).toContain("opentag cancel --run "); + expect(controlPlane).toContain("opentag pair --relay "); + expect(controlPlane).toContain("Hosted Control V1"); + expect(controlPlane).toContain("bootstrap pairing token"); + expect(controlPlane).toContain("Do not use `--no-register`"); + expect(controlPlane).toContain("opentag config show"); + expect(completion).toContain("executor success is not completion"); + expect(completion).toContain("all observed checks pass on the current head"); + expect(completion).toContain("opentag status --work-thread "); + expect(completion).toContain("opentag status --attention"); + expect(completion).toContain("opentag completion escalations --run "); + expect(completion).toContain("opentag completion waive"); + expect(completion).toContain("Do not fabricate provider evidence"); + const teams = repoFile("skills/opentag/references/teams-setup.md"); + expect(teams).toContain("opentag setup --platform teams"); + expect(teams).toContain("Do not put `--teams-app-password`"); + expect(teams).toContain("no standalone Teams channel-binding CLI command"); + expect(teams).toContain("docs/platforms/teams.en.md"); + const teamsGuide = repoFile("docs/platforms/teams.en.md"); + expect(teamsGuide).not.toContain("--teams-app-password "); + expect(teamsGuide).toContain("Do not put the client secret in command-line arguments"); expect(skill).toContain( "Stop before entering any credential, token, app ID, app secret, signing secret, channel ID, repository name, or unconfirmed project path." ); diff --git a/skills/opentag/SKILL.md b/skills/opentag/SKILL.md index c8341f96..b4bf9bb8 100644 --- a/skills/opentag/SKILL.md +++ b/skills/opentag/SKILL.md @@ -1,11 +1,11 @@ --- name: opentag -description: Set up, run, and troubleshoot OpenTag with the published CLI across Slack, GitHub, GitLab, Linear, Lark / Feishu, Codex, Claude Code, OpenClaw, local config, platform credentials, and unified delivery. +description: Use when installing, pairing, operating, or troubleshooting OpenTag through the published CLI, self-hosted Control Plane, governed completion, supported collaboration platforms, or built-in coding agents. --- # OpenTag -OpenTag connects collaboration platforms to a local coding agent. Use this skill when a user wants help with `opentag setup`, `opentag start`, Slack, GitHub, GitLab, Linear, Lark / Feishu, Codex, Claude Code, OpenClaw, local OpenTag config, or end-to-end setup verification. +OpenTag connects collaboration platforms to local coding agents. Use this skill for published CLI setup, background service operation, hosted Control Plane pairing, governed completion, platform configuration, and end-to-end verification. ## Default Path @@ -14,7 +14,7 @@ Use the published CLI first. Do not start from repo-internal apps, old shell scr Recommended user path: ```bash -npm install -g @opentag/cli +npm install -g @opentag/cli@latest opentag setup opentag start ``` @@ -33,7 +33,10 @@ Read only the reference needed for the user's path: - First setup or Echo test loop: `references/local-echo.md` - Slack setup: `references/slack-setup.md` - GitHub setup: `references/github-setup.md` +- Microsoft Teams setup: `references/teams-setup.md` - ACP coding-agent execution: `references/codex-runner.md` +- Self-hosted Control Plane or trusted remote relay: `references/control-plane.md` +- Runs waiting on governed completion: `references/completion-governance.md` - Broken setup, missing provider delivery, rejected runs, or auth errors: `references/troubleshooting.md` For platform credential steps, use the repository docs as the source of truth: @@ -43,11 +46,15 @@ For platform credential steps, use the repository docs as the source of truth: - GitLab: `docs/platforms/gitlab.en.md` - Linear: `docs/platforms/linear.en.md` - Lark / Feishu: `docs/platforms/lark.en.md` +- Telegram: `docs/platforms/telegram.en.md` +- Discord: `docs/platforms/discord.en.md` +- Microsoft Teams: `docs/platforms/teams.en.md` ## Working Rules - Keep setup user-led. Never invent tokens, app IDs, Slack team/channel IDs, GitHub owner/repo names, or local project paths. -- Prefer Slack, then GitHub, then GitLab, then Linear, then Lark / Feishu when listing platforms. +- Use `@latest` by default. If the user explicitly requests a release, pin that version consistently in both install and `npx` commands and verify it with `opentag --version`. +- Prefer Slack, then GitHub, then GitLab, then Linear, then Lark / Feishu when listing established paths; label Telegram, Discord, and Microsoft Teams as previews. - Ask the user which platform and coding agent they want if it is not already clear outside Codex. - In Codex Plan mode, use `request_user_input` / askhuman to collect non-secret setup choices before running `opentag setup`, then pass those choices as CLI flags so the terminal wizard does not silently choose defaults. - Codex Default mode cannot render askhuman choice cards. If setup choices are needed and the current host does not expose a runtime transition into Plan mode, stop and explain that askhuman cannot render from Default mode in this run. Do not claim a Plan-mode handoff happened, do not ask the user to switch modes, do not ask the same choices in plain text, do not continue with CLI defaults, and do not run `opentag setup` until the choices are explicitly collected. @@ -55,7 +62,9 @@ For platform credential steps, use the repository docs as the source of truth: - Prefer Codex or Claude Code when the corresponding local login is ready, Hermes when its ACP profile and provider are ready, OpenClaw when its Gateway is ready, and Echo only for dev/test verification. - Do not ask setup users to invoke an agent directly. OpenTag provides built-in Generic ACP launches for Codex, Claude Code, Cursor, OpenCode, Hermes, and OpenClaw; diagnose them with `opentag doctor` and the built-in ACP conformance gate. - OpenClaw currently reports `cancel=no`. A cancellation request stops OpenTag's local bridge, but Gateway-owned tool subprocess termination is not guaranteed; inspect provider-owned processes before starting conflicting follow-up work. -- Treat `opentag start` as a foreground process. Tell the user to keep it running and stop it with Ctrl-C. +- Treat `opentag start` as a foreground process. Tell the user to keep it running and stop it with Ctrl-C. For a global install on a supported host, use `opentag service install` and the service lifecycle commands instead. +- Treat a relay as a remote control plane for the local runner. Pair only with a user-operated or explicitly trusted HTTPS origin, and never request a bootstrap pairing token through chat. +- Executor success is not governed completion. Inspect provider evidence and the WorkThread before acknowledging, resolving, or waiving a gate; never invent actor identity, reasons, or provider facts. - Do not expose secrets in responses. Use `opentag config show` for redacted config. - When credentials are needed, point the user to the matching platform guide and walk them through the official setup. @@ -94,7 +103,7 @@ Only use a proxy URL the user provides or that is already active in the environm When helping a Codex user install or configure OpenTag, collect these non-secret choices with `request_user_input` / askhuman only when the current Codex host is actually in Plan mode and the tool is available: -- Platform: Slack, GitHub, GitLab, Linear, Lark / Feishu, Telegram, or Discord. +- Platform: Slack, GitHub, GitLab, Linear, Lark / Feishu, Telegram, Discord, or Microsoft Teams. - Coding agent: Codex, Claude Code, Cursor, OpenCode, Hermes, OpenClaw, or Echo, using local detection from `opentag executors` when available. - Local project: the current working directory as the recommended option, plus a free-form path option inside askhuman for another path. - Platform mode choices that are not credentials, such as Slack Socket Mode vs Events API, Lark / Feishu tenant for manual app setup, Lark scan vs manual setup, and default project binding vs bind later. @@ -116,7 +125,7 @@ After the user chooses, run `opentag setup` with matching flags, for example `-- Completion: `opentag setup` has collected platform, executor, project path, and credentials. 4. Start OpenTag. - Completion: `opentag start` reports the dispatcher and selected platform listener. + Completion: `opentag start` reports the dispatcher and selected platform listener, or `opentag service status` reports a healthy installed service. 5. Verify the setup. Completion: `opentag status` or `opentag doctor` explains the current state, and one platform mention creates a visible response or a specific actionable error. @@ -142,8 +151,15 @@ Default state and isolated worktrees: ```bash opentag setup +opentag pair --relay opentag start +opentag service install +opentag service status +opentag service logs opentag status +opentag status --attention +opentag cancel --run +opentag completion escalations --run opentag doctor opentag platforms opentag executors diff --git a/skills/opentag/references/completion-governance.md b/skills/opentag/references/completion-governance.md new file mode 100644 index 00000000..0989f611 --- /dev/null +++ b/skills/opentag/references/completion-governance.md @@ -0,0 +1,48 @@ +# Governed Completion + +Use this path when an executor has stopped but the run remains open, a +WorkThread needs attention, or a human escalation or waiver must be handled. + +## Interpret The State + +For governed runs, executor success is not completion. A GitHub-backed run that +ships a pull request remains open until provider webhook evidence confirms that +the pull request exists and all observed checks pass on the current head. +Explicit repository completion policies still take precedence. Runs that ship +no pull request retain executor-success semantics. + +Inspect before acting: + +```bash +opentag status --run +opentag status --work-thread +opentag status --attention +opentag completion escalations --run +``` + +Distinguish pending provider evidence from failed checks, a human escalation, +an expired obligation, and a delivery problem. An executor-reported pull +request URL can identify the target, but it is not provider proof. + +## Human Actions + +Use the CLI help for the required actor and reason fields: + +```bash +opentag completion acknowledge --help +opentag completion resolve --help +opentag completion waive --help +``` + +- Acknowledge records that an attributed human saw an escalation; it does not + resolve the blocking decision. +- Resolve records one bounded, attributed decision. Resume work through a new + source-thread task rather than pretending the old executor is still running. +- Waive only selected current gates under explicit human authority, with the + real actor, policy scope, reason, and optional expiry. Do not fabricate provider evidence. + Do not use a waiver to rewrite observed GitHub state. + +If checks appear stale, verify GitHub webhook delivery, repository binding, the +current pull-request head, and the observed check rollup before changing any +completion policy. Change `defaultGitHubCompletion` to `compat` only when the +user explicitly chooses legacy executor-success behavior. diff --git a/skills/opentag/references/control-plane.md b/skills/opentag/references/control-plane.md new file mode 100644 index 00000000..c7861f70 --- /dev/null +++ b/skills/opentag/references/control-plane.md @@ -0,0 +1,52 @@ +# Self-Hosted Control Plane Pairing + +Use this path when a user wants a local runner to claim work from a self-hosted +Control Plane or another explicitly trusted relay. + +## Pairing Path + +For a new configuration, use setup so platform, executor, project target, and +relay choices are collected together: + +```bash +opentag setup --relay https:// +``` + +For an existing local configuration: + +```bash +opentag pair --relay +``` + +The CLI checks relay health and capabilities before Hosted Control V1 +registration. Successful registration binds the runner and Project Targets, +stores the issued runner credential atomically, and removes the bootstrap +pairing token from local config. Do not use `--no-register` with Hosted Control +V1; registration and recovery reject that option. + +## Trust And Secrets + +- Pair only with an HTTPS origin the user operates or explicitly trusts. The + relay is a remote control plane for this local runner. +- Have the user enter the bootstrap pairing token through the local setup or + config workflow. Never ask for it in chat, command output, screenshots, or + committed files. +- Do not reuse bootstrap, recovery, fencing, login-throttle, or provider + secrets. Follow `docs/control-plane-deployment.md` for deployment authority + and rotation boundaries. +- Treat the Control Plane console as a bounded operational surface for runners, + targets, hosted runs, permissions, and audit—not as a general chat cockpit. + +## Verify + +```bash +opentag config show +opentag status +opentag doctor +``` + +Confirm the redacted config reports relay mode and a paired Hosted Control V1 +registration, the expected runner and Project Targets are present, and no +Control Plane alert is failing. If pairing reports recovery-required state, +stop and use the deployment's separately held recovery process; never invent or +reuse a credential. diff --git a/skills/opentag/references/teams-setup.md b/skills/opentag/references/teams-setup.md new file mode 100644 index 00000000..fbb4ebeb --- /dev/null +++ b/skills/opentag/references/teams-setup.md @@ -0,0 +1,49 @@ +# Microsoft Teams Setup + +Use this path for the Microsoft Teams preview. Teams ingress currently runs +only in the local runtime; hosted/custom relay mode does not mount the webhook. +Use `docs/platforms/teams.en.md` for the Azure Bot, Teams app package, tunnel, +Messaging endpoint, and permission checklist. + +## Safe Setup + +Use the interactive prompt so the Azure Bot client secret does not enter shell +history: + +```bash +opentag setup --platform teams +``` + +Do not put `--teams-app-password` and its value in a command, chat message, +issue, screenshot, or committed file. Have the user enter it locally when +prompted. + +Configure the Azure Bot Messaging endpoint as public HTTPS ending in +`/teams/messages`, install the Teams app in the target team/channel, then keep +`opentag start` running or use the background service. + +## Channel Binding + +Capture the tenant ID and base channel conversation ID from an authenticated +Teams activity. Prefer `channelData.channel.id` or +`channelData.teamsChannelId`; do not bind a thread-specific +`;messageid=` value. + +There is currently no standalone Teams channel-binding CLI command. Use the +deployment's operator-controlled local config or dispatcher API to create the +binding, following `docs/platforms/teams.en.md`. Do not invent an endpoint or +config shape. A repository target is optional for general ACP work and required +for repository-backed coding or `apply 1`. + +## Verify + +```bash +opentag service status +opentag doctor +opentag status +``` + +Send a read-only `@OpenTag investigate ...` mention first. Confirm Teams posts +to `/teams/messages`, OpenTag accepts the activity, a run starts in the intended +checkout or scratch workspace, and the reply returns to the same channel +thread. From 89b61fd4b2bbaaf512ce096e2ab1b7ce9aaab348 Mon Sep 17 00:00:00 2001 From: Ming Yoo Date: Mon, 17 Aug 2026 15:59:19 +0800 Subject: [PATCH 2/3] fix(completion): fail closed on incomplete GitHub checks Carry provider rollup completeness into governance and tighten skill guidance for pinned CLI installs, relay trust, and Teams bindings. Co-authored-by: Cursor --- packages/cli/test/docs-contract.test.ts | 23 ++++- .../dispatcher/src/completion-governance.ts | 11 ++- packages/dispatcher/src/server.ts | 1 + .../test/completion-governance.test.ts | 47 ++++++++++ packages/github/src/completion-evidence.ts | 67 ++++++++++----- .../github/test/completion-evidence.test.ts | 85 +++++++++++++++---- packages/github/test/ingress.test.ts | 10 ++- skills/opentag/SKILL.md | 14 +-- skills/opentag/references/codex-runner.md | 2 +- .../references/completion-governance.md | 7 +- skills/opentag/references/control-plane.md | 15 ++-- skills/opentag/references/github-setup.md | 2 +- skills/opentag/references/local-echo.md | 2 +- skills/opentag/references/slack-setup.md | 2 +- skills/opentag/references/teams-setup.md | 6 +- 15 files changed, 226 insertions(+), 68 deletions(-) diff --git a/packages/cli/test/docs-contract.test.ts b/packages/cli/test/docs-contract.test.ts index 65c9b224..40467cd6 100644 --- a/packages/cli/test/docs-contract.test.ts +++ b/packages/cli/test/docs-contract.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from "node:fs"; +import { globSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; @@ -137,10 +137,18 @@ describe("platform setup docs contract", () => { it("keeps the OpenTag skill aligned with Codex askhuman setup guidance", () => { const skill = repoFile("skills/opentag/SKILL.md"); + const skillDocs = globSync("skills/opentag/**/*.md") + .map((path) => repoFile(path)) + .join("\n"); expect(skill).toMatch(/^---\nname: opentag\ndescription: Use when /u); const controlPlane = repoFile("skills/opentag/references/control-plane.md"); const completion = repoFile("skills/opentag/references/completion-governance.md"); + expect(skillDocs).not.toContain("@opentag/cli@latest"); + expect(skillDocs).not.toMatch(/\bnpx(?: --yes)? @opentag\/cli(?:\s|$)/u); + expect(skillDocs).not.toMatch(/\bnpm install -g @opentag\/cli(?:\s|$)/u); + expect(skill).toContain("npm install -g @opentag/cli@0.10.0"); + expect(skill).toContain("npx @opentag/cli@0.10.0 setup"); expect(skill).toContain("request_user_input"); expect(skill).toContain("askhuman"); expect(skill).toContain("Codex Plan mode"); @@ -172,7 +180,7 @@ describe("platform setup docs contract", () => { 'HTTPS_PROXY="" HTTP_PROXY="" npm view @opentag/cli version --fetch-timeout=15000' ); expect(skill).toContain("Only after npm registry metadata is reachable"); - expect(skill).toContain("npx --yes @opentag/cli --help"); + expect(skill).toContain("npx --yes @opentag/cli@0.10.0 --help"); expect(skill).toContain("do not permanently change `npm config` without explicit user confirmation"); expect(skill).toContain("Only use a proxy URL the user provides or that is already active in the environment"); expect(skill).toContain("npm cache metadata exists"); @@ -205,9 +213,14 @@ describe("platform setup docs contract", () => { expect(controlPlane).toContain("Hosted Control V1"); expect(controlPlane).toContain("bootstrap pairing token"); expect(controlPlane).toContain("Do not use `--no-register`"); + expect(controlPlane).toContain("without calling `/healthz`"); + expect(controlPlane).toContain("empty capabilities list"); + expect(controlPlane).toContain("does not bind Project Targets"); + expect(controlPlane).toContain("run metadata, command text, and progress"); + expect(controlPlane).toContain("controls which queued runs the local runner claims"); expect(controlPlane).toContain("opentag config show"); expect(completion).toContain("executor success is not completion"); - expect(completion).toContain("all observed checks pass on the current head"); + expect(completion).toContain("complete current-head check rollup"); expect(completion).toContain("opentag status --work-thread "); expect(completion).toContain("opentag status --attention"); expect(completion).toContain("opentag completion escalations --run "); @@ -216,10 +229,12 @@ describe("platform setup docs contract", () => { const teams = repoFile("skills/opentag/references/teams-setup.md"); expect(teams).toContain("opentag setup --platform teams"); expect(teams).toContain("Do not put `--teams-app-password`"); + expect(teams).toContain("`activity.conversation.id`"); + expect(teams).toContain("removing only a trailing `;messageid=` suffix"); expect(teams).toContain("no standalone Teams channel-binding CLI command"); expect(teams).toContain("docs/platforms/teams.en.md"); const teamsGuide = repoFile("docs/platforms/teams.en.md"); - expect(teamsGuide).not.toContain("--teams-app-password "); + expect(teamsGuide).not.toMatch(/--teams-app-password(?:\s+|=)/u); expect(teamsGuide).toContain("Do not put the client secret in command-line arguments"); expect(skill).toContain( "Stop before entering any credential, token, app ID, app secret, signing secret, channel ID, repository name, or unconfirmed project path." diff --git a/packages/dispatcher/src/completion-governance.ts b/packages/dispatcher/src/completion-governance.ts index 259282ac..8cf69228 100644 --- a/packages/dispatcher/src/completion-governance.ts +++ b/packages/dispatcher/src/completion-governance.ts @@ -269,16 +269,21 @@ function githubCompletionSemanticDigest(snapshot: GitHubVerifiedPullRequestSnaps provider: snapshot.provider, repository: snapshot.repository, pullRequest: snapshot.pullRequest, - checks: snapshot.checks + checks: snapshot.checks, + checksComplete: snapshot.checksComplete }; return `sha256:${createHash("sha256") .update(JSON.stringify(canonicalizeGitHubCompletionValue(semanticSnapshot))) .digest("hex")}`; } -function observedChecksRollupOutcome(checks: Record): "passed" | "failed" | "pending" { +function observedChecksRollupOutcome( + checks: Record, + checksComplete: boolean +): "passed" | "failed" | "pending" { const states = Object.values(checks); if (states.some((state) => state === "failed")) return "failed"; + if (!checksComplete || states.length === 0) return "pending"; if (states.some((state) => state === "pending")) return "pending"; return "passed"; } @@ -342,7 +347,7 @@ function githubFactTemplates(input: { kind: "source_control.observed_checks_rollup", claim: { predicate: "checks_rollup", - outcome: observedChecksRollupOutcome(input.snapshot.checks), + outcome: observedChecksRollupOutcome(input.snapshot.checks, input.snapshot.checksComplete), observations: input.snapshot.checks }, provenance: provenance("source_control.observed_checks_rollup") diff --git a/packages/dispatcher/src/server.ts b/packages/dispatcher/src/server.ts index e6af40d1..135f2196 100644 --- a/packages/dispatcher/src/server.ts +++ b/packages/dispatcher/src/server.ts @@ -719,6 +719,7 @@ const GitHubCompletionEvidenceSchema = z.object({ state: z.enum(["open", "closed", "merged"]) }).strict(), checks: z.record(z.string().min(1), z.enum(["passed", "failed", "pending"])), + checksComplete: z.boolean().default(false), observedAt: z.string().datetime(), payloadDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/u) }).strict(); diff --git a/packages/dispatcher/test/completion-governance.test.ts b/packages/dispatcher/test/completion-governance.test.ts index 647f8203..fbb6e38b 100644 --- a/packages/dispatcher/test/completion-governance.test.ts +++ b/packages/dispatcher/test/completion-governance.test.ts @@ -31,6 +31,7 @@ function githubSnapshot(input: { headSha?: string; state?: "open" | "closed" | "merged"; checks?: Record; + checksComplete?: boolean; observedAt?: string; resourceRef?: string; }) { @@ -48,6 +49,7 @@ function githubSnapshot(input: { state: input.state ?? "merged" }, checks: input.checks ?? { build: "passed", test: "passed" }, + checksComplete: input.checksComplete ?? true, observedAt: input.observedAt ?? "2026-07-21T10:05:00.000Z", payloadDigest: `sha256:${(input.deliveryId === "delivery-old" ? "d" : "e").repeat(64)}` }; @@ -706,6 +708,51 @@ describe("dispatcher completion governance", () => { }); }); + it("keeps the zero-config observed-checks gate unsatisfied when no checks were observed", async () => { + const setup = await startRun({ runId: "run_default_empty_checks" }); + await completeRun({ setup, runId: "run_default_empty_checks", conclusion: "success" }); + + const evidence = await setup.app.request( + "/v1/completion-evidence/github", + jsonRequest(githubSnapshot({ + deliveryId: "delivery-default-empty", + state: "open", + checks: {} + })) + ); + + expect(evidence.status).toBe(201); + await expect(evidence.json()).resolves.toMatchObject({ + completion: { + completion: "unsatisfied", + failedGateIds: ["observed_checks"] + } + }); + }); + + it("keeps the zero-config observed-checks gate unsatisfied for an incomplete all-passed rollup", async () => { + const setup = await startRun({ runId: "run_default_incomplete_checks" }); + await completeRun({ setup, runId: "run_default_incomplete_checks", conclusion: "success" }); + + const evidence = await setup.app.request( + "/v1/completion-evidence/github", + jsonRequest(githubSnapshot({ + deliveryId: "delivery-default-incomplete", + state: "open", + checks: { build: "passed" }, + checksComplete: false + })) + ); + + expect(evidence.status).toBe(201); + await expect(evidence.json()).resolves.toMatchObject({ + completion: { + completion: "unsatisfied", + failedGateIds: ["observed_checks"] + } + }); + }); + it("upgrades a compatibility thread to the default verified contract once a run ships a pull request", async () => { const setup = await startRun({ runId: "run_upgrade_1" }); const first = await completeRun({ setup, runId: "run_upgrade_1", conclusion: "success", omitPullRequest: true }); diff --git a/packages/github/src/completion-evidence.ts b/packages/github/src/completion-evidence.ts index 19df076c..8c65e64b 100644 --- a/packages/github/src/completion-evidence.ts +++ b/packages/github/src/completion-evidence.ts @@ -16,6 +16,7 @@ export type GitHubVerifiedPullRequestSnapshot = { state: "open" | "closed" | "merged"; }; checks: Record; + checksComplete: boolean; observedAt: string; payloadDigest: string; }; @@ -28,17 +29,23 @@ export type GitHubCompletionApi = { head: { sha: string }; base: { ref: string; sha: string; repo?: { full_name?: string } | null }; }>; - listCheckRunsForRef(input: { owner: string; repo: string; ref: string }): Promise>; - getCombinedStatusForRef(input: { owner: string; repo: string; ref: string }): Promise>; + listCheckRunsForRef(input: { owner: string; repo: string; ref: string }): Promise<{ + totalCount: number; + checkRuns: Array<{ + name: string; + status: string; + conclusion: string | null; + head_sha: string; + }>; + }>; + getCombinedStatusForRef(input: { owner: string; repo: string; ref: string }): Promise<{ + totalCount: number; + statuses: Array<{ + context: string; + state: string; + sha: string; + }>; + }>; listPullRequestsForCommit(input: { owner: string; repo: string; ref: string }): Promise>; }; @@ -54,6 +61,10 @@ function positiveInteger(value: unknown): number | null { return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null; } +function nonNegativeInteger(value: unknown): number | null { + return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null; +} + function repositoryFromPayload(payload: unknown): { owner: string; repo: string } | null { if (!isRecord(payload) || !isRecord(payload["repository"])) return null; const repository = payload["repository"]; @@ -139,8 +150,8 @@ function commitStatusState(state: string): GitHubCheckState { function normalizedChecks(input: { headSha: string; - checkRuns: Awaited>; - statuses: Awaited>; + checkRuns: Awaited>["checkRuns"]; + statuses: Awaited>["statuses"]; }): Record { const checks = new Map(); for (const run of input.checkRuns) { @@ -183,11 +194,20 @@ export async function reconcileGitHubCompletionEvidence(input: { if (pullRequest.base.repo?.full_name && pullRequest.base.repo.full_name.toLowerCase() !== expectedRepository) { throw new Error("GitHub pull request reconciliation returned a mismatched target repository."); } - const [checkRuns, statuses] = await Promise.all([ + const [checkRunPage, statusPage] = await Promise.all([ input.api.listCheckRunsForRef({ ...correlation.repository, ref: pullRequest.head.sha }), input.api.getCombinedStatusForRef({ ...correlation.repository, ref: pullRequest.head.sha }) ]); - const checks = normalizedChecks({ headSha: pullRequest.head.sha, checkRuns, statuses }); + const checks = normalizedChecks({ + headSha: pullRequest.head.sha, + checkRuns: checkRunPage.checkRuns, + statuses: statusPage.statuses + }); + const checksComplete = + checkRunPage.checkRuns.length === checkRunPage.totalCount + && statusPage.statuses.length === statusPage.totalCount + && checkRunPage.checkRuns.every((run) => run.head_sha === pullRequest.head.sha) + && statusPage.statuses.every((status) => status.sha === pullRequest.head.sha); const state: GitHubVerifiedPullRequestSnapshot["pullRequest"]["state"] = pullRequest.merged ? "merged" : pullRequest.state === "closed" @@ -207,13 +227,15 @@ export async function reconcileGitHubCompletionEvidence(input: { state }, checks, + checksComplete, observedAt: input.now() }; const semanticSnapshot = { provider: snapshotWithoutDigest.provider, repository: snapshotWithoutDigest.repository, pullRequest: snapshotWithoutDigest.pullRequest, - checks: snapshotWithoutDigest.checks + checks: snapshotWithoutDigest.checks, + checksComplete: snapshotWithoutDigest.checksComplete }; snapshots.push({ ...snapshotWithoutDigest, payloadDigest: digest(semanticSnapshot) }); } @@ -264,8 +286,10 @@ export function createGitHubCompletionApi(input: { }, async listCheckRunsForRef({ owner, repo, ref }) { const value = await request(`/repos/${segment(owner)}/${segment(repo)}/commits/${segment(ref)}/check-runs?filter=latest&per_page=100`); - if (!isRecord(value) || !Array.isArray(value["check_runs"])) throw new Error("GitHub check-run reconciliation returned an invalid response."); - return value["check_runs"].map((candidate) => { + if (!isRecord(value)) throw new Error("GitHub check-run reconciliation returned an invalid response."); + const totalCount = nonNegativeInteger(value["total_count"]); + if (totalCount === null || !Array.isArray(value["check_runs"])) throw new Error("GitHub check-run reconciliation returned an invalid response."); + const checkRuns = value["check_runs"].map((candidate) => { if (!isRecord(candidate) || !nonEmptyString(candidate["name"]) || !nonEmptyString(candidate["status"]) || !nonEmptyString(candidate["head_sha"]) || (candidate["conclusion"] !== null && typeof candidate["conclusion"] !== "string")) { @@ -278,14 +302,16 @@ export function createGitHubCompletionApi(input: { head_sha: candidate["head_sha"] as string }; }); + return { totalCount, checkRuns }; }, async getCombinedStatusForRef({ owner, repo, ref }) { const value = await request(`/repos/${segment(owner)}/${segment(repo)}/commits/${segment(ref)}/status?per_page=100`); if (!isRecord(value)) throw new Error("GitHub commit-status reconciliation returned an invalid response."); const sha = nonEmptyString(value["sha"]); + const totalCount = nonNegativeInteger(value["total_count"]); const statuses = value["statuses"]; - if (!sha || !Array.isArray(statuses)) throw new Error("GitHub commit-status reconciliation returned an invalid response."); - return statuses.map((candidate) => { + if (!sha || totalCount === null || !Array.isArray(statuses)) throw new Error("GitHub commit-status reconciliation returned an invalid response."); + const normalizedStatuses = statuses.map((candidate) => { if (!isRecord(candidate) || !nonEmptyString(candidate["context"]) || !nonEmptyString(candidate["state"])) { throw new Error("GitHub commit-status reconciliation returned an invalid response."); @@ -296,6 +322,7 @@ export function createGitHubCompletionApi(input: { sha }; }); + return { totalCount, statuses: normalizedStatuses }; }, async listPullRequestsForCommit({ owner, repo, ref }) { const value = await request(`/repos/${segment(owner)}/${segment(repo)}/commits/${segment(ref)}/pulls?per_page=100`); diff --git a/packages/github/test/completion-evidence.test.ts b/packages/github/test/completion-evidence.test.ts index 8e092d4b..a9663302 100644 --- a/packages/github/test/completion-evidence.test.ts +++ b/packages/github/test/completion-evidence.test.ts @@ -22,13 +22,19 @@ function completionApi(overrides: Partial = {}): GitHubComp }; }, async listCheckRunsForRef() { - return [ - { name: "build", status: "completed", conclusion: "success", head_sha: HEAD_CURRENT }, - { name: "test", status: "completed", conclusion: "failure", head_sha: HEAD_OLD } - ]; + return { + totalCount: 2, + checkRuns: [ + { name: "build", status: "completed", conclusion: "success", head_sha: HEAD_CURRENT }, + { name: "test", status: "completed", conclusion: "failure", head_sha: HEAD_OLD } + ] + }; }, async getCombinedStatusForRef() { - return [{ context: "test", state: "success", sha: HEAD_CURRENT }]; + return { + totalCount: 1, + statuses: [{ context: "test", state: "success", sha: HEAD_CURRENT }] + }; }, async listPullRequestsForCommit() { return [{ number: 7 }]; @@ -149,12 +155,15 @@ describe("GitHub completion evidence", () => { api: completionApi({ async listCheckRunsForRef() { await barrier; - return [{ - name: "build", - status: "completed", - conclusion: checks === "passed" ? "success" : "failure", - head_sha: HEAD_CURRENT - }]; + return { + totalCount: 1, + checkRuns: [{ + name: "build", + status: "completed", + conclusion: checks === "passed" ? "success" : "failure", + head_sha: HEAD_CURRENT + }] + }; } }), now @@ -186,6 +195,7 @@ describe("GitHub completion evidence", () => { it("uses the combined-status response SHA for status entries", async () => { const fetchImpl = vi.fn(async () => Response.json({ sha: HEAD_CURRENT, + total_count: 2, statuses: [ { context: "build", state: "success" }, { context: "test", state: "pending" } @@ -193,9 +203,54 @@ describe("GitHub completion evidence", () => { })); const api = createGitHubCompletionApi({ token: "github_token", fetchImpl }); - await expect(api.getCombinedStatusForRef({ owner: "acme", repo: "demo", ref: HEAD_CURRENT })).resolves.toEqual([ - { context: "build", state: "success", sha: HEAD_CURRENT }, - { context: "test", state: "pending", sha: HEAD_CURRENT } - ]); + await expect(api.getCombinedStatusForRef({ owner: "acme", repo: "demo", ref: HEAD_CURRENT })).resolves.toEqual({ + totalCount: 2, + statuses: [ + { context: "build", state: "success", sha: HEAD_CURRENT }, + { context: "test", state: "pending", sha: HEAD_CURRENT } + ] + }); + }); + + it("marks the check rollup incomplete when a GitHub result page is truncated", async () => { + const fetchImpl = vi.fn(async (url: string | URL | Request) => { + const path = String(url); + if (path.includes("/pulls/7")) { + return Response.json({ + number: 7, + state: "open", + merged: false, + head: { sha: HEAD_CURRENT }, + base: { ref: "main", sha: BASE_SHA, repo: { full_name: "acme/demo" } } + }); + } + if (path.includes("/check-runs")) { + return Response.json({ + total_count: 2, + check_runs: [ + { name: "build", status: "completed", conclusion: "success", head_sha: HEAD_CURRENT } + ] + }); + } + if (path.includes("/status?")) { + return Response.json({ sha: HEAD_CURRENT, total_count: 0, statuses: [] }); + } + throw new Error(`Unexpected GitHub API request: ${path}`); + }); + const api = createGitHubCompletionApi({ token: "github_token", fetchImpl }); + + const [snapshot] = await reconcileGitHubCompletionEvidence({ + eventName: "pull_request", + deliveryId: "delivery-truncated-checks", + payload: { + number: 7, + repository: { name: "demo", owner: { login: "acme" } } + }, + api, + now: () => "2026-07-21T10:00:00.000Z" + }); + + expect(snapshot).toHaveProperty("checksComplete", false); + expect(snapshot?.checks).toEqual({ build: "passed" }); }); }); diff --git a/packages/github/test/ingress.test.ts b/packages/github/test/ingress.test.ts index 191a558b..265b3233 100644 --- a/packages/github/test/ingress.test.ts +++ b/packages/github/test/ingress.test.ts @@ -18,10 +18,16 @@ function completionApi(): GitHubCompletionApi { }; }, async listCheckRunsForRef() { - return [{ name: "build", status: "completed", conclusion: "success", head_sha: COMPLETION_HEAD }]; + return { + totalCount: 1, + checkRuns: [{ name: "build", status: "completed", conclusion: "success", head_sha: COMPLETION_HEAD }] + }; }, async getCombinedStatusForRef() { - return [{ context: "test", state: "success", sha: COMPLETION_HEAD }]; + return { + totalCount: 1, + statuses: [{ context: "test", state: "success", sha: COMPLETION_HEAD }] + }; }, async listPullRequestsForCommit() { return [{ number: 7 }]; diff --git a/skills/opentag/SKILL.md b/skills/opentag/SKILL.md index b4bf9bb8..54f066b7 100644 --- a/skills/opentag/SKILL.md +++ b/skills/opentag/SKILL.md @@ -14,7 +14,7 @@ Use the published CLI first. Do not start from repo-internal apps, old shell scr Recommended user path: ```bash -npm install -g @opentag/cli@latest +npm install -g @opentag/cli@0.10.0 opentag setup opentag start ``` @@ -22,8 +22,8 @@ opentag start No global install: ```bash -npx @opentag/cli setup -npx @opentag/cli start +npx @opentag/cli@0.10.0 setup +npx @opentag/cli@0.10.0 start ``` ## Route The Request @@ -53,7 +53,7 @@ For platform credential steps, use the repository docs as the source of truth: ## Working Rules - Keep setup user-led. Never invent tokens, app IDs, Slack team/channel IDs, GitHub owner/repo names, or local project paths. -- Use `@latest` by default. If the user explicitly requests a release, pin that version consistently in both install and `npx` commands and verify it with `opentag --version`. +- Use the reviewed CLI version `0.10.0` consistently in install and `npx` commands, and verify it with `opentag --version`. Update the pin only after reviewing a newer release. - Prefer Slack, then GitHub, then GitLab, then Linear, then Lark / Feishu when listing established paths; label Telegram, Discord, and Microsoft Teams as previews. - Ask the user which platform and coding agent they want if it is not already clear outside Codex. - In Codex Plan mode, use `request_user_input` / askhuman to collect non-secret setup choices before running `opentag setup`, then pass those choices as CLI flags so the terminal wizard does not silently choose defaults. @@ -70,7 +70,7 @@ For platform credential steps, use the repository docs as the source of truth: ## Npm Registry And Network Failures -If `npm install -g @opentag/cli` or `npx @opentag/cli ...` fails before the OpenTag CLI starts, keep the exact npm error and diagnose the package delivery path before giving up. Treat errors such as `ENOTFOUND`, `EAI_AGAIN`, `ETIMEDOUT`, `ECONNRESET`, `fetch failed`, proxy connection failures, and TLS certificate errors as network or npm-environment issues, not as OpenTag setup failures. +If `npm install -g @opentag/cli@0.10.0` or `npx @opentag/cli@0.10.0 ...` fails before the OpenTag CLI starts, keep the exact npm error and diagnose the package delivery path before giving up. Treat errors such as `ENOTFOUND`, `EAI_AGAIN`, `ETIMEDOUT`, `ECONNRESET`, `fetch failed`, proxy connection failures, and TLS certificate errors as network or npm-environment issues, not as OpenTag setup failures. Use safe, non-secret checks first: @@ -94,7 +94,7 @@ HTTPS_PROXY="" HTTP_PROXY="" npm view @opentag/cli version Only after npm registry metadata is reachable, retry the CLI help command: ```bash -npx --yes @opentag/cli --help +npx --yes @opentag/cli@0.10.0 --help ``` Only use a proxy URL the user provides or that is already active in the environment. Do not invent proxy hosts, tokens, certificates, or registry credentials. If npm cache metadata exists but `npx --offline` or `npm pack --offline` still fails, do not claim the CLI is available offline; report that the cache is not executable and wait for registry access to recover. @@ -119,7 +119,7 @@ After the user chooses, run `opentag setup` with matching flags, for example `-- 2. Install or run the CLI. If npm cannot reach the published package, follow "Npm Registry And Network Failures" before treating setup as blocked. - Completion: `opentag --help` or `npx @opentag/cli --help` works. + Completion: `opentag --help` or `npx @opentag/cli@0.10.0 --help` works. 3. Run setup. Completion: `opentag setup` has collected platform, executor, project path, and credentials. diff --git a/skills/opentag/references/codex-runner.md b/skills/opentag/references/codex-runner.md index 2099a2e1..e8f2219a 100644 --- a/skills/opentag/references/codex-runner.md +++ b/skills/opentag/references/codex-runner.md @@ -33,7 +33,7 @@ The user also needs a local project checkout that the chosen executor can safely ## User Path ```bash -npm install -g @opentag/cli +npm install -g @opentag/cli@0.10.0 opentag setup ``` diff --git a/skills/opentag/references/completion-governance.md b/skills/opentag/references/completion-governance.md index 0989f611..531542b5 100644 --- a/skills/opentag/references/completion-governance.md +++ b/skills/opentag/references/completion-governance.md @@ -7,9 +7,10 @@ WorkThread needs attention, or a human escalation or waiver must be handled. For governed runs, executor success is not completion. A GitHub-backed run that ships a pull request remains open until provider webhook evidence confirms that -the pull request exists and all observed checks pass on the current head. -Explicit repository completion policies still take precedence. Runs that ship -no pull request retain executor-success semantics. +the pull request exists, the complete current-head check rollup was fetched, and +every observed check passes. Empty or truncated check results remain pending. +Explicit repository completion policies still take precedence. Runs that ship no +pull request retain executor-success semantics. Inspect before acting: diff --git a/skills/opentag/references/control-plane.md b/skills/opentag/references/control-plane.md index c7861f70..a6e075cb 100644 --- a/skills/opentag/references/control-plane.md +++ b/skills/opentag/references/control-plane.md @@ -18,16 +18,17 @@ For an existing local configuration: opentag pair --relay ``` -The CLI checks relay health and capabilities before Hosted Control V1 -registration. Successful registration binds the runner and Project Targets, -stores the issued runner credential atomically, and removes the bootstrap -pairing token from local config. Do not use `--no-register` with Hosted Control -V1; registration and recovery reject that option. +The CLI reads Hosted Control V1 capabilities directly without calling `/healthz`. +Registration sends only the runner ID and an empty capabilities list; it does not bind Project Targets. +Successful registration stores the issued runner credential atomically and +removes the bootstrap pairing token from local config. Do not use `--no-register` +with Hosted Control V1; registration and recovery reject that option. ## Trust And Secrets - Pair only with an HTTPS origin the user operates or explicitly trusts. The relay is a remote control plane for this local runner. +- The relay can access run metadata, command text, and progress, and it controls which queued runs the local runner claims. - Have the user enter the bootstrap pairing token through the local setup or config workflow. Never ask for it in chat, command output, screenshots, or committed files. @@ -46,7 +47,7 @@ opentag doctor ``` Confirm the redacted config reports relay mode and a paired Hosted Control V1 -registration, the expected runner and Project Targets are present, and no -Control Plane alert is failing. If pairing reports recovery-required state, +runner registration, the expected Project Targets remain in local config, and +no Control Plane alert is failing. If pairing reports recovery-required state, stop and use the deployment's separately held recovery process; never invent or reuse a credential. diff --git a/skills/opentag/references/github-setup.md b/skills/opentag/references/github-setup.md index a50b1b1a..f09a967f 100644 --- a/skills/opentag/references/github-setup.md +++ b/skills/opentag/references/github-setup.md @@ -28,7 +28,7 @@ Never invent tokens, owner names, repository names, webhook secrets, or project ## User Path ```bash -npm install -g @opentag/cli +npm install -g @opentag/cli@0.10.0 opentag setup ``` diff --git a/skills/opentag/references/local-echo.md b/skills/opentag/references/local-echo.md index 7d352909..874fa8c0 100644 --- a/skills/opentag/references/local-echo.md +++ b/skills/opentag/references/local-echo.md @@ -13,7 +13,7 @@ Do not recommend Echo for real use unless the user explicitly wants a smoke test Install or run the CLI: ```bash -npm install -g @opentag/cli +npm install -g @opentag/cli@0.10.0 opentag setup ``` diff --git a/skills/opentag/references/slack-setup.md b/skills/opentag/references/slack-setup.md index 3c55af06..d457fd55 100644 --- a/skills/opentag/references/slack-setup.md +++ b/skills/opentag/references/slack-setup.md @@ -37,7 +37,7 @@ Never invent these values. Walk the user through Slack's app page and ask them t ## User Path ```bash -npm install -g @opentag/cli +npm install -g @opentag/cli@0.10.0 opentag setup ``` diff --git a/skills/opentag/references/teams-setup.md b/skills/opentag/references/teams-setup.md index fbb4ebeb..78e121cd 100644 --- a/skills/opentag/references/teams-setup.md +++ b/skills/opentag/references/teams-setup.md @@ -25,9 +25,9 @@ Configure the Azure Bot Messaging endpoint as public HTTPS ending in ## Channel Binding Capture the tenant ID and base channel conversation ID from an authenticated -Teams activity. Prefer `channelData.channel.id` or -`channelData.teamsChannelId`; do not bind a thread-specific -`;messageid=` value. +Teams activity. Use `activity.conversation.id` as `conversationId`, removing only a trailing `;messageid=` suffix when present. +Do not use `channelData.channel.id` or `channelData.teamsChannelId` as +`conversationId`. There is currently no standalone Teams channel-binding CLI command. Use the deployment's operator-controlled local config or dispatcher API to create the From 13725aeab9cf874d857b8ed0ceb728b13907ef1b Mon Sep 17 00:00:00 2001 From: Ming Yoo Date: Mon, 17 Aug 2026 16:10:06 +0800 Subject: [PATCH 3/3] docs(skill): match CLI version verification path Use the pinned npx command when the user chooses the no-global-install workflow. Co-authored-by: Cursor --- packages/cli/test/docs-contract.test.ts | 2 ++ skills/opentag/SKILL.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/docs-contract.test.ts b/packages/cli/test/docs-contract.test.ts index 40467cd6..3717a100 100644 --- a/packages/cli/test/docs-contract.test.ts +++ b/packages/cli/test/docs-contract.test.ts @@ -149,6 +149,8 @@ describe("platform setup docs contract", () => { expect(skillDocs).not.toMatch(/\bnpm install -g @opentag\/cli(?:\s|$)/u); expect(skill).toContain("npm install -g @opentag/cli@0.10.0"); expect(skill).toContain("npx @opentag/cli@0.10.0 setup"); + expect(skill).toContain("For a global install, verify with `opentag --version`"); + expect(skill).toContain("For the no-global path, verify with `npx @opentag/cli@0.10.0 --version`"); expect(skill).toContain("request_user_input"); expect(skill).toContain("askhuman"); expect(skill).toContain("Codex Plan mode"); diff --git a/skills/opentag/SKILL.md b/skills/opentag/SKILL.md index 54f066b7..b0ec809f 100644 --- a/skills/opentag/SKILL.md +++ b/skills/opentag/SKILL.md @@ -53,7 +53,7 @@ For platform credential steps, use the repository docs as the source of truth: ## Working Rules - Keep setup user-led. Never invent tokens, app IDs, Slack team/channel IDs, GitHub owner/repo names, or local project paths. -- Use the reviewed CLI version `0.10.0` consistently in install and `npx` commands, and verify it with `opentag --version`. Update the pin only after reviewing a newer release. +- Use the reviewed CLI version `0.10.0` consistently in install and `npx` commands. For a global install, verify with `opentag --version`. For the no-global path, verify with `npx @opentag/cli@0.10.0 --version`. Update the pin only after reviewing a newer release. - Prefer Slack, then GitHub, then GitLab, then Linear, then Lark / Feishu when listing established paths; label Telegram, Discord, and Microsoft Teams as previews. - Ask the user which platform and coding agent they want if it is not already clear outside Codex. - In Codex Plan mode, use `request_user_input` / askhuman to collect non-secret setup choices before running `opentag setup`, then pass those choices as CLI flags so the terminal wizard does not silently choose defaults.