From ba89ffa9dadc0b02d114cca56b74802ef909f398 Mon Sep 17 00:00:00 2001 From: Yoni Melki Date: Tue, 25 Aug 2026 13:25:15 +0300 Subject: [PATCH 1/5] AX-2162 - Align OpenCode README with shared flow and AX cross-links --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 4bbd823..f2d3679 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ scanning, supply-chain best practices, and Agent Guard. The plugin ships the off JFrog [Agent Skills](https://opencode.ai/docs/skills/) with the package and registers them with OpenCode at load time, plus the JFrog Platform MCP server. +> **Install flow:** Follow the [shared install, verify, and recovery guide](https://github.com/jfrog/claude-plugin/blob/main/docs/shared-install-and-verify.md) for the cross-harness rules on initialization, environment variables, restart, verification, and recovery. This README documents **OpenCode-only** differences. +> +> **Related OpenCode work:** installation doc improvements ([AX-1780](https://jfrog-int.atlassian.net/browse/AX-1780)) and init support ([AX-2122](https://jfrog-int.atlassian.net/browse/AX-2122), [AX-2124](https://jfrog-int.atlassian.net/browse/AX-2124)). + ## Features The JFrog plugin provides the following capabilities, grouped by component: @@ -58,6 +62,19 @@ release. For an organization-wide rollout, set the plugin in OpenCode's [remote configuration](https://opencode.ai/docs/config/#remote) so every developer gets it automatically. +**Restart OpenCode completely** after editing `opencode.json` or changing any JFrog +environment variable. Starting a new session, or reloading the window, is not enough — +the plugin reads its configuration at load time. + +#### Config file location + +| Platform | Path | +| --- | --- | +| macOS / Linux | `~/.config/opencode/opencode.json` | +| Windows | `%APPDATA%\opencode\opencode.json` | + +OpenCode Desktop and the CLI share this config. + ### Local development Test an uncommitted checkout without publishing. Build the module, then point your @@ -146,6 +163,24 @@ skill's body loads only when it is invoked. --- +## Verify + +Verification is a required install step, not a troubleshooting fallback. After +restarting OpenCode, confirm all four: + +1. OpenCode's startup output resolves `@jfrog/opencode-jfrog-plugin` without a plugin load error. +2. `/skills` in a session — `jfrog` and the other bundled skills are listed. +3. `opencode mcp list` — `jfrog` shows as connected after `opencode mcp auth jfrog`. + (Skip this if you set `JFROG_MCP_DISABLE=true`.) +4. `jf rt ping` — succeeds against your configured JFrog server. + +Skills loading while the MCP stays disconnected usually means `JFROG_PLATFORM_URL` was +set after OpenCode started, or the OAuth sign-in has not been completed. Setting +environment variables without a full restart does not repair it — see the +[shared recovery playbook](https://github.com/jfrog/claude-plugin/blob/main/docs/shared-install-and-verify.md#recovery-playbook). + +--- + ## Usage Once configured, interact with the JFrog plugin through natural language. Examples are From e7f95d7443a5c78af4004ef39ba57e3917fdb2a7 Mon Sep 17 00:00:00 2001 From: Yoni Melki Date: Tue, 25 Aug 2026 18:33:54 +0300 Subject: [PATCH 2/5] AX-2162 - Keep OpenCode install docs self-contained Inline verify and recovery in this README instead of sending readers to the Claude plugin repository. Co-authored-by: Cursor --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f2d3679..cde1f05 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ scanning, supply-chain best practices, and Agent Guard. The plugin ships the off JFrog [Agent Skills](https://opencode.ai/docs/skills/) with the package and registers them with OpenCode at load time, plus the JFrog Platform MCP server. -> **Install flow:** Follow the [shared install, verify, and recovery guide](https://github.com/jfrog/claude-plugin/blob/main/docs/shared-install-and-verify.md) for the cross-harness rules on initialization, environment variables, restart, verification, and recovery. This README documents **OpenCode-only** differences. -> > **Related OpenCode work:** installation doc improvements ([AX-1780](https://jfrog-int.atlassian.net/browse/AX-1780)) and init support ([AX-2122](https://jfrog-int.atlassian.net/browse/AX-2122), [AX-2124](https://jfrog-int.atlassian.net/browse/AX-2124)). ## Features @@ -176,8 +174,13 @@ restarting OpenCode, confirm all four: Skills loading while the MCP stays disconnected usually means `JFROG_PLATFORM_URL` was set after OpenCode started, or the OAuth sign-in has not been completed. Setting -environment variables without a full restart does not repair it — see the -[shared recovery playbook](https://github.com/jfrog/claude-plugin/blob/main/docs/shared-install-and-verify.md#recovery-playbook). +environment variables without a full restart does not repair it. + +| Symptom | Do this | Do **not** do this | +| --- | --- | --- | +| MCP disconnected after install | Set `JFROG_PLATFORM_URL` in the environment that **launches** OpenCode, run `opencode mcp auth jfrog`, **quit and restart OpenCode**, then `opencode mcp list`. | Expect a new session or window reload to pick up env vars. | +| Skills load but MCP is missing | Confirm `JFROG_MCP_DISABLE` is not `true`, set the host before start, complete OAuth. | Set `JFROG_ACCESS_TOKEN` — MCP auth is OAuth only. | +| Config change not applied | Edit `opencode.json`, then fully quit and restart OpenCode. | Start a new chat without restarting. | --- From d73dc53681bde4c2bfcb111ace593f915e323d72 Mon Sep 17 00:00:00 2001 From: Yoni Melki Date: Tue, 25 Aug 2026 19:06:54 +0300 Subject: [PATCH 3/5] Remove internal ticket links from the README Public plugin docs must not point at JFrog Jira. Keep ticket references on the pull request instead. Co-authored-by: Cursor --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index cde1f05..9f582ab 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ scanning, supply-chain best practices, and Agent Guard. The plugin ships the off JFrog [Agent Skills](https://opencode.ai/docs/skills/) with the package and registers them with OpenCode at load time, plus the JFrog Platform MCP server. -> **Related OpenCode work:** installation doc improvements ([AX-1780](https://jfrog-int.atlassian.net/browse/AX-1780)) and init support ([AX-2122](https://jfrog-int.atlassian.net/browse/AX-2122), [AX-2124](https://jfrog-int.atlassian.net/browse/AX-2124)). - ## Features The JFrog plugin provides the following capabilities, grouped by component: From c907a921ef77d01a8700f2ee9b371058c0612df8 Mon Sep 17 00:00:00 2001 From: Yoni Melki Date: Thu, 27 Aug 2026 15:40:32 +0300 Subject: [PATCH 4/5] Add a Recovery heading and the install-docs CI guard. --- .github/workflows/pr.yml | 18 +++++ README.md | 5 +- scripts/validate-install-docs.mjs | 97 ++++++++++++++++++++++++++ scripts/validate-install-docs.test.mjs | 68 ++++++++++++++++++ 4 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 scripts/validate-install-docs.mjs create mode 100644 scripts/validate-install-docs.test.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d63535f..2eaf3b6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -38,3 +38,21 @@ jobs: mise run build mise run pkgjsonlint mise run pack:check + + install-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Validate install/recovery docs + run: | + node --test scripts/validate-install-docs.test.mjs + JFROG_PLUGIN_HARNESS=opencode node scripts/validate-install-docs.mjs diff --git a/README.md b/README.md index 9f582ab..720a845 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,10 @@ restarting OpenCode, confirm all four: Skills loading while the MCP stays disconnected usually means `JFROG_PLATFORM_URL` was set after OpenCode started, or the OAuth sign-in has not been completed. Setting -environment variables without a full restart does not repair it. +environment variables without a full restart does not repair it. If a check fails, +see [Recovery](#recovery). + +## Recovery | Symptom | Do this | Do **not** do this | | --- | --- | --- | diff --git a/scripts/validate-install-docs.mjs b/scripts/validate-install-docs.mjs new file mode 100644 index 0000000..33f5268 --- /dev/null +++ b/scripts/validate-install-docs.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 +// Validates install/recovery documentation invariants. + +import { readFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = process.cwd(); + +const HARNESS_OWN_REPO = { + claude: 'claude-plugin', + codex: 'codex-plugin', + cursor: 'cursor-plugin', + devin: 'devin-plugin', + opencode: 'opencode-jfrog-plugin', + vscode: 'vscode-plugin', +}; + +const ALL_PLUGIN_REPOS = Object.values(HARNESS_OWN_REPO); + +const REQUIRED_README_MARKERS = ['## Verify', '## Recovery']; + +const FORBIDDEN_PATTERNS = [ + { + re: /setting\s+(?:the\s+)?environment\s+variables?\s+after\s+a\s+failed\s+init\s+may\s+repair/i, + message: 'must not claim env vars repair failed init', + }, + { + re: /JFROG_URL/, + message: 'must not document the legacy JFROG_URL env var; use JFROG_PLATFORM_URL', + }, + { + re: /atlassian\.net/i, + message: 'must not reference JFrog Jira (atlassian.net) in repo files', + }, + { + re: /\b(?:AX|MLD)-\d+\b/, + message: 'must not include Jira ticket keys in repo files', + }, +]; + +export function validateInstallDocs({ repoRoot: root, harness }) { + const errors = []; + const readmePath = join(root, 'README.md'); + if (!existsSync(readmePath)) { + return [`${harness}: missing README.md`]; + } + const files = [{ label: 'README.md', text: readFileSync(readmePath, 'utf8') }]; + + const readme = files[0].text; + for (const marker of REQUIRED_README_MARKERS) { + if (!readme.includes(marker)) { + errors.push(`${harness}: README.md missing required marker: ${marker}`); + } + } + + const ownRepo = HARNESS_OWN_REPO[harness]; + const otherRepos = ALL_PLUGIN_REPOS.filter((name) => name !== ownRepo); + + for (const { label, text } of files) { + for (const { re, message } of FORBIDDEN_PATTERNS) { + if (re.test(text)) errors.push(`${harness}: ${label} ${message}`); + } + for (const other of otherRepos) { + if (text.includes(`github.com/jfrog/${other}`)) { + errors.push(`${harness}: ${label} must not link to github.com/jfrog/${other}`); + } + } + } + + return errors; +} + +function main() { + const harness = process.env.JFROG_PLUGIN_HARNESS ?? inferHarness(repoRoot); + const errors = validateInstallDocs({ repoRoot, harness }); + if (errors.length) { + console.error('install-docs validation failed:'); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); + } + console.log('install-docs validation passed'); +} + +function inferHarness(root) { + if (existsSync(join(root, '.codex-plugin'))) return 'codex'; + if (existsSync(join(root, '.devin-plugin'))) return 'devin'; + if (existsSync(join(root, '.claude-plugin'))) return 'claude'; + if (existsSync(join(root, 'plugins', 'jfrog', '.cursor-plugin'))) return 'cursor'; + if (existsSync(join(root, 'plugin', '.claude-plugin'))) return 'vscode'; + if (existsSync(join(root, 'package.json')) && root.endsWith('opencode-jfrog-plugin')) return 'opencode'; + return 'unknown'; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) main(); diff --git a/scripts/validate-install-docs.test.mjs b/scripts/validate-install-docs.test.mjs new file mode 100644 index 0000000..b52e3f0 --- /dev/null +++ b/scripts/validate-install-docs.test.mjs @@ -0,0 +1,68 @@ +// Copyright (c) JFrog Ltd. 2026 +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { validateInstallDocs } from './validate-install-docs.mjs'; + +function writeReadme(root, body) { + writeFileSync(join(root, 'README.md'), body); +} + +test('validateInstallDocs passes when README has Verify, Recovery, and no other-plugin links', () => { + const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); + writeReadme(root, '# OpenCode\n\n## Verify\n\n1. list plugins\n\n## Recovery\n\n'); + assert.deepEqual(validateInstallDocs({ repoRoot: root, harness: 'opencode' }), []); +}); + +test('validateInstallDocs flags missing Verify section', () => { + const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); + writeReadme(root, '# OpenCode\n\nInstall the plugin.\n'); + const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); + assert.ok(errors.some((e) => e.includes('## Verify'))); +}); + +test('validateInstallDocs flags missing Recovery section', () => { + const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); + writeReadme(root, '# OpenCode\n\n## Verify\n\n1. list plugins\n'); + const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); + assert.ok(errors.some((e) => e.includes('## Recovery'))); +}); + +test('validateInstallDocs rejects contradictory failed-init env-var recovery claims', () => { + const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); + writeReadme( + root, + '# x\n## Verify\n## Recovery\nSetting environment variables after a failed init may repair MCP registration.' + ); + const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); + assert.ok(errors.some((e) => e.includes('env vars repair failed init'))); +}); + +test('validateInstallDocs rejects the legacy JFROG_URL env var', () => { + const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); + writeReadme(root, '# OpenCode\n## Verify\n## Recovery\nSet `JFROG_URL` to your platform.\n'); + const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); + assert.ok(errors.some((e) => e.includes('JFROG_URL'))); +}); + +test('validateInstallDocs rejects links to other plugin GitHub repos', () => { + const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); + writeReadme( + root, + '# OpenCode\n## Verify\n## Recovery\nSee https://github.com/jfrog/claude-plugin/blob/main/README.md\n' + ); + const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); + assert.ok(errors.some((e) => e.includes('claude-plugin'))); +}); + +test('validateInstallDocs rejects Jira URLs and ticket keys', () => { + const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); + const host = ['jfrog-int', 'atlassian', 'net'].join('.'); + const key = ['AX', '1780'].join('-'); + writeReadme(root, `# OpenCode\n## Verify\n## Recovery\nSee [${key}](https://${host}/browse/${key}).\n`); + const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); + assert.ok(errors.some((e) => e.includes('atlassian.net'))); + assert.ok(errors.some((e) => e.includes('Jira ticket keys'))); +}); From ce7a9873769f2411dc1d62a1c8f56fe99769b5e3 Mon Sep 17 00:00:00 2001 From: Yoni Melki Date: Thu, 27 Aug 2026 16:22:35 +0300 Subject: [PATCH 5/5] Drop the install-docs README checker. --- .github/workflows/pr.yml | 18 ----- scripts/validate-install-docs.mjs | 97 -------------------------- scripts/validate-install-docs.test.mjs | 68 ------------------ 3 files changed, 183 deletions(-) delete mode 100644 scripts/validate-install-docs.mjs delete mode 100644 scripts/validate-install-docs.test.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2eaf3b6..d63535f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -38,21 +38,3 @@ jobs: mise run build mise run pkgjsonlint mise run pack:check - - install-docs: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Validate install/recovery docs - run: | - node --test scripts/validate-install-docs.test.mjs - JFROG_PLUGIN_HARNESS=opencode node scripts/validate-install-docs.mjs diff --git a/scripts/validate-install-docs.mjs b/scripts/validate-install-docs.mjs deleted file mode 100644 index 33f5268..0000000 --- a/scripts/validate-install-docs.mjs +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env node -// Copyright (c) JFrog Ltd. 2026 -// Licensed under the Apache License, Version 2.0 -// Validates install/recovery documentation invariants. - -import { readFileSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const repoRoot = process.cwd(); - -const HARNESS_OWN_REPO = { - claude: 'claude-plugin', - codex: 'codex-plugin', - cursor: 'cursor-plugin', - devin: 'devin-plugin', - opencode: 'opencode-jfrog-plugin', - vscode: 'vscode-plugin', -}; - -const ALL_PLUGIN_REPOS = Object.values(HARNESS_OWN_REPO); - -const REQUIRED_README_MARKERS = ['## Verify', '## Recovery']; - -const FORBIDDEN_PATTERNS = [ - { - re: /setting\s+(?:the\s+)?environment\s+variables?\s+after\s+a\s+failed\s+init\s+may\s+repair/i, - message: 'must not claim env vars repair failed init', - }, - { - re: /JFROG_URL/, - message: 'must not document the legacy JFROG_URL env var; use JFROG_PLATFORM_URL', - }, - { - re: /atlassian\.net/i, - message: 'must not reference JFrog Jira (atlassian.net) in repo files', - }, - { - re: /\b(?:AX|MLD)-\d+\b/, - message: 'must not include Jira ticket keys in repo files', - }, -]; - -export function validateInstallDocs({ repoRoot: root, harness }) { - const errors = []; - const readmePath = join(root, 'README.md'); - if (!existsSync(readmePath)) { - return [`${harness}: missing README.md`]; - } - const files = [{ label: 'README.md', text: readFileSync(readmePath, 'utf8') }]; - - const readme = files[0].text; - for (const marker of REQUIRED_README_MARKERS) { - if (!readme.includes(marker)) { - errors.push(`${harness}: README.md missing required marker: ${marker}`); - } - } - - const ownRepo = HARNESS_OWN_REPO[harness]; - const otherRepos = ALL_PLUGIN_REPOS.filter((name) => name !== ownRepo); - - for (const { label, text } of files) { - for (const { re, message } of FORBIDDEN_PATTERNS) { - if (re.test(text)) errors.push(`${harness}: ${label} ${message}`); - } - for (const other of otherRepos) { - if (text.includes(`github.com/jfrog/${other}`)) { - errors.push(`${harness}: ${label} must not link to github.com/jfrog/${other}`); - } - } - } - - return errors; -} - -function main() { - const harness = process.env.JFROG_PLUGIN_HARNESS ?? inferHarness(repoRoot); - const errors = validateInstallDocs({ repoRoot, harness }); - if (errors.length) { - console.error('install-docs validation failed:'); - for (const e of errors) console.error(` - ${e}`); - process.exit(1); - } - console.log('install-docs validation passed'); -} - -function inferHarness(root) { - if (existsSync(join(root, '.codex-plugin'))) return 'codex'; - if (existsSync(join(root, '.devin-plugin'))) return 'devin'; - if (existsSync(join(root, '.claude-plugin'))) return 'claude'; - if (existsSync(join(root, 'plugins', 'jfrog', '.cursor-plugin'))) return 'cursor'; - if (existsSync(join(root, 'plugin', '.claude-plugin'))) return 'vscode'; - if (existsSync(join(root, 'package.json')) && root.endsWith('opencode-jfrog-plugin')) return 'opencode'; - return 'unknown'; -} - -if (process.argv[1] === fileURLToPath(import.meta.url)) main(); diff --git a/scripts/validate-install-docs.test.mjs b/scripts/validate-install-docs.test.mjs deleted file mode 100644 index b52e3f0..0000000 --- a/scripts/validate-install-docs.test.mjs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) JFrog Ltd. 2026 -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { mkdtempSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { validateInstallDocs } from './validate-install-docs.mjs'; - -function writeReadme(root, body) { - writeFileSync(join(root, 'README.md'), body); -} - -test('validateInstallDocs passes when README has Verify, Recovery, and no other-plugin links', () => { - const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); - writeReadme(root, '# OpenCode\n\n## Verify\n\n1. list plugins\n\n## Recovery\n\n'); - assert.deepEqual(validateInstallDocs({ repoRoot: root, harness: 'opencode' }), []); -}); - -test('validateInstallDocs flags missing Verify section', () => { - const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); - writeReadme(root, '# OpenCode\n\nInstall the plugin.\n'); - const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); - assert.ok(errors.some((e) => e.includes('## Verify'))); -}); - -test('validateInstallDocs flags missing Recovery section', () => { - const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); - writeReadme(root, '# OpenCode\n\n## Verify\n\n1. list plugins\n'); - const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); - assert.ok(errors.some((e) => e.includes('## Recovery'))); -}); - -test('validateInstallDocs rejects contradictory failed-init env-var recovery claims', () => { - const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); - writeReadme( - root, - '# x\n## Verify\n## Recovery\nSetting environment variables after a failed init may repair MCP registration.' - ); - const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); - assert.ok(errors.some((e) => e.includes('env vars repair failed init'))); -}); - -test('validateInstallDocs rejects the legacy JFROG_URL env var', () => { - const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); - writeReadme(root, '# OpenCode\n## Verify\n## Recovery\nSet `JFROG_URL` to your platform.\n'); - const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); - assert.ok(errors.some((e) => e.includes('JFROG_URL'))); -}); - -test('validateInstallDocs rejects links to other plugin GitHub repos', () => { - const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); - writeReadme( - root, - '# OpenCode\n## Verify\n## Recovery\nSee https://github.com/jfrog/claude-plugin/blob/main/README.md\n' - ); - const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); - assert.ok(errors.some((e) => e.includes('claude-plugin'))); -}); - -test('validateInstallDocs rejects Jira URLs and ticket keys', () => { - const root = mkdtempSync(join(tmpdir(), 'opencode-docs-')); - const host = ['jfrog-int', 'atlassian', 'net'].join('.'); - const key = ['AX', '1780'].join('-'); - writeReadme(root, `# OpenCode\n## Verify\n## Recovery\nSee [${key}](https://${host}/browse/${key}).\n`); - const errors = validateInstallDocs({ repoRoot: root, harness: 'opencode' }); - assert.ok(errors.some((e) => e.includes('atlassian.net'))); - assert.ok(errors.some((e) => e.includes('Jira ticket keys'))); -});