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/.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..5fd754b --- /dev/null +++ b/extension/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## Unreleased + +### Added + +- 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 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 + +- 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 four 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/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 f007b87..c6f3dcc 100644 --- a/extension/package.json +++ b/extension/package.json @@ -11,6 +11,44 @@ "categories": [ "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" @@ -83,7 +121,12 @@ }, { "command": "workspacejson.openWalkthrough", - "title": "Getting Started", + "title": "Open Getting Started Walkthrough", + "category": "workspace.json" + }, + { + "command": "workspacejson.generateIntelligence", + "title": "Generate Intelligence", "category": "workspace.json" } ], @@ -99,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", @@ -123,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 3b84f8c..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]; @@ -60,7 +61,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); } }); @@ -111,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-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 b094bc2..2b816ee 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", @@ -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..686cd99 --- /dev/null +++ b/scripts/check-generator-version.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +// 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"; + +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. 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[]} requiredSurfaces + * @returns {string[]} violation messages; empty when clean + */ +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 []; + + const detail = refs.map((r) => `${r.file} -> ${r.version}`).join(", "); + return [`${GENERATOR_PACKAGE} version disagrees across surfaces (${detail}). Pin one version in every reference.`]; +} + +function main() { + const refs = collectVersionRefs(root, SURFACES); + const violations = findVersionMismatches(refs); + 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..2155d68 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(""); @@ -484,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".'); } } 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( diff --git a/tests/unit/check-generator-version.test.ts b/tests/unit/check-generator-version.test.ts new file mode 100644 index 0000000..5c4726c --- /dev/null +++ b/tests/unit/check-generator-version.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { findVersionMismatches } from "../../scripts/check-generator-version.mjs"; + +describe("findVersionMismatches", () => { + 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", () => { + const refs = [ + { file: "README.md", version: "0.4.3" }, + { file: "scripts/install.mjs", version: "0.4.3" }, + ]; + expect(findVersionMismatches(refs, ["README.md", "scripts/install.mjs"])).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, ["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"); + expect(violations[0]).toContain("scripts/install.mjs -> 0.4.2"); + }); + + 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" }, + ]; + expect(findVersionMismatches(refs, ["README.md", "scripts/install.mjs"])).toEqual([]); + }); +});