Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -101,15 +105,25 @@ code --install-extension workspacejson-codex-decorations-<version>.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).

</details>

Expand Down
3 changes: 3 additions & 0 deletions extension/.vscodeignore
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions extension/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions extension/SUPPORT.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions extension/assets/walkthrough/generate.md
Original file line number Diff line number Diff line change
@@ -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)
58 changes: 56 additions & 2 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
}
],
Expand All @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions extension/src/commandIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<publisher>.<extension>#<walkthroughId>`. */
Expand Down
23 changes: 22 additions & 1 deletion extension/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
}
});

Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
91 changes: 91 additions & 0 deletions scripts/check-generator-version.mjs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading
Loading