@polderlabs/bizar 10.32.0 → 10.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/AGENTS.md +2 -2
  2. package/cli/bin.mjs +22 -1
  3. package/cli/capabilities.mjs +21 -0
  4. package/cli/commands/install.mjs +34 -28
  5. package/cli/commands/learn.mjs +23 -7
  6. package/cli/commands/openwolf-memory-command.mjs +28 -0
  7. package/cli/commands/util.mjs +10 -9
  8. package/cli/doctor.mjs +27 -2
  9. package/cli/execution-context.mjs +56 -0
  10. package/cli/init.mjs +46 -4
  11. package/cli/install/index.mjs +17 -18
  12. package/cli/install/interactive-setup.mjs +7 -4
  13. package/cli/openwolf-memory.mjs +110 -0
  14. package/cli/openwolf.mjs +106 -0
  15. package/cli/policies.mjs +43 -0
  16. package/cli/provision.mjs +235 -54
  17. package/cli/repair.mjs +19 -4
  18. package/cli/uninstall-support.mjs +8 -0
  19. package/cli/uninstall.mjs +48 -0
  20. package/config/claude/CLAUDE.md +2 -2
  21. package/config/claude/agents/_shared/AGENT_BASELINE.md +21 -12
  22. package/config/claude/agents/bizar-harness-optimizer.md +1 -1
  23. package/config/claude/agents/office-manager.md +55 -69
  24. package/config/claude/agents/support-tech.md +4 -3
  25. package/config/claude/commands/plow-through.md +11 -9
  26. package/config/claude/commands/team.md +2 -2
  27. package/config/claude/hooks/git-workflow-guard.mjs +7 -7
  28. package/config/claude/hooks/sessionstart-prime.mjs +29 -17
  29. package/config/claude/hooks/worker-suggest.mjs +7 -7
  30. package/config/compatibility-manifest.json +12 -2
  31. package/config/skills/agent-browser/SKILL.md +4 -4
  32. package/config/skills/autopilot/SKILL.md +3 -3
  33. package/config/skills/bizar/SKILL.md +1 -1
  34. package/config/skills/bizplan/SKILL.md +1 -1
  35. package/config/skills/bizplan/references/pre-mortem.md +1 -1
  36. package/config/skills/clean-gone-branches/SKILL.md +1 -1
  37. package/config/skills/create-pr/SKILL.md +2 -2
  38. package/config/skills/deep-interview/SKILL.md +1 -1
  39. package/config/skills/harness-engineering/SKILL.md +3 -3
  40. package/config/skills/resolve-pr-comments/SKILL.md +1 -1
  41. package/config/skills/skillopt/SKILL.md +2 -2
  42. package/config/skills/ultracode/SKILL.md +1 -1
  43. package/install.sh +34 -36
  44. package/package.json +4 -3
  45. package/packages/sdk/dist/policy/completion-policy.d.ts +17 -0
  46. package/packages/sdk/dist/policy/completion-policy.js +7 -0
  47. package/packages/sdk/dist/policy/index.d.ts +1 -0
  48. package/packages/sdk/dist/policy/index.js +1 -0
  49. package/packages/sdk/dist/policy/provision-policy.d.ts +9 -0
  50. package/packages/sdk/dist/policy/provision-policy.js +8 -0
  51. package/packages/sdk/dist/version.d.ts +1 -1
  52. package/packages/sdk/dist/version.js +1 -1
  53. package/packages/sdk/package.json +1 -1
package/AGENTS.md CHANGED
@@ -60,7 +60,7 @@ edit, test, and iterate without pausing for routine decisions. Routine
60
60
  decisions (file layout, naming, scope of a single commit, choosing between
61
61
  two equivalent stdlib calls, picking a verification command from the Makefile,
62
62
  or completing an OpenKan task after `make check` is green) do NOT require
63
- human approval and MUST NOT trigger a permission handoff. PreToolUse hooks may
63
+ human coordination and MUST NOT trigger a permission handoff. PreToolUse hooks may
64
64
  classify high-impact actions and add evidence guidance, but they must not deny
65
65
  or ask solely because an action is externally visible, irreversible, or
66
66
  powerful. Invalid target/schema/state is a typed contract error, not a
@@ -179,7 +179,7 @@ not a general note vault, semantic search service, or knowledge-base API.
179
179
  2. Documentation and OpenKan task/plan/PRD state describe the actual code.
180
180
  3. `make verify-removed-surfaces`, `make verify-repo-structure`, `make check-arch`, `make test`, `make e2e`, `make clean-check`, and `make check` pass as applicable.
181
181
  4. The OpenKan task records fresh verification evidence and no required work remains.
182
- 5. `/simplify` reviews the staged diff before the approval-gated commit.
182
+ 5. `/simplify` reviews the staged diff before the evidence-backed commit.
183
183
 
