diff --git a/.claude/settings.json b/.claude/settings.json index c0dfef0..705b93f 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,8 +1,12 @@ { - "//": "forgekit dogfoods its own plugin during local dev. These hooks mirror hooks/hooks.json but resolve through ${CLAUDE_PROJECT_DIR} (this repo) instead of ${CLAUDE_PLUGIN_ROOT}, so the guards run without a marketplace install. Every guard is advisory/fail-open; the completion gate honors FORGE_STOPGATE=0.", + "//": "forgekit dogfoods its own plugin during local dev. These hooks mirror hooks/hooks.json but resolve through ${CLAUDE_PROJECT_DIR} (this repo) instead of ${CLAUDE_PLUGIN_ROOT}, so the guards run without a marketplace install. Exec form via global/guards/run.mjs: node finds bash (Git Bash on Windows, where bash is usually NOT on PATH) and passes stdin/stdout/exit code through untouched. Every guard is advisory/fail-open; the completion gate honors FORGE_STOPGATE=0.", "statusLine": { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/statusline.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/statusline.sh" + ] }, "hooks": { "SessionStart": [ @@ -11,15 +15,28 @@ "hooks": [ { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/recall-load.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/recall-load.sh" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh session-start" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh", + "session-start" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/session-start.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-start.sh" + ] } ] } @@ -29,11 +46,21 @@ "hooks": [ { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh prompt" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh", + "prompt" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh preflight" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh", + "preflight" + ] } ] } @@ -44,15 +71,27 @@ "hooks": [ { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/protect-paths.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/protect-paths.sh" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cost-budget.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cost-budget.sh" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/doom-loop.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/doom-loop.sh" + ] } ] }, @@ -61,7 +100,12 @@ "hooks": [ { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh pre-edit" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh", + "pre-edit" + ] } ] } @@ -72,11 +116,20 @@ "hooks": [ { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/format-on-edit.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/format-on-edit.sh" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh capture" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh", + "capture" + ] } ] }, @@ -85,11 +138,20 @@ "hooks": [ { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/secret-redact.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/secret-redact.sh" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh capture" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh", + "capture" + ] } ] } @@ -99,15 +161,28 @@ "hooks": [ { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/completion-gate.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/completion-gate.sh" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/lean-guard.sh" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/lean-guard.sh" + ] }, { "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}\"/global/guards/cortex.sh stop" + "command": "node", + "args": [ + "${CLAUDE_PROJECT_DIR}/global/guards/run.mjs", + "${CLAUDE_PROJECT_DIR}/global/guards/cortex.sh", + "stop" + ] } ] } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 4bef723..a3027b5 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -15,7 +15,7 @@ "interface": { "displayName": "Forge", "shortDescription": "Cognitive substrate and one config for every AI coding tool.", - "longDescription": "Forge adds a pre-action cognitive substrate for coding agents: assumption gating, transparent model routing, impact prediction, scope decomposition, memory/learning context, and verification checklists. It also emits shared rules and MCP config for Codex, Claude Code, Cursor, Gemini, Aider, Copilot, Windsurf/Devin, Zed, Continue, and Roo.", + "longDescription": "Forge adds a pre-action cognitive substrate for coding agents: assumption gating, transparent model routing, impact prediction, scope decomposition, memory/learning context, and verification checklists. It also emits shared rules and MCP config for Codex, Claude Code, Cursor, Gemini, Aider, Copilot, Windsurf/Devin, Zed, Continue, OpenClaw, and Roo.", "developerName": "CodeWithJuber", "category": "Productivity", "capabilities": ["MCP", "Skills", "Code Review", "Verification"], diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 171eb68..f583f0a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -29,7 +29,7 @@ for the full list. Brand stored as **one token** (the `brand` key in `brand.json`); rebrand = 1 edit. - **Distributable id = `forgekit`** (npm package + marketplace id) — fixed even if the brand token changes, so a rename never breaks install. -- **Scope = full multi-tool day 1** — nine tools plus MCP, from one canonical source. +- **Scope = full multi-tool day 1** — ten tools plus MCP, from one canonical source. - **Install = all three channels** (plugin + hardened installer + npm CLI), all three pointing at the _same_ tree ("one tree, three front doors"). - **Own `lean` + `atlas`** — as _thin layers over proven primitives_, not @@ -443,7 +443,7 @@ guards through `${CLAUDE_PROJECT_DIR}`. ## Verified cross-tool emit matrix -_(All rows confirmed against vendor docs.)_ Forge emits config for **nine tools**, plus +_(All rows confirmed against vendor docs.)_ Forge emits config for **ten tools**, plus an **MCP server** for Roo Code and VS Code. | Tool | Native target | How Forge emits | @@ -457,10 +457,39 @@ an **MCP server** for Roo Code and VS Code. | **Windsurf/Devin** | `AGENTS.md` auto-discovered; caps 6k/12k chars | Root `AGENTS.md` under caps; detect `.windsurf` vs `.devin` at init | | **Zed** | first match of a precedence list incl. `AGENTS.md` | Emit `AGENTS.md` + doctor flags any earlier-precedence legacy file shadowing it | | **Continue** | `.continue/rules/*.md` + `.continue/mcpServers/*.yaml` | Emit a rules file plus the Forge MCP server config | +| **OpenClaw** | execution-folder `AGENTS.md` as project context; MCP registry is global | Rely on root `AGENTS.md`; write an OpenClaw-shaped `.openclaw/mcp.json` the operator applies with one `openclaw mcp add` | Roo Code and VS Code receive the Forge MCP server via `forge init` (`.roo/mcp.json`, `.vscode/mcp.json`) rather than a rules file. +### OpenClaw: what is automatic and what is not + +OpenClaw appends the execution folder's `AGENTS.md` after its configured agent-workspace +files as project context, so the canonical rules reach it with **no** extra instruction +file — the same deal as Codex, Cursor and Copilot. Only `AGENTS.md` travels this way: +OpenClaw deliberately does not load `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md` or +`BOOTSTRAP.md` from the execution folder, so anything Forge wants OpenClaw to read has to +be inside the canonical body. + +For the **config compiler path**, MCP is deliberately not automatic. OpenClaw's server +registry is `mcp.servers` in the user's global `~/.openclaw/openclaw.json`; Forge never +writes to another tool's global config. Instead `forge sync` emits a repo-local, +OpenClaw-shaped fragment at `.openclaw/mcp.json` and reports the exact command that +registers it: + +```bash +openclaw mcp add forge-cortex --command forge --arg cortex-mcp +openclaw mcp doctor forge-cortex --probe # prove it starts and lists tools +``` + +There is also a separate **bundle installation path**. The published package already ships +`.codex-plugin/plugin.json`, `global/tools`, and `.mcp.json`; OpenClaw auto-detects that +layout as a Codex bundle. Installing a trusted local directory or packed archive through +`openclaw plugins install` loads Forge's skills and bundle-scoped `forge-cortex` MCP server, +so the manual global registration above is unnecessary for that installation. This does not +turn Forge's Claude `hooks/hooks.json` automation into OpenClaw guards: only OpenClaw-style +hook packs execute. Forge therefore provides no ambient pre-action guard on OpenClaw. + ## Repo layout — one tree, three front doors ``` @@ -552,20 +581,22 @@ from the tree it describes. ```mermaid %%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%% flowchart LR - test["test
100 files"] - src["src
94 files"] + test["test
106 files"] + src["src
97 files"] landing["landing
61 files"] research["research
35 files"] + global["global
3 files"] bench["bench
2 files"] - global["global
2 files"] scripts["scripts
2 files"] docs["docs
1 file"] examples["examples
1 file"] - test -- 195 --> src + test -- 206 --> src bench -- 7 --> src examples -- 4 --> src test -- 2 --> scripts scripts --> src + src --> global test --> bench + test --> global ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index c4dd9ea..5b6f09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,57 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- **OpenClaw is a first-class emit target — the compiler's tenth tool.** Instructions need + no new file: OpenClaw appends the execution folder's `AGENTS.md` after its configured + agent-workspace files as project context, so the canonical source reaches it the same way + it reaches Codex, Cursor and Copilot. MCP is registered explicitly rather than silently: + OpenClaw keeps its server registry in the user's global `~/.openclaw/openclaw.json`, which + Forge will not write to, so `forge sync` emits an OpenClaw-shaped fragment to + `.openclaw/mcp.json` and reports the exact enabling command + (`openclaw mcp add forge-cortex --command forge --arg cortex-mcp`). `.openclaw/mcp.json` + is an ordinary managed MCP target: idempotent, per-target ownership (a same-name server + you wrote yourself is preserved until `--adopt`), and reversible via + `forge integrations remove`. `openclaw` is now selectable and auto-detected by + `forge tools`. The packaged `.codex-plugin/plugin.json`, `global/tools`, and `.mcp.json` + also form an OpenClaw-compatible Codex bundle: installing a trusted checkout or packed + archive loads Forge's skills and bundle-scoped MCP server without the config-only path's + manual global registration. Forge installs **nothing** into OpenClaw's hook system — there + are no ambient guards there, only `AGENTS.md`/skill text and the MCP tools. + +### Changed + +- **MCP targets address their server bucket by dotted key path.** `emit/mcp.js` resolved a + single top-level key (`mcpServers`, `servers`, `context_servers`); OpenClaw nests its + registry under `mcp.servers`. The resolver now walks a path, creating missing objects only + on write, and refuses to restructure a file where any step already holds a non-object — + that shape is the user's and is reported, never rewritten. + +### Fixed + +- **Claude Code hooks no longer fail on Windows with `spawn bash ENOENT`.** Every Forge hook + (the plugin's `hooks/hooks.json`, the `settings.template.json` that `forge init` merges, the + statusline) was exec form with `command: "bash"`. Exec-form hooks are spawned directly — no + shell, a plain `PATH` lookup — and a default Git for Windows install puts `git.exe` on `PATH` + (`Git\cmd`) but not `bash.exe` (`Git\bin`, `Git\usr\bin`), so SessionStart and every other + guard died before it ran. Hooks now spawn the zero-dependency launcher + `global/guards/run.mjs` (`node run.mjs .sh …`), which resolves bash — `FORGE_BASH`, + `CLAUDE_CODE_GIT_BASH_PATH`, the Git install that owns `git` on `PATH`, the standard install + dirs, then `PATH` (never WSL's System32 `bash.exe`) — and passes stdin, stdout and the exit + code through verbatim, so exit-2 blocks are unchanged. POSIX behaviour is identical (`bash` + from `PATH`). `forge init` heals a Forge-owned install left in the old `bash` spelling in + place — ownership manifest included, so uninstall still reverses it — while a hand-written + hook at a Forge path is left alone; `forge doctor` shows the resolved bash, flags stale hooks + (`--fix` re-merges) and requires the launcher as an install asset. Regression tests cover the + Windows default-install `PATH` shape, paths with spaces on both OSes, the no-bash failure + mode (exit 1 + hint, never a fabricated block) and the packed archive. +- **`protect-paths` no longer dies (exit 1, fail-open) on machines without `jq`.** Its grep + fallback ran under `set -euo pipefail`, so a payload missing `command` (every Write/Edit) or + `file_path` (every Bash call) aborted the guard before it could decide — invisible in CI, where + `jq` is preinstalled, but the norm on Windows. The fallback now yields an empty field exactly + like the `jq` branch, so `.env` writes and destructive `rm` are blocked without `jq`. + ## [0.32.1] - 2026-08-22 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de94c9c..25cc35a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ small and dependency-free is the whole point, so please read this before opening - **Node.js ESM only.** All code is ES modules (`"type": "module"`). No CommonJS. - **Supported Node versions:** 20 and 22 (the `>=20` engines floor; Node 18 is EOL). - **Cross-tool first.** New behavior should work across the tools forgekit targets - (Claude Code, Codex, Cursor, Gemini, Aider, …), emitted from one source — not + (Claude Code, Codex, Cursor, Gemini, Aider, OpenClaw, …), emitted from one source — not Claude-only. Say so in the PR if a piece is unavoidably tool-specific. ## Getting started diff --git a/ONBOARDING.md b/ONBOARDING.md index 590e50c..384d378 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -5,7 +5,7 @@ context window, wiped every call — so it has no memory of what your team learn foresight about what an edit breaks, and no enforced guardrails. forgekit is the **cognitive substrate** that supplies exactly those three things, and it delivers them as native config to Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, -and Continue at once. Author the brain once; every tool reads it. +Continue, and OpenClaw at once. Author the brain once; every tool reads it. This page is the fast path: install, configure a repo, do a task, and watch the ledger start paying off on day two. @@ -51,9 +51,22 @@ cd ~/your-project forge init # emits AGENTS.md, CLAUDE.md, .gemini/settings.json, .aider.conf.yml … ``` -Now Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, and Continue all -read the **same** rules — each from its own native file (plus MCP server config for Roo -Code and VS Code). +Now Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, Continue, and +OpenClaw all read the **same** rules — each from its own native file (plus MCP server +config for Roo Code and VS Code). + +On OpenClaw the rules arrive automatically (it reads the execution folder's `AGENTS.md` +as project context), but the MCP server is a deliberate one-command step, because +OpenClaw's registry lives in your global `~/.openclaw/openclaw.json` and Forge does not +write there: + +```bash +openclaw mcp add forge-cortex --command forge --arg cortex-mcp +openclaw mcp doctor forge-cortex --probe +``` + +`forge sync` writes the same definition to `.openclaw/mcp.json` so you can review or +merge it instead of retyping the flags. Change a rule later by editing `source/rules.json` (or dropping a per-repo `.forge/rules.json`), then: @@ -172,7 +185,7 @@ Forge would rather ship an honest subset with a clear boundary than a vague clai - **Add a rule** → a bullet in `source/rules.json`, then `forge sync`. - **Add a tool (skill)** → `global/tools//SKILL.md` with `name` + `description` frontmatter. -- **Add a guard** → `global/guards/.sh` (source `_guardlib.sh` for fields + the lock), then wire it in `global/settings.template.json` and `hooks/hooks.json`. +- **Add a guard** → `global/guards/.sh` (source `_guardlib.sh` for fields + the lock), then wire it in `global/settings.template.json` and `hooks/hooks.json` as `node …/guards/run.mjs …/guards/.sh [mode]` — through the portable launcher (bash is not on `PATH` on Windows), never a bare `bash`. - **Rebrand** → edit `brand.json` (+ `package.json` bin, `.claude-plugin/plugin.json` name). Every command with worked examples and the full extension guide live in diff --git a/README.md b/README.md index ef18ec1..1bec4a4 100644 --- a/README.md +++ b/README.md @@ -22,23 +22,29 @@ delivers them into every tool you use. > content-addressed memory (we call it "proof-carrying memory" / PCM — see the honesty note > below), heuristic impact foresight, and guardrail hooks (automatic on Claude Code; > instructions and MCP tools elsewhere) — authored once and delivered as native config to -> Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, and Continue (plus MCP -> config for Roo and VS Code). Guardrails reduce risk; they are not a security sandbox. +> Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, Continue, and OpenClaw +> (plus MCP config for Roo and VS Code). Guardrails reduce risk; they are not a security +> sandbox. > **Status: beta — read before you rely on it.** > > - The core (`init`, `sync`, `substrate`, `impact`, `ledger`, guards) is tested and in daily > use; some flags may change before `1.0`. > - **Claude Code is the deepest-tested integration** (full plugin, ambient `UserPromptSubmit` -> guards). The other eight tools receive native config plus MCP tools, but have had less -> real-world exercise. +> guards). The other nine tools receive native config plus MCP tools, but have had less +> real-world exercise. On OpenClaw specifically, rules arrive via `AGENTS.md` project +> context; the config-only path uses a one-command MCP registration, while installing the +> package as a compatible Codex bundle loads its skills and bundle-scoped MCP server. +> Neither path provides ambient hooks (see +> [OpenClaw in ARCHITECTURE](ARCHITECTURE.md#openclaw-what-is-automatic-and-what-is-not)). > - **Impact/blast-radius analysis is heuristic** — a regex-approximate, conservative code > graph, not a sound call graph. Treat its output as advisory. > - **"Proof-carrying memory" is a name, not a formal proof.** Claims are content-addressed and > carry evidence references; confidence moves only when independent oracles (tests, CI, a > human) raise it. There is no theorem-prover in the loop. > - Some integrations shell out — `forge harden`, `forge scan`, and the git-native ledger -> assume **Bash, Git, and (for a few paths) `jq`** are available. +> assume **Bash, Git, and (for a few paths) `jq`** are available. Claude hooks on Windows do +> not require `bash` on `PATH`: their Node launcher finds Git Bash and preserves guard exits. ## Start in 60 seconds @@ -128,7 +134,7 @@ The day-to-day value first — the substrate gives a frozen model what it can't (`forge docs sync` sweeps the diff for stale prose, `forge handoff` writes the bounded session snapshot the next session resumes from, `forge decide` records choices so no session re-decides them). -- **One config for 9 tools.** Author your rules once; Forge emits each tool's native config, +- **One config for 10 tools.** Author your rules once; Forge emits each tool's native config, plus MCP for Roo and VS Code. Zero runtime dependencies — one Node CLI, plain files in git, no server. diff --git a/biome.json b/biome.json index 1a74a63..90c726b 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,7 @@ "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": true, - "includes": ["src/**", "test/**", "scripts/**", "bench/**"] + "includes": ["src/**", "test/**", "scripts/**", "bench/**", "global/guards/run.mjs"] }, "formatter": { "enabled": true, diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 526b6a6..133ff44 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -704,10 +704,10 @@ tools` fixes that without changing what `sync` emits. - `forge tools` — show the detected/primary tool (from `.forge/config.json`, else auto-detected from which agent folder exists — `CLAUDE.md`, `.cursor/`, `.gemini/`, - `.codex/`, `.zed/`, `.vscode/`, `.aider.conf.yml`, `.continue/`, `.windsurf/`, `.roo/`) - and which targets are currently gitignored. + `.codex/`, `.zed/`, `.vscode/`, `.aider.conf.yml`, `.continue/`, `.windsurf/`, `.roo/`, + `.openclaw/`) and which targets are currently gitignored. - `forge tools ` — record `` (`claude` · `cursor` · `gemini` · `codex` · - `zed` · `vscode` · `aider` · `continue` · `windsurf` · `roo`) as this repo's primary tool in + `zed` · `vscode` · `aider` · `continue` · `windsurf` · `roo` · `openclaw`) as this repo's primary tool in `.forge/config.json`, then write a **marked, reversible** block into `.gitignore` (`# forge:gitignore:begin … # forge:gitignore:end`) that ignores every OTHER tool's emitted artifacts. Your own `.gitignore` lines are never touched, and the shared @@ -1209,6 +1209,17 @@ Forge substrate — pre-action advisory (advisory, never blocks): Nothing to wire — the plugin's [`hooks/hooks.json`](../hooks/hooks.json) installs the `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` guards for you. +**Windows.** Hooks are exec form (`command` + `args`), which Claude Code spawns directly with a +plain `PATH` lookup and no shell — and a default Git for Windows install puts `git` on `PATH` +but **not** `bash`. Every hook therefore runs through the portable launcher +`global/guards/run.mjs` (`node run.mjs .sh [mode]`): `node` is always present, and the +launcher finds Git Bash itself — `FORGE_BASH`, then `CLAUDE_CODE_GIT_BASH_PATH`, then the Git +install that owns the `git` on `PATH`, the standard install dirs, and finally a `bash.exe` on +`PATH` (never WSL's System32 launcher) — and passes stdin, stdout and the exit code through +untouched, so a guard's exit 2 still blocks. `forge doctor` shows which bash it resolved and +flags hooks left in the old `bash …` spelling; `forge doctor --fix` (or `forge init`) heals +Forge-owned ones in place. On macOS/Linux nothing changes: `bash` from `PATH`, as before. + Three more ambient layers ride the same hooks: **Session rehydration (SessionStart).** Besides lessons and the anchored goal, every @@ -1298,6 +1309,54 @@ emitted `.mcp.json`): Forge never pretends it can force a hook into a tool that has none — **ambient on Claude Code, agent-invoked everywhere else.** +### OpenClaw — emitted config or installable bundle + +OpenClaw appends the execution folder's `AGENTS.md` after its configured agent-workspace +files as project context, so `forge sync` needs no OpenClaw-specific instruction file: +the canonical rules arrive on their own. Only `AGENTS.md` travels that way — OpenClaw does +not read `SOUL.md`, `IDENTITY.md`, `USER.md`, `MEMORY.md` or `BOOTSTRAP.md` from the +execution folder — so keep anything OpenClaw must see inside the canonical body. + +When you use only `forge sync`, the MCP server is **not** wired automatically, on purpose. +OpenClaw keeps its registry in your global `~/.openclaw/openclaw.json` under `mcp.servers`, +and Forge does not write to another tool's global config. Instead `forge sync` emits the +OpenClaw-shaped definition to `.openclaw/mcp.json`: + +```json +{ "mcp": { "servers": { "forge-cortex": { "command": "forge", "args": ["cortex-mcp"] } } } } +``` + +Register it once, then prove it: + +```bash +openclaw mcp add forge-cortex --command forge --arg cortex-mcp +openclaw mcp doctor forge-cortex --probe +``` + +`.openclaw/mcp.json` is an ordinary managed MCP target: re-running `sync` is idempotent, a +same-name server you wrote yourself is preserved (claim it with +`forge integrations add --adopt`), and `forge integrations remove ` reverses +the add there exactly as it does for every other tool. Removing the entry from the file +does **not** unregister it inside OpenClaw — use `openclaw mcp unset forge-cortex` for that. + +Alternatively, install ForgeKit itself as a compatible bundle. The npm package includes its +Codex bundle manifest, skills, and MCP definition; OpenClaw maps those to native skills and +an embedded bundle-scoped MCP server: + +```bash +# Local development checkout +openclaw plugins install --link . --accept-capabilities + +# Or install a trusted archive produced by `npm pack` +openclaw plugins install ./codewithjuber-forgekit-.tgz --accept-capabilities +``` + +Confirm `Format: bundle`, `Bundle format: codex`, and the `forge-cortex` MCP server with +`openclaw plugins inspect forgekit`. This bundle path does not require the manual global +`openclaw mcp add` step above. It also does not activate Forge's Claude +`hooks/hooks.json`: OpenClaw executes only OpenClaw-style hook packs, so Forge still has no +ambient pre-action guard there. + --- ## Reading substrate output @@ -1354,8 +1413,9 @@ picked up by the plugin and by `forge catalog`. Create `global/guards/.sh` (source `_guardlib.sh` for the shared fields + the re-entrancy lock), then wire it in `global/settings.template.json` **and** -[`hooks/hooks.json`](../hooks/hooks.json). Guards must be idempotent and fail-safe — -worst case they do nothing. +[`hooks/hooks.json`](../hooks/hooks.json) as `node …/guards/run.mjs …/guards/.sh [mode]` +— through the portable launcher, never a bare `bash` (not on `PATH` on Windows). Guards must +be idempotent and fail-safe — worst case they do nothing. ### Add a crew member (sub-agent) @@ -1426,6 +1486,8 @@ code reads but this table misses fails CI on the forge repo): | `FORGE_LEDGER_ONLY` | Ledger-only is the DEFAULT (the ledger is the sole store). `0` is the escape hatch — restores the legacy `lessons/*.md` + recall/brain file store while external tooling migrates | | `FORGE_EMBED` / `FORGE_EMBED_MODEL` / `FORGE_EMBED_TIMEOUT_MS` | optional embeddings tier (ADR-0005) | | `FORGE_HOME` | override `~/.forge` (recall store location) | +| `FORGE_BASH` | explicit bash for the hook guards (the `global/guards/run.mjs` launcher). Resolution order: this, `CLAUDE_CODE_GIT_BASH_PATH`, then on Windows the Git for Windows install that owns `git` on `PATH`, the standard install dirs, a non-WSL `bash.exe` on `PATH`; POSIX uses `bash` from `PATH` | +| `CLAUDE_CODE_GIT_BASH_PATH` | Claude Code's own Git Bash location — honored by the hook launcher when `FORGE_BASH` is unset | | `FORGE_ROOT` | repo root override for the MCP server | | `FORGE_AUTHOR` | identity stamped on ledger provenance (defaults to git identity) | | `FORGE_COST_CEILING` | daily spend (USD) the cost-budget guard warns at (default 10) | diff --git a/docs/cognitive-substrate/README.md b/docs/cognitive-substrate/README.md index d1622b5..62e94ae 100644 --- a/docs/cognitive-substrate/README.md +++ b/docs/cognitive-substrate/README.md @@ -25,8 +25,8 @@ forge atlas # build the code graph (needed for blast-radius checks) ``` `forge init` configures Claude Code, Codex, Cursor, Gemini, Aider, Copilot, Windsurf, Zed, -and Continue (plus MCP config for Roo and VS Code). On Claude Code the check then runs on -every prompt automatically. +Continue, and OpenClaw (plus MCP config for Roo and VS Code). On Claude Code the check then +runs on every prompt automatically. --- @@ -157,6 +157,13 @@ can call directly. Details + the exact rule wording: Forge never pretends it can force a hook into a tool that has none — it's ambient on Claude Code, and agent-invoked everywhere else. +**OpenClaw** is agent-invoked in exactly this sense. It picks up the rules on its own (the +execution folder's `AGENTS.md` becomes project context), but its MCP registry is your global +`~/.openclaw/openclaw.json`, which Forge will not write to. `forge sync` leaves the +definition in `.openclaw/mcp.json`; you register it once with +`openclaw mcp add forge-cortex --command forge --arg cortex-mcp`. Forge installs nothing +into OpenClaw's own hook system. + --- ## Use it in a script diff --git a/global/guards/protect-paths.sh b/global/guards/protect-paths.sh index 630bb6e..edde9d7 100755 --- a/global/guards/protect-paths.sh +++ b/global/guards/protect-paths.sh @@ -11,9 +11,9 @@ if command -v jq >/dev/null 2>&1; then fpath="$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')" cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')" else - tool="$(printf '%s' "$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')" - fpath="$(printf '%s' "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')" - cmd="$(printf '%s' "$input" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')" + tool="$(printf '%s' "$input" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)" + fpath="$(printf '%s' "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)" + cmd="$(printf '%s' "$input" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)" fi deny() { diff --git a/global/guards/run.mjs b/global/guards/run.mjs new file mode 100644 index 0000000..b7dd4dd --- /dev/null +++ b/global/guards/run.mjs @@ -0,0 +1,153 @@ +#!/usr/bin/env node +// Forge hook launcher — runs a bash guard from an exec-form Claude Code hook on every OS. +// +// Why this exists: exec-form hooks (`command` + `args`) are spawned directly — no shell, just a +// plain PATH lookup of `command`. Forge's hooks were `command: "bash"`. On Windows, Git for Windows +// puts `git.exe` on PATH (`Git\cmd`) but NOT `bash.exe` (`Git\bin`, `Git\usr\bin`), so every hook +// — SessionStart first — died with `spawn bash ENOENT` before a guard ever ran. `node` IS a real +// executable on PATH wherever this package is installed, so hooks now spawn +// `node run.mjs [args…]` and THIS file finds bash: $FORGE_BASH, Claude Code's own +// $CLAUDE_CODE_GIT_BASH_PATH, the Git for Windows install that owns the `git` on PATH, the standard +// install dirs, then PATH itself — skipping the WSL launcher in System32, which is not Git Bash. +// POSIX is unchanged: `bash` from PATH, exactly as before. +// +// It must never weaken a guard: stdin (the hook JSON), stdout (SessionStart context, +// `updatedToolOutput`) and stderr are inherited untouched, and bash's exit code is returned +// verbatim — a guard's exit 2 still blocks. With no bash anywhere it prints ONE actionable line and +// exits 1: the same visible, non-blocking hook error the ENOENT was, minus the mystery. Node +// built-ins only — a launcher that itself failed to load would be exactly the silent no-op the +// guards exist to prevent. +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { basename, win32 } from "node:path"; +import { fileURLToPath } from "node:url"; + +export const NO_BASH_HINT = + "no bash found to run the hook guards — install Git for Windows (bash ships with it) or point " + + "FORGE_BASH (or CLAUDE_CODE_GIT_BASH_PATH) at your bash executable"; + +/** + * Windows dirs that ship a `bash.exe` which is NOT Git Bash (the WSL launcher in System32). + * @param {string} dir + * @param {NodeJS.ProcessEnv} env + */ +function isSystemDir(dir, env) { + /** @param {string} p */ + const norm = (p) => String(p).toLowerCase().replaceAll("/", "\\").replace(/\\+$/, ""); + const d = norm(dir); + return [env.SystemRoot, env.windir, "C:\\Windows"] + .filter(Boolean) + .some((r) => d === norm(r) || d.startsWith(`${norm(r)}\\`)); +} + +/** + * Locate the bash that runs the guards. `path` is what to spawn (`null` when nothing usable + * exists); `via` says how it was found (doctor prints it). Pure — env, platform and the + * filesystem probe are injectable, so the Windows logic is unit-tested on every OS. + * @param {{env?: NodeJS.ProcessEnv, platform?: string, exists?: (p: string) => boolean}} [opts] + * @returns {{ path: string | null, via: string }} + */ +export function resolveBash({ + env = process.env, + platform = process.platform, + exists = existsSync, +} = {}) { + // 1. Explicit overrides win on every OS: FORGE_BASH (ours), then CLAUDE_CODE_GIT_BASH_PATH — + // Claude Code's own setting for the same problem (portable Git, MSYS2, a custom prefix). + /** @type {[string, string | undefined][]} */ + const overrides = [ + ["FORGE_BASH", env.FORGE_BASH], + ["CLAUDE_CODE_GIT_BASH_PATH", env.CLAUDE_CODE_GIT_BASH_PATH], + ]; + for (const [via, p] of overrides) if (p && exists(p)) return { path: p, via }; + // 2. POSIX: bash from PATH, exactly as the hooks always did (spawn reports ENOENT if absent). + if (platform !== "win32") return { path: "bash", via: "PATH" }; + // 3. Windows — Windows path semantics regardless of the host running this logic (tests). + const P = win32; + const pathDirs = String(env.PATH ?? env.Path ?? "") + .split(P.delimiter) + .filter(Boolean); + const seen = new Set(); + /** @type {string[]} */ + const candidates = []; + /** @param {string} p */ + const add = (p) => { + const k = p.toLowerCase(); + if (!seen.has(k)) { + seen.add(k); + candidates.push(p); + } + }; + // 3a. The Git for Windows install that owns the `git` on PATH: a default install exposes + // `Git\cmd\git.exe` only; its siblings `Git\bin\bash.exe` / `Git\usr\bin\bash.exe` are the + // bash the guards need. Also covers `Git\bin` or `Git\mingw64\bin` being the PATH entry. + for (const dir of pathDirs) { + if (isSystemDir(dir, env) || !exists(P.join(dir, "git.exe"))) continue; + const root = P.dirname(dir); + for (const r of [root, P.dirname(root)]) { + add(P.join(r, "bin", "bash.exe")); + add(P.join(r, "usr", "bin", "bash.exe")); + } + } + // 3b. Standard install dirs: system-wide, per-user, scoop. + for (const base of [env.ProgramFiles, env.ProgramW6432, env["ProgramFiles(x86)"]]) { + if (base) add(P.join(base, "Git", "bin", "bash.exe")); + } + if (env.LOCALAPPDATA) add(P.join(env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe")); + if (env.USERPROFILE) + add(P.join(env.USERPROFILE, "scoop", "apps", "git", "current", "bin", "bash.exe")); + for (const p of candidates) if (exists(p)) return { path: p, via: "git-for-windows" }; + // 3c. A bash.exe on PATH — but never the WSL launcher in System32. + for (const dir of pathDirs) { + if (isSystemDir(dir, env)) continue; + const p = P.join(dir, "bash.exe"); + if (exists(p)) return { path: p, via: "PATH" }; + } + return { path: null, via: "none" }; +} + +/** + * `${CLAUDE_PLUGIN_ROOT}` is substituted as a native `C:\…` string on Windows; Git Bash's + * `dirname`/`cd` are happiest with forward slashes. No-op on POSIX. + * @param {string} p + */ +const toPosix = (p) => String(p).replaceAll("\\", "/"); + +/** + * Spawn `bash