From 4380d634aa19c5dc64a7fd88b266c5723f00d1f9 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Fri, 17 Jul 2026 12:24:45 -0400 Subject: [PATCH 01/10] polish(extension): Marketplace listing metadata, CHANGELOG.md, SUPPORT.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds categories, keywords, gallery banner, badges, homepage, pricing, and author to the manifest; bundles CHANGELOG.md into the VSIX for the Marketplace's Changelog tab; adds SUPPORT.md as a GitHub community health file (deliberately excluded from the packaged VSIX via .vscodeignore — it drives GitHub's issue-creation flow, not a Marketplace tab). Untracked/uncommitted work found sitting in the working tree from a prior session, predating today's 0.1.3 publish. Verified: valid JSON, clean typecheck/build, 82/82 tests, and vsce accepts every new manifest field (validated with a real `npm run package` build). Corrected the changelog's stale "0.1.2" heading to "Unreleased" since none of this shipped in any published version yet — it lands in whatever version is tagged next. --- extension/.vscodeignore | 3 +++ extension/CHANGELOG.md | 20 ++++++++++++++++++++ extension/SUPPORT.md | 14 ++++++++++++++ extension/package.json | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 extension/CHANGELOG.md create mode 100644 extension/SUPPORT.md diff --git a/extension/.vscodeignore b/extension/.vscodeignore index 31347b4..d3b60be 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -1,9 +1,12 @@ .vscode/** .gitignore +.DS_Store src/** test/** out/test/** tsconfig.json node_modules/** +package-lock.json *.vsix assets/marketplace-icon.svg +SUPPORT.md diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md new file mode 100644 index 0000000..cc6ab57 --- /dev/null +++ b/extension/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## Unreleased + +### Added + +- Marketplace metadata: categories, keywords, gallery banner, badges, homepage, pricing, preview flag, and author. +- This `CHANGELOG.md`, bundled into the VSIX for the Marketplace's Changelog tab. +- `SUPPORT.md` — a GitHub community health file (intentionally excluded from the packaged VSIX via `.vscodeignore`; it drives GitHub's own issue-creation flow, not a Marketplace tab). + +## 0.1.0 + +### Added + +- Explorer decorations for files by their role in the current change (denied, omitted partner, included). +- Activity Bar view showing the deterministic decision, denied reasons, and omitted co-change partners. +- Status-bar heartbeat mirroring the current decision. +- Receipt-backed advisory review with GPT-5.6 verdict rendering. +- Commands: Show Current Change, Inspect Evidence, Run Verification, Run Advisory Review, Inspect Review Receipt, Open Intelligence File, Getting Started. +- Walkthrough with three guided steps. diff --git a/extension/SUPPORT.md b/extension/SUPPORT.md new file mode 100644 index 0000000..465f548 --- /dev/null +++ b/extension/SUPPORT.md @@ -0,0 +1,14 @@ +# Support + +## Report a bug or request a feature + +[Open an issue on GitHub](https://github.com/workspace-json/codex-mcp/issues) + +## Documentation + +- [Main repository](https://github.com/workspace-json/codex-mcp) +- [workspace.json site](https://workspacejson.dev/implementations/codex) + +## License + +Apache-2.0 diff --git a/extension/package.json b/extension/package.json index f007b87..3f9d026 100644 --- a/extension/package.json +++ b/extension/package.json @@ -9,8 +9,49 @@ "vscode": "^1.85.0" }, "categories": [ + "Linters", + "SCM Providers", + "Testing", "Other" ], + "keywords": [ + "workspace.json", + "code-review", + "fragility", + "co-change", + "agent-safety" + ], + "galleryBanner": { + "color": "#07100e", + "theme": "dark" + }, + "badges": [ + { + "url": "https://img.shields.io/badge/license-Apache--2.0-blue", + "href": "https://github.com/workspace-json/codex-mcp/blob/main/LICENSE", + "description": "License" + }, + { + "url": "https://github.com/workspace-json/codex-mcp/actions/workflows/ci.yml/badge.svg", + "href": "https://github.com/workspace-json/codex-mcp/actions/workflows/ci.yml", + "description": "CI Status" + } + ], + "bugs": { + "url": "https://github.com/workspace-json/codex-mcp/issues" + }, + "homepage": "https://workspacejson.dev/implementations/codex", + "pricing": "Free", + "preview": true, + "qna": false, + "markdown": "github", + "extensionKind": [ + "workspace" + ], + "author": { + "name": "workspace.json", + "url": "https://workspacejson.dev" + }, "main": "./out/src/extension.js", "activationEvents": [ "onStartupFinished" From 61e4514e49078dd8004111463fecf9124e41ea8a Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Fri, 17 Jul 2026 12:31:15 -0400 Subject: [PATCH 02/10] fix: address adversarial review findings on marketplace metadata WIP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert categories to ["Other"]: Linters/SCM Providers/Testing were false — the extension implements none of those VS Code APIs (no DiagnosticCollection, no SCM provider, no TestController). This is exactly the overclaiming this codebase's own copy elsewhere refuses to do (tooltips.ts, semanticModel.ts). - Fix CHANGELOG.md: walkthrough has four steps, not three. - Fix CHANGELOG.md: SUPPORT.md isn't at a location GitHub recognizes as a community health file, so it doesn't "drive" anything yet — reworded to stop claiming a wiring that doesn't exist. Missing 0.1.1/0.1.2/0.1.3 changelog entries left as a known gap (flagged, not fabricated) rather than reconstructed from an unlabeled commit history with no clean per-version boundary. --- extension/CHANGELOG.md | 6 +++--- extension/package.json | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md index cc6ab57..23b96ee 100644 --- a/extension/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -4,9 +4,9 @@ ### Added -- Marketplace metadata: categories, keywords, gallery banner, badges, homepage, pricing, preview flag, and author. +- Marketplace metadata: keywords, gallery banner, badges, homepage, pricing, preview flag, and author. - This `CHANGELOG.md`, bundled into the VSIX for the Marketplace's Changelog tab. -- `SUPPORT.md` — a GitHub community health file (intentionally excluded from the packaged VSIX via `.vscodeignore`; it drives GitHub's own issue-creation flow, not a Marketplace tab). +- `SUPPORT.md` — a plain support-links doc, kept out of the packaged VSIX via `.vscodeignore` (it isn't at a location GitHub recognizes as a community health file, so it isn't wired into any GitHub or Marketplace flow yet — it exists for a human to link to directly). ## 0.1.0 @@ -17,4 +17,4 @@ - Status-bar heartbeat mirroring the current decision. - Receipt-backed advisory review with GPT-5.6 verdict rendering. - Commands: Show Current Change, Inspect Evidence, Run Verification, Run Advisory Review, Inspect Review Receipt, Open Intelligence File, Getting Started. -- Walkthrough with three guided steps. +- Walkthrough with four guided steps. diff --git a/extension/package.json b/extension/package.json index 3f9d026..8a33b20 100644 --- a/extension/package.json +++ b/extension/package.json @@ -9,9 +9,6 @@ "vscode": "^1.85.0" }, "categories": [ - "Linters", - "SCM Providers", - "Testing", "Other" ], "keywords": [ From 4359d8b539736fc2a67923133500ce4cdc0f78b9 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Fri, 17 Jul 2026 12:36:21 -0400 Subject: [PATCH 03/10] polish(extension): onboarding UX consistency (naming + dead-end warning) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requested a /sc:analyze pass on the extension's architecture and command-palette/onboarding integration, grounded against the official VS Code Command Palette UX guidelines (docs confirm: use clear names, group commands under one category — the latter already holds here). Two findings, both fixed: 1. 6 of 7 visible commands use verb-first imperative titles (Show, Inspect, Run, Open); "Getting Started" was the one gerund noun phrase. Renamed to "Open Getting Started Walkthrough" for pattern consistency. (Ruled out a VS Code built-in title collision as the root cause of the reported onboarding bug: VS Code's own entries are "Help: Open Walkthrough..." and "Help: Welcome", not "Getting Started" — no direct string clash. Traced WALKTHROUGH_ID history: a real publisher-id typo existed once, historically, but was fixed in d753ede, long before today, and is confirmed correct on main.) 2. The Command-Palette-only "Open Intelligence File" command's no-file warning was a dead end — every other surface (welcome states, walkthrough) points to Getting Started when workspace.json is missing; this one didn't. Added the same actionable button. No logic, decision, or evidence-model changes. 82/82 tests still pass. --- extension/package.json | 2 +- extension/src/commands.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/extension/package.json b/extension/package.json index 8a33b20..c06e77f 100644 --- a/extension/package.json +++ b/extension/package.json @@ -121,7 +121,7 @@ }, { "command": "workspacejson.openWalkthrough", - "title": "Getting Started", + "title": "Open Getting Started Walkthrough", "category": "workspace.json" } ], diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 3b84f8c..2714a54 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -60,7 +60,14 @@ export function registerCommands(model: WorkspaceIntelligenceModel, context: vsc try { await vscode.window.showTextDocument(uri); } catch { - void vscode.window.showWarningMessage("workspace.json: .agents/workspace.json was not found in this workspace."); + // Same next step the welcome states already offer (§4.2) — this command is + // reachable outside the tree view (Command Palette), so it shouldn't be the + // one dead end that doesn't point back to Getting Started. + const choice = await vscode.window.showWarningMessage( + "workspace.json: .agents/workspace.json was not found in this workspace.", + "Getting Started", + ); + if (choice === "Getting Started") await vscode.commands.executeCommand(COMMAND_IDS.openWalkthrough); } }); From 95a78af8132e6e9725c377f1efd392a27af2d2ed Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Fri, 17 Jul 2026 12:44:42 -0400 Subject: [PATCH 04/10] docs: note the onboarding command rename in CHANGELOG Unreleased section --- extension/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md index 23b96ee..5fd754b 100644 --- a/extension/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -8,6 +8,11 @@ - This `CHANGELOG.md`, bundled into the VSIX for the Marketplace's Changelog tab. - `SUPPORT.md` — a plain support-links doc, kept out of the packaged VSIX via `.vscodeignore` (it isn't at a location GitHub recognizes as a community health file, so it isn't wired into any GitHub or Marketplace flow yet — it exists for a human to link to directly). +### Changed + +- Renamed the "Getting Started" command to "Open Getting Started Walkthrough" for consistency with the other verb-first command titles. +- The "Open Intelligence File" command's no-file warning now offers a "Getting Started" button instead of being a dead end. + ## 0.1.0 ### Added From a874ba8b869828d30440c95d1b42d2757ebc1ccc Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Sun, 19 Jul 2026 02:34:33 -0400 Subject: [PATCH 05/10] fix(extension): generate-command onboarding, honest install receipt, version drift gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the extension's "Generate Intelligence" command/walkthrough step so a user with no .agents/workspace.json can invoke the reference generator (agents-audit) without the extension authoring the artifact itself (sendText only, never fs.write). Names the producer on every consumer surface (README, extension, walkthrough) that previously never mentioned it, per the HAC-197 cold-run finding. Installer: removes the unrequested .gitignore auto-write (HAC-203) in favor of printing the ignore instruction; the install receipt now checks for .agents/workspace.json and prints generate instructions instead of reporting PASS when no artifact exists. Adds scripts/check-generator-version.mjs (+ CI wiring) to catch drift across the now six-plus copies of the pinned agents-audit version instead of hand-syncing them. README: corrects the "Verify in two minutes" section, which previously told readers to generate a file then rely on fragility flags that `generate` never produces — now points that demo at fixture/, which carries hand-authored manual evidence. Does NOT touch the reviewer-key documentation gap (missing WORKSPACEJSON_REVIEWER_MODEL, the "no network calls" claim sitting in the same sentence as the advisory review) — that stays open under HAC-205. Reviewed adversarially across three rounds this session; all BLOCK findings resolved, remaining observations (registry-coupling in the version-check gate, walkthrough onContext over-completion on non-happy states) accepted as known, non-blocking trade-offs. --- .github/workflows/ci.yml | 3 + README.md | 24 ++++- extension/assets/walkthrough/generate.md | 5 + extension/package.json | 18 +++- extension/src/commandIds.ts | 1 + extension/src/commands.ts | 14 +++ package.json | 3 +- scripts/check-generator-version.mjs | 111 +++++++++++++++++++++ scripts/install.mjs | 21 +++- tests/unit/check-generator-version.test.ts | 56 +++++++++++ 10 files changed, 248 insertions(+), 8 deletions(-) create mode 100644 extension/assets/walkthrough/generate.md create mode 100644 scripts/check-generator-version.mjs create mode 100644 tests/unit/check-generator-version.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2be0df3..06d8e6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: - name: Check repository structure run: npm run check:structure + - name: Check generator version pin + run: npm run check:generator-version + - name: Typecheck run: npm run typecheck diff --git a/README.md b/README.md index 65d461a..dc724dd 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ Without the hook you still get the read tools, but not deterministic enforcement ### CI / repo-native check — no editor required ```bash +# After `install --with-hook` (the installed path, works in any repo): +git diff --name-only | node .codex/workspacejson-codex-mcp/hooks/pre-edit-check.mjs --paths-stdin + +# From a checkout of this repo (the source path): git diff --name-only | node hooks/pre-edit-check.mjs --paths-stdin ``` @@ -101,15 +105,25 @@ code --install-extension workspacejson-codex-decorations-.vsix Demo and fixture repos may recommend the exact extension ID through `.vscode/extensions.json`; that's discovery only and never installs anything on its own. +### Generate workspace.json + +The MCP server and hook consume `.agents/workspace.json`. The reference generator is [`agents-audit`](https://github.com/workspace-json/agents-audit) — a separate package in the same org: + +```bash +npx agents-audit@0.4.3 generate . +``` + +This writes `.agents/workspace.json` with repository topology and hygiene. Today, `generated.fileIndex` is empty and `manual` fragility/co-change evidence is not auto-generated — those remain human-authored (ASSERTED tier at minimum, OBSERVED when backed by evidence records). The generator does not guess risk signals; guessed churn has no evidence records, remains ASSERTED, and cannot block. See [`fixture/`](fixture/) for a worked example with manual evidence. + ### Verify in two minutes -From a repo that has a committed `.agents/workspace.json`: +`generate` (above) writes repository topology only — no fragility or co-change evidence, so a freshly generated `workspace.json` has nothing to deny yet. To see the deny path itself, use this repo's `fixture/`, whose `manual` evidence is hand-authored for exactly this demo: -1. In Codex, ask it to edit a file the workspace flags as fragile. -2. Watch the hook refuse the patch, citing the recorded evidence and the co-change partner the change left out. -3. Ask Codex to include the partner and retry — the edit proceeds. +1. Open `fixture/` in Codex. In Codex, ask it to edit `src/routes/checkout.ts`. +2. Watch the hook refuse the patch, citing the recorded evidence and the co-change partners the change left out. +3. Ask Codex to include both partners and retry — the edit proceeds. -No configuration beyond step 1 above. The `fixture/` in this repo reproduces the exact denial shown in the demo. +No configuration beyond step 1 above. On your own repo, the same deny path activates once you've authored `manual.fragileFiles` / `manual.coChangePatterns` yourself — see [`docs/workspace-contract.md`](docs/workspace-contract.md). diff --git a/extension/assets/walkthrough/generate.md b/extension/assets/walkthrough/generate.md new file mode 100644 index 0000000..44dbad8 --- /dev/null +++ b/extension/assets/walkthrough/generate.md @@ -0,0 +1,5 @@ +Run `npx agents-audit@0.4.3 generate .` in your repo root. + +This writes `.agents/workspace.json` with repository topology and hygiene. Today, `generated.fileIndex` is empty — the generator does not guess risk signals. `manual` fragility and co-change evidence remain human-authored: ASSERTED at minimum, OBSERVED when backed by evidence records. + +[Generate Intelligence](command:workspacejson.generateIntelligence) diff --git a/extension/package.json b/extension/package.json index c06e77f..c6f3dcc 100644 --- a/extension/package.json +++ b/extension/package.json @@ -123,6 +123,11 @@ "command": "workspacejson.openWalkthrough", "title": "Open Getting Started Walkthrough", "category": "workspace.json" + }, + { + "command": "workspacejson.generateIntelligence", + "title": "Generate Intelligence", + "category": "workspace.json" } ], "menus": { @@ -137,7 +142,7 @@ { "view": "workspacejsonCodexChangeset", "when": "workspacejsonCodex.viewState == noFile", - "contents": "No `.agents/workspace.json` was found.\n\nThe generator derives repository structure; humans or an observer author evidence. It leaves `manual` empty on purpose: guessed churn signals have no evidence records, remain ASSERTED, and cannot block. See [`fixture/`](https://github.com/workspace-json/codex-mcp/tree/main/fixture) for a worked example.\n\n[Getting Started](command:workspacejson.openWalkthrough)" + "contents": "No `.agents/workspace.json` was found.\n\nGenerate one with `npx agents-audit@0.4.3 generate .` — it writes repository topology and hygiene. Today, `fileIndex` is empty and `manual` fragility/co-change evidence is human-authored (ASSERTED tier, not auto-generated).\n\n[Generate Intelligence](command:workspacejson.generateIntelligence)\n\n[Getting Started](command:workspacejson.openWalkthrough)" }, { "view": "workspacejsonCodexChangeset", @@ -161,6 +166,17 @@ "title": "workspace.json: Getting Started", "description": "Find the current-change intelligence view, read the deterministic decision, resolve it, and use advisory review.", "steps": [ + { + "id": "generate", + "title": "Generate workspace.json", + "description": "The MCP server and hook consume `.agents/workspace.json`. Generate one with `npx agents-audit@0.4.3 generate .` — it writes repository topology and hygiene. Today, `fileIndex` is empty and `manual` fragility/co-change evidence is human-authored (ASSERTED tier, not auto-generated).\n[Generate Intelligence](command:workspacejson.generateIntelligence)", + "media": { + "markdown": "assets/walkthrough/generate.md" + }, + "completionEvents": [ + "onContext:workspacejsonCodex.viewState != noFile" + ] + }, { "id": "find", "title": "Open the workspace.json intelligence view", diff --git a/extension/src/commandIds.ts b/extension/src/commandIds.ts index e50f951..94b3248 100644 --- a/extension/src/commandIds.ts +++ b/extension/src/commandIds.ts @@ -13,6 +13,7 @@ export const COMMAND_IDS = { focusCurrentChange: "workspacejson.focusCurrentChange", openIntelligenceFile: "workspacejson.openIntelligenceFile", openWalkthrough: "workspacejson.openWalkthrough", + generateIntelligence: "workspacejson.generateIntelligence", } as const; /** The published walkthrough id: `.#`. */ diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 2714a54..389bb4b 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -7,6 +7,7 @@ import type { WorkspaceIntelligenceModel } from "./workspaceIntelligence.js"; const ARTIFACT_PATH = ".agents/workspace.json"; const REVIEW_TERMINAL = "workspace.json review"; const VERIFY_TERMINAL = "workspace.json verify"; +const GENERATE_TERMINAL = "workspace.json generate"; function firstFolder(): vscode.WorkspaceFolder | undefined { return vscode.workspace.workspaceFolders?.[0]; @@ -118,6 +119,19 @@ export function registerCommands(model: WorkspaceIntelligenceModel, context: vsc ); }); + register(COMMAND_IDS.generateIntelligence, () => { + const term = terminal(GENERATE_TERMINAL); + // Pre-fill only — the developer runs it. Not auto-executed. + // The extension is a pure consumer: it types a command, the user presses + // enter, and the canonical agents-audit generator runs. The extension + // writes nothing to the working tree. + term.sendText("npx agents-audit@0.4.3 generate .", false); + term.show(); + void vscode.window.showInformationMessage( + "workspace.json: generate command staged in the terminal. Review it, then press Enter to run. Today, generate writes repository topology and hygiene with an empty fileIndex; manual evidence is human-authored.", + ); + }); + register(COMMAND_IDS.inspectReceipt, async () => { const view = viewOf(model); const dir = view?.review.artifactDir; diff --git a/package.json b/package.json index b094bc2..7190998 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,9 @@ "test:watch": "vitest --exclude '.claude/**'", "smoke": "node scripts/smoke.mjs", "check:structure": "node scripts/check-repo-structure.mjs", + "check:generator-version": "node scripts/check-generator-version.mjs", "pack:check": "npm pack --dry-run && npx publint", - "check": "npm run check:structure && npm run typecheck && npm run lint && npm run build && npm run test && npm run smoke", + "check": "npm run check:structure && npm run check:generator-version && npm run typecheck && npm run lint && npm run build && npm run test && npm run smoke", "verify": "npm run check", "prepublishOnly": "npm run check && npm run build:extension && npm run pack:check" }, diff --git a/scripts/check-generator-version.mjs b/scripts/check-generator-version.mjs new file mode 100644 index 0000000..9dd21a9 --- /dev/null +++ b/scripts/check-generator-version.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// The reference generator (`agents-audit`) is invoked by version-pinned command +// string, copy-pasted across README, the extension manifest, its source, its +// walkthrough media, and the installer receipt — a Second Copy by construction +// (see META-140). This +// gate does not hand-sync those strings; it asserts they stay in sync with +// each other and with what the registry actually publishes, so drift fails +// loudly instead of shipping a stale pin. +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, ".."); + +const GENERATOR_PACKAGE = "agents-audit"; +const VERSION_PATTERN = /agents-audit@(\d+\.\d+\.\d+)/g; + +const SURFACES = [ + "README.md", + "extension/package.json", + "extension/src/commands.ts", + "scripts/install.mjs", + "extension/assets/walkthrough/generate.md", +]; + +/** + * @param {string} rootDir + * @param {string[]} files + * @returns {{ file: string, version: string }[]} + */ +export function collectVersionRefs(rootDir, files) { + const refs = []; + for (const file of files) { + const text = readFileSync(join(rootDir, file), "utf8"); + for (const match of text.matchAll(VERSION_PATTERN)) { + refs.push({ file, version: match[1] }); + } + } + return refs; +} + +/** + * Pure comparison logic — no filesystem or network — so it's unit-testable + * without hitting the registry. + * @param {{ file: string, version: string }[]} refs + * @param {string | null} registryVersion null when the registry couldn't be reached + * @returns {string[]} violation messages; empty when clean + */ +export function findVersionMismatches(refs, registryVersion) { + if (refs.length === 0) return []; + + const violations = []; + const distinctVersions = [...new Set(refs.map((r) => r.version))]; + + if (distinctVersions.length > 1) { + const detail = refs.map((r) => `${r.file} -> ${r.version}`).join(", "); + violations.push( + `${GENERATOR_PACKAGE} version disagrees across surfaces (${detail}). Pin one version in every reference.`, + ); + return violations; // cross-surface disagreement already fails; skip the registry check + } + + const pinned = distinctVersions[0]; + if (registryVersion && pinned !== registryVersion) { + const surfaceList = refs.map((r) => r.file).join(", "); + violations.push( + `${GENERATOR_PACKAGE}@${pinned} is pinned in every surface (${surfaceList}), but the npm registry's current version is ${registryVersion}. Update every reference to ${registryVersion}.`, + ); + } + + return violations; +} + +async function fetchRegistryVersion(pkg) { + const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, { signal: AbortSignal.timeout(5000) }); + if (!res.ok) throw new Error(`registry responded ${res.status}`); + const data = await res.json(); + return data.version; +} + +async function main() { + const refs = collectVersionRefs(root, SURFACES); + + let registryVersion = null; + try { + registryVersion = await fetchRegistryVersion(GENERATOR_PACKAGE); + } catch (error) { + console.warn( + `check:generator-version: could not reach the npm registry for ${GENERATOR_PACKAGE} (${error.message}); skipping the live-version check, still checking cross-surface agreement.`, + ); + } + + const violations = findVersionMismatches(refs, registryVersion); + if (violations.length > 0) { + console.error(`${GENERATOR_PACKAGE} version check failed:`); + for (const violation of violations) console.error(` - ${violation}`); + process.exitCode = 1; + return; + } + + console.log( + refs.length > 0 + ? `${GENERATOR_PACKAGE} version check passed (${refs.length} reference(s), pinned to ${refs[0].version}).` + : `${GENERATOR_PACKAGE} version check passed (no references found).`, + ); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/scripts/install.mjs b/scripts/install.mjs index ad97f1a..6305937 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -448,6 +448,15 @@ async function runInstall(opts = {}) { await mkdir(codexDir, { recursive: true }); await writeAtomic(configPath, content); console.log(`Wrote ${configPath}`); + if (withHook) { + // HAC-203: print, never write. .codex/config.toml is commonly a tracked, + // team-shared file (this repo does exactly that) — only the vendored + // runtime this installer owns is worth ignoring, and only the user's own + // .gitignore should say so. + console.log(""); + console.log("Add this to .gitignore:"); + console.log(" .codex/workspacejson-codex-mcp/"); + } console.log(""); console.log("Next steps:"); console.log(" 1. Restart Codex."); @@ -472,10 +481,20 @@ async function runInstall(opts = {}) { // Compact install receipt: what integrated, and where to go next. const pad = (label) => label.padEnd(28); + const artifactPath = resolve(repoRoot, ".agents", "workspace.json"); + const artifactExists = existsSync(artifactPath); console.log(""); - console.log("workspace.json installed"); + console.log("workspace.json MCP/hook installed"); console.log(` ${pad("Repository integration")}PASS`); console.log(` ${pad("Deterministic hook")}${withHook ? "PASS" : "not requested (--with-hook)"}`); + console.log(` ${pad("Intelligence artifact")}${artifactExists ? "PASS" : "NOT FOUND"}`); + if (!artifactExists) { + console.log(""); + console.log(" .agents/workspace.json does not exist yet. Generate it:"); + console.log(" npx agents-audit@0.4.3 generate ."); + console.log(" Today, generate writes repository topology and hygiene with an empty"); + console.log(" fileIndex; manual fragility and co-change evidence is human-authored."); + } if (withExtension) console.log(` ${pad("VS Code extension")}${extension.status}`); if (withExtension && extension.status === "PASS") { console.log(""); diff --git a/tests/unit/check-generator-version.test.ts b/tests/unit/check-generator-version.test.ts new file mode 100644 index 0000000..9f7dc47 --- /dev/null +++ b/tests/unit/check-generator-version.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { findVersionMismatches } from "../../scripts/check-generator-version.mjs"; + +describe("findVersionMismatches", () => { + it("passes with no references", () => { + expect(findVersionMismatches([], null)).toEqual([]); + }); + + it("passes when every surface agrees and the registry wasn't reachable", () => { + const refs = [ + { file: "README.md", version: "0.4.3" }, + { file: "scripts/install.mjs", version: "0.4.3" }, + ]; + expect(findVersionMismatches(refs, null)).toEqual([]); + }); + + it("passes when every surface agrees and matches the registry", () => { + const refs = [ + { file: "README.md", version: "0.4.3" }, + { file: "scripts/install.mjs", version: "0.4.3" }, + ]; + expect(findVersionMismatches(refs, "0.4.3")).toEqual([]); + }); + + it("fails when surfaces disagree with each other", () => { + const refs = [ + { file: "README.md", version: "0.4.3" }, + { file: "scripts/install.mjs", version: "0.4.2" }, + ]; + const violations = findVersionMismatches(refs, null); + expect(violations).toHaveLength(1); + expect(violations[0]).toMatch(/disagrees across surfaces/); + expect(violations[0]).toContain("README.md -> 0.4.3"); + expect(violations[0]).toContain("scripts/install.mjs -> 0.4.2"); + }); + + it("fails when every surface agrees but is behind the registry", () => { + const refs = [ + { file: "README.md", version: "0.4.3" }, + { file: "scripts/install.mjs", version: "0.4.3" }, + ]; + const violations = findVersionMismatches(refs, "0.4.4"); + expect(violations).toHaveLength(1); + expect(violations[0]).toMatch(/registry's current version is 0\.4\.4/); + }); + + it("reports only the cross-surface disagreement, not also a registry mismatch, when both would fail", () => { + const refs = [ + { file: "README.md", version: "0.4.3" }, + { file: "scripts/install.mjs", version: "0.4.2" }, + ]; + const violations = findVersionMismatches(refs, "0.4.4"); + expect(violations).toHaveLength(1); + expect(violations[0]).toMatch(/disagrees across surfaces/); + }); +}); From 199bbdf242551c6f38a1bb61765b5e617290fd54 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Sun, 19 Jul 2026 02:50:49 -0400 Subject: [PATCH 06/10] fix(gate): drop generator version-pin registry match-latest half (HAC-204) The pin is the contract; the registry is the world. Failing CI when an upstream agents-audit release moves ahead of a deliberate pin makes every upstream publish an incident in this downstream repo. Remove the registry fetch and the pinned!=latest failure; keep the cross-surface agreement check (every surface pins the same version as every other). Reconciling the pin against the *installed* agents-audit version is the redesign left to HAC-204. A regression test locks in the intentional non-behavior: a pin behind the registry latest is not a violation. --- scripts/check-generator-version.mjs | 69 +++++++--------------- tests/unit/check-generator-version.test.ts | 35 +++-------- 2 files changed, 30 insertions(+), 74 deletions(-) diff --git a/scripts/check-generator-version.mjs b/scripts/check-generator-version.mjs index 9dd21a9..e3a1182 100644 --- a/scripts/check-generator-version.mjs +++ b/scripts/check-generator-version.mjs @@ -1,11 +1,16 @@ #!/usr/bin/env node -// The reference generator (`agents-audit`) is invoked by version-pinned command -// string, copy-pasted across README, the extension manifest, its source, its -// walkthrough media, and the installer receipt — a Second Copy by construction -// (see META-140). This -// gate does not hand-sync those strings; it asserts they stay in sync with -// each other and with what the registry actually publishes, so drift fails -// loudly instead of shipping a stale pin. +// The reference generator (`agents-audit`) is invoked by a version-pinned +// command string, copy-pasted across README, the extension manifest, its +// source, its walkthrough media, and the installer receipt — a Second Copy by +// construction (see HAC-204). This gate does not hand-sync those strings; it +// asserts they stay in sync with EACH OTHER, so a stale pin in one surface +// fails loudly instead of shipping. +// +// It deliberately does NOT judge the pin against the npm registry's latest: +// the pin is the contract, the registry is the world, and a downstream repo's +// CI must not turn red merely because an upstream release moved ahead of a +// deliberate pin. Reconciling the pin against the *installed* agents-audit +// version is the redesign tracked in HAC-204. import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -41,57 +46,25 @@ export function collectVersionRefs(rootDir, files) { } /** - * Pure comparison logic — no filesystem or network — so it's unit-testable - * without hitting the registry. + * Pure comparison logic — no filesystem or network. Asserts every surface pins + * the same generator version as every other surface. It does NOT judge that + * version against the registry (see file header / HAC-204). * @param {{ file: string, version: string }[]} refs - * @param {string | null} registryVersion null when the registry couldn't be reached * @returns {string[]} violation messages; empty when clean */ -export function findVersionMismatches(refs, registryVersion) { +export function findVersionMismatches(refs) { if (refs.length === 0) return []; - const violations = []; const distinctVersions = [...new Set(refs.map((r) => r.version))]; + if (distinctVersions.length <= 1) return []; - if (distinctVersions.length > 1) { - const detail = refs.map((r) => `${r.file} -> ${r.version}`).join(", "); - violations.push( - `${GENERATOR_PACKAGE} version disagrees across surfaces (${detail}). Pin one version in every reference.`, - ); - return violations; // cross-surface disagreement already fails; skip the registry check - } - - const pinned = distinctVersions[0]; - if (registryVersion && pinned !== registryVersion) { - const surfaceList = refs.map((r) => r.file).join(", "); - violations.push( - `${GENERATOR_PACKAGE}@${pinned} is pinned in every surface (${surfaceList}), but the npm registry's current version is ${registryVersion}. Update every reference to ${registryVersion}.`, - ); - } - - return violations; + const detail = refs.map((r) => `${r.file} -> ${r.version}`).join(", "); + return [`${GENERATOR_PACKAGE} version disagrees across surfaces (${detail}). Pin one version in every reference.`]; } -async function fetchRegistryVersion(pkg) { - const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, { signal: AbortSignal.timeout(5000) }); - if (!res.ok) throw new Error(`registry responded ${res.status}`); - const data = await res.json(); - return data.version; -} - -async function main() { +function main() { const refs = collectVersionRefs(root, SURFACES); - - let registryVersion = null; - try { - registryVersion = await fetchRegistryVersion(GENERATOR_PACKAGE); - } catch (error) { - console.warn( - `check:generator-version: could not reach the npm registry for ${GENERATOR_PACKAGE} (${error.message}); skipping the live-version check, still checking cross-surface agreement.`, - ); - } - - const violations = findVersionMismatches(refs, registryVersion); + const violations = findVersionMismatches(refs); if (violations.length > 0) { console.error(`${GENERATOR_PACKAGE} version check failed:`); for (const violation of violations) console.error(` - ${violation}`); diff --git a/tests/unit/check-generator-version.test.ts b/tests/unit/check-generator-version.test.ts index 9f7dc47..6952b63 100644 --- a/tests/unit/check-generator-version.test.ts +++ b/tests/unit/check-generator-version.test.ts @@ -3,23 +3,15 @@ import { findVersionMismatches } from "../../scripts/check-generator-version.mjs describe("findVersionMismatches", () => { it("passes with no references", () => { - expect(findVersionMismatches([], null)).toEqual([]); + expect(findVersionMismatches([])).toEqual([]); }); - it("passes when every surface agrees and the registry wasn't reachable", () => { + it("passes when every surface agrees", () => { const refs = [ { file: "README.md", version: "0.4.3" }, { file: "scripts/install.mjs", version: "0.4.3" }, ]; - expect(findVersionMismatches(refs, null)).toEqual([]); - }); - - it("passes when every surface agrees and matches the registry", () => { - const refs = [ - { file: "README.md", version: "0.4.3" }, - { file: "scripts/install.mjs", version: "0.4.3" }, - ]; - expect(findVersionMismatches(refs, "0.4.3")).toEqual([]); + expect(findVersionMismatches(refs)).toEqual([]); }); it("fails when surfaces disagree with each other", () => { @@ -27,30 +19,21 @@ describe("findVersionMismatches", () => { { file: "README.md", version: "0.4.3" }, { file: "scripts/install.mjs", version: "0.4.2" }, ]; - const violations = findVersionMismatches(refs, null); + const violations = findVersionMismatches(refs); expect(violations).toHaveLength(1); expect(violations[0]).toMatch(/disagrees across surfaces/); expect(violations[0]).toContain("README.md -> 0.4.3"); expect(violations[0]).toContain("scripts/install.mjs -> 0.4.2"); }); - it("fails when every surface agrees but is behind the registry", () => { + it("does not fail merely because the pin is behind the registry's latest", () => { + // Intentional non-behavior: the pin is the contract; an upstream release + // moving ahead must not red downstream CI. Reconciliation against the + // installed version is the redesign tracked in HAC-204. const refs = [ { file: "README.md", version: "0.4.3" }, { file: "scripts/install.mjs", version: "0.4.3" }, ]; - const violations = findVersionMismatches(refs, "0.4.4"); - expect(violations).toHaveLength(1); - expect(violations[0]).toMatch(/registry's current version is 0\.4\.4/); - }); - - it("reports only the cross-surface disagreement, not also a registry mismatch, when both would fail", () => { - const refs = [ - { file: "README.md", version: "0.4.3" }, - { file: "scripts/install.mjs", version: "0.4.2" }, - ]; - const violations = findVersionMismatches(refs, "0.4.4"); - expect(violations).toHaveLength(1); - expect(violations[0]).toMatch(/disagrees across surfaces/); + expect(findVersionMismatches(refs)).toEqual([]); }); }); From b194bcbf9ce3191a6605c605a4eb5b3da83b3953 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Sun, 19 Jul 2026 04:05:11 -0400 Subject: [PATCH 07/10] release: bump @workspacejson/codex-mcp to 0.1.6 Stages the 0.1.6 version across the three sync points (package.json, package-lock, runtime VERSION in src/index.ts) so the release cut is a single merge. Extension stays 0.1.2 (separate track; republish decision tracked in HAC-200). Proven demo path is against agents-audit@0.4.3. --- package-lock.json | 4 ++-- package.json | 13 +++++++++++-- src/index.ts | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ddb8b3..5325fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@workspacejson/codex-mcp", - "version": "0.1.5", + "version": "0.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@workspacejson/codex-mcp", - "version": "0.1.5", + "version": "0.1.6", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.20.0", diff --git a/package.json b/package.json index 7190998..63eeb0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@workspacejson/codex-mcp", - "version": "0.1.5", + "version": "0.1.6", "description": "MCP server that surfaces workspace.json fragility and co-change intelligence to OpenAI Codex before it edits code.", "license": "Apache-2.0", "type": "module", @@ -34,7 +34,16 @@ "import": "./dist/index.js" } }, - "files": ["dist", "hooks", "scripts/install.mjs", ".codex-plugin", ".mcp.json", "vsix", "README.md", "LICENSE"], + "files": [ + "dist", + "hooks", + "scripts/install.mjs", + ".codex-plugin", + ".mcp.json", + "vsix", + "README.md", + "LICENSE" + ], "scripts": { "build": "tsc", "build:extension": "rm -rf extension/node_modules extension/*.vsix vsix/*.vsix && npm --prefix extension ci && npm --prefix extension run package && mkdir -p vsix && cp extension/workspacejson-codex-decorations-*.vsix vsix/", diff --git a/src/index.ts b/src/index.ts index c0b19b6..0a100e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { isVerifyEnabled } from "./config.js"; import { SERVER_INSTRUCTIONS } from "./constants.js"; import { registerWorkspaceTools } from "./tools/workspace.js"; -const VERSION = "0.1.5"; +const VERSION = "0.1.6"; function buildServer(): McpServer { const server = new McpServer( From 3a6522ac661efbf71907ff24488356410eb96d2c Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Sun, 19 Jul 2026 10:26:45 -0400 Subject: [PATCH 08/10] chore(release): satisfy package formatting gate --- package.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/package.json b/package.json index 63eeb0b..2b816ee 100644 --- a/package.json +++ b/package.json @@ -34,16 +34,7 @@ "import": "./dist/index.js" } }, - "files": [ - "dist", - "hooks", - "scripts/install.mjs", - ".codex-plugin", - ".mcp.json", - "vsix", - "README.md", - "LICENSE" - ], + "files": ["dist", "hooks", "scripts/install.mjs", ".codex-plugin", ".mcp.json", "vsix", "README.md", "LICENSE"], "scripts": { "build": "tsc", "build:extension": "rm -rf extension/node_modules extension/*.vsix vsix/*.vsix && npm --prefix extension ci && npm --prefix extension run package && mkdir -p vsix && cp extension/workspacejson-codex-decorations-*.vsix vsix/", From 7df8e30dfe2668131064c9aa25c469744c1c73a3 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Sun, 19 Jul 2026 10:42:38 -0400 Subject: [PATCH 09/10] fix(gate): require every generator pin surface --- scripts/check-generator-version.mjs | 11 +++++++++-- tests/unit/check-generator-version.test.ts | 14 +++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/scripts/check-generator-version.mjs b/scripts/check-generator-version.mjs index e3a1182..686cd99 100644 --- a/scripts/check-generator-version.mjs +++ b/scripts/check-generator-version.mjs @@ -50,10 +50,17 @@ export function collectVersionRefs(rootDir, files) { * the same generator version as every other surface. It does NOT judge that * version against the registry (see file header / HAC-204). * @param {{ file: string, version: string }[]} refs + * @param {string[]} requiredSurfaces * @returns {string[]} violation messages; empty when clean */ -export function findVersionMismatches(refs) { - if (refs.length === 0) return []; +export function findVersionMismatches(refs, requiredSurfaces = SURFACES) { + const representedSurfaces = new Set(refs.map((ref) => ref.file)); + const missingSurfaces = requiredSurfaces.filter((file) => !representedSurfaces.has(file)); + if (missingSurfaces.length > 0) + return [ + `${GENERATOR_PACKAGE} pin is missing from required surface(s): ${missingSurfaces.join(", ")}. ` + + `Each declared surface must contain a version-pinned ${GENERATOR_PACKAGE}@x.y.z command.`, + ]; const distinctVersions = [...new Set(refs.map((r) => r.version))]; if (distinctVersions.length <= 1) return []; diff --git a/tests/unit/check-generator-version.test.ts b/tests/unit/check-generator-version.test.ts index 6952b63..5c4726c 100644 --- a/tests/unit/check-generator-version.test.ts +++ b/tests/unit/check-generator-version.test.ts @@ -2,8 +2,12 @@ import { describe, expect, it } from "vitest"; import { findVersionMismatches } from "../../scripts/check-generator-version.mjs"; describe("findVersionMismatches", () => { - it("passes with no references", () => { - expect(findVersionMismatches([])).toEqual([]); + it("fails when a required surface has no valid pinned generator reference", () => { + const refs = [{ file: "README.md", version: "0.4.3" }]; + const violations = findVersionMismatches(refs, ["README.md", "scripts/install.mjs"]); + expect(violations).toHaveLength(1); + expect(violations[0]).toMatch(/pin is missing from required surface/); + expect(violations[0]).toContain("scripts/install.mjs"); }); it("passes when every surface agrees", () => { @@ -11,7 +15,7 @@ describe("findVersionMismatches", () => { { file: "README.md", version: "0.4.3" }, { file: "scripts/install.mjs", version: "0.4.3" }, ]; - expect(findVersionMismatches(refs)).toEqual([]); + expect(findVersionMismatches(refs, ["README.md", "scripts/install.mjs"])).toEqual([]); }); it("fails when surfaces disagree with each other", () => { @@ -19,7 +23,7 @@ describe("findVersionMismatches", () => { { file: "README.md", version: "0.4.3" }, { file: "scripts/install.mjs", version: "0.4.2" }, ]; - const violations = findVersionMismatches(refs); + const violations = findVersionMismatches(refs, ["README.md", "scripts/install.mjs"]); expect(violations).toHaveLength(1); expect(violations[0]).toMatch(/disagrees across surfaces/); expect(violations[0]).toContain("README.md -> 0.4.3"); @@ -34,6 +38,6 @@ describe("findVersionMismatches", () => { { file: "README.md", version: "0.4.3" }, { file: "scripts/install.mjs", version: "0.4.3" }, ]; - expect(findVersionMismatches(refs)).toEqual([]); + expect(findVersionMismatches(refs, ["README.md", "scripts/install.mjs"])).toEqual([]); }); }); From b9ec0d94500ebd6dd73cb6bea519bbcae56726b3 Mon Sep 17 00:00:00 2001 From: Qwynn Marcelle Date: Sun, 19 Jul 2026 11:44:01 -0400 Subject: [PATCH 10/10] fix(installer): name walkthrough command accurately --- scripts/install.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install.mjs b/scripts/install.mjs index 6305937..2155d68 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -503,7 +503,7 @@ async function runInstall(opts = {}) { console.log(" 2. Select the workspace.json icon in the Activity Bar (the { • } glyph)."); console.log(' 3. Follow the "workspace.json: Getting Started" walkthrough (opens on first install).'); console.log(""); - console.log(' Reopen later: Command Palette → "workspace.json: Getting Started".'); + console.log(' Reopen later: Command Palette → "workspace.json: Open Getting Started Walkthrough".'); } }