From 23bfabb9181ab1e8bdcc5dda3c05e48c031dfdec Mon Sep 17 00:00:00 2001
From: nottyjay
Date: Thu, 7 May 2026 13:27:27 +0800
Subject: [PATCH 1/9] feat: add codex hook integration
---
README.md | 29 +++-
src/cli/index.ts | 8 +-
src/cli/init.test.ts | 122 +++++++++++++
src/cli/init.ts | 269 +++++++++++++++++++++++------
src/cli/status.ts | 30 +++-
src/cli/update.ts | 158 +++++++++++++----
src/hooks/post-read.ts | 4 +-
src/hooks/post-write.ts | 4 +-
src/hooks/pre-read.ts | 4 +-
src/hooks/shared.ts | 8 +-
src/templates/agents-md-snippet.md | 5 +
11 files changed, 529 insertions(+), 112 deletions(-)
create mode 100644 src/cli/init.test.ts
create mode 100644 src/templates/agents-md-snippet.md
diff --git a/README.md b/README.md
index c19756e4..37e8faf1 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
OpenWolf
- A second brain for Claude Code.
+ A second brain for Claude Code and Codex.
Project intelligence, token tracking, and invisible enforcement through 6 hook scripts. Zero workflow changes.
@@ -45,11 +45,18 @@ cd your-project
openwolf init
```
-That's it. Use `claude` normally. OpenWolf is watching.
+By default, `openwolf init` installs both Claude and Codex integration.
+
+```bash
+openwolf init claude # only Claude integration
+openwolf init codex # only Codex integration
+```
+
+That's it. Use `claude` or `codex` normally. OpenWolf is watching.
## What It Creates
-`openwolf init` creates a `.wolf/` directory in your project:
+`openwolf init` creates a `.wolf/` directory in your project and installs agent-specific entry files:
| File | Purpose |
|------|---------|
@@ -58,10 +65,15 @@ That's it. Use `claude` normally. OpenWolf is watching.
| `memory.md` | Chronological action log with token estimates |
| `buglog.json` | Bug fix memory, searchable, prevents re-discovery |
| `token-ledger.json` | Lifetime token tracking and session history |
-| `hooks/` | 6 Claude Code lifecycle hooks (pure Node.js) |
+| `hooks/` | Provider-specific hook scripts under `.wolf/hooks/claude/` and `.wolf/hooks/codex/` |
| `config.json` | Configuration with sensible defaults |
| `identity.md` | Agent persona for this project |
-| `OPENWOLF.md` | Instructions Claude follows every session |
+| `OPENWOLF.md` | Shared OpenWolf operating protocol |
+| `CLAUDE.md` | Claude entry file that points Claude at `.wolf/OPENWOLF.md` |
+| `AGENTS.md` | Codex entry file that points Codex at `.wolf/OPENWOLF.md` |
+
+When Claude integration is enabled, OpenWolf also writes `.claude/settings.json` and `.claude/rules/openwolf.md` to register hooks and rules.
+When Codex integration is enabled, OpenWolf writes `.codex/hooks.json` and `.codex/config.toml` to register Codex hooks.
## How It Works
@@ -178,7 +190,7 @@ Before fixing anything, Claude checks if the fix is already known. After fixing,
## Commands
```
-openwolf init Initialize .wolf/ and register hooks
+openwolf init [target] Initialize .wolf/ and install Claude/Codex integration
openwolf status Show health, stats, file integrity
openwolf scan Refresh the project structure map
openwolf scan --check Verify anatomy matches filesystem (exits 1 if stale)
@@ -212,12 +224,12 @@ Ask Claude to help you pick a UI framework. OpenWolf ships a curated knowledge b
## How OpenWolf Compares
-OpenWolf is not an AI wrapper. It is 6 hook scripts and a `.wolf/` directory. It doesn't run your AI for you or change your workflow. It gives Claude Code what it lacks: a project map so it reads less, a memory so it learns faster, and a ledger so you see where tokens go.
+OpenWolf is not an AI wrapper. It is 6 hook scripts and a `.wolf/` directory. It doesn't run your AI for you or change your workflow. It gives Claude Code and Codex what they lack: a project map so they read less, a memory so they learn faster, and a ledger so you see where tokens go.
## Requirements
- Node.js 20+
-- Claude Code CLI
+- Claude Code CLI or Codex
- Windows, macOS, or Linux
- Optional: PM2 for persistent background tasks
- Optional: `puppeteer-core` for Design QC screenshots
@@ -225,6 +237,7 @@ OpenWolf is not an AI wrapper. It is 6 hook scripts and a `.wolf/` directory. It
## Limitations
- Claude Code hooks are a relatively new feature. OpenWolf falls back to `CLAUDE.md` instructions when hooks don't fire.
+- Codex hooks require Codex project hook support via `.codex/hooks.json` and `.codex/config.toml`, plus `AGENTS.md` project instructions.
- Token tracking is estimation-based (character-to-token ratio), not exact API counts. Accurate to within ~15%.
- `cerebrum.md` depends on Claude following instructions to update it after corrections. Compliance is ~85-90%, not 100%.
- This is v1.0.4. Things may break. [File issues](https://github.com/cytostack/openwolf/issues).
diff --git a/src/cli/index.ts b/src/cli/index.ts
index c2bbf5f1..db70563a 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -29,9 +29,11 @@ export function createProgram(): Command {
.version(getVersion());
program
- .command("init")
- .description("Initialize .wolf/ in current project")
- .action(initCommand);
+ .command("init [target]")
+ .description("Initialize .wolf/ and install Claude/Codex integration in current project")
+ .action(async (target?: string) => {
+ await initCommand(target);
+ });
program
.command("status")
diff --git a/src/cli/init.test.ts b/src/cli/init.test.ts
new file mode 100644
index 00000000..1988d8bf
--- /dev/null
+++ b/src/cli/init.test.ts
@@ -0,0 +1,122 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import * as fs from "node:fs";
+import * as os from "node:os";
+import * as path from "node:path";
+import { initCommand } from "./init.js";
+
+const runInit = initCommand as unknown as (target?: string) => Promise;
+
+function makeProject(name: string): string {
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), `openwolf-${name}-`));
+ fs.writeFileSync(
+ path.join(projectRoot, "package.json"),
+ JSON.stringify({ name, version: "1.0.0" }, null, 2),
+ "utf-8"
+ );
+ fs.writeFileSync(
+ path.join(projectRoot, "index.ts"),
+ "export const value = 1;\n",
+ "utf-8"
+ );
+ return projectRoot;
+}
+
+function read(filePath: string): string {
+ return fs.readFileSync(filePath, "utf-8");
+}
+
+function readJSON(filePath: string): Record {
+ return JSON.parse(read(filePath)) as Record;
+}
+
+test("default init installs both Claude and Codex integration files", async () => {
+ const projectRoot = makeProject("dual-target");
+ const previousCwd = process.cwd();
+ const previousHome = process.env.HOME;
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "openwolf-home-"));
+
+ process.chdir(projectRoot);
+ process.env.HOME = fakeHome;
+
+ try {
+ await runInit();
+
+ assert.ok(fs.existsSync(path.join(projectRoot, ".wolf", "OPENWOLF.md")));
+ assert.ok(fs.existsSync(path.join(projectRoot, "CLAUDE.md")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".claude", "settings.json")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".claude", "rules", "openwolf.md")));
+ assert.ok(fs.existsSync(path.join(projectRoot, "AGENTS.md")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".codex", "hooks.json")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".codex", "config.toml")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "claude", "session-start.js")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "codex", "session-start.js")));
+ assert.equal(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "session-start.js")), false);
+
+ assert.match(read(path.join(projectRoot, "CLAUDE.md")), /@\.wolf\/OPENWOLF\.md/);
+ assert.match(read(path.join(projectRoot, "AGENTS.md")), /@\.wolf\/OPENWOLF\.md/);
+ assert.match(
+ JSON.stringify(readJSON(path.join(projectRoot, ".codex", "hooks.json"))),
+ /\.wolf\/hooks\/codex\/session-start\.js/
+ );
+ } finally {
+ process.chdir(previousCwd);
+ process.env.HOME = previousHome;
+ fs.rmSync(projectRoot, { recursive: true, force: true });
+ fs.rmSync(fakeHome, { recursive: true, force: true });
+ }
+});
+
+test("claude target only installs Claude integration files", async () => {
+ const projectRoot = makeProject("claude-only");
+ const previousCwd = process.cwd();
+ const previousHome = process.env.HOME;
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "openwolf-home-"));
+
+ process.chdir(projectRoot);
+ process.env.HOME = fakeHome;
+
+ try {
+ await runInit("claude");
+
+ assert.ok(fs.existsSync(path.join(projectRoot, "CLAUDE.md")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".claude", "settings.json")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "claude", "session-start.js")));
+ assert.equal(fs.existsSync(path.join(projectRoot, ".codex", "hooks.json")), false);
+ assert.equal(fs.existsSync(path.join(projectRoot, ".codex", "config.toml")), false);
+ assert.equal(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "codex", "session-start.js")), false);
+ assert.equal(fs.existsSync(path.join(projectRoot, "AGENTS.md")), false);
+ } finally {
+ process.chdir(previousCwd);
+ process.env.HOME = previousHome;
+ fs.rmSync(projectRoot, { recursive: true, force: true });
+ fs.rmSync(fakeHome, { recursive: true, force: true });
+ }
+});
+
+test("codex target only installs Codex integration files", async () => {
+ const projectRoot = makeProject("codex-only");
+ const previousCwd = process.cwd();
+ const previousHome = process.env.HOME;
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "openwolf-home-"));
+
+ process.chdir(projectRoot);
+ process.env.HOME = fakeHome;
+
+ try {
+ await runInit("codex");
+
+ assert.ok(fs.existsSync(path.join(projectRoot, "AGENTS.md")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".codex", "hooks.json")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".codex", "config.toml")));
+ assert.ok(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "codex", "session-start.js")));
+ assert.equal(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "claude", "session-start.js")), false);
+ assert.equal(fs.existsSync(path.join(projectRoot, "CLAUDE.md")), false);
+ assert.equal(fs.existsSync(path.join(projectRoot, ".claude", "settings.json")), false);
+ } finally {
+ process.chdir(previousCwd);
+ process.env.HOME = previousHome;
+ fs.rmSync(projectRoot, { recursive: true, force: true });
+ fs.rmSync(fakeHome, { recursive: true, force: true });
+ }
+});
diff --git a/src/cli/init.ts b/src/cli/init.ts
index 0414bb75..3c464fba 100644
--- a/src/cli/init.ts
+++ b/src/cli/init.ts
@@ -44,8 +44,10 @@ const CREATE_IF_MISSING = [
"suggestions.json",
];
+type InitTarget = "claude" | "codex";
+
// Use $CLAUDE_PROJECT_DIR so hooks resolve correctly even if CWD changes during a session
-const HOOK_SETTINGS = {
+const CLAUDE_HOOK_SETTINGS = {
hooks: {
SessionStart: [
{
@@ -53,7 +55,7 @@ const HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/session-start.js"',
+ command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/session-start.js"',
timeout: 5,
},
],
@@ -65,7 +67,7 @@ const HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-read.js"',
+ command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-read.js"',
timeout: 5,
},
],
@@ -75,7 +77,7 @@ const HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-write.js"',
+ command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-write.js"',
timeout: 5,
},
],
@@ -87,7 +89,7 @@ const HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-read.js"',
+ command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-read.js"',
timeout: 5,
},
],
@@ -97,7 +99,7 @@ const HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"',
+ command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-write.js"',
timeout: 10,
},
],
@@ -109,8 +111,86 @@ const HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/stop.js"',
+ command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/stop.js"',
+ timeout: 10,
+ },
+ ],
+ },
+ ],
+ },
+};
+
+const CODEX_HOOK_SETTINGS = {
+ hooks: {
+ SessionStart: [
+ {
+ matcher: "startup|resume|clear",
+ hooks: [
+ {
+ type: "command",
+ command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/session-start.js"',
+ timeout: 5,
+ statusMessage: "OpenWolf session bootstrap",
+ },
+ ],
+ },
+ ],
+ PreToolUse: [
+ {
+ matcher: "Read",
+ hooks: [
+ {
+ type: "command",
+ command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-read.js"',
+ timeout: 5,
+ statusMessage: "OpenWolf read precheck",
+ },
+ ],
+ },
+ {
+ matcher: "Edit|Write|MultiEdit|apply_patch",
+ hooks: [
+ {
+ type: "command",
+ command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-write.js"',
+ timeout: 5,
+ statusMessage: "OpenWolf write precheck",
+ },
+ ],
+ },
+ ],
+ PostToolUse: [
+ {
+ matcher: "Read",
+ hooks: [
+ {
+ type: "command",
+ command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-read.js"',
+ timeout: 5,
+ statusMessage: "OpenWolf read tracking",
+ },
+ ],
+ },
+ {
+ matcher: "Edit|Write|MultiEdit|apply_patch",
+ hooks: [
+ {
+ type: "command",
+ command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-write.js"',
+ timeout: 10,
+ statusMessage: "OpenWolf write tracking",
+ },
+ ],
+ },
+ ],
+ Stop: [
+ {
+ hooks: [
+ {
+ type: "command",
+ command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/stop.js"',
timeout: 10,
+ statusMessage: "OpenWolf session finalize",
},
],
},
@@ -118,7 +198,11 @@ const HOOK_SETTINGS = {
},
};
-export async function initCommand(): Promise {
+const CODEX_CONFIG_TOML = `[features]
+codex_hooks = true
+`;
+
+export async function initCommand(targetArg?: string): Promise {
// Check Node.js version
const nodeVersion = parseInt(process.version.slice(1), 10);
if (nodeVersion < 20) {
@@ -126,6 +210,8 @@ export async function initCommand(): Promise {
process.exit(1);
}
+ const targets = resolveInitTargets(targetArg);
+
// Detect project root
const projectRoot = findProjectRoot();
console.log(`Project root: ${projectRoot}`);
@@ -180,37 +266,13 @@ export async function initCommand(): Promise {
}
// --- Hook scripts: always update (bug fixes, new features) ---
- copyHookScripts(wolfDir);
-
- // --- Claude settings: replace OpenWolf hooks (upgrade old paths) ---
- const claudeDir = path.join(projectRoot, ".claude");
- ensureDir(claudeDir);
+ copyHookScripts(wolfDir, targets);
- const settingsPath = path.join(claudeDir, "settings.json");
- if (fs.existsSync(settingsPath)) {
- const existing = readJSON>(settingsPath, {});
- const merged = replaceOpenWolfHooks(existing, HOOK_SETTINGS);
- writeJSON(settingsPath, merged);
- } else {
- writeJSON(settingsPath, HOOK_SETTINGS);
+ if (targets.includes("claude")) {
+ installClaudeIntegration(projectRoot, actualTemplatesDir);
}
-
- // --- Claude rules: always update ---
- const rulesDir = path.join(claudeDir, "rules");
- ensureDir(rulesDir);
- const rulesContent = readTemplateContent("claude-rules-openwolf.md", actualTemplatesDir);
- writeText(path.join(rulesDir, "openwolf.md"), rulesContent);
-
- // --- CLAUDE.md: add snippet if missing ---
- const claudeMdPath = path.join(projectRoot, "CLAUDE.md");
- const snippetContent = readTemplateContent("claude-md-snippet.md", actualTemplatesDir);
- if (fs.existsSync(claudeMdPath)) {
- const existing = readText(claudeMdPath);
- if (!existing.includes("OpenWolf")) {
- writeText(claudeMdPath, snippetContent + "\n\n" + existing);
- }
- } else {
- writeText(claudeMdPath, snippetContent);
+ if (targets.includes("codex")) {
+ installCodexIntegration(projectRoot, actualTemplatesDir);
}
// --- Anatomy scan: only on fresh init ---
@@ -273,18 +335,30 @@ export async function initCommand(): Promise {
console.log(` ✓ All .wolf data preserved (${skippedCount} files: cerebrum, memory, anatomy, buglog, ledger)`);
console.log(` ✓ Hook scripts updated (6 hooks)`);
console.log(` ✓ ${createdCount} config files updated`);
+ if (targets.includes("claude")) {
+ console.log(` ✓ Claude integration refreshed`);
+ }
+ if (targets.includes("codex")) {
+ console.log(` ✓ Codex integration refreshed`);
+ }
console.log(` ✓ Anatomy: ${fileCount} files tracked (unchanged)`);
} else {
console.log(` ✓ OpenWolf v${version} initialized`);
console.log(` ✓ .wolf/ created with ${createdCount} files`);
- console.log(` ✓ Claude Code hooks registered (6 hooks)`);
- console.log(` ✓ CLAUDE.md updated`);
- console.log(` ✓ .claude/rules/openwolf.md created`);
+ if (targets.includes("claude")) {
+ console.log(` ✓ Claude Code hooks registered (6 hooks)`);
+ console.log(` ✓ CLAUDE.md updated`);
+ console.log(` ✓ .claude/rules/openwolf.md created`);
+ }
+ if (targets.includes("codex")) {
+ console.log(` ✓ Codex hooks registered (.codex/hooks.json)`);
+ console.log(` ✓ AGENTS.md updated for Codex`);
+ }
console.log(` ✓ Anatomy scan: ${fileCount} files indexed`);
}
console.log(` ✓ Daemon: ${daemonStatus}`);
console.log("");
- console.log(" You're ready. Just use 'claude' as normal — OpenWolf is watching.");
+ console.log(` You're ready. Just use '${formatTargetsForHumans(targets)}' as normal — OpenWolf is watching.`);
console.log("");
}
@@ -303,6 +377,52 @@ function findTemplatesDir(): string {
return candidates[0]; // fallback — generateTemplate will handle missing files
}
+function resolveInitTargets(targetArg?: string): InitTarget[] {
+ if (!targetArg) return ["claude", "codex"];
+ if (targetArg === "claude" || targetArg === "codex") {
+ return [targetArg];
+ }
+
+ console.error(`Unknown init target: ${targetArg}`);
+ console.error("Valid targets: claude, codex");
+ process.exit(1);
+}
+
+function installClaudeIntegration(projectRoot: string, templatesDir: string): void {
+ const claudeDir = path.join(projectRoot, ".claude");
+ ensureDir(claudeDir);
+
+ const settingsPath = path.join(claudeDir, "settings.json");
+ if (fs.existsSync(settingsPath)) {
+ const existing = readJSON>(settingsPath, {});
+ const merged = replaceOpenWolfHooks(existing, CLAUDE_HOOK_SETTINGS);
+ writeJSON(settingsPath, merged);
+ } else {
+ writeJSON(settingsPath, CLAUDE_HOOK_SETTINGS);
+ }
+
+ const rulesDir = path.join(claudeDir, "rules");
+ ensureDir(rulesDir);
+ const rulesContent = readTemplateContent("claude-rules-openwolf.md", templatesDir);
+ writeText(path.join(rulesDir, "openwolf.md"), rulesContent);
+
+ const claudeMdPath = path.join(projectRoot, "CLAUDE.md");
+ const snippetContent = readTemplateContent("claude-md-snippet.md", templatesDir);
+ prependSnippetIfMissing(claudeMdPath, snippetContent);
+}
+
+function installCodexIntegration(projectRoot: string, templatesDir: string): void {
+ const codexDir = path.join(projectRoot, ".codex");
+ ensureDir(codexDir);
+
+ writeJSON(path.join(codexDir, "hooks.json"), CODEX_HOOK_SETTINGS);
+ writeText(path.join(codexDir, "config.toml"), CODEX_CONFIG_TOML);
+
+ const agentsPath = path.join(projectRoot, "AGENTS.md");
+ const snippetContent = readTemplateContent("agents-md-snippet.md", templatesDir);
+ prependSnippetIfMissing(agentsPath, snippetContent);
+}
+
function writeTemplateFile(templatesDir: string, wolfDir: string, file: string): void {
const srcPath = path.join(templatesDir, file);
const destPath = path.join(wolfDir, file);
@@ -324,11 +444,29 @@ function readTemplateContent(filename: string, templatesDir: string): string {
function getEmbeddedTemplate(filename: string): string {
const templates: Record = {
"claude-md-snippet.md": `# OpenWolf\n\n@.wolf/OPENWOLF.md\n\nThis project uses OpenWolf for context management. Read and follow .wolf/OPENWOLF.md every session. Check .wolf/cerebrum.md before generating code. Check .wolf/anatomy.md before reading files.`,
+ "agents-md-snippet.md": `# OpenWolf\n\n@.wolf/OPENWOLF.md\n\nThis project uses OpenWolf for context management. Read and follow .wolf/OPENWOLF.md every session. Check .wolf/cerebrum.md before generating code. Check .wolf/anatomy.md before reading files.`,
"claude-rules-openwolf.md": `---\ndescription: OpenWolf protocol enforcement — active on all files\nglobs: **/*\n---\n\n- Check .wolf/anatomy.md before reading any project file\n- Check .wolf/cerebrum.md Do-Not-Repeat list before generating code\n- After writing or editing files, update .wolf/anatomy.md and append to .wolf/memory.md\n- After receiving a user correction, update .wolf/cerebrum.md immediately (Preferences, Learnings, or Do-Not-Repeat)\n- LEARN from every interaction: if you discover a convention, user preference, or project pattern, add it to .wolf/cerebrum.md. Low threshold — when in doubt, log it.\n- BEFORE fixing any bug or error: read .wolf/buglog.json for known fixes\n- AFTER fixing any bug, error, failed test, failed build, or user-reported problem: ALWAYS log to .wolf/buglog.json with error_message, root_cause, fix, and tags\n- If you edit a file more than twice in a session, that likely indicates a bug — log it to .wolf/buglog.json\n- When the user asks to check/evaluate UI design: run \`openwolf designqc\` to capture screenshots, then read them from .wolf/designqc-captures/\n- When the user asks to change/pick/migrate UI framework: read .wolf/reframe-frameworks.md, ask decision questions, recommend a framework, then execute with the framework's prompt`,
};
return templates[filename] ?? "";
}
+function prependSnippetIfMissing(filePath: string, snippetContent: string): void {
+ if (fs.existsSync(filePath)) {
+ const existing = readText(filePath);
+ if (!existing.includes("OpenWolf")) {
+ writeText(filePath, snippetContent + "\n\n" + existing);
+ }
+ return;
+ }
+
+ writeText(filePath, snippetContent);
+}
+
+function formatTargetsForHumans(targets: InitTarget[]): string {
+ if (targets.length === 2) return "claude' or 'codex";
+ return targets[0];
+}
+
function generateTemplate(destPath: string, file: string): void {
const templates: Record = {
"OPENWOLF.md": `# OpenWolf Operating Protocol\n\nYou are working in an OpenWolf-managed project. These rules apply every turn.\n\n## File Navigation\n\n1. Check \`.wolf/anatomy.md\` BEFORE reading any file.\n2. If the description is sufficient, do NOT read the full file.\n3. If a file is not in anatomy.md, search with Grep/Glob.\n\n## Code Generation\n\n1. Read \`.wolf/cerebrum.md\` and respect every entry.\n2. Check \`## Do-Not-Repeat\` section.\n\n## After Actions\n\n1. Append to \`.wolf/memory.md\`.\n2. After file changes: update \`.wolf/anatomy.md\`.\n\n## Token Discipline\n\n- Never re-read a file already read this session.\n- Prefer anatomy.md descriptions over full reads.\n`,
@@ -404,7 +542,7 @@ function seedIdentity(wolfDir: string, projectRoot: string): void {
writeText(identityPath, content);
}
-function copyHookScripts(wolfDir: string): void {
+function copyHookScripts(wolfDir: string, targets: InitTarget[]): void {
const hooksDir = path.join(wolfDir, "hooks");
ensureDir(hooksDir);
@@ -437,23 +575,33 @@ function copyHookScripts(wolfDir: string): void {
let copiedAny = false;
if (sourceDir) {
- for (const file of hookFiles) {
- const src = path.join(sourceDir, file);
- if (fs.existsSync(src)) {
- fs.copyFileSync(src, path.join(hooksDir, file));
- copiedAny = true;
+ removeLegacyTopLevelHooks(hooksDir, hookFiles);
+ for (const target of targets) {
+ const providerDir = path.join(hooksDir, target);
+ ensureDir(providerDir);
+ for (const file of hookFiles) {
+ const src = path.join(sourceDir, file);
+ if (fs.existsSync(src)) {
+ fs.copyFileSync(src, path.join(providerDir, file));
+ copiedAny = true;
+ }
}
}
} else if (fs.existsSync(srcHooksDir)) {
// Dev mode: compile TS hooks inline using a simple copy with note
// In practice, user should run `pnpm build:hooks` first
- for (const file of hookFiles) {
- const tsFile = file.replace(".js", ".ts");
- const src = path.join(srcHooksDir, tsFile);
- if (fs.existsSync(src)) {
- const loaderContent = `#!/usr/bin/env node\n// Auto-generated by openwolf init — run 'pnpm build:hooks' for compiled version\nimport("${src.replace(/\\/g, "/")}");\n`;
- fs.writeFileSync(path.join(hooksDir, file), loaderContent, "utf-8");
- copiedAny = true;
+ removeLegacyTopLevelHooks(hooksDir, hookFiles);
+ for (const target of targets) {
+ const providerDir = path.join(hooksDir, target);
+ ensureDir(providerDir);
+ for (const file of hookFiles) {
+ const tsFile = file.replace(".js", ".ts");
+ const src = path.join(srcHooksDir, tsFile);
+ if (fs.existsSync(src)) {
+ const loaderContent = `#!/usr/bin/env node\n// Auto-generated by openwolf init — run 'pnpm build:hooks' for compiled version\nimport("${src.replace(/\\/g, "/")}");\n`;
+ fs.writeFileSync(path.join(providerDir, file), loaderContent, "utf-8");
+ copiedAny = true;
+ }
}
}
}
@@ -467,6 +615,17 @@ function copyHookScripts(wolfDir: string): void {
fs.writeFileSync(hooksPkgPath, JSON.stringify({ type: "module" }, null, 2) + "\n", "utf-8");
}
+function removeLegacyTopLevelHooks(hooksDir: string, hookFiles: string[]): void {
+ for (const file of hookFiles) {
+ const legacyPath = path.join(hooksDir, file);
+ if (fs.existsSync(legacyPath)) {
+ try {
+ fs.unlinkSync(legacyPath);
+ } catch {}
+ }
+ }
+}
+
/**
* Replace all OpenWolf hook entries in settings.json with the current version.
* Removes old-style relative-path hooks and inserts the new $CLAUDE_PROJECT_DIR hooks.
@@ -474,7 +633,7 @@ function copyHookScripts(wolfDir: string): void {
*/
function replaceOpenWolfHooks(
existing: Record,
- hookSettings: typeof HOOK_SETTINGS
+ hookSettings: typeof CLAUDE_HOOK_SETTINGS
): Record {
const merged = { ...existing };
if (!merged.hooks) {
diff --git a/src/cli/status.ts b/src/cli/status.ts
index 0fb04c35..cbead460 100644
--- a/src/cli/status.ts
+++ b/src/cli/status.ts
@@ -40,14 +40,16 @@ export async function statusCommand(): Promise {
"post-read.js", "post-write.js", "stop.js", "shared.js",
];
const hooksDir = path.join(wolfDir, "hooks");
- let hooksMissing = 0;
- for (const file of hookFiles) {
- if (!fs.existsSync(path.join(hooksDir, file))) hooksMissing++;
- }
- if (hooksMissing === 0) {
- console.log(` ✓ All ${hookFiles.length} hook scripts present`);
- } else {
- console.log(` ✗ Missing ${hooksMissing} hook scripts`);
+ for (const provider of ["claude", "codex"]) {
+ let hooksMissing = 0;
+ for (const file of hookFiles) {
+ if (!fs.existsSync(path.join(hooksDir, provider, file))) hooksMissing++;
+ }
+ if (hooksMissing === 0) {
+ console.log(` ✓ All ${hookFiles.length} ${provider} hook scripts present`);
+ } else {
+ console.log(` ✗ Missing ${hooksMissing} ${provider} hook scripts`);
+ }
}
// Claude settings check
@@ -63,6 +65,18 @@ export async function statusCommand(): Promise {
console.log(" ✗ .claude/settings.json not found");
}
+ const codexHooksPath = path.join(projectRoot, ".codex", "hooks.json");
+ if (fs.existsSync(codexHooksPath)) {
+ const hooks = readJSON>(codexHooksPath, {});
+ const configured = hooks.hooks as Record | undefined;
+ if (configured) {
+ const hookCount = Object.values(configured).reduce((sum, arr) => sum + arr.length, 0);
+ console.log(` ✓ Codex hooks registered (${hookCount} matchers)`);
+ }
+ } else {
+ console.log(" ✗ .codex/hooks.json not found");
+ }
+
// Token ledger stats
const ledger = readJSON<{
lifetime: {
diff --git a/src/cli/update.ts b/src/cli/update.ts
index 33cf5cde..b8b94973 100644
--- a/src/cli/update.ts
+++ b/src/cli/update.ts
@@ -43,21 +43,40 @@ const BACKUP_FILES = [
...USER_DATA_FILES,
];
-const HOOK_SETTINGS = {
+const CLAUDE_HOOK_SETTINGS = {
hooks: {
- SessionStart: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/session-start.js"', timeout: 5 }] }],
+ SessionStart: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/session-start.js"', timeout: 5 }] }],
PreToolUse: [
- { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-read.js"', timeout: 5 }] },
- { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-write.js"', timeout: 5 }] },
+ { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-read.js"', timeout: 5 }] },
+ { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-write.js"', timeout: 5 }] },
],
PostToolUse: [
- { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-read.js"', timeout: 5 }] },
- { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js"', timeout: 10 }] },
+ { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-read.js"', timeout: 5 }] },
+ { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-write.js"', timeout: 10 }] },
],
- Stop: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/stop.js"', timeout: 10 }] }],
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/stop.js"', timeout: 10 }] }],
},
};
+const CODEX_HOOK_SETTINGS = {
+ hooks: {
+ SessionStart: [{ matcher: "startup|resume|clear", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/session-start.js"', timeout: 5, statusMessage: "OpenWolf session bootstrap" }] }],
+ PreToolUse: [
+ { matcher: "Read", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-read.js"', timeout: 5, statusMessage: "OpenWolf read precheck" }] },
+ { matcher: "Edit|Write|MultiEdit|apply_patch", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-write.js"', timeout: 5, statusMessage: "OpenWolf write precheck" }] },
+ ],
+ PostToolUse: [
+ { matcher: "Read", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-read.js"', timeout: 5, statusMessage: "OpenWolf read tracking" }] },
+ { matcher: "Edit|Write|MultiEdit|apply_patch", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-write.js"', timeout: 10, statusMessage: "OpenWolf write tracking" }] },
+ ],
+ Stop: [{ hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/stop.js"', timeout: 10, statusMessage: "OpenWolf session finalize" }] }],
+ },
+};
+
+const CODEX_CONFIG_TOML = `[features]
+codex_hooks = true
+`;
+
interface UpdateResult {
project: RegisteredProject;
status: "updated" | "skipped" | "error";
@@ -185,10 +204,10 @@ async function updateProject(
const settingsPath = path.join(claudeDir, "settings.json");
if (fs.existsSync(settingsPath)) {
const existing = readJSON>(settingsPath, {});
- const merged = replaceOpenWolfHooks(existing, HOOK_SETTINGS);
+ const merged = replaceOpenWolfHooks(existing, CLAUDE_HOOK_SETTINGS);
writeJSON(settingsPath, merged);
} else {
- writeJSON(settingsPath, HOOK_SETTINGS);
+ writeJSON(settingsPath, CLAUDE_HOOK_SETTINGS);
}
console.log(` ✓ Claude settings updated`);
@@ -208,9 +227,32 @@ async function updateProject(
writeText(claudeMdPath, snippetContent + "\n\n" + existing);
console.log(` ✓ CLAUDE.md updated`);
}
+ } else {
+ writeText(claudeMdPath, snippetContent);
+ console.log(` ✓ CLAUDE.md created`);
+ }
+
+ // 7. Update Codex hooks + AGENTS.md
+ const codexDir = path.join(root, ".codex");
+ ensureDir(codexDir);
+ writeJSON(path.join(codexDir, "hooks.json"), CODEX_HOOK_SETTINGS);
+ writeText(path.join(codexDir, "config.toml"), CODEX_CONFIG_TOML);
+ console.log(` ✓ Codex hooks updated`);
+
+ const agentsPath = path.join(root, "AGENTS.md");
+ const agentsSnippet = readTemplateContent("agents-md-snippet.md", templatesDir);
+ if (fs.existsSync(agentsPath)) {
+ const existing = readText(agentsPath);
+ if (!existing.includes("OpenWolf")) {
+ writeText(agentsPath, agentsSnippet + "\n\n" + existing);
+ console.log(` ✓ AGENTS.md updated`);
+ }
+ } else {
+ writeText(agentsPath, agentsSnippet);
+ console.log(` ✓ AGENTS.md created`);
}
- // 7. Clean up stale .tmp files
+ // 8. Clean up stale .tmp files
try {
const files = fs.readdirSync(wolfDir);
let cleaned = 0;
@@ -222,7 +264,7 @@ async function updateProject(
if (cleaned > 0) console.log(` ✓ Cleaned ${cleaned} stale .tmp file(s)`);
} catch {}
- // 8. Update registry entry
+ // 9. Update registry entry
registerProject(root, name, version);
return {
@@ -258,15 +300,8 @@ function createBackup(wolfDir: string): string {
const hooksDir = path.join(wolfDir, "hooks");
if (fs.existsSync(hooksDir)) {
const hooksBackup = path.join(backupDir, "hooks");
- ensureDir(hooksBackup);
try {
- const hookFiles = fs.readdirSync(hooksDir);
- for (const f of hookFiles) {
- const src = path.join(hooksDir, f);
- if (fs.statSync(src).isFile()) {
- fs.copyFileSync(src, path.join(hooksBackup, f));
- }
- }
+ copyDirectoryRecursive(hooksDir, hooksBackup);
} catch {}
}
@@ -285,6 +320,23 @@ function createBackup(wolfDir: string): string {
fs.copyFileSync(claudeRules, path.join(rulesBackup, "openwolf.md"));
}
+ const codexHooks = path.join(projectRoot, ".codex", "hooks.json");
+ if (fs.existsSync(codexHooks)) {
+ const codexBackup = path.join(backupDir, ".codex");
+ ensureDir(codexBackup);
+ fs.copyFileSync(codexHooks, path.join(codexBackup, "hooks.json"));
+ }
+ const codexConfig = path.join(projectRoot, ".codex", "config.toml");
+ if (fs.existsSync(codexConfig)) {
+ const codexBackup = path.join(backupDir, ".codex");
+ ensureDir(codexBackup);
+ fs.copyFileSync(codexConfig, path.join(codexBackup, "config.toml"));
+ }
+ const agentsPath = path.join(projectRoot, "AGENTS.md");
+ if (fs.existsSync(agentsPath)) {
+ fs.copyFileSync(agentsPath, path.join(backupDir, "AGENTS.md"));
+ }
+
return backupDir;
}
@@ -310,6 +362,7 @@ function readTemplateContent(filename: string, templatesDir: string): string {
}
const templates: Record = {
"claude-md-snippet.md": `# OpenWolf\n\n@.wolf/OPENWOLF.md\n\nThis project uses OpenWolf for context management. Read and follow .wolf/OPENWOLF.md every session. Check .wolf/cerebrum.md before generating code. Check .wolf/anatomy.md before reading files.`,
+ "agents-md-snippet.md": `# OpenWolf\n\n@.wolf/OPENWOLF.md\n\nThis project uses OpenWolf for context management. Read and follow .wolf/OPENWOLF.md every session. Check .wolf/cerebrum.md before generating code. Check .wolf/anatomy.md before reading files.`,
"claude-rules-openwolf.md": `---\ndescription: OpenWolf protocol enforcement — active on all files\nglobs: **/*\n---\n\n- Check .wolf/anatomy.md before reading any project file\n- Check .wolf/cerebrum.md Do-Not-Repeat list before generating code\n- After writing or editing files, update .wolf/anatomy.md and append to .wolf/memory.md\n- After receiving a user correction, update .wolf/cerebrum.md immediately (Preferences, Learnings, or Do-Not-Repeat)\n- LEARN from every interaction: if you discover a convention, user preference, or project pattern, add it to .wolf/cerebrum.md. Low threshold — when in doubt, log it.\n- BEFORE fixing any bug or error: read .wolf/buglog.json for known fixes\n- AFTER fixing any bug, error, failed test, failed build, or user-reported problem: ALWAYS log to .wolf/buglog.json with error_message, root_cause, fix, and tags\n- If you edit a file more than twice in a session, that likely indicates a bug — log it to .wolf/buglog.json\n- When the user asks to check/evaluate UI design: run \`openwolf designqc\` to capture screenshots, then read them from .wolf/designqc-captures/\n- When the user asks to change/pick/migrate UI framework: read .wolf/reframe-frameworks.md, ask decision questions, recommend a framework, then execute with the framework's prompt`,
};
return templates[filename] ?? "";
@@ -339,10 +392,15 @@ function copyHookScripts(wolfDir: string): void {
];
if (sourceDir) {
- for (const file of hookFiles) {
- const src = path.join(sourceDir, file);
- if (fs.existsSync(src)) {
- fs.copyFileSync(src, path.join(hooksDir, file));
+ removeLegacyTopLevelHooks(hooksDir, hookFiles);
+ for (const provider of ["claude", "codex"]) {
+ const providerDir = path.join(hooksDir, provider);
+ ensureDir(providerDir);
+ for (const file of hookFiles) {
+ const src = path.join(sourceDir, file);
+ if (fs.existsSync(src)) {
+ fs.copyFileSync(src, path.join(providerDir, file));
+ }
}
}
}
@@ -352,9 +410,20 @@ function copyHookScripts(wolfDir: string): void {
fs.writeFileSync(hooksPkgPath, JSON.stringify({ type: "module" }, null, 2) + "\n", "utf-8");
}
+function removeLegacyTopLevelHooks(hooksDir: string, hookFiles: string[]): void {
+ for (const file of hookFiles) {
+ const legacyPath = path.join(hooksDir, file);
+ if (fs.existsSync(legacyPath)) {
+ try {
+ fs.unlinkSync(legacyPath);
+ } catch {}
+ }
+ }
+}
+
function replaceOpenWolfHooks(
existing: Record,
- hookSettings: typeof HOOK_SETTINGS
+ hookSettings: typeof CLAUDE_HOOK_SETTINGS
): Record {
const merged = { ...existing };
if (!merged.hooks) merged.hooks = {};
@@ -449,12 +518,8 @@ export function restoreCommand(backupName?: string): void {
// Restore hooks if present
const hooksBackup = path.join(backupDir, "hooks");
if (fs.existsSync(hooksBackup)) {
- const hookFiles = fs.readdirSync(hooksBackup);
const hooksDir = path.join(wolfDir, "hooks");
- ensureDir(hooksDir);
- for (const f of hookFiles) {
- fs.copyFileSync(path.join(hooksBackup, f), path.join(hooksDir, f));
- }
+ copyDirectoryRecursive(hooksBackup, hooksDir);
}
// Restore .claude settings if present
@@ -475,5 +540,40 @@ export function restoreCommand(backupName?: string): void {
}
}
+ const codexBackup = path.join(backupDir, ".codex");
+ if (fs.existsSync(codexBackup)) {
+ const projectRoot = path.dirname(wolfDir);
+ const hooksBackup = path.join(codexBackup, "hooks.json");
+ if (fs.existsSync(hooksBackup)) {
+ const dest = path.join(projectRoot, ".codex", "hooks.json");
+ ensureDir(path.dirname(dest));
+ fs.copyFileSync(hooksBackup, dest);
+ }
+ const configBackup = path.join(codexBackup, "config.toml");
+ if (fs.existsSync(configBackup)) {
+ const dest = path.join(projectRoot, ".codex", "config.toml");
+ ensureDir(path.dirname(dest));
+ fs.copyFileSync(configBackup, dest);
+ }
+ }
+
+ const agentsBackup = path.join(backupDir, "AGENTS.md");
+ if (fs.existsSync(agentsBackup)) {
+ fs.copyFileSync(agentsBackup, path.join(path.dirname(wolfDir), "AGENTS.md"));
+ }
+
console.log(`Restored ${files.length} files from backup "${backupName}".`);
}
+
+function copyDirectoryRecursive(srcDir: string, destDir: string): void {
+ ensureDir(destDir);
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
+ const src = path.join(srcDir, entry.name);
+ const dest = path.join(destDir, entry.name);
+ if (entry.isDirectory()) {
+ copyDirectoryRecursive(src, dest);
+ } else if (entry.isFile()) {
+ fs.copyFileSync(src, dest);
+ }
+ }
+}
diff --git a/src/hooks/post-read.ts b/src/hooks/post-read.ts
index 17c82804..39c1f98b 100644
--- a/src/hooks/post-read.ts
+++ b/src/hooks/post-read.ts
@@ -1,5 +1,5 @@
import * as path from "node:path";
-import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath } from "./shared.js";
+import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath, getProjectDir } from "./shared.js";
interface SessionData {
files_read: Record;
@@ -28,7 +28,7 @@ async function main(): Promise {
const normalizedFile = normalizePath(filePath);
// Skip tracking for .wolf/ internal files — consistent with pre-read
- const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
+ const projectDir = normalizePath(getProjectDir());
const relToProject = normalizedFile.startsWith(projectDir)
? normalizedFile.slice(projectDir.length).replace(/^\//, "")
: "";
diff --git a/src/hooks/post-write.ts b/src/hooks/post-write.ts
index 3190cb20..d8a1925e 100644
--- a/src/hooks/post-write.ts
+++ b/src/hooks/post-write.ts
@@ -3,7 +3,7 @@ import * as path from "node:path";
import * as crypto from "node:crypto";
import {
getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, serializeAnatomy,
- extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath
+ extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath, getProjectDir
} from "./shared.js";
interface SessionData {
@@ -35,7 +35,7 @@ async function main(): Promise {
const wolfDir = getWolfDir();
const hooksDir = path.join(wolfDir, "hooks");
const sessionFile = path.join(hooksDir, "_session.json");
- const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
+ const projectRoot = getProjectDir();
const raw = await readStdin();
let input: { tool_name?: string; tool_input?: { file_path?: string; path?: string; content?: string; old_string?: string; new_string?: string } };
diff --git a/src/hooks/pre-read.ts b/src/hooks/pre-read.ts
index 61e933f0..0d5e947a 100644
--- a/src/hooks/pre-read.ts
+++ b/src/hooks/pre-read.ts
@@ -1,7 +1,7 @@
import * as path from "node:path";
import {
getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy,
- estimateTokens, readStdin, normalizePath
+ estimateTokens, readStdin, normalizePath, getProjectDir
} from "./shared.js";
interface SessionData {
@@ -35,7 +35,7 @@ async function main(): Promise {
// Skip tracking for .wolf/ internal files — they're infrastructure, not project files.
// Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning.
- const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd());
+ const projectDir = normalizePath(getProjectDir());
const relToProject = normalizedFile.startsWith(projectDir)
? normalizedFile.slice(projectDir.length).replace(/^\//, "")
: "";
diff --git a/src/hooks/shared.ts b/src/hooks/shared.ts
index 890a20e8..a7f44530 100644
--- a/src/hooks/shared.ts
+++ b/src/hooks/shared.ts
@@ -2,10 +2,12 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as crypto from "node:crypto";
+export function getProjectDir(): string {
+ return process.cwd();
+}
+
export function getWolfDir(): string {
- // Prefer CLAUDE_PROJECT_DIR so hooks work even if CWD changes during a session
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
- return path.join(projectDir, ".wolf");
+ return path.join(getProjectDir(), ".wolf");
}
/**
diff --git a/src/templates/agents-md-snippet.md b/src/templates/agents-md-snippet.md
new file mode 100644
index 00000000..11942b2f
--- /dev/null
+++ b/src/templates/agents-md-snippet.md
@@ -0,0 +1,5 @@
+# OpenWolf
+
+@.wolf/OPENWOLF.md
+
+This project uses OpenWolf for context management. Read and follow .wolf/OPENWOLF.md every session. Check .wolf/cerebrum.md before generating code. Check .wolf/anatomy.md before reading files.
From 54a0ae4b332a151a8e087fbf5d6f2629db931bc7 Mon Sep 17 00:00:00 2001
From: nottyjay
Date: Tue, 9 Jun 2026 16:39:41 +0800
Subject: [PATCH 2/9] Fix Codex hook paths for non-git projects
---
package.json | 2 +-
src/cli/codex-config.ts | 5 +++++
src/cli/init.test.ts | 17 +++++++++++++++++
src/cli/init.ts | 21 ++++++++++-----------
src/cli/update.ts | 21 ++++++++++-----------
src/hooks/session-start.ts | 31 +++++++++++++++++++++----------
src/hooks/shared.ts | 6 ++++++
7 files changed, 70 insertions(+), 33 deletions(-)
create mode 100644 src/cli/codex-config.ts
diff --git a/package.json b/package.json
index 92ae06e9..767ede57 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openwolf",
- "version": "1.0.4",
+ "version": "1.0.5",
"description": "Token-conscious AI brain for Claude Code projects",
"type": "module",
"bin": {
diff --git a/src/cli/codex-config.ts b/src/cli/codex-config.ts
new file mode 100644
index 00000000..cdaa11fb
--- /dev/null
+++ b/src/cli/codex-config.ts
@@ -0,0 +1,5 @@
+export function getCodexConfigToml(): string {
+ return `[features]
+hooks = true
+`;
+}
diff --git a/src/cli/init.test.ts b/src/cli/init.test.ts
index 1988d8bf..f130b06c 100644
--- a/src/cli/init.test.ts
+++ b/src/cli/init.test.ts
@@ -34,10 +34,12 @@ test("default init installs both Claude and Codex integration files", async () =
const projectRoot = makeProject("dual-target");
const previousCwd = process.cwd();
const previousHome = process.env.HOME;
+ const previousCodexVersion = process.env.OPENWOLF_CODEX_VERSION;
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "openwolf-home-"));
process.chdir(projectRoot);
process.env.HOME = fakeHome;
+ process.env.OPENWOLF_CODEX_VERSION = "0.129.0";
try {
await runInit();
@@ -59,9 +61,16 @@ test("default init installs both Claude and Codex integration files", async () =
JSON.stringify(readJSON(path.join(projectRoot, ".codex", "hooks.json"))),
/\.wolf\/hooks\/codex\/session-start\.js/
);
+ assert.match(
+ JSON.stringify(readJSON(path.join(projectRoot, ".codex", "hooks.json"))),
+ /git rev-parse --show-toplevel 2>\/dev\/null \|\| pwd/
+ );
+ assert.match(read(path.join(projectRoot, ".codex", "config.toml")), /hooks = true/);
+ assert.doesNotMatch(read(path.join(projectRoot, ".codex", "config.toml")), /codex_hooks = true/);
} finally {
process.chdir(previousCwd);
process.env.HOME = previousHome;
+ process.env.OPENWOLF_CODEX_VERSION = previousCodexVersion;
fs.rmSync(projectRoot, { recursive: true, force: true });
fs.rmSync(fakeHome, { recursive: true, force: true });
}
@@ -71,10 +80,12 @@ test("claude target only installs Claude integration files", async () => {
const projectRoot = makeProject("claude-only");
const previousCwd = process.cwd();
const previousHome = process.env.HOME;
+ const previousCodexVersion = process.env.OPENWOLF_CODEX_VERSION;
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "openwolf-home-"));
process.chdir(projectRoot);
process.env.HOME = fakeHome;
+ process.env.OPENWOLF_CODEX_VERSION = "0.129.0";
try {
await runInit("claude");
@@ -89,6 +100,7 @@ test("claude target only installs Claude integration files", async () => {
} finally {
process.chdir(previousCwd);
process.env.HOME = previousHome;
+ process.env.OPENWOLF_CODEX_VERSION = previousCodexVersion;
fs.rmSync(projectRoot, { recursive: true, force: true });
fs.rmSync(fakeHome, { recursive: true, force: true });
}
@@ -98,10 +110,12 @@ test("codex target only installs Codex integration files", async () => {
const projectRoot = makeProject("codex-only");
const previousCwd = process.cwd();
const previousHome = process.env.HOME;
+ const previousCodexVersion = process.env.OPENWOLF_CODEX_VERSION;
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "openwolf-home-"));
process.chdir(projectRoot);
process.env.HOME = fakeHome;
+ process.env.OPENWOLF_CODEX_VERSION = "0.129.0";
try {
await runInit("codex");
@@ -113,9 +127,12 @@ test("codex target only installs Codex integration files", async () => {
assert.equal(fs.existsSync(path.join(projectRoot, ".wolf", "hooks", "claude", "session-start.js")), false);
assert.equal(fs.existsSync(path.join(projectRoot, "CLAUDE.md")), false);
assert.equal(fs.existsSync(path.join(projectRoot, ".claude", "settings.json")), false);
+ assert.match(read(path.join(projectRoot, ".codex", "config.toml")), /hooks = true/);
+ assert.doesNotMatch(read(path.join(projectRoot, ".codex", "config.toml")), /codex_hooks = true/);
} finally {
process.chdir(previousCwd);
process.env.HOME = previousHome;
+ process.env.OPENWOLF_CODEX_VERSION = previousCodexVersion;
fs.rmSync(projectRoot, { recursive: true, force: true });
fs.rmSync(fakeHome, { recursive: true, force: true });
}
diff --git a/src/cli/init.ts b/src/cli/init.ts
index 3c464fba..cc96a6ae 100644
--- a/src/cli/init.ts
+++ b/src/cli/init.ts
@@ -8,6 +8,7 @@ import { readJSON, writeJSON, readText, writeText } from "../utils/fs-safe.js";
import { ensureDir } from "../utils/paths.js";
import { isWindows } from "../utils/platform.js";
import { registerProject } from "./registry.js";
+import { getCodexConfigToml } from "./codex-config.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -120,6 +121,8 @@ const CLAUDE_HOOK_SETTINGS = {
},
};
+const CODEX_PROJECT_ROOT = '$(git rev-parse --show-toplevel 2>/dev/null || pwd)';
+
const CODEX_HOOK_SETTINGS = {
hooks: {
SessionStart: [
@@ -128,7 +131,7 @@ const CODEX_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/session-start.js"',
+ command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/session-start.js"`,
timeout: 5,
statusMessage: "OpenWolf session bootstrap",
},
@@ -141,7 +144,7 @@ const CODEX_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-read.js"',
+ command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/pre-read.js"`,
timeout: 5,
statusMessage: "OpenWolf read precheck",
},
@@ -152,7 +155,7 @@ const CODEX_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-write.js"',
+ command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/pre-write.js"`,
timeout: 5,
statusMessage: "OpenWolf write precheck",
},
@@ -165,7 +168,7 @@ const CODEX_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-read.js"',
+ command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/post-read.js"`,
timeout: 5,
statusMessage: "OpenWolf read tracking",
},
@@ -176,7 +179,7 @@ const CODEX_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-write.js"',
+ command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/post-write.js"`,
timeout: 10,
statusMessage: "OpenWolf write tracking",
},
@@ -188,7 +191,7 @@ const CODEX_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/stop.js"',
+ command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/stop.js"`,
timeout: 10,
statusMessage: "OpenWolf session finalize",
},
@@ -198,10 +201,6 @@ const CODEX_HOOK_SETTINGS = {
},
};
-const CODEX_CONFIG_TOML = `[features]
-codex_hooks = true
-`;
-
export async function initCommand(targetArg?: string): Promise {
// Check Node.js version
const nodeVersion = parseInt(process.version.slice(1), 10);
@@ -416,7 +415,7 @@ function installCodexIntegration(projectRoot: string, templatesDir: string): voi
ensureDir(codexDir);
writeJSON(path.join(codexDir, "hooks.json"), CODEX_HOOK_SETTINGS);
- writeText(path.join(codexDir, "config.toml"), CODEX_CONFIG_TOML);
+ writeText(path.join(codexDir, "config.toml"), getCodexConfigToml());
const agentsPath = path.join(projectRoot, "AGENTS.md");
const snippetContent = readTemplateContent("agents-md-snippet.md", templatesDir);
diff --git a/src/cli/update.ts b/src/cli/update.ts
index b8b94973..62dc7b4d 100644
--- a/src/cli/update.ts
+++ b/src/cli/update.ts
@@ -13,6 +13,7 @@ import { fileURLToPath } from "node:url";
import { getRegisteredProjects, registerProject, type RegisteredProject } from "./registry.js";
import { readJSON, writeJSON, readText, writeText } from "../utils/fs-safe.js";
import { ensureDir } from "../utils/paths.js";
+import { getCodexConfigToml } from "./codex-config.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -58,25 +59,23 @@ const CLAUDE_HOOK_SETTINGS = {
},
};
+const CODEX_PROJECT_ROOT = '$(git rev-parse --show-toplevel 2>/dev/null || pwd)';
+
const CODEX_HOOK_SETTINGS = {
hooks: {
- SessionStart: [{ matcher: "startup|resume|clear", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/session-start.js"', timeout: 5, statusMessage: "OpenWolf session bootstrap" }] }],
+ SessionStart: [{ matcher: "startup|resume|clear", hooks: [{ type: "command", command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/session-start.js"`, timeout: 5, statusMessage: "OpenWolf session bootstrap" }] }],
PreToolUse: [
- { matcher: "Read", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-read.js"', timeout: 5, statusMessage: "OpenWolf read precheck" }] },
- { matcher: "Edit|Write|MultiEdit|apply_patch", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/pre-write.js"', timeout: 5, statusMessage: "OpenWolf write precheck" }] },
+ { matcher: "Read", hooks: [{ type: "command", command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/pre-read.js"`, timeout: 5, statusMessage: "OpenWolf read precheck" }] },
+ { matcher: "Edit|Write|MultiEdit|apply_patch", hooks: [{ type: "command", command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/pre-write.js"`, timeout: 5, statusMessage: "OpenWolf write precheck" }] },
],
PostToolUse: [
- { matcher: "Read", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-read.js"', timeout: 5, statusMessage: "OpenWolf read tracking" }] },
- { matcher: "Edit|Write|MultiEdit|apply_patch", hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/post-write.js"', timeout: 10, statusMessage: "OpenWolf write tracking" }] },
+ { matcher: "Read", hooks: [{ type: "command", command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/post-read.js"`, timeout: 5, statusMessage: "OpenWolf read tracking" }] },
+ { matcher: "Edit|Write|MultiEdit|apply_patch", hooks: [{ type: "command", command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/post-write.js"`, timeout: 10, statusMessage: "OpenWolf write tracking" }] },
],
- Stop: [{ hooks: [{ type: "command", command: 'node "$(git rev-parse --show-toplevel)/.wolf/hooks/codex/stop.js"', timeout: 10, statusMessage: "OpenWolf session finalize" }] }],
+ Stop: [{ hooks: [{ type: "command", command: `node "${CODEX_PROJECT_ROOT}/.wolf/hooks/codex/stop.js"`, timeout: 10, statusMessage: "OpenWolf session finalize" }] }],
},
};
-const CODEX_CONFIG_TOML = `[features]
-codex_hooks = true
-`;
-
interface UpdateResult {
project: RegisteredProject;
status: "updated" | "skipped" | "error";
@@ -236,7 +235,7 @@ async function updateProject(
const codexDir = path.join(root, ".codex");
ensureDir(codexDir);
writeJSON(path.join(codexDir, "hooks.json"), CODEX_HOOK_SETTINGS);
- writeText(path.join(codexDir, "config.toml"), CODEX_CONFIG_TOML);
+ writeText(path.join(codexDir, "config.toml"), getCodexConfigToml());
console.log(` ✓ Codex hooks updated`);
const agentsPath = path.join(root, "AGENTS.md");
diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts
index 78206240..b13404dd 100644
--- a/src/hooks/session-start.ts
+++ b/src/hooks/session-start.ts
@@ -1,10 +1,12 @@
import * as fs from "node:fs";
import * as path from "node:path";
-import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort } from "./shared.js";
+import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort, getHookProvider } from "./shared.js";
async function main(): Promise {
ensureWolfDir();
const wolfDir = getWolfDir();
+ const provider = getHookProvider();
+ const notices: string[] = [];
// Clean up stale .tmp files left from failed atomic writes
try {
@@ -53,13 +55,9 @@ async function main(): Promise {
});
if (entryLines.length < 3) {
- process.stderr.write(
- `💡 OpenWolf: cerebrum.md has only ${entryLines.length} entries. Learn from this session — record user preferences, project conventions, and mistakes to .wolf/cerebrum.md.\n`
- );
+ notices.push(`OpenWolf: cerebrum.md has only ${entryLines.length} entries. Learn from this session and record preferences, conventions, and mistakes to .wolf/cerebrum.md.`);
} else if (daysSinceUpdate > 3) {
- process.stderr.write(
- `💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.\n`
- );
+ notices.push(`OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.`);
}
} catch {}
@@ -68,9 +66,7 @@ async function main(): Promise {
const buglogPath = path.join(wolfDir, "buglog.json");
const buglog = readJSON<{ bugs: unknown[] }>(buglogPath, { bugs: [] });
if (buglog.bugs.length === 0) {
- process.stderr.write(
- `📋 OpenWolf: buglog.json is empty. If you encounter or fix any bugs, errors, or failed tests this session, log them to .wolf/buglog.json.\n`
- );
+ notices.push("OpenWolf: buglog.json is empty. If you encounter or fix bugs, errors, or failed tests this session, log them to .wolf/buglog.json.");
}
} catch {}
@@ -84,6 +80,21 @@ async function main(): Promise {
ledger.lifetime.total_sessions++;
writeJSON(ledgerPath, ledger);
+ if (notices.length > 0) {
+ if (provider === "codex") {
+ process.stdout.write(JSON.stringify({
+ hookSpecificOutput: {
+ hookEventName: "SessionStart",
+ additionalContext: notices.join("\n"),
+ },
+ }));
+ } else {
+ for (const notice of notices) {
+ process.stderr.write(`${notice}\n`);
+ }
+ }
+ }
+
process.exit(0);
}
diff --git a/src/hooks/shared.ts b/src/hooks/shared.ts
index a7f44530..6c25d38e 100644
--- a/src/hooks/shared.ts
+++ b/src/hooks/shared.ts
@@ -6,6 +6,12 @@ export function getProjectDir(): string {
return process.cwd();
}
+export function getHookProvider(): "claude" | "codex" {
+ const scriptPath = normalizePath(process.argv[1] || "");
+ if (scriptPath.includes("/codex/")) return "codex";
+ return "claude";
+}
+
export function getWolfDir(): string {
return path.join(getProjectDir(), ".wolf");
}
From a230430cc8788d3b2931ebe0b7cfc1626ef4b39f Mon Sep 17 00:00:00 2001
From: nottyjay
Date: Tue, 9 Jun 2026 17:37:55 +0800
Subject: [PATCH 3/9] change name
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 767ede57..abc18559 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "openwolf",
+ "name": "@alptech/openwolf",
"version": "1.0.5",
"description": "Token-conscious AI brain for Claude Code projects",
"type": "module",
From 9e24b688ad27f7b194c160829d8b4d26a253fc84 Mon Sep 17 00:00:00 2001
From: nottyjay
Date: Wed, 24 Jun 2026 15:14:26 +0800
Subject: [PATCH 4/9] fix: Claude Code hook compatibility with new hook system
- Switch hook config from shell form to exec form with args array
- Pass project directory as CLI arg to hooks (no longer rely on process.cwd())
- Fix placeholder format: $CLAUDE_PROJECT_DIR -> ${CLAUDE_PROJECT_DIR}
- SessionStart: use additionalContext JSON stdout for both Claude and Codex
- replaceOpenWolfHooks: detect hooks by args in addition to command
- Update README for fork relationship and new npm package name
- Update package.json repository URLs and author info
---
README.md | 12 +++++++-----
package.json | 10 ++++++----
src/cli/init.ts | 28 ++++++++++++++++++----------
src/cli/update.ts | 19 ++++++++++---------
src/hooks/session-start.ts | 21 +++++++--------------
src/hooks/shared.ts | 5 +++++
6 files changed, 53 insertions(+), 42 deletions(-)
diff --git a/README.md b/README.md
index 37e8faf1..2cf5ee17 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-OpenWolf
+@alptech/openwolf
A second brain for Claude Code and Codex.
@@ -10,11 +10,13 @@
-
+
+> **This is a fork of [openwolf](https://www.npmjs.com/package/openwolf) by [Cytostack](https://github.com/cytostack).** It preserves all original capabilities and adds native, first-class [Codex](https://github.com/openai/codex) integration alongside the existing Claude Code support. Both agents get equal treatment — dedicated hook scripts, project entry files, and config scaffolding.
+
---
## Why OpenWolf Exists
@@ -40,7 +42,7 @@ Across 20 projects, 132+ sessions: average token reduction of 65.8%, with 71% of
## Quick Start
```bash
-npm install -g openwolf
+npm install -g @alptech/openwolf
cd your-project
openwolf init
```
@@ -240,7 +242,7 @@ OpenWolf is not an AI wrapper. It is 6 hook scripts and a `.wolf/` directory. It
- Codex hooks require Codex project hook support via `.codex/hooks.json` and `.codex/config.toml`, plus `AGENTS.md` project instructions.
- Token tracking is estimation-based (character-to-token ratio), not exact API counts. Accurate to within ~15%.
- `cerebrum.md` depends on Claude following instructions to update it after corrections. Compliance is ~85-90%, not 100%.
-- This is v1.0.4. Things may break. [File issues](https://github.com/cytostack/openwolf/issues).
+- This is v1.0.5. Things may break. [File issues](https://github.com/nottyjay/openwolf/issues).
## Origin Story
@@ -252,4 +254,4 @@ We were building products with Claude Code at Cytostack when we noticed somethin
## Author
-Built by Farhan Palathinkal Afsal - [Cytostack](https://github.com/cytostack)
+Original project by Farhan Palathinkal Afsal — [Cytostack](https://github.com/cytostack). Forked and extended with first-class Codex integration by [@alptech](https://www.npmjs.com/package/@alptech/openwolf).
diff --git a/package.json b/package.json
index abc18559..0e965e86 100644
--- a/package.json
+++ b/package.json
@@ -49,22 +49,24 @@
"node": ">=20.0.0"
},
"license": "AGPL-3.0-only",
- "author": "Cytostack Pvt Ltd",
+ "author": "Cytostack Pvt Ltd (original) / alptech (fork)",
"repository": {
"type": "git",
- "url": "https://github.com/cytostack/openwolf.git"
+ "url": "https://github.com/nottyjay/openwolf.git"
},
- "homepage": "https://github.com/cytostack/openwolf",
+ "homepage": "https://github.com/nottyjay/openwolf",
"bugs": {
- "url": "https://github.com/cytostack/openwolf/issues"
+ "url": "https://github.com/nottyjay/openwolf/issues"
},
"keywords": [
"claude",
+ "codex",
"ai",
"context",
"token",
"openwolf",
"claude-code",
+ "codex-cli",
"developer-tools"
],
"files": [
diff --git a/src/cli/init.ts b/src/cli/init.ts
index cc96a6ae..547e7fc1 100644
--- a/src/cli/init.ts
+++ b/src/cli/init.ts
@@ -47,7 +47,8 @@ const CREATE_IF_MISSING = [
type InitTarget = "claude" | "codex";
-// Use $CLAUDE_PROJECT_DIR so hooks resolve correctly even if CWD changes during a session
+// Use ${CLAUDE_PROJECT_DIR} with exec form so hooks resolve correctly regardless of CWD.
+// The second arg passes the project dir to the hook scripts, making them independent of process.cwd().
const CLAUDE_HOOK_SETTINGS = {
hooks: {
SessionStart: [
@@ -56,7 +57,8 @@ const CLAUDE_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/session-start.js"',
+ command: "node",
+ args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/session-start.js", "${CLAUDE_PROJECT_DIR}"],
timeout: 5,
},
],
@@ -68,7 +70,8 @@ const CLAUDE_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-read.js"',
+ command: "node",
+ args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/pre-read.js", "${CLAUDE_PROJECT_DIR}"],
timeout: 5,
},
],
@@ -78,7 +81,8 @@ const CLAUDE_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-write.js"',
+ command: "node",
+ args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/pre-write.js", "${CLAUDE_PROJECT_DIR}"],
timeout: 5,
},
],
@@ -90,7 +94,8 @@ const CLAUDE_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-read.js"',
+ command: "node",
+ args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/post-read.js", "${CLAUDE_PROJECT_DIR}"],
timeout: 5,
},
],
@@ -100,7 +105,8 @@ const CLAUDE_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-write.js"',
+ command: "node",
+ args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/post-write.js", "${CLAUDE_PROJECT_DIR}"],
timeout: 10,
},
],
@@ -112,7 +118,8 @@ const CLAUDE_HOOK_SETTINGS = {
hooks: [
{
type: "command",
- command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/stop.js"',
+ command: "node",
+ args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/stop.js", "${CLAUDE_PROJECT_DIR}"],
timeout: 10,
},
],
@@ -638,17 +645,18 @@ function replaceOpenWolfHooks(
if (!merged.hooks) {
merged.hooks = {};
}
- const hooks = merged.hooks as Record }>>;
+ const hooks = merged.hooks as Record }>>;
for (const [event, newMatchers] of Object.entries(hookSettings.hooks)) {
if (!hooks[event]) {
hooks[event] = [];
}
- // Remove any existing OpenWolf hook entries (match by .wolf/hooks/ in command)
+ // Remove any existing OpenWolf hook entries (match by .wolf/hooks/ in command or args)
hooks[event] = hooks[event].filter((entry) => {
const isOpenWolfHook = entry.hooks?.some(
- (h) => h.command && h.command.includes(".wolf/hooks/")
+ (h) => (h.command && h.command.includes(".wolf/hooks/")) ||
+ (h.args && h.args.some((a: string) => a.includes(".wolf/hooks/")))
);
return !isOpenWolfHook;
});
diff --git a/src/cli/update.ts b/src/cli/update.ts
index 62dc7b4d..90503b4b 100644
--- a/src/cli/update.ts
+++ b/src/cli/update.ts
@@ -46,16 +46,16 @@ const BACKUP_FILES = [
const CLAUDE_HOOK_SETTINGS = {
hooks: {
- SessionStart: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/session-start.js"', timeout: 5 }] }],
+ SessionStart: [{ matcher: "", hooks: [{ type: "command", command: "node", args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/session-start.js", "${CLAUDE_PROJECT_DIR}"], timeout: 5 }] }],
PreToolUse: [
- { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-read.js"', timeout: 5 }] },
- { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/pre-write.js"', timeout: 5 }] },
+ { matcher: "Read", hooks: [{ type: "command", command: "node", args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/pre-read.js", "${CLAUDE_PROJECT_DIR}"], timeout: 5 }] },
+ { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: "node", args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/pre-write.js", "${CLAUDE_PROJECT_DIR}"], timeout: 5 }] },
],
PostToolUse: [
- { matcher: "Read", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-read.js"', timeout: 5 }] },
- { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/post-write.js"', timeout: 10 }] },
+ { matcher: "Read", hooks: [{ type: "command", command: "node", args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/post-read.js", "${CLAUDE_PROJECT_DIR}"], timeout: 5 }] },
+ { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: "node", args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/post-write.js", "${CLAUDE_PROJECT_DIR}"], timeout: 10 }] },
],
- Stop: [{ matcher: "", hooks: [{ type: "command", command: 'node "$CLAUDE_PROJECT_DIR/.wolf/hooks/claude/stop.js"', timeout: 10 }] }],
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: "node", args: ["${CLAUDE_PROJECT_DIR}/.wolf/hooks/claude/stop.js", "${CLAUDE_PROJECT_DIR}"], timeout: 10 }] }],
},
};
@@ -426,15 +426,16 @@ function replaceOpenWolfHooks(
): Record {
const merged = { ...existing };
if (!merged.hooks) merged.hooks = {};
- const hooks = merged.hooks as Record }>>;
+ const hooks = merged.hooks as Record }>>;
for (const [event, newMatchers] of Object.entries(hookSettings.hooks)) {
if (!hooks[event]) hooks[event] = [];
- // Remove existing OpenWolf hook entries
+ // Remove existing OpenWolf hook entries (match by .wolf/hooks/ in command or args)
hooks[event] = hooks[event].filter((entry) => {
const isOpenWolfHook = entry.hooks?.some(
- (h) => h.command && h.command.includes(".wolf/hooks/")
+ (h) => (h.command && h.command.includes(".wolf/hooks/")) ||
+ (h.args && h.args.some((a: string) => a.includes(".wolf/hooks/")))
);
return !isOpenWolfHook;
});
diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts
index b13404dd..e197835f 100644
--- a/src/hooks/session-start.ts
+++ b/src/hooks/session-start.ts
@@ -1,11 +1,10 @@
import * as fs from "node:fs";
import * as path from "node:path";
-import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort, getHookProvider } from "./shared.js";
+import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort } from "./shared.js";
async function main(): Promise {
ensureWolfDir();
const wolfDir = getWolfDir();
- const provider = getHookProvider();
const notices: string[] = [];
// Clean up stale .tmp files left from failed atomic writes
@@ -81,18 +80,12 @@ async function main(): Promise {
writeJSON(ledgerPath, ledger);
if (notices.length > 0) {
- if (provider === "codex") {
- process.stdout.write(JSON.stringify({
- hookSpecificOutput: {
- hookEventName: "SessionStart",
- additionalContext: notices.join("\n"),
- },
- }));
- } else {
- for (const notice of notices) {
- process.stderr.write(`${notice}\n`);
- }
- }
+ process.stdout.write(JSON.stringify({
+ hookSpecificOutput: {
+ hookEventName: "SessionStart",
+ additionalContext: notices.join("\n"),
+ },
+ }));
}
process.exit(0);
diff --git a/src/hooks/shared.ts b/src/hooks/shared.ts
index 6c25d38e..bd267310 100644
--- a/src/hooks/shared.ts
+++ b/src/hooks/shared.ts
@@ -3,6 +3,11 @@ import * as path from "node:path";
import * as crypto from "node:crypto";
export function getProjectDir(): string {
+ // argv[2] is the first user argument after the script path, passed by hook config.
+ // Newer Claude Code may not run hooks from the project directory — rely on the
+ // explicit project-dir argument when available, falling back to process.cwd().
+ const argDir = process.argv[2];
+ if (argDir && fs.existsSync(argDir)) return argDir;
return process.cwd();
}
From f3023024426e6742737191d604bb10e66c681604 Mon Sep 17 00:00:00 2001
From: nottyjay
Date: Wed, 24 Jun 2026 15:16:03 +0800
Subject: [PATCH 5/9] chore: bump version to 1.0.6
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 0e965e86..73d7479b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@alptech/openwolf",
- "version": "1.0.5",
+ "version": "1.0.6",
"description": "Token-conscious AI brain for Claude Code projects",
"type": "module",
"bin": {
From 4b553cae8a0a71e5cfeb8ec99728c1ba0606f1a1 Mon Sep 17 00:00:00 2001
From: Nottyjay
Date: Wed, 12 Aug 2026 14:40:17 +0800
Subject: [PATCH 6/9] sync: upstream business features from cytostack/openwolf
Track upstream multi-agent adapters, PreCompact, durable anatomy store,
measured token usage, dashboard redesign, and bundled skills. Keep
@alptech/openwolf package name, author, and version 1.0.6. Update README,
add Chinese README with upstream acknowledgments, retire Design QC.
---
CHANGELOG.md | 190 ++++++
README.md | 380 ++++++------
README.zh-CN.md | 227 ++++++++
RELEASE_NOTES_2.0.0.md | 63 ++
assets/openwolf-dashboard.png | Bin 0 -> 102743 bytes
package.json | 17 +-
pnpm-lock.yaml | 549 ++++++++----------
pnpm-workspace.yaml | 2 +
scripts/openwolf-check.mjs | 126 ++++
src/agents/antigravity.ts | 20 +
src/agents/codex.ts | 79 +++
src/agents/cursor.ts | 26 +
src/agents/gemini.ts | 21 +
src/agents/index.ts | 98 ++++
src/agents/markers.ts | 33 ++
src/agents/opencode.ts | 50 ++
src/agents/skills.ts | 43 ++
src/agents/types.ts | 27 +
src/buglog/bug-tracker.ts | 20 +-
src/cli/codex-config.ts | 5 -
src/cli/cron-cmd.ts | 4 +-
src/cli/daemon-cmd.ts | 30 +-
src/cli/dashboard.ts | 89 ++-
src/cli/designqc-cmd.ts | 56 --
src/cli/index.ts | 32 +-
src/cli/init.test.ts | 139 -----
src/cli/init.ts | 486 ++++++++--------
src/cli/report.ts | 70 +++
src/cli/status.ts | 30 +-
src/cli/update.ts | 314 +++++-----
src/daemon/cron-engine.ts | 42 +-
src/daemon/file-watcher.ts | 7 +
src/daemon/wolf-daemon.ts | 79 ++-
src/dashboard/app/App.tsx | 62 +-
.../app/components/layout/Header.tsx | 28 -
.../app/components/layout/Layout.tsx | 2 +-
.../app/components/layout/Sidebar.tsx | 105 ----
.../app/components/layout/TopNav.tsx | 92 +++
.../app/components/panels/AISuggestions.tsx | 2 +-
.../components/panels/ActivityTimeline.tsx | 4 +-
.../app/components/panels/AnatomyBrowser.tsx | 30 +-
.../app/components/panels/CerebrumViewer.tsx | 4 +-
.../app/components/panels/CronStatus.tsx | 21 +-
.../app/components/panels/DesignQC.tsx | 56 --
.../app/components/panels/ProjectOverview.tsx | 216 +++++--
.../app/components/panels/TokenUsage.tsx | 192 ++++--
.../app/components/shared/DotBar.tsx | 71 +++
.../app/components/shared/LiveIndicator.tsx | 6 +-
.../app/components/shared/StatTile.tsx | 36 ++
.../app/components/shared/StatusBadge.tsx | 52 +-
.../app/components/shared/TokenBadge.tsx | 5 +-
src/dashboard/app/hooks/useWolfData.ts | 119 +++-
src/dashboard/app/index.html | 2 +-
src/dashboard/app/lib/file-parsers.ts | 12 +-
src/dashboard/app/lib/wolf-client.ts | 23 +-
src/dashboard/app/styles/globals.css | 175 ++++--
src/designqc/designqc-capture.ts | 256 --------
src/designqc/designqc-engine.ts | 158 -----
src/designqc/designqc-types.ts | 34 --
src/hooks/anatomy-lock.ts | 104 ++++
src/hooks/anatomy-store.ts | 338 +++++++++++
src/hooks/post-read.ts | 19 +-
src/hooks/post-write.ts | 124 ++--
src/hooks/pre-read.ts | 38 +-
src/hooks/precompact.ts | 36 ++
src/hooks/session-start.ts | 224 ++++++-
src/hooks/shared.ts | 168 ++++--
src/hooks/stop.ts | 121 +++-
src/hooks/symbol-extractor.ts | 94 +++
src/scanner/anatomy-scanner.ts | 251 +++-----
src/templates/OPENWOLF.md | 28 +-
src/templates/STATUS.md | 64 ++
src/templates/claude-rules-openwolf.md | 3 +-
src/templates/config.json | 20 +-
src/templates/opencode-md-snippet.md | 5 +
src/templates/opencode-plugin/anatomy.ts | 275 +++++++++
src/templates/opencode-plugin/fs.ts | 64 ++
src/templates/opencode-plugin/index.ts | 99 ++++
src/templates/opencode-plugin/post-read.ts | 57 ++
src/templates/opencode-plugin/post-write.ts | 270 +++++++++
src/templates/opencode-plugin/pre-read.ts | 63 ++
src/templates/opencode-plugin/pre-write.ts | 105 ++++
src/templates/opencode-plugin/session.ts | 89 +++
src/templates/opencode-plugin/stop.ts | 126 ++++
src/templates/opencode-plugin/types.ts | 41 ++
src/templates/reframe-frameworks.md | 85 ++-
src/templates/skills/reframe.md | 41 ++
src/templates/skills/security-audit.md | 40 ++
src/utils/dashboard-auth.ts | 25 +
src/utils/fs-safe.ts | 57 +-
tests/anatomy-lock.test.ts | 76 +++
tests/anatomy-store.test.ts | 142 +++++
tests/security.test.ts | 105 ++++
tests/symbol-extractor.test.ts | 98 ++++
tests/token-measurement.test.ts | 38 ++
tsconfig.json | 12 +-
96 files changed, 6211 insertions(+), 2451 deletions(-)
create mode 100644 CHANGELOG.md
create mode 100644 README.zh-CN.md
create mode 100644 RELEASE_NOTES_2.0.0.md
create mode 100644 assets/openwolf-dashboard.png
create mode 100644 pnpm-workspace.yaml
create mode 100644 scripts/openwolf-check.mjs
create mode 100644 src/agents/antigravity.ts
create mode 100644 src/agents/codex.ts
create mode 100644 src/agents/cursor.ts
create mode 100644 src/agents/gemini.ts
create mode 100644 src/agents/index.ts
create mode 100644 src/agents/markers.ts
create mode 100644 src/agents/opencode.ts
create mode 100644 src/agents/skills.ts
create mode 100644 src/agents/types.ts
delete mode 100644 src/cli/codex-config.ts
delete mode 100644 src/cli/designqc-cmd.ts
delete mode 100644 src/cli/init.test.ts
create mode 100644 src/cli/report.ts
delete mode 100644 src/dashboard/app/components/layout/Header.tsx
delete mode 100644 src/dashboard/app/components/layout/Sidebar.tsx
create mode 100644 src/dashboard/app/components/layout/TopNav.tsx
delete mode 100644 src/dashboard/app/components/panels/DesignQC.tsx
create mode 100644 src/dashboard/app/components/shared/DotBar.tsx
create mode 100644 src/dashboard/app/components/shared/StatTile.tsx
delete mode 100644 src/designqc/designqc-capture.ts
delete mode 100644 src/designqc/designqc-engine.ts
delete mode 100644 src/designqc/designqc-types.ts
create mode 100644 src/hooks/anatomy-lock.ts
create mode 100644 src/hooks/anatomy-store.ts
create mode 100644 src/hooks/precompact.ts
create mode 100644 src/hooks/symbol-extractor.ts
create mode 100644 src/templates/STATUS.md
create mode 100644 src/templates/opencode-md-snippet.md
create mode 100644 src/templates/opencode-plugin/anatomy.ts
create mode 100644 src/templates/opencode-plugin/fs.ts
create mode 100644 src/templates/opencode-plugin/index.ts
create mode 100644 src/templates/opencode-plugin/post-read.ts
create mode 100644 src/templates/opencode-plugin/post-write.ts
create mode 100644 src/templates/opencode-plugin/pre-read.ts
create mode 100644 src/templates/opencode-plugin/pre-write.ts
create mode 100644 src/templates/opencode-plugin/session.ts
create mode 100644 src/templates/opencode-plugin/stop.ts
create mode 100644 src/templates/opencode-plugin/types.ts
create mode 100644 src/templates/skills/reframe.md
create mode 100644 src/templates/skills/security-audit.md
create mode 100644 src/utils/dashboard-auth.ts
create mode 100644 tests/anatomy-lock.test.ts
create mode 100644 tests/anatomy-store.test.ts
create mode 100644 tests/security.test.ts
create mode 100644 tests/symbol-extractor.test.ts
create mode 100644 tests/token-measurement.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..4dd12d4a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,190 @@
+# Changelog
+
+All notable changes to OpenWolf are documented here. The format follows
+[Keep a Changelog](https://keepachangelog.com/), and OpenWolf uses
+[Semantic Versioning](https://semver.org/).
+
+> This fork (`@alptech/openwolf`) keeps its own package version. Upstream
+> feature history below is retained for reference; package version remains
+> under the fork's numbering (currently 1.0.6).
+
+## [1.0.6] - 2026-08-12
+
+### Changed
+
+- Synced business code from upstream [cytostack/openwolf](https://github.com/cytostack/openwolf)
+ (through 2.0.2 feature set): multi-agent adapters, PreCompact, durable
+ anatomy store, measured token usage, redesigned dashboard, bundled skills.
+- Retired Design QC path in favor of upstream architecture.
+- README rewritten for the synced feature set; added Chinese README and
+ upstream acknowledgments. Package name/author/version remain this fork's.
+
+## [2.0.2] - 2026-07-15
+
+### Added
+
+- Antigravity agent adapter (beta, context-level via `AGENTS.md`).
+ `openwolf init --agent antigravity` and `--agent all` now include it, and
+ auto-detection picks it up when Antigravity is installed.
+
+### Changed
+
+- Documentation and website refreshed to reflect v2 throughout: the seven
+ lifecycle hooks including PreCompact, the durable anatomy store with
+ symbol-level reads, measured token usage, the redesigned dashboard, the
+ `/reframe` skill, and per-project dashboard ports. Retired Design QC
+ content removed. Positioning generalized across supported agents.
+
+## [2.0.1] - 2026-07-15
+
+### Fixed
+
+- Dashboard no longer white-screens when the server rejects the token. A 401
+ now renders a clear "token rejected" message with guidance instead of
+ crashing the page. Root cause: `StatusBadge` threw on an undefined status,
+ and failed API responses were being fed into component state.
+- Multi-project port collisions resolved. Projects upgraded from 1.x all kept
+ the shared default dashboard and daemon ports, so only the first project's
+ dashboard would ever open. Three fixes work together: `openwolf update`
+ reassigns a free port pair when a project's ports collide with another
+ registered project, `openwolf dashboard` starts this project's server on a
+ free port when the configured one is held by another project's daemon
+ (instead of opening a URL that gets a 401), and the daemon accepts an
+ `OPENWOLF_DASHBOARD_PORT` override. Fresh installs already received unique
+ ports; this brings upgraded projects to parity.
+
+## [2.0.0] - 2026-07-15
+
+OpenWolf 2.0 turns the second brain for Claude Code into a context layer for
+every AI coding assistant, with verifiable token measurement, a hardened
+security posture, and a re-architected project index.
+
+### Added
+
+Multi-agent support:
+
+- Agent adapter architecture: `openwolf init` now auto-detects the coding
+ agents installed on your machine and wires each of them to the same `.wolf/`
+ brain. Explicit control via `--agent codex opencode gemini cursor`, `--agent all`,
+ or `--agent claude` to opt out.
+- Codex CLI integration: project-level lifecycle hooks via `.codex/hooks.json`
+ plus an `AGENTS.md` protocol block.
+- OpenCode integration: a native plugin installed to `.opencode/plugin/` that
+ maps OpenCode tool events onto the `.wolf/` state.
+- Gemini CLI integration: `GEMINI.md` protocol block.
+- Cursor integration: an always-applied rule at `.cursor/rules/openwolf.mdc`.
+- Protocol blocks are marker-fenced and idempotent: your own content in
+ `AGENTS.md` or `GEMINI.md` is never modified, and re-running init never
+ duplicates anything.
+
+Measured token usage:
+
+- The Stop hook reads real API usage from the harness transcript (input,
+ output, cache read, cache write tokens, and API call count) into the token
+ ledger. Estimates and measurements are reported side by side.
+- New `openwolf report` command: estimated vs measured usage in the terminal.
+- Per-agent session attribution: every ledger session records which agent ran it.
+
+Context management:
+
+- Session digest: at session start, a token-budget-capped digest of the most
+ valuable state (STATUS.md next phase, Do-Not-Repeat list, recent bug fixes,
+ anatomy pointer) is injected directly into the model's context.
+ Budgets are configurable per agent in `config.json`.
+- Compaction survival: a new PreCompact lifecycle hook
+ snapshots in-flight session state, and session start after compaction
+ re-injects a digest of the files already modified. Session state is no
+ longer wiped on resume or compaction.
+- Anatomy staleness detection: scans pin the git HEAD; if the HEAD moves or
+ the scan ages past the configured interval, the agent is told to rescan
+ before trusting the index.
+- End-of-turn reminders now reach the model through the `additionalContext`
+ channel instead of invisible stderr.
+- `STATUS.md` session handoff document: resume any session in one small read.
+
+Anatomy re-architecture:
+
+- Durable store: the source of truth for the project index moved from
+ `anatomy.md` itself to `.wolf/anatomy-index.json`, with `anatomy.md`
+ rendered from it. Concurrent writers now coordinate through a
+ cross-platform lock; simultaneous edits no longer lose entries.
+- Version-skew safe: markdown written by older hooks or edited by hand is
+ detected by content hash and absorbed additively into the store.
+- Symbol-level entries: files above 500 estimated tokens index their
+ top-level functions and classes with line ranges and per-slice token
+ estimates (TypeScript, JavaScript, Python, Go, Rust). The pre-read hint
+ points agents at exact line ranges so they can read one function with
+ offset/limit instead of the whole file. Hints are suppressed automatically
+ if the file on disk has changed since indexing.
+
+Skills and tooling:
+
+- Bundled skills installed on init for Claude Code, Codex, and OpenCode:
+ `/security-audit` (layered audit: dependencies, secrets, injection
+ surfaces, authorization, ranked report) and `/reframe` (framework
+ selection and migration plus a design audit/fix mode).
+- `scripts/openwolf-check.mjs`: a standalone, read-only inspector that
+ reports whether OpenWolf is installed in a project, which agents are
+ wired, recency, and lifetime plus recent-session statistics.
+- `openwolf update` now has parity with init: it creates missing files,
+ re-runs the recorded agent adapters, refreshes bundled skills, and
+ performs one-time data migrations, all after taking a timestamped backup.
+
+Dashboard 2.0:
+
+- Complete redesign: monochrome dot-matrix design system with a single
+ signal-red accent, top navigation, bento stat tiles, and hash-based deep
+ links to panels.
+- Surfaces the 2.0 data: measured vs estimated tokens, cache economics,
+ per-agent breakdown table, wired-agents widget, context health (scan
+ freshness, pinned git HEAD, digest budget), and the STATUS.md handoff.
+- Reliable Run Now for cron tasks over authenticated HTTP with visible
+ running/queued/failed feedback.
+
+### Changed
+
+- Reframe now leads with an anti-generic design mandate: a blocklist of the
+ recognizable AI-generated aesthetic plus positive principles, applied to
+ every framework migration prompt. Distinctiveness is an acceptance criterion.
+- Astryx added as the 13th framework in the Reframe knowledge base.
+- Contributors are credited in the README; detailed attribution lives in
+ commit trailers.
+- STATUS.md template localized to English.
+
+### Fixed
+
+- CRLF line endings no longer wipe `anatomy.md` on Windows (#50, #24).
+- Concurrent post-write hooks no longer lose anatomy entries.
+- Old `config.json` files without newer sections no longer crash commands (#26, #27).
+- `openwolf init` and `openwolf update` no longer reset per-project ports;
+ fresh projects get a free port pair automatically (#37, #38).
+- `bug search` is null-safe across buglog schema drift (#44).
+- `EPERM` on WSL2 with EFS-encrypted directories fixed via a copy shim (#33).
+- Files outside the project root no longer pollute the index (#56).
+- Documentation and config edits are no longer mislogged as bug fixes, and
+ auto-detection can be disabled (#28, #57).
+- Dart language support in the scanner (#10).
+
+### Security
+
+- Dashboard binds to 127.0.0.1 by default and requires a per-project token
+ (timing-safe comparison) for all API and WebSocket access (#30, #34).
+- Command injection eliminated: every dynamic process invocation uses
+ argument arrays; a shell-mode spawn was removed from the cron engine.
+- Path traversal guards (realpath-based, symlink-safe) on cron AI task file access.
+- File-watcher broadcasts capped at 1 MB to prevent memory abuse.
+- Secret-bearing files (keys, keystores, credential files, `.npmrc`, and
+ more, not just `.env`) are excluded from all index and memory capture (#54).
+- A security regression test suite runs with `pnpm test`, including a guard
+ test that fails the build if injectable process calls ever return.
+
+### Removed
+
+- Design QC screenshot capture (agents capture and read their own
+ screenshots now); the `puppeteer-core` dependency is gone.
+- The unverifiable token comparison chart in the dashboard; only measured
+ numbers or clearly labeled estimates are shown.
+
+## [1.0.4] - 2026-03-20
+
+Final 1.x release. Claude Code only.
diff --git a/README.md b/README.md
index 2cf5ee17..750fce7d 100644
--- a/README.md
+++ b/README.md
@@ -5,39 +5,67 @@
@alptech/openwolf
- A second brain for Claude Code and Codex.
- Project intelligence, token tracking, and invisible enforcement through 6 hook scripts. Zero workflow changes.
+ The second brain for Claude Code. Now for every AI coding assistant.
+
+
+
+ Improved context management, optimized architecture scaffolding, and smarter token utilization,
+ delivered through 7 invisible lifecycle hooks. Zero workflow changes.
+
-> **This is a fork of [openwolf](https://www.npmjs.com/package/openwolf) by [Cytostack](https://github.com/cytostack).** It preserves all original capabilities and adds native, first-class [Codex](https://github.com/openai/codex) integration alongside the existing Claude Code support. Both agents get equal treatment — dedicated hook scripts, project entry files, and config scaffolding.
-
----
-
-## Why OpenWolf Exists
-
-Claude Code is powerful but it works blind. It doesn't know what a file contains until it opens it. It can't tell a 50-token config from a 2,000-token module. It reads the same file multiple times in one session without noticing. It has no index of your project, no memory of your corrections, and no awareness of what it already tried.
-
-OpenWolf gives Claude a second brain: a file index so it knows what files contain before reading them, a learning memory that accumulates your preferences and past mistakes, and a token ledger that tracks everything. All through 6 invisible hook scripts that fire on every Claude action.
+
+ Quick Start ·
+ Supported Agents ·
+ Context Management ·
+ Token Intelligence ·
+ Security ·
+ Dashboard ·
+ Changelog
+
-## Token Comparison
+> **This is a fork of [openwolf](https://github.com/cytostack/openwolf) by [Cytostack](https://github.com/cytostack).** It tracks upstream business features while keeping the `@alptech/openwolf` package identity.
-Tested on a large active project. Same codebase, same prompts, different setups:
+---
-```
-OpenClaw + Claude ██████████████████████████████████████ ~3.4M tokens
-Claude CLI (no OpenWolf) ████████████████████████████████ ~2.5M tokens
-OpenWolf + Claude CLI ████████ ~425K tokens
-```
+| Without OpenWolf | With OpenWolf |
+|---|---|
+| The agent rereads a file it already saw (~2,000 tokens) | It reads the one-line description first, or skips the read entirely |
+| Whole-file reads just to find one function | Symbol-level hints give exact line ranges for `offset`/`limit` reads |
+| Context compaction wipes what the session did | A PreCompact snapshot and restore keep the work in context |
+| Every agent starts from a cold prompt | One shared `.wolf/` brain across Codex, OpenCode, Claude Code, Cursor, and Antigravity |
+| No idea where your tokens went | Usage measured from harness transcripts, plus a live local dashboard |
-**OpenWolf saved ~80% of tokens** compared to bare Claude CLI on the same project.
+---
-Across 20 projects, 132+ sessions: average token reduction of 65.8%, with 71% of repeated file reads caught and blocked. These are numbers from real usage, not benchmarks. Your results will vary by project size and usage patterns.
+## Why OpenWolf?
+
+Coding agents are powerful but they work blind. An agent does not know what a
+file contains until it opens it. It cannot tell a 50-token config from a
+2,000-token module. It rereads the same file in one session without noticing,
+forgets your corrections between sessions, and loses everything when its
+context window compacts.
+
+OpenWolf gives your agent a second brain that fixes all of that:
+
+- **Context management.** A budget-capped digest of your project's most
+ valuable state (current goals, known mistakes, fixed bugs, the project map)
+ is injected at every session start. A PreCompact hook plus a
+ compaction-aware restart mean context compaction no longer erases what the
+ session already did.
+- **Architecture scaffolding.** A durable, self-healing project index maps
+ every file with a description, a token estimate, and (for large files) its
+ functions and classes with exact line ranges. Agents navigate your codebase
+ instead of rediscovering it.
+- **Token utilization.** Repeated reads are caught, whole-file reads become
+ targeted slice reads, and real usage is measured from harness transcripts
+ so you can verify the savings instead of trusting an estimate.
## Quick Start
@@ -47,206 +75,229 @@ cd your-project
openwolf init
```
-By default, `openwolf init` installs both Claude and Codex integration.
+That is it. `init` auto-detects the coding agents installed on your machine
+and wires each of them to the same `.wolf/` brain. Use your agents normally;
+OpenWolf works underneath.
+
+## Supported Agents
+
+One `.wolf/` brain, many agents:
+
+| Agent | Integration | Depth |
+|-------|-------------|-------|
+| **Codex CLI** | `.codex/hooks.json` lifecycle hooks + `AGENTS.md` | Full (hooks + context) |
+| **OpenCode** | Native plugin + `AGENTS.md` | Full (hooks + context) |
+| **Claude Code** | 7 lifecycle hooks + `CLAUDE.md` | Full (hooks + context) |
+| **Cursor** | `.cursor/rules/openwolf.mdc` (always applied) | Beta (context) |
+| **Antigravity** | `AGENTS.md` protocol block | Beta (context) |
+| **Gemini CLI** | `GEMINI.md` protocol block | Beta (context) |
```bash
-openwolf init claude # only Claude integration
-openwolf init codex # only Codex integration
+openwolf init # auto-detect installed agents (recommended)
+openwolf init --agent codex opencode # wire exactly these
+openwolf init --agent all # wire every detected agent
+openwolf init --agent claude # Claude Code only
```
-That's it. Use `claude` or `codex` normally. OpenWolf is watching.
+Protocol blocks are marker-fenced: your own content in `AGENTS.md` or
+`GEMINI.md` is never touched, and re-running init never duplicates anything.
## What It Creates
-`openwolf init` creates a `.wolf/` directory in your project and installs agent-specific entry files:
+`openwolf init` creates a `.wolf/` directory in your project:
| File | Purpose |
|------|---------|
-| `anatomy.md` | Project file map with descriptions and token estimates |
+| `anatomy-index.json` | Durable project index: descriptions, token estimates, content hashes, symbols |
+| `anatomy.md` | Human-readable render of the index, kept in sync automatically |
| `cerebrum.md` | Learned preferences, corrections, Do-Not-Repeat list |
| `memory.md` | Chronological action log with token estimates |
-| `buglog.json` | Bug fix memory, searchable, prevents re-discovery |
-| `token-ledger.json` | Lifetime token tracking and session history |
-| `hooks/` | Provider-specific hook scripts under `.wolf/hooks/claude/` and `.wolf/hooks/codex/` |
-| `config.json` | Configuration with sensible defaults |
-| `identity.md` | Agent persona for this project |
-| `OPENWOLF.md` | Shared OpenWolf operating protocol |
-| `CLAUDE.md` | Claude entry file that points Claude at `.wolf/OPENWOLF.md` |
-| `AGENTS.md` | Codex entry file that points Codex at `.wolf/OPENWOLF.md` |
-
-When Claude integration is enabled, OpenWolf also writes `.claude/settings.json` and `.claude/rules/openwolf.md` to register hooks and rules.
-When Codex integration is enabled, OpenWolf writes `.codex/hooks.json` and `.codex/config.toml` to register Codex hooks.
+| `STATUS.md` | Session handoff: resume any session in one small read |
+| `buglog.json` | Bug fix memory, searchable, prevents rediscovery |
+| `token-ledger.json` | Estimated and measured token usage, per session and per agent |
+| `hooks/` | 7 lifecycle hooks (pure Node.js, zero dependencies) |
+| `config.json` | Configuration, including per-agent context budgets |
+| `OPENWOLF.md` | The operating protocol your agents follow |
## How It Works
-Before Claude reads a file, OpenWolf tells it what the file contains and how large it is. If Claude already read that file this session, OpenWolf warns it. Before Claude writes code, OpenWolf checks your `cerebrum.md` for known mistakes. After every write, it auto-updates the project map and logs token usage. You see none of this. It just happens.
-
```
-You type a message
- ↓
-Claude decides to read a file
- ↓
-OpenWolf: "anatomy.md says this file is ~380 tokens. Description: Main entry point."
- ↓
-Claude reads the file
- ↓
-OpenWolf: logs the read, estimates tokens, checks for repeated reads
- ↓
-Claude writes code
- ↓
-OpenWolf: checks cerebrum.md - no known mistakes matched
- ↓
-Claude finishes
- ↓
-OpenWolf: updates anatomy.md, appends to memory.md, updates token ledger
+Session starts
+ |
+OpenWolf injects a token-budgeted digest: current goals, known mistakes,
+recent bug fixes, project map pointer
+ |
+Agent decides to read a big file
+ |
+OpenWolf: "auth.ts (~2,900 tok). Symbols: validateToken L82-140 ~450 tok.
+Read with offset/limit to fetch just the part you need."
+ |
+Agent edits files
+ |
+OpenWolf updates the index under a cross-process lock, logs the action,
+estimates the cost
+ |
+Context compacts mid-session
+ |
+OpenWolf snapshots state before compaction and re-injects a digest of the
+files already modified, so the agent does not redo finished work
+ |
+Session ends
+ |
+OpenWolf reads the real token usage from the transcript into the ledger
```
-## The .wolf/ Files
-
-
-cerebrum.md - the learning memory
+## Context Management
-Claude updates this file when you correct it, express a preference, or make a decision. The Do-Not-Repeat list prevents the same mistake across sessions.
+- **Session digest.** The highest-value state is pushed into the model's
+ context at session start, capped to a configurable token budget per agent.
+ The model gets what it needs without reading six files.
+- **Compaction survival.** The PreCompact hook snapshots in-flight session
+ state; after compaction the digest lists the files already modified with a
+ pointer to the action log. Resume and compaction no longer reset tracking.
+- **Staleness detection.** Scans pin the git HEAD. If the HEAD moves or the
+ scan ages out, the agent is told to rescan before trusting the map. A wrong
+ index is never silently trusted.
+- **STATUS.md handoff.** End-of-phase state lives in one small document, so a
+ fresh session reaches productive context in a single read.
-```markdown
-## Do-Not-Repeat
+## Project Anatomy
-- 2026-03-10: Never use `var` - always `const` or `let`
-- 2026-03-11: Don't mock the database in integration tests - use the real connection
-- 2026-03-14: The auth middleware reads from `cfg.talk`, not `cfg.tts` - got burned twice
+The index is a durable store (`anatomy-index.json`) with a rendered,
+human-readable view (`anatomy.md`). Writers coordinate through a
+cross-process lock, so concurrent hook fires cannot lose entries. Edits made
+to the markdown by hand or by older hook versions are detected by content
+hash and absorbed additively.
-## User Preferences
+Files above 500 estimated tokens also index their top-level symbols:
-- Prefers functional components over class components
-- Always use named exports, never default exports
-- Tests go in `__tests__/` next to the source file
-
-## Key Learnings
-
-- This project uses pnpm workspaces with strict hoisting
-- The API rate limiter uses a sliding window, not fixed buckets
-- Auth middleware reads from env.JWT_SECRET, not config file
+```
+- `shared.ts` (~3,200 tok)
+ - fn `parseAnatomy` L82-104 (~180 tok)
+ - fn `serializeAnatomy` L106-129 (~200 tok)
```
-
+Before the agent reads a large file, the hint lists the biggest symbols with
+line ranges so it can fetch one function with offset/limit instead of the
+whole file. Hints are suppressed automatically if the file changed since
+indexing; a stale range is never allowed to misdirect a read.
+Languages with symbol support today: TypeScript, JavaScript, Python, Go, Rust.
-
-anatomy.md - the project map
+## Token Intelligence
-Every file gets a description and token estimate. Claude reads this instead of opening files when the summary is enough.
+Estimates are useful; measurements are trustworthy. At session end OpenWolf
+reads the real usage from the harness transcript: input tokens, output
+tokens, cache reads, cache writes, and API calls, attributed to the agent
+that ran the session.
-```markdown
-## src/
+```bash
+openwolf report
+```
-- `index.ts` - Main entry point. Exports createProgram() for CLI. (~180 tok)
-- `server.ts` - Express HTTP server with middleware chain. (~520 tok)
+```
+ Estimated (char-ratio heuristic)
+ Total tokens: 1,549,658
+ Est. savings vs bare: 1,772,690
+
+ Measured (from harness transcripts)
+ API calls: 29
+ Input tokens: 57,489
+ Cache reads: 309,141
+```
-## src/api/
+Field results from 1.x deployments (20 projects, 132+ sessions) averaged a
+65.8% estimated token reduction, with 71% of repeated file reads caught and
+blocked. Those figures are heuristic estimates; measured numbers in 2.x let
+you verify savings on your own workload.
-- `auth.ts` - JWT validation middleware. Reads from env.JWT_SECRET. (~340 tok)
-- `users.ts` - CRUD endpoints for /api/users. Pagination via query params. (~890 tok)
-```
+## Security
-
-
-
-token-ledger.json - the receipt
-
-Every session gets a line item. Lifetime totals tell you if OpenWolf is actually saving tokens.
-
-```json
-{
- "lifetime": {
- "total_tokens_estimated": 503978,
- "total_reads": 287,
- "total_writes": 269,
- "total_sessions": 15,
- "anatomy_hits": 198,
- "anatomy_misses": 89,
- "repeated_reads_blocked": 106,
- "estimated_savings_vs_bare_cli": 2066959
- }
-}
-```
+- Dashboard binds to 127.0.0.1 and requires a per-project token
+ (timing-safe comparison) for all API and WebSocket access.
+- Every dynamic process invocation uses argument arrays; no shell
+ interpolation anywhere.
+- Path traversal guards on all cron file access, realpath-based and symlink-safe.
+- Secret-bearing files (keys, keystores, credential files, `.npmrc`, `.env`
+ and friends) never enter the index or the memory log.
+- A security regression suite runs with `pnpm test`.
-
+## Bundled Skills
-
-buglog.json - the bug memory
+`openwolf init` installs two slash commands into every configured agent
+(Claude Code, Codex, OpenCode):
-Before fixing anything, Claude checks if the fix is already known. After fixing, it logs the solution.
+- **`/security-audit [scope]`**: layered audit covering dependencies,
+ secrets, injection surfaces, and authorization, ending in a
+ severity-ranked report wired into `.wolf/buglog.json`.
+- **`/reframe [migrate | audit | fix]`**: the design brain. Pick or migrate a
+ UI framework using a curated knowledge base of 13 frameworks (shadcn/ui,
+ Aceternity, Magic UI, DaisyUI, HeroUI, Chakra, Flowbite, Preline, Park UI,
+ Origin UI, Headless UI, Cult UI, Astryx), or audit and fix existing UI
+ against an anti-generic design mandate.
-```json
-{
- "id": "bug-012",
- "error_message": "TypeError: Cannot read properties of undefined (reading 'map')",
- "file": "src/components/UserList.tsx",
- "root_cause": "API response was null when users array was expected",
- "fix": "Added optional chaining: data?.users?.map() and fallback empty array",
- "tags": ["null-check", "api-response", "react"]
-}
+## Dashboard
+
+```bash
+openwolf daemon start
+openwolf dashboard
```
-
+A local, token-authenticated dashboard: measured vs estimated tokens, cache
+economics, per-agent usage, context health, session handoff, live activity,
+cron control, and the full anatomy browser with per-file symbols.
## Commands
```
-openwolf init [target] Initialize .wolf/ and install Claude/Codex integration
-openwolf status Show health, stats, file integrity
-openwolf scan Refresh the project structure map
-openwolf scan --check Verify anatomy matches filesystem (exits 1 if stale)
-openwolf dashboard Open the real-time web dashboard
-openwolf daemon start Start background task scheduler
-openwolf daemon stop Stop the scheduler
-openwolf daemon restart Restart the scheduler
-openwolf daemon logs View scheduler logs
-openwolf cron list Show all scheduled tasks
-openwolf cron run Trigger a task manually
-openwolf cron retry Retry a dead-lettered task
-openwolf designqc Capture full-page screenshots for design evaluation
-openwolf bug search Search bug memory for known fixes
-openwolf update Update all registered projects to latest version
-openwolf restore [backup] Restore .wolf/ from a timestamped backup
+openwolf init Initialize .wolf/ and wire detected agents
+openwolf status Health, stats, file integrity
+openwolf scan Rebuild the project index
+openwolf scan --check Verify the index matches the filesystem (CI-friendly)
+openwolf report Token report: estimated vs measured
+openwolf dashboard Open the web dashboard
+openwolf daemon start Start the background daemon
+openwolf daemon stop Stop the daemon
+openwolf cron list Scheduled tasks
+openwolf cron run Trigger a task
+openwolf bug search Search the bug memory
+openwolf update Update every registered project (with backup)
+openwolf restore [backup] Roll back .wolf/ from a timestamped backup
```
-## Design QC
-
-Capture full-page screenshots of your running app and let Claude evaluate the design.
+There is also a standalone inspector that needs nothing installed:
```bash
-openwolf designqc
+node scripts/openwolf-check.mjs [projectDir] # read-only usage report
```
-Auto-detects your dev server, captures viewport-height JPEG sections of every route, and saves them to `.wolf/designqc-captures/`. Then tell Claude to read the screenshots and evaluate. Requires `puppeteer-core`.
-
-## Reframe
-
-Ask Claude to help you pick a UI framework. OpenWolf ships a curated knowledge base of 12 frameworks (shadcn/ui, Aceternity, Magic UI, DaisyUI, HeroUI, Chakra, Flowbite, Preline, Park UI, Origin UI, Headless UI, Cult UI) with battle-tested migration prompts. Claude reads `.wolf/reframe-frameworks.md`, asks you a few questions, and executes the migration with the right prompt for your project.
-
-## How OpenWolf Compares
-
-OpenWolf is not an AI wrapper. It is 6 hook scripts and a `.wolf/` directory. It doesn't run your AI for you or change your workflow. It gives Claude Code and Codex what they lack: a project map so they read less, a memory so they learn faster, and a ledger so you see where tokens go.
-
## Requirements
- Node.js 20+
-- Claude Code CLI or Codex
+- At least one supported coding agent
- Windows, macOS, or Linux
-- Optional: PM2 for persistent background tasks
-- Optional: `puppeteer-core` for Design QC screenshots
+- Optional: PM2 for a persistent background daemon
## Limitations
-- Claude Code hooks are a relatively new feature. OpenWolf falls back to `CLAUDE.md` instructions when hooks don't fire.
-- Codex hooks require Codex project hook support via `.codex/hooks.json` and `.codex/config.toml`, plus `AGENTS.md` project instructions.
-- Token tracking is estimation-based (character-to-token ratio), not exact API counts. Accurate to within ~15%.
-- `cerebrum.md` depends on Claude following instructions to update it after corrections. Compliance is ~85-90%, not 100%.
-- This is v1.0.5. Things may break. [File issues](https://github.com/nottyjay/openwolf/issues).
+- Estimated figures use a character-ratio heuristic (accurate to roughly
+ 15%); measured figures come from harness transcripts and are exact.
+- Hook coverage varies by agent: Claude Code and Codex have full lifecycle
+ hooks, OpenCode uses its plugin events, Gemini CLI and Cursor are
+ context-only integrations.
+- Protocol compliance (updating cerebrum, logging bugs) depends on the model
+ following instructions; the hooks enforce what can be enforced and remind
+ about the rest.
+- Found something broken? [File an issue](https://github.com/nottyjay/openwolf/issues).
+
+## Acknowledgments
-## Origin Story
+This project is based on the original **[OpenWolf](https://github.com/cytostack/openwolf)**
+by [Cytostack](https://github.com/cytostack) / Farhan Palathinkal Afsal.
+Thank you to the upstream authors and contributors for the architecture,
+hooks, and ongoing improvements. This fork tracks upstream business features
+while publishing as `@alptech/openwolf`.
-We were building products with Claude Code at Cytostack when we noticed something off. Sessions were eating through tokens faster than they should. When we dug in, we found Claude re-reading the same files multiple times, scanning entire directories to find one function, and having no way to know what a file contained without opening it. There was no project map, no read awareness, no token visibility. So we built the tooling we wished existed -- a file index so Claude reads less, a learning memory so it gets smarter, and a ledger that tracks every token. That became OpenWolf.
+Upstream repository: https://github.com/cytostack/openwolf
## License
@@ -254,4 +305,5 @@ We were building products with Claude Code at Cytostack when we noticed somethin
## Author
-Original project by Farhan Palathinkal Afsal — [Cytostack](https://github.com/cytostack). Forked and extended with first-class Codex integration by [@alptech](https://www.npmjs.com/package/@alptech/openwolf).
+Original project by Farhan Palathinkal Afsal — [Cytostack](https://github.com/cytostack).
+Maintained as `@alptech/openwolf` by [alptech](https://github.com/nottyjay) / [@nottyjay](https://github.com/nottyjay).
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 00000000..3243efc2
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,227 @@
+
+
+
+
+@alptech/openwolf
+
+
+ Claude Code 的第二大脑。现已支持主流 AI 编程助手。
+
+
+
+ 更优的上下文管理、架构索引与 Token 利用,
+ 通过 7 个不可见生命周期 Hook 自动生效,零工作流改动。
+
+
+
+
+
+
+
+
+
+> **本仓库是 [openwolf](https://github.com/cytostack/openwolf)([Cytostack](https://github.com/cytostack))的 fork。** 同步上游业务能力,并保留 `@alptech/openwolf` 包名与项目身份。
+
+---
+
+| 没有 OpenWolf | 有 OpenWolf |
+|---|---|
+| Agent 重复读取已看过的文件(约 2,000 tokens) | 先读一行描述,或直接跳过读取 |
+| 为找一个函数读完整文件 | 符号级提示给出精确行号,支持 `offset`/`limit` |
+| 上下文压缩会丢掉本轮工作 | PreCompact 快照与恢复,压缩后仍可续作 |
+| 每个 Agent 都从冷启动开始 | 共享同一套 `.wolf/` 大脑(Codex / OpenCode / Claude Code / Cursor / Antigravity 等) |
+| 不知道 Token 花在哪里 | 从 harness 转录读取实测用量,并提供本地 Dashboard |
+
+---
+
+## 为什么需要 OpenWolf?
+
+编程 Agent 很强,但默认是“盲操作”:不知道文件内容与体量,会在同一次会话里重复读文件,跨会话忘记你的纠正,上下文压缩后又像失忆。
+
+OpenWolf 提供第二大脑:
+
+- **上下文管理**:会话开始注入预算受限的高价值摘要;PreCompact + 压缩后重启,避免工作进度被抹掉。
+- **架构脚手架**:可自愈的项目索引,含描述、Token 估算,大文件还有函数/类与行号范围。
+- **Token 利用**:拦截重复读取,整文件读变切片读,并从 harness 转录测量真实用量。
+
+## 快速开始
+
+```bash
+npm install -g @alptech/openwolf
+cd your-project
+openwolf init
+```
+
+`init` 会自动检测本机已安装的编程 Agent,并接入同一套 `.wolf/` 大脑。之后照常使用 Agent 即可。
+
+## 支持的 Agent
+
+| Agent | 集成方式 | 深度 |
+|-------|----------|------|
+| **Codex CLI** | `.codex/hooks.json` 生命周期 Hook + `AGENTS.md` | 完整(Hook + 上下文) |
+| **OpenCode** | 原生插件 + `AGENTS.md` | 完整(Hook + 上下文) |
+| **Claude Code** | 7 个生命周期 Hook + `CLAUDE.md` | 完整(Hook + 上下文) |
+| **Cursor** | `.cursor/rules/openwolf.mdc`(始终应用) | Beta(上下文) |
+| **Antigravity** | `AGENTS.md` 协议块 | Beta(上下文) |
+| **Gemini CLI** | `GEMINI.md` 协议块 | Beta(上下文) |
+
+```bash
+openwolf init # 自动检测已安装 Agent(推荐)
+openwolf init --agent codex opencode # 只接入指定 Agent
+openwolf init --agent all # 接入全部可检测 Agent
+openwolf init --agent claude # 仅 Claude Code
+```
+
+协议块使用 marker 围栏:不会改动你自己写在 `AGENTS.md` / `GEMINI.md` 中的内容,重复执行 `init` 也不会重复插入。
+
+## 会创建什么
+
+`openwolf init` 会在项目中创建 `.wolf/`:
+
+| 文件 | 作用 |
+|------|------|
+| `anatomy-index.json` | 持久化项目索引:描述、Token 估算、内容哈希、符号 |
+| `anatomy.md` | 索引的可读渲染,自动保持同步 |
+| `cerebrum.md` | 学习记忆:偏好、纠正、Do-Not-Repeat |
+| `memory.md` | 按时间顺序的操作日志与 Token 估算 |
+| `STATUS.md` | 会话交接:少量阅读即可恢复进度 |
+| `buglog.json` | Bug 修复记忆,可搜索,避免重复踩坑 |
+| `token-ledger.json` | 估算与实测 Token 用量(按会话/Agent) |
+| `hooks/` | 7 个生命周期 Hook(纯 Node.js,零依赖) |
+| `config.json` | 配置,含各 Agent 上下文预算 |
+| `OPENWOLF.md` | Agent 需遵循的操作协议 |
+
+## 工作原理
+
+```
+会话开始
+ |
+OpenWolf 注入受 Token 预算限制的摘要:当前目标、已知错误、近期修复、项目地图指针
+ |
+Agent 准备读取大文件
+ |
+OpenWolf: "auth.ts (~2,900 tok)。符号: validateToken L82-140 ~450 tok。
+可用 offset/limit 只读需要的部分。"
+ |
+Agent 编辑文件
+ |
+OpenWolf 在跨进程锁下更新索引、记录动作、估算成本
+ |
+会话中发生上下文压缩
+ |
+OpenWolf 在压缩前快照,压缩后重新注入已修改文件摘要,避免重复劳动
+ |
+会话结束
+ |
+OpenWolf 从转录中读取真实 Token 用量写入 ledger
+```
+
+## 上下文管理
+
+- **会话摘要**:在可配置的每 Agent Token 预算内注入最高价值状态。
+- **压缩存活**:PreCompact 快照 + 压缩后摘要,续作不再从零开始。
+- **过期检测**:扫描会钉住 git HEAD;HEAD 变动或扫描过期时会提示重扫。
+- **STATUS.md 交接**:阶段结束状态集中在一个小文件,新会话一次读取即可恢复。
+
+## 项目 Anatomy
+
+索引以 `anatomy-index.json` 持久化,并以 `anatomy.md` 可读呈现。写操作通过跨进程锁协调;对手写或旧版 Hook 修改的 markdown,会按内容哈希做增量吸收。
+
+估算超过 500 tokens 的文件还会索引顶层符号:
+
+```
+- `shared.ts` (~3,200 tok)
+ - fn `parseAnatomy` L82-104 (~180 tok)
+ - fn `serializeAnatomy` L106-129 (~200 tok)
+```
+
+当前支持符号提取的语言:TypeScript、JavaScript、Python、Go、Rust。
+
+## Token 智能
+
+估算有用,测量更可信。会话结束时 OpenWolf 从 harness 转录读取真实用量:input / output / cache read / cache write / API 调用次数,并归属到对应 Agent。
+
+```bash
+openwolf report
+```
+
+1.x 实战数据(20 个项目、132+ 会话)平均估算节省约 65.8% Token,71% 的重复读被拦截。2.x 起可用实测数据在你自己的工作负载上验证。
+
+## 安全
+
+- Dashboard 仅绑定 `127.0.0.1`,API/WebSocket 使用每项目 token(时序安全比较)
+- 动态进程调用一律参数数组,无 shell 拼接
+- Cron 文件访问有路径穿越防护(realpath,符号链接安全)
+- 密钥类文件(keys、keystore、credential、`.npmrc`、`.env` 等)不进入索引与 memory
+- `pnpm test` 包含安全回归套件
+
+## 内置 Skills
+
+`openwolf init` 会为已配置 Agent(Claude Code、Codex、OpenCode)安装:
+
+- **`/security-audit [scope]`**:依赖、密钥、注入面、鉴权等分层审计,结果写入 `.wolf/buglog.json`
+- **`/reframe [migrate | audit | fix]`**:UI 框架选型/迁移与反“AI 味”设计审计(含 13 个框架知识库)
+
+## Dashboard
+
+```bash
+openwolf daemon start
+openwolf dashboard
+```
+
+本地、需 token 认证的控制台:估算 vs 实测 Token、缓存经济、按 Agent 用量、上下文健康、会话交接、实时活动、cron 控制、带符号的 anatomy 浏览器。
+
+## 命令
+
+```
+openwolf init 初始化 .wolf/ 并接入检测到的 Agent
+openwolf status 健康状态、统计、文件完整性
+openwolf scan 重建项目索引
+openwolf scan --check 校验索引是否与文件系统一致(适合 CI)
+openwolf report Token 报告:估算 vs 实测
+openwolf dashboard 打开 Web Dashboard
+openwolf daemon start 启动后台 daemon
+openwolf daemon stop 停止 daemon
+openwolf cron list 查看定时任务
+openwolf cron run 手动触发任务
+openwolf bug search 搜索 bug 记忆
+openwolf update 更新所有已注册项目(带备份)
+openwolf restore [backup] 从时间戳备份回滚 .wolf/
+```
+
+也可使用无需安装的只读检查脚本:
+
+```bash
+node scripts/openwolf-check.mjs [projectDir]
+```
+
+## 环境要求
+
+- Node.js 20+
+- 至少一个受支持的编程 Agent
+- Windows / macOS / Linux
+- 可选:PM2(持久化后台 daemon)
+
+## 局限
+
+- 估算基于字符比启发式(大约 ±15%);实测来自 harness 转录,更精确
+- 不同 Agent 的 Hook 覆盖不同:Claude Code / Codex 完整,OpenCode 走插件事件,Gemini / Cursor 主要为上下文级
+- cerebrum / buglog 的协议遵从仍依赖模型执行指令;Hook 负责可强制的部分
+- 发现问题请提交:[Issues](https://github.com/nottyjay/openwolf/issues)
+
+## 致谢
+
+本项目基于 **[OpenWolf](https://github.com/cytostack/openwolf)** 原项目,作者为
+[Cytostack](https://github.com/cytostack) / Farhan Palathinkal Afsal。
+感谢上游作者与贡献者提供的架构、Hook 体系与持续改进。本 fork 同步上游业务功能,并以 `@alptech/openwolf` 发布。
+
+上游仓库:https://github.com/cytostack/openwolf
+
+## 许可证
+
+[AGPL-3.0](LICENSE)
+
+## 作者
+
+原项目作者:Farhan Palathinkal Afsal — [Cytostack](https://github.com/cytostack)。
+本 fork 维护方:`@alptech/openwolf` — [alptech](https://github.com/nottyjay) / [@nottyjay](https://github.com/nottyjay)。
diff --git a/RELEASE_NOTES_2.0.0.md b/RELEASE_NOTES_2.0.0.md
new file mode 100644
index 00000000..6272316c
--- /dev/null
+++ b/RELEASE_NOTES_2.0.0.md
@@ -0,0 +1,63 @@
+# OpenWolf 2.0.0
+
+The second brain for Claude Code is now a context layer for every AI coding assistant.
+
+OpenWolf 2.0 is a ground-up upgrade focused on three things: improved context
+management, optimized architecture scaffolding, and smarter token utilization.
+It integrates 10 community pull requests (every author is credited in the
+README), closes 12 issues, and ships the first test suite for the project's
+most load-bearing internals.
+
+## Highlights
+
+**Works with your whole toolbox.** `openwolf init` auto-detects the coding
+agents installed on your machine and wires each one to the same `.wolf/`
+brain: Claude Code and Codex get full lifecycle hooks, OpenCode gets a native
+plugin, Gemini CLI and Cursor get protocol files. One project memory, shared
+by every assistant you use.
+
+**Tokens you can verify.** OpenWolf now reads real API usage from harness
+transcripts at the end of every session: input, output, and cache tokens,
+per agent. `openwolf report` and the dashboard show measured numbers next to
+the estimates instead of asking you to trust a heuristic.
+
+**Context that survives.** A budget-capped digest of your project's most
+valuable state is injected at session start. A new PreCompact hook plus a
+compaction-aware session start mean context compaction no longer erases what
+the session already did. STATUS.md gives every session a one-read handoff.
+
+**A project index that cannot corrupt itself.** The anatomy index moved to a
+durable store guarded by a cross-process lock; concurrent edits no longer
+lose entries, and markdown edits by older hooks or humans are absorbed
+instead of fought. Big files now index their functions and classes with line
+ranges, so agents read one function with offset/limit instead of the whole file.
+
+**Hardened by default.** The dashboard binds to localhost with token
+authentication, command injection surfaces are gone, path traversal is
+blocked, and secret-bearing files never enter the index.
+
+**A dashboard worth opening.** Complete redesign: dot-matrix numerals,
+monochrome surfaces with a single signal red, and panels for measured
+tokens, per-agent usage, context health, and session handoff.
+
+## Breaking and behavioral changes
+
+- `openwolf init` wires all detected agents by default. Use `--agent claude`
+ for the previous Claude-only behavior.
+- The dashboard now requires a token (printed by `openwolf dashboard`,
+ stored at `.wolf/dashboard-token`). Bookmarks without it will see 401.
+- Design QC was removed; agents capture their own screenshots now, and the
+ `puppeteer-core` dependency is gone.
+- `.wolf/anatomy-index.json` is the new source of truth for the project
+ index. `anatomy.md` is rendered from it and remains fully readable.
+ Existing projects are migrated automatically by `openwolf update`.
+
+## Upgrading
+
+```bash
+npm install -g openwolf@2
+openwolf update # every registered project: backup, migrate, rewire
+```
+
+Full details in CHANGELOG.md. Community contributions are credited in the
+README Contributors section.
diff --git a/assets/openwolf-dashboard.png b/assets/openwolf-dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..54dd76ba3c5018f87fc4e2e091df335956e3013f
GIT binary patch
literal 102743
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