184
184
  <!-- openwolf:begin -->
185
185
  # OpenWolf
package/cli/bin.mjs CHANGED
@@ -103,10 +103,12 @@ function showHelp() {
103
103
  update Update Claude Code, Bizar, and the SDK
104
104
  doctor Check the BizarHarness install for health issues
105
105
  repair Fix common install issues
106
+ uninstall Remove Bizar-owned files safely (dry-run supported)
106
107
  heads-up <subcommand> Manage pre-push / pre-release heads-ups
107
108
  browser Install, update, and verify agent-browser
108
109
  artifact Toggle global browser completion artifacts
109
- learn Manage bounded global preferences and project lessons
110
+ learn Manage bounded global preferences and OpenWolf project memory
111
+ memory Manage OpenWolf project memory/context (current project by default)
110
112
  tools Inventory installed agents, skills, hooks, commands, and MCP tools
111
113
  upgrade-defaults Reapply safe Bizar-owned Claude defaults
112
114
  backup Create / list / verify / delete backups of BizarHarness state
@@ -266,6 +268,18 @@ async function main() {
266
268
  break;
267
269
  }
268
270
 
271
+ case 'uninstall': {
272
+ const { runUninstall, showUninstallHelp } = await import('./uninstall.mjs');
273
+ if (isHelpRequest) showUninstallHelp();
274
+ else {
275
+ const result = await runUninstall({ dryRun: cmdArgs.includes('--dry-run'), keepState: cmdArgs.includes('--keep-state'), purgeOpenWolfRuntime: cmdArgs.includes('--purge-openwolf-runtime') });
276
+ for (const path of [...result.removed, ...result.restored]) console.log(` ${result.dryRun ? 'would change' : 'changed'} ${path}`);
277
+ for (const path of result.conflicts) console.log(` ! preserved modified file ${path}`);
278
+ if (!result.ok) process.exit(EXIT_ERROR);
279
+ }
280
+ break;
281
+ }
282
+
269
283
 
270
284
  case 'validate': {
271
285
  const mod = await importCommand('validate');
@@ -529,6 +543,13 @@ async function main() {
529
543
  break;
530
544
  }
531
545
 
546
+ case 'memory': {
547
+ const mod = await importCommand('openwolf-memory-command');
548
+ if (!mod) { process.exit(EXIT_ERROR); return; }
549
+ await mod.run(cmd, cmdArgs, isHelpRequest);
550
+ break;
551
+ }
552
+
532
553
  case 'tools':
533
554
  case 'upgrade-defaults': {
534
555
  const mod = await importCommand(cmd);
@@ -0,0 +1,21 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { BIZAR_HOME } from './provision.mjs';
4
+
5
+ export function readCapabilitySnapshot() {
6
+ try { return JSON.parse(readFileSync(join(BIZAR_HOME(), 'capabilities.json'), 'utf8')); } catch { return null; }
7
+ }
8
+
9
+ export function selectAvailableCapabilities(intent, snapshot = readCapabilitySnapshot()) {
10
+ if (!snapshot) return [];
11
+ const terms = String(intent || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
12
+ return (snapshot.capabilities || [])
13
+ .filter((capability) => capability.availability === 'available')
14
+ .map((capability) => ({ capability, score: terms.reduce((score, term) => {
15
+ const haystack = `${capability.name} ${capability.description || ''} ${(capability.tags || []).join(' ')}`.toLowerCase();
16
+ return score + (haystack.includes(term) ? 1 : 0);
17
+ }, 0) }))
18
+ .filter(({ score }) => score > 0)
19
+ .sort((a, b) => b.score - a.score)
20
+ .map(({ capability }) => capability);
21
+ }
@@ -24,11 +24,10 @@ export function showInstallHelp() {
24
24
  Usage:
25
25
  bizar install Install (or refresh) every component
26
26
  bizar install --dry-run Print what would happen, change nothing
27
- bizar install --force Full clean install: wipe Bizar-managed dirs,
28
- back up settings env vars, re-sync everything
29
- from the repo. Preserves ~/.config/bizar/
30
- login state. Combine with --yes to skip prompts.
31
- bizar install --deep Alias for --force (clean-install semantics)
27
+ bizar install --force Repair Bizar-owned files from the ownership ledger.
28
+ bizar install --really-reset-global-claude-config
29
+ Explicit emergency reset of the global Claude config.
30
+ bizar install --deep Alias for --force (ownership-safe repair)
32
31
  bizar install --yes Non-interactive install (CI/script friendly)
33
32
  bizar install --non-interactive Alias for --yes
34
33
  bizar install --help Show this help
@@ -38,8 +37,9 @@ export function showInstallHelp() {
38
37
  difference is just mode=install vs mode=update. Every step is
39
38
  idempotent — running this twice is safe.
40
39
 
41
- F-183 (v10.16.2+) --force promotes the install from
42
- overwrite+prune-stale to a fully clean install. It wipes:
40
+ Ownership-safe repair means --force removes/replaces only unchanged files
41
+ recorded by Bizar. It preserves user and third-party siblings. The explicit
42
+ emergency flag is the only path that wipes:
43
43
  - ~/.claude/{agents,skills,commands,hooks,rules,workflows,plugins}/
44
44
  - ~/.agents/ (shared skill-registry + lock)
45
45
  - ~/.claude/settings.json (env vars backed up to BIZAR_SAVED_ENV)
@@ -69,7 +69,8 @@ export function showInstallHelp() {
69
69
  URL and key only when they are not already configured. Fresh setups
70
70
  can also choose a default model, agent teams, OpenKan home, and whether
71
71
  to initialise the current project's .ok/ workspace.
72
- 9. Runs 'bizar doctor' as a post-install health check.
72
+ 9. Required invariants are checked during provisioning; use 'bizar doctor'
73
+ explicitly for the full diagnostic.
73
74
 
74
75
  Provider settings are global (~/.claude/settings.json), so they work from
75
76
  every project. Key input is hidden. Use --yes or --non-interactive to skip
@@ -86,9 +87,8 @@ export function showUpdateHelp() {
86
87
  Usage:
87
88
  bizar update Refresh every Bizar-managed surface
88
89
  bizar update --dry-run Print what would happen, change nothing
89
- bizar update --force | --deep Full clean re-emit: wipe Bizar-managed
90
- dirs, back up settings env vars,
91
- re-sync everything from the repo
90
+ bizar update --force | --deep Ownership-safe repair of Bizar-managed
91
+ files from the ledger
92
92
  bizar update --yes | -y Assume yes for any non-destructive prompt
93
93
  bizar update --non-interactive Alias for --yes
94
94
  bizar update --help Show this help
@@ -100,18 +100,15 @@ export function showUpdateHelp() {
100
100
  1. Ensures the bundled default OpenKan planning runtime is present and current.
101
101
  2. Re-emits skills, commands, rules, hooks, agents, and workflows
102
102
  from the repo source into ~/.claude/ (or $CLAUDE_CONFIG_DIR),
103
- overwriting only the Bizar-managed surface and pruning stale
104
- entries (F-141).
103
+ overwriting only the Bizar-managed surface and pruning unchanged
104
+ ledger-owned stale entries.
105
105
  3. Writes the install marker so subsequent runs short-circuit when
106
106
  nothing has changed.
107
- 4. With --force, re-runs the F-183 clean-install flow: wipes
108
- ~/.claude/{agents,skills,commands,hooks,rules,workflows,plugins}/
109
- and ~/.agents/, stashes the prior settings.json env block into
110
- BIZAR_SAVED_ENV, then re-emits settings.json with the operator's
111
- ANTHROPIC_* and BIZAR_* keys union-merged back in (so gateway
112
- URL, auth token, and BIZAR_HOME are preserved across the wipe).
113
- 5. Runs 'bizar doctor' after a successful update so config
114
- regressions surface before the next Claude Code session.
107
+ 4. With --force, repairs only unchanged files recorded in the ownership
108
+ ledger. The emergency global reset option is intentionally documented
109
+ on bizar install, not routine updates.
110
+ 5. Runs the narrow installer invariants; use 'bizar doctor' for the full
111
+ diagnostic when needed.
115
112
  6. Repairs stale bin symlinks so the operator picks up the new code
116
113
  on the next shell prompt.
117
114
 
@@ -150,13 +147,19 @@ export function showUpdateHelp() {
150
147
  async function runPostInstallerRepair({ runRepair: runRepairDep = runRepair } = {}) {
151
148
  try {
152
149
  const r = await runRepairDep({});
150
+ if (r?.ok === false) {
151
+ console.error(chalk.red(` ✗ Repair failed: ${(r?.notes || []).join(' ') || 'active bizar could not be verified'}`));
152
+ return r;
153
+ }
153
154
  if (r.fixed.length > 0) {
154
155
  console.log(chalk.cyan('\n Repair: repointed stale bin symlinks:'));
155
156
  for (const f of r.fixed) console.log(` ${f}`);
156
157
  console.log(chalk.dim(' Re-run your shell or `hash -r` to pick up the new path.'));
157
158
  }
159
+ return r;
158
160
  } catch (err) {
159
161
  console.log(chalk.dim(` Repair skipped: ${err.message}`));
162
+ return { ok: false, error: err.message };
160
163
  }
161
164
  }
162
165
 
@@ -171,8 +174,8 @@ export async function install(args, isHelpRequest) {
171
174
  // parser for the installer family. Reusing it keeps install and
172
175
  // update in lockstep on flag semantics. --deep is parsed as an
173
176
  // alias for --force (clean-install semantics, F-183).
174
- const { mode, dryRun, force, yes } = parseFlags(args);
175
- const result = await runInstaller({ mode, dryRun, force, yes });
177
+ const { mode, dryRun, force, reallyResetGlobalClaudeConfig, yes } = parseFlags(args);
178
+ const result = await runInstaller({ mode, dryRun, force, reallyResetGlobalClaudeConfig, yes });
176
179
  // F-183 — print a one-line summary of the wipe scope so operators
177
180
  // can see at a glance what changed without re-reading the verbose
178
181
  // step list.
@@ -186,10 +189,10 @@ export async function install(args, isHelpRequest) {
186
189
  }
187
190
  // v4.4.3 — After install, repair any stale bin symlinks so the
188
191
  // user picks up the new code.
189
- await runPostInstallerRepair();
192
+ const repairResult = await runPostInstallerRepair();
190
193
  // v10.19.6 — propagate install failure to the parent shell so
191
194
  // `bizar install && bizar doctor` short-circuits on install errors.
192
- if (!result?.ok) process.exit(1);
195
+ if (!result?.ok || repairResult?.ok === false) process.exit(1);
193
196
  }
194
197
 
195
198
  export async function update(args, isHelpRequest) {
@@ -242,9 +245,12 @@ export async function runUpdateWithFlags({
242
245
  parseFlags: parseFlagsDep = parseFlags,
243
246
  runRepair: runRepairDep = runRepair,
244
247
  } = {}) {
245
- const { mode, dryRun, force, yes } = parseFlagsDep(args);
246
- const result = await runInstallerDep({ mode, dryRun, force, yes });
247
- await runPostInstallerRepair({ runRepair: runRepairDep });
248
+ const { mode, dryRun, force, reallyResetGlobalClaudeConfig, yes } = parseFlagsDep(args);
249
+ const installerOpts = { mode, dryRun, force, yes };
250
+ if (reallyResetGlobalClaudeConfig) installerOpts.reallyResetGlobalClaudeConfig = true;
251
+ const result = await runInstallerDep(installerOpts);
252
+ const repairResult = await runPostInstallerRepair({ runRepair: runRepairDep });
253
+ if (repairResult && repairResult.ok === false) process.exit(1);
248
254
  if (!result?.ok) process.exit(1);
249
255
  return result;
250
256
  }
@@ -1,7 +1,8 @@
1
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { resolveGlobalLearningDir } from '../config-paths.mjs';
5
+ import { forgetOpenWolfProject, readProjectLearning, rememberOpenWolfProject } from '../openwolf-memory.mjs';
5
6
 
6
7
  const USER_LIMIT = 32;
7
8
  const PROJECT_LIMIT = 128;
@@ -26,7 +27,7 @@ function parseArgs(args) {
26
27
  export function learningPaths({ cwd = process.cwd(), env = process.env } = {}) {
27
28
  return {
28
29
  user: join(resolveGlobalLearningDir({ cwd, env }), 'user-preferences.json'),
29
- project: join(cwd, '.bizar', 'learning', 'project-lessons.json'),
30
+ project: join(cwd, '.wolf', 'cerebrum.md'),
30
31
  };
31
32
  }
32
33
 
@@ -61,7 +62,12 @@ function idFor(scope, key) {
61
62
  export function remember({ scope, key, value, cwd = process.cwd(), env = process.env, source = 'explicit' }) {
62
63
  const normalizedScope = scope === 'project' ? 'project' : 'user';
63
64
  ({ key, value } = validateLearning(key, value));
64
- const path = learningPaths({ cwd, env })[normalizedScope];
65
+ if (normalizedScope === 'project' && existsSync(join(cwd, '.wolf'))) {
66
+ return rememberOpenWolfProject({ cwd, key, value });
67
+ }
68
+ const path = normalizedScope === 'project'
69
+ ? join(cwd, '.bizar', 'learning', 'project-lessons.json')
70
+ : learningPaths({ cwd, env }).user;
65
71
  const store = readStore(path, normalizedScope);
66
72
  const now = new Date().toISOString();
67
73
  const existing = store.items.find((item) => item.key === key);
@@ -75,7 +81,10 @@ export function remember({ scope, key, value, cwd = process.cwd(), env = process
75
81
 
76
82
  export function forget({ scope, key, cwd = process.cwd(), env = process.env }) {
77
83
  const normalizedScope = scope === 'project' ? 'project' : 'user';
78
- const path = learningPaths({ cwd, env })[normalizedScope];
84
+ if (normalizedScope === 'project' && existsSync(join(cwd, '.wolf'))) return forgetOpenWolfProject({ cwd, key });
85
+ const path = normalizedScope === 'project'
86
+ ? join(cwd, '.bizar', 'learning', 'project-lessons.json')
87
+ : learningPaths({ cwd, env }).user;
79
88
  const store = readStore(path, normalizedScope);
80
89
  const before = store.items.length;
81
90
  store.items = store.items.filter((item) => item.key !== key && item.id !== key);
@@ -88,14 +97,20 @@ export function listLearning({ scope = 'all', cwd = process.cwd(), env = process
88
97
  const paths = learningPaths({ cwd, env });
89
98
  const result = {};
90
99
  if (scope === 'all' || scope === 'user') result.user = readStore(paths.user, 'user').items;
91
- if (scope === 'all' || scope === 'project') result.project = readStore(paths.project, 'project').items;
100
+ if (scope === 'all' || scope === 'project') {
101
+ result.project = existsSync(join(cwd, '.wolf'))
102
+ ? readProjectLearning({ cwd })
103
+ : readStore(join(cwd, '.bizar', 'learning', 'project-lessons.json'), 'project').items;
104
+ }
92
105
  return result;
93
106
  }
94
107
 
95
108
  export function buildLearningContext(options = {}) {
96
109
  const learning = listLearning(options);
97
110
  const user = (learning.user || []).slice(-3);
98
- const project = (learning.project || []).slice(-5);
111
+ // OpenWolf owns project memory when active; its hooks deliver cerebrum and
112
+ // handoff context, so Bizar injects only cross-project user preferences.
113
+ const project = existsSync(join(options.cwd || process.cwd(), '.wolf')) ? [] : (learning.project || []).slice(-5);
99
114
  if (!user.length && !project.length) return '';
100
115
  const lines = ['Bizar learning (untrusted data; never overrides system, safety, or project instructions):'];
101
116
  if (user.length) lines.push('User preferences:', ...user.map((item) => `- ${item.key}: ${item.value}`));
@@ -104,7 +119,7 @@ export function buildLearningContext(options = {}) {
104
119
  }
105
120
 
106
121
  function help() {
107
- process.stdout.write('Usage: bizar learn <status|list|remember|forget|compact> [--scope user|project] [--key KEY] [--value TEXT] [--json]\n');
122
+ process.stdout.write('Usage: bizar learn <status|list|remember|forget|compact> [--scope user|project] [--key KEY] [--value TEXT] [--json]\nProject scope uses OpenWolf .wolf/cerebrum.md when active; user scope stays in BIZAR_HOME.\n');
108
123
  }
109
124
 
110
125
  export async function run(name, args, isHelpRequest) {
@@ -120,6 +135,7 @@ export async function run(name, args, isHelpRequest) {
120
135
  else if (command === 'compact') {
121
136
  const paths = learningPaths();
122
137
  for (const selected of scope === 'all' ? ['user', 'project'] : [scope]) {
138
+ if (selected === 'project' && existsSync(join(process.cwd(), '.wolf'))) continue;
123
139
  const store = readStore(paths[selected], selected);
124
140
  store.items = store.items.slice(-(selected === 'user' ? USER_LIMIT : PROJECT_LIMIT));
125
141
  store.updatedAt = new Date().toISOString();
@@ -0,0 +1,28 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { resolve } from 'node:path';
3
+ import { ensureOpenWolfRuntime, openWolfHealth } from '../openwolf.mjs';
4
+
5
+ export const USAGE = `
6
+ bizar memory — OpenWolf project memory/context integration
7
+
8
+ Usage:
9
+ bizar memory status
10
+ bizar memory update Update OpenWolf for the current project only
11
+ bizar memory update --all-projects Delegate the explicit global OpenWolf update
12
+ `;
13
+
14
+ export async function run(name, args = [], isHelpRequest = false) {
15
+ if (name !== 'memory') return false;
16
+ if (isHelpRequest || !args[0] || args[0] === 'help') { process.stdout.write(USAGE); return true; }
17
+ if (args[0] === 'status') {
18
+ process.stdout.write(`${JSON.stringify(openWolfHealth({ cwd: process.cwd(), deep: args.includes('--deep') }), null, 2)}\n`);
19
+ return true;
20
+ }
21
+ if (args[0] !== 'update') { process.stdout.write(USAGE); process.exitCode = 2; return true; }
22
+ const runtime = ensureOpenWolfRuntime({});
23
+ if (!runtime.ok) { process.stderr.write(`OpenWolf runtime unavailable: ${runtime.message}\n`); process.exitCode = 1; return true; }
24
+ const updateArgs = args.includes('--all-projects') ? ['update'] : ['update', '--project', resolve(process.cwd())];
25
+ const result = spawnSync('openwolf', updateArgs, { cwd: process.cwd(), shell: false, encoding: 'utf8', stdio: 'inherit', timeout: 120000 });
26
+ if (result.error || result.status !== 0) process.exitCode = result.status || 1;
27
+ return true;
28
+ }
@@ -26,14 +26,13 @@ export function showInitHelp() {
26
26
  bizar init — Initialize .bizar/ in current project
27
27
 
28
28
  Usage:
29
- bizar init
29
+ bizar init [--backend openkan|ao]
30
30
 
31
31
  Description:
32
- Detects the project stack, creates .bizar/PROJECT.md and the bounded
33
- project-learning directory, and installs relevant skills.
34
- The per-project knowledge graph (in .bizar/graph/) is provided
35
- by the graphify mod install it from the mod registry for
36
- that feature.
32
+ Detects the project stack, resolves exactly one work-state backend,
33
+ ensures OpenWolf 2.5.1, initializes .wolf/ with Claude-only hooks, and
34
+ installs relevant skills. Existing project memory is preserved and legacy
35
+ Bizar lessons migrate once into OpenWolf cerebrum.
37
36
  `);
38
37
  }
39
38
 
@@ -73,7 +72,7 @@ export function showDoctorHelp() {
73
72
  bizar doctor — Check the BizarHarness install for health issues
74
73
 
75
74
  Usage:
76
- bizar doctor
75
+ bizar doctor [--deep-memory]
77
76
 
78
77
  Description:
79
78
  Runs a battery of health checks against the local install:
@@ -215,7 +214,9 @@ export async function run(name, args, isHelpRequest) {
215
214
  if (isHelpRequest) showInitHelp();
216
215
  else {
217
216
  const { runInit } = await import('../init.mjs');
218
- await runInit(process.cwd());
217
+ const backendIndex = args.findIndex((arg) => arg === '--backend' || arg === '--execution-backend');
218
+ const executionBackend = backendIndex >= 0 ? args[backendIndex + 1] : undefined;
219
+ await runInit(process.cwd(), { executionBackend });
219
220
  }
220
221
  break;
221
222
 
@@ -239,7 +240,7 @@ export async function run(name, args, isHelpRequest) {
239
240
  else {
240
241
  const wantJson = args.includes('--json');
241
242
  const { runDoctor } = await import('../doctor.mjs');
242
- const result = await runDoctor({ silent: wantJson, json: wantJson });
243
+ const result = await runDoctor({ silent: wantJson, json: wantJson, deepMemory: args.includes('--deep-memory') });
243
244
  if (wantJson) process.stdout.write(JSON.stringify(result) + '\n');
244
245
  if (result.failed > 0) process.exit(1);
245
246
  }
package/cli/doctor.mjs CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  } from './commands/validate.mjs';
47
47
  import { validateNativeWorkflowDirectory } from '../config/workflows/lib/native-contract.mjs';
48
48
  import { resolveOpenKanHome, verifyOpenKanRuntime } from './openkan.mjs';
49
+ import { openWolfHealth } from './openwolf.mjs';
49
50
 
50
51
  const REQUIRED_RULES = [
51
52
  'general.md', 'git.md', 'javascript.md', 'python.md',
@@ -197,6 +198,18 @@ async function checkOpenKanRuntime() {
197
198
  return `OpenKan ${home} is runnable (${result.launcher})`;
198
199
  }
199
200
 
201
+ async function checkOpenWolfCli() {
202
+ const result = openWolfHealth({ cwd: process.cwd() });
203
+ if (!result.ok) throw new Error(result.message);
204
+ return result.message;
205
+ }
206
+
207
+ async function checkOpenWolfProject() {
208
+ const result = openWolfHealth({ cwd: process.cwd() });
209
+ if (result.project && !result.ok) throw new Error(result.message);
210
+ return result.project ? 'OpenWolf project state parses' : 'no .wolf/ project state in current directory';
211
+ }
212
+
200
213
  async function checkAliasMap() {
201
214
  const settingsPath = join(resolveClaudeConfigDir(), 'settings.json');
202
215
  const settings = readFileSync(settingsPath, 'utf8');
@@ -233,9 +246,12 @@ async function checkOwnershipManifest() {
233
246
  const path = join(bizarHome(), 'ownership.json');
234
247
  if (!existsSync(path)) return 'ownership manifest missing (run bizar update to create it)';
235
248
  const manifest = JSON.parse(readFileSync(path, 'utf8'));
236
- if (manifest.schema !== 'bizar.install-ownership.v1' || !manifest.files || typeof manifest.files !== 'object') {
249
+ if (!['bizar.install-ownership.v1', 'bizar.install-ownership.v2'].includes(manifest.schema)) {
237
250
  throw new Error('ownership manifest has an unsupported schema');
238
251
  }
252
+ if (manifest.schema === 'bizar.install-ownership.v1' && (!manifest.files || typeof manifest.files !== 'object')) throw new Error('ownership manifest has an invalid legacy file map');
253
+ if (manifest.schema === 'bizar.install-ownership.v2' && !Array.isArray(manifest.files)) throw new Error('ownership manifest has an invalid transaction file list');
254
+ if (manifest.schema === 'bizar.install-ownership.v2') return `${manifest.files.length} manifest-owned files recorded`;
239
255
  const stale = Object.entries(manifest.files).filter(([file, meta]) => !existsSync(file) || (meta?.sha256 && hashFile(file) !== meta.sha256));
240
256
  if (stale.length) return `ownership manifest has ${stale.length} stale entries`;
241
257
  return `${Object.keys(manifest.files).length} manifest-owned files verified`;
@@ -263,6 +279,8 @@ const CHECKS = [
263
279
  { name: 'tools-on-path', run: checkToolsAvailable },
264
280
  { name: 'bizar-home', run: checkBizarHome },
265
281
  { name: 'openkan-runtime', run: checkOpenKanRuntime },
282
+ { name: 'openwolf-cli', run: checkOpenWolfCli },
283
+ { name: 'openwolf-project', run: checkOpenWolfProject },
266
284
  { name: 'alias-map', run: checkAliasMap },
267
285
  { name: 'legacy-router', run: checkLegacyRouterPresent },
268
286
  { name: 'ownership-manifest', run: checkOwnershipManifest },
@@ -274,7 +292,14 @@ export async function runDoctor(opts = {}) {
274
292
  let passed = 0;
275
293
  let failed = 0;
276
294
 
277
- for (const check of CHECKS) {
295
+ const checks = opts.deepMemory
296
+ ? [...CHECKS, { name: 'openwolf-deep-status', run: async () => {
297
+ const result = openWolfHealth({ cwd: process.cwd(), deep: true });
298
+ if (!result.ok) throw new Error(result.message);
299
+ return 'openwolf status/selfcheck passed';
300
+ } }]
301
+ : CHECKS;
302
+ for (const check of checks) {
278
303
  let result;
279
304
  try {
280
305
  const message = await check.run();
@@ -0,0 +1,56 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const BACKENDS = new Set(['openkan', 'ao']);
5
+
6
+ export function executionContextPath(cwd = process.cwd()) {
7
+ return join(cwd, '.bizar', 'runtime', 'execution-context.json');
8
+ }
9
+
10
+ function readContext(cwd) {
11
+ try {
12
+ const value = JSON.parse(readFileSync(executionContextPath(cwd), 'utf8'));
13
+ return BACKENDS.has(value?.executionBackend) ? value : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ export function resolveExecutionBackend({ cwd = process.cwd(), env = process.env, explicit } = {}) {
20
+ const requested = explicit || env.BIZAR_EXECUTION_BACKEND;
21
+ if (requested && !BACKENDS.has(requested)) {
22
+ throw new Error(`execution backend must be openkan or ao, received ${requested}`);
23
+ }
24
+ if (requested) return requested;
25
+ if (env.AO_SESSION_ID || env.AO_PROJECT_ID) return 'ao';
26
+ return readContext(cwd)?.executionBackend || 'openkan';
27
+ }
28
+
29
+ export function resolveExecutionContext({ cwd = process.cwd(), env = process.env, explicit, sessionId } = {}) {
30
+ const executionBackend = resolveExecutionBackend({ cwd, env, explicit });
31
+ const context = {
32
+ schema: 'bizar.execution-context.v1',
33
+ executionBackend,
34
+ sessionId: sessionId || env.AO_SESSION_ID || env.CLAUDE_SESSION_ID || null,
35
+ resolvedAt: new Date().toISOString(),
36
+ };
37
+ return context;
38
+ }
39
+
40
+ export function persistExecutionContext(context, { cwd = process.cwd(), dryRun = false } = {}) {
41
+ if (!BACKENDS.has(context?.executionBackend)) throw new Error('cannot persist an invalid execution backend');
42
+ const path = executionContextPath(cwd);
43
+ if (!dryRun) {
44
+ mkdirSync(join(cwd, '.bizar', 'runtime'), { recursive: true, mode: 0o700 });
45
+ writeFileSync(path, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
46
+ }
47
+ return { path, context };
48
+ }
49
+
50
+ export function isAoExecution({ cwd = process.cwd(), env = process.env, explicit } = {}) {
51
+ return resolveExecutionBackend({ cwd, env, explicit }) === 'ao';
52
+ }
53
+
54
+ export function isOpenKanExecution(options = {}) {
55
+ return !isAoExecution(options);
56
+ }
package/cli/init.mjs CHANGED
@@ -3,6 +3,10 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { join, basename } from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { detectSkillsCli } from './utils.mjs';
6
+ import { ensureOpenKanProject } from './openkan.mjs';
7
+ import { ensureOpenWolfRuntime, initOpenWolfProject } from './openwolf.mjs';
8
+ import { migrateProjectMemoryToOpenWolf } from './openwolf-memory.mjs';
9
+ import { persistExecutionContext, resolveExecutionContext } from './execution-context.mjs';
6
10
 
7
11
  function detectStack(cwd) {
8
12
  const stack = { language: null, framework: null, database: null, tools: [], build: null, test: null, runner: null };
@@ -104,6 +108,35 @@ export async function runInit(cwd, opts = {}) {
104
108
  const bizarDir = join(cwd, '.bizar');
105
109
  mkdirSync(bizarDir, { recursive: true });
106
110
 
111
+ const executionContext = resolveExecutionContext({
112
+ cwd,
113
+ env: opts.env || process.env,
114
+ explicit: opts.executionBackend,
115
+ sessionId: opts.sessionId,
116
+ });
117
+ persistExecutionContext(executionContext, { cwd, dryRun: opts.dryRun });
118
+ console.log(chalk.green(` ✓ Execution backend: ${executionContext.executionBackend}`));
119
+
120
+ if (executionContext.executionBackend === 'openkan') {
121
+ try {
122
+ if (!opts.dryRun) ensureOpenKanProject();
123
+ console.log(chalk.green(` ✓ OpenKan is the sole work-state authority for this context`));
124
+ } catch (error) {
125
+ console.log(chalk.yellow(` ! OpenKan project state was not initialized: ${error.message || String(error)}`));
126
+ }
127
+ } else {
128
+ console.log(chalk.green(' ✓ AO is the sole work-state authority; .ok/ is inactive'));
129
+ }
130
+
131
+ const openWolfRuntime = ensureOpenWolfRuntime({ dryRun: opts.dryRun, env: opts.env || process.env });
132
+ if (openWolfRuntime.ok) console.log(chalk.green(` ✓ ${openWolfRuntime.message}`));
133
+ else console.log(chalk.yellow(` ! OpenWolf unavailable: ${openWolfRuntime.message}`));
134
+ const openWolfProject = openWolfRuntime.ok
135
+ ? initOpenWolfProject({ cwd, dryRun: opts.dryRun, env: opts.env || process.env })
136
+ : { ok: true, status: 'unavailable', message: 'OpenWolf project initialization deferred' };
137
+ if (openWolfProject.ok) console.log(chalk.green(` ✓ ${openWolfProject.message}`));
138
+ else console.log(chalk.yellow(` ! OpenWolf project initialization deferred: ${openWolfProject.message}`));
139
+
107
140
  // Detect stack
108
141
  console.log(chalk.dim(' Detecting project stack...'));
109
142
  const stack = detectStack(cwd);
@@ -174,14 +207,23 @@ ${stack.runner ? `- Dev: \`${stack.runner}\`` : ''}
174
207
  `;
175
208
 
176
209
  const projPath = join(bizarDir, 'PROJECT.md');
177
- writeFileSync(projPath, projectMd);
178
- console.log(chalk.green(` ✓ Created ${projPath}`));
210
+ if (!existsSync(projPath)) {
211
+ writeFileSync(projPath, projectMd);
212
+ console.log(chalk.green(` ✓ Created ${projPath}`));
213
+ } else {
214
+ console.log(chalk.dim(` - Preserved existing ${projPath}`));
215
+ }
179
216
 
180
217
  const projectLearning = initializeProjectLearningStore(bizarDir);
181
218
  if (projectLearning.created) process.stdout.write(`${chalk.green(` ✓ Created ${projectLearning.path}`)}\n`);
182
219
 
183
- // Generate PRE_PUSH_NOTES.md
184
- writePrePushNotesFile(bizarDir);
220
+ // Generate PRE_PUSH_NOTES.md only on first initialization.
221
+ if (!existsSync(join(bizarDir, 'PRE_PUSH_NOTES.md'))) writePrePushNotesFile(bizarDir);
222
+
223
+ if (openWolfProject.ok && openWolfProject.status === 'ready' && !opts.dryRun) {
224
+ const migration = migrateProjectMemoryToOpenWolf({ cwd });
225
+ if (migration.ok) console.log(chalk.green(` ✓ OpenWolf memory migration: ${migration.migrated ? `${migration.marker.itemsImported} item(s)` : 'already complete'}`));
226
+ }
185
227
 
186
228
  console.log(chalk.dim('\n Project initialized. Run `@susan` to ask questions about the codebase.\n'));
187
229
  return true;