@polderlabs/bizar 10.30.0 → 10.32.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 (65) hide show
  1. package/AGENTS.md +36 -52
  2. package/README.md +21 -15
  3. package/cli/banner.mjs +1 -1
  4. package/cli/commands/ao.mjs +35 -6
  5. package/cli/commands/improve-proposal.mjs +12 -4
  6. package/cli/commands/improve.mjs +44 -13
  7. package/cli/commands/install.mjs +10 -0
  8. package/cli/commands/openkan.mjs +1 -1
  9. package/cli/commands/release-provenance.mjs +35 -20
  10. package/cli/commands/worker.mjs +3 -0
  11. package/cli/core/backup-store.mjs +51 -19
  12. package/cli/core/runtime-manifest.mjs +31 -0
  13. package/cli/doctor.mjs +20 -0
  14. package/cli/install/banner.mjs +1 -1
  15. package/cli/install/paths.mjs +1 -1
  16. package/cli/openkan.mjs +8 -11
  17. package/cli/provision.mjs +39 -22
  18. package/cli/update.mjs +44 -0
  19. package/config/ao/worker-rules.md +15 -0
  20. package/config/claude/CLAUDE.md +32 -52
  21. package/config/claude/commands/bizar.md +3 -3
  22. package/config/claude/compatibility.mjs +22 -0
  23. package/config/claude/hooks/permission-request.mjs +14 -182
  24. package/config/claude/hooks/persistent-mode.mjs +1 -1
  25. package/config/claude/settings.json +3 -5
  26. package/config/compatibility-manifest.json +13 -0
  27. package/config/policy/AGENT_BEHAVIOR.md +18 -0
  28. package/config/skills/bizar/SKILL.md +9 -3
  29. package/package.json +5 -3
  30. package/packages/sdk/dist/federation/index.d.ts +5 -0
  31. package/packages/sdk/dist/federation/index.js +31 -4
  32. package/packages/sdk/dist/federation/pii.js +14 -9
  33. package/packages/sdk/dist/federation/policy.d.ts +1 -1
  34. package/packages/sdk/dist/federation/policy.js +3 -0
  35. package/packages/sdk/dist/index.d.ts +2 -0
  36. package/packages/sdk/dist/index.js +4 -0
  37. package/packages/sdk/dist/mcp/bin.js +2 -25
  38. package/packages/sdk/dist/mcp/server.d.ts +4 -0
  39. package/packages/sdk/dist/mcp/server.js +53 -10
  40. package/packages/sdk/dist/mcp/stdio.d.ts +6 -0
  41. package/packages/sdk/dist/mcp/stdio.js +66 -0
  42. package/packages/sdk/dist/policy/action-policy.d.ts +13 -0
  43. package/packages/sdk/dist/policy/action-policy.js +8 -0
  44. package/packages/sdk/dist/policy/authority.d.ts +2 -0
  45. package/packages/sdk/dist/policy/authority.js +2 -0
  46. package/packages/sdk/dist/policy/capability-registry.d.ts +46 -0
  47. package/packages/sdk/dist/policy/capability-registry.js +24 -0
  48. package/packages/sdk/dist/policy/completion-policy.d.ts +25 -0
  49. package/packages/sdk/dist/policy/completion-policy.js +10 -0
  50. package/packages/sdk/dist/policy/evidence-policy.d.ts +2 -0
  51. package/packages/sdk/dist/policy/evidence-policy.js +2 -0
  52. package/packages/sdk/dist/policy/execution-context.d.ts +28 -0
  53. package/packages/sdk/dist/policy/execution-context.js +43 -0
  54. package/packages/sdk/dist/policy/index.d.ts +9 -0
  55. package/packages/sdk/dist/policy/index.js +9 -0
  56. package/packages/sdk/dist/policy/retry-policy.d.ts +10 -0
  57. package/packages/sdk/dist/policy/retry-policy.js +12 -0
  58. package/packages/sdk/dist/policy/tool-behavior.d.ts +37 -0
  59. package/packages/sdk/dist/policy/tool-behavior.js +19 -0
  60. package/packages/sdk/dist/release/known-good-releases.d.ts +2 -1
  61. package/packages/sdk/dist/release/known-good-releases.js +4 -16
  62. package/packages/sdk/dist/release/signature.js +13 -2
  63. package/packages/sdk/dist/version.d.ts +1 -1
  64. package/packages/sdk/dist/version.js +1 -1
  65. package/packages/sdk/package.json +1 -1
package/AGENTS.md CHANGED
@@ -32,7 +32,9 @@ make session-end # lifecycle compatibility target
32
32
  - **MUST** verify evidence before claiming completion.
33
33
  - **MUST NOT** commit `console.log`, `debugger`, `.only()`, credentials, generated secrets, or runtime logs.
34
34
  - **MUST NOT** use a persistent Claude daemon. Claude Code and the Agent SDK run in-process; background work uses Claude Code's Agent tool.
35
- - **MUST NOT** rebase or force-push under the default project policy.
35
+ - **MUST NOT** rebase or force-push unless an explicit operator restriction or
36
+ host/runtime contract forbids it; Bizar itself does not gate high-impact
37
+ actions by impact.
36
38
 
37
39
  ### Worktree discipline
38
40
 
@@ -58,16 +60,16 @@ edit, test, and iterate without pausing for routine decisions. Routine
58
60
  decisions (file layout, naming, scope of a single commit, choosing between
59
61
  two equivalent stdlib calls, picking a verification command from the Makefile,
60
62
  or completing an OpenKan task after `make check` is green) do NOT require
61
- human approval and MUST NOT trigger a permission handoff. PreToolUse hooks
62
- still deny prohibited actions and escalate externally visible or irreversible
63
- actions with `permissionDecision: "ask"`; that escalation list is the
64
- authoritative floor, not a starting point.
65
-
66
- Mike performs bounded read-only orientation, then uses a native Agent team as
67
- the default for substantive work. Direct work is reserved for an unmistakably
68
- tiny single-target copy/style/format edit or the user's explicit `/quick`
69
- request. A single Agent or native workflow is used only when the user
70
- explicitly requests that mode or a durable workflow must be resumed. Ask one
63
+ human approval and MUST NOT trigger a permission handoff. PreToolUse hooks may
64
+ classify high-impact actions and add evidence guidance, but they must not deny
65
+ or ask solely because an action is externally visible, irreversible, or
66
+ powerful. Invalid target/schema/state is a typed contract error, not a
67
+ permission handoff.
68
+
69
+ Mike performs bounded read-only orientation, then chooses the smallest capable
70
+ execution surface for the requested outcome. Direct work is the default when
71
+ one agent can safely complete and verify the task; native Agent teams are used
72
+ only when independent lanes materially improve throughput or correctness. Ask one
71
73
  concise clarification question only when a material choice, acceptance
72
74
  criterion, safety boundary, or unresolved constraint would change the work;
73
75
  otherwise continue autonomously. The team is host-side state under
@@ -86,46 +88,25 @@ default. If no enabled configured candidate exists, dispatch fails with an
86
88
  actionable configuration error. Bizar never retries a failed dispatch by cycling
87
89
  aliases, providers, or tiers.
88
90
 
89
- The authoritative hard approval list (cannot be auto-approved) is: pushes, pull-request
91
+ The authoritative default is maximum autonomy; pushes, pull-request
90
92
  mutations, releases, package publication, deployments, production/shared-
91
93
  infrastructure writes, credential changes, public exposure, irreversible
92
- destruction. Everything else proceeds.
93
-
94
- Under F-176 the seven-category floor above is enforced by `permission-request.mjs`
95
- (the destructive subset: force-push, rebase, root deletion, system-destructive
96
- commands) and surfaced for the rest via `git-workflow-guard.mjs` as advisory
97
- reminders in `hookSpecificOutput.additionalContext`. `permissions.deny` and
98
- `permissions.ask` are emptied by design — the floor is enforced by hook output,
99
- not by Claude Code prompts.
100
-
101
- > Note: `config/claude/settings.json` ships `permissions.ask` moved into
102
- > `permissions.allow` so subagents do not prompt for commits, pushes, PRs,
103
- > or deploys. Operators who want HITL back can move the following exact
104
- > patterns from `permissions.allow` into `permissions.ask` in their local
105
- > `~/.claude/settings.json` override:
106
- >
107
- > - `Bash(git push *)`
108
- > - `Bash(git -C * push *)`
109
- > - `Bash(git --git-dir=* push *)`
110
- > - `Bash(git push --force *)`
111
- > - `Bash(git push -f *)`
112
- > - `Bash(git rebase *)`
113
- > - `Bash(gh pr create *)`
114
- > - `Bash(gh pr merge *)`
115
- > - `Bash(gh release create *)`
116
- > - `Bash(npm publish *)`
117
- > - `Bash(bun publish *)`
118
- > - `Bash(pnpm publish *)`
119
- > - `Bash(vercel deploy *)`
120
- > - `Bash(wrangler deploy *)`
121
- > - `Bash(flyctl deploy *)`
94
+ destruction proceed when explicitly requested and contract-valid. Stronger
95
+ evidence and rollback handling are required; a permission prompt is not.
96
+
97
+ The default maximum-autonomy profile is implemented by the host runtime;
98
+ `permission-request.mjs` is a fail-open compatibility hook. The workflow and
99
+ PreToolUse hooks may emit advisory evidence guidance but never create a Bizar
100
+ approval gate.
101
+ Historical F-176 approval-floor wording is retained only in superseded audit
102
+ records. Operators may configure their host independently, but Bizar does not
103
+ reintroduce an impact-based or role-based gate.
122
104
 
123
105
  > Note: `disableAutoCompact: false` is shipped by default so Claude Code can
124
106
  > compact automatically before the context limit. Hook `precompact-priorities.sh`
125
107
  > snapshots bounded state and preserves evidence and decisions on compaction.
126
- Local `git commit` is always allowed silently via `permissions.allow`; the seven
127
- HITL categories above are gated by `permission-request.mjs` plus the advisory
128
- hook chain.
108
+ Local and external mutations follow the same action-contract path; hooks add
109
+ evidence guidance only.
129
110
 
130
111
  Agents fetch current official documentation via WebSearch + WebFetch before
131
112
  acting on an external API, library, framework, CLI, configuration format, or
@@ -140,14 +121,11 @@ The autonomy and approval policy above governs this execution model. The project
140
121
  Every non-empty primary request enters Bizar through `office-manager` (`@mike`).
141
122
  The installer sets Claude Code's global `agent` setting to Mike's frontmatter
142
123
  name (`mike`),
143
- and the routing hook supplies the team-first coordination policy. For non-tiny
144
- work, Mike first gathers only bounded read-only context. When the inferred
145
- outcome, acceptance criteria, and safety boundary are clear, it forms the
146
- default Agent team and continues autonomously. It asks one concise
124
+ and the routing hook supplies the coordination policy. Mike gathers bounded
125
+ read-only context, selects direct execution or a narrowly scoped Agent team
126
+ based on coordination value, and continues autonomously. It asks one concise
147
127
  clarification only when a material choice or unresolved constraint would
148
- change the work. `/quick` explicitly selects direct primary-session execution;
149
- single Agents and native workflows are explicit or resumed modes. Mike owns
150
- integration and final verification.
128
+ change the work. Mike owns integration and final verification.
151
129
  A Bizar custom agent already executing its assigned role does not recursively
152
130
  dispatch itself.
153
131
 
@@ -202,3 +180,9 @@ not a general note vault, semantic search service, or knowledge-base API.
202
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.
203
181
  4. The OpenKan task records fresh verification evidence and no required work remains.
204
182
  5. `/simplify` reviews the staged diff before the approval-gated commit.
183
+
184
+ <!-- openwolf:begin -->
185
+ # OpenWolf
186
+
187
+ This project uses OpenWolf for context management. Read and follow .wolf/OPENWOLF.md at session start. Check .wolf/cerebrum.md before generating code. Grep .wolf/anatomy.md for a file's path before reading it (never read the whole index).
188
+ <!-- openwolf:end -->
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="docs/assets/bizar-banner.svg" alt="Bizar: guarded autonomy for Agent Orchestrator" width="100%" />
3
+ <img src="docs/assets/bizar-banner.svg" alt="Bizar: maximum autonomy for Agent Orchestrator" width="100%" />
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/%40polderlabs%2Fbizar?color=0f766e&label=npm)](https://www.npmjs.com/package/@polderlabs/bizar)
6
6
  [![license](https://img.shields.io/badge/license-MIT-0f172a)](LICENSE)
@@ -9,7 +9,7 @@
9
9
  ![OmniRoute routing](https://img.shields.io/badge/routing-OmniRoute-0ea5e9)
10
10
  [![Standalone planning](https://img.shields.io/badge/standalone%20planning-OpenKan-f59e0b)](https://www.npmjs.com/package/@polderlabs/openkan)
11
11
 
12
- ### Guarded autonomy for Agent Orchestrator workers
12
+ ### Maximum autonomy for Agent Orchestrator workers
13
13
 
14
14
  Run focused Codex workers under Agent Orchestrator. Bizar supplies their
15
15
  repository policy, guardrails, skills, and verification evidence.
@@ -27,9 +27,10 @@ each worker's implementation and verification discipline explicit. AO owns
27
27
  multi-agent coordination, worktrees, branches, PR/review/CI feedback, previews,
28
28
  and browser state; Bizar does not duplicate those surfaces.
29
29
 
30
- It keeps the operator in control of model selection and high-impact actions.
31
- AO model choices live in the registered project's AO configuration. Standalone
32
- Claude aliases live in the global Claude configuration.
30
+ It records evidence and rollback context for high-impact actions while keeping
31
+ execution autonomous by default. AO model choices live in the registered
32
+ project's AO configuration. Standalone Claude aliases live in global Claude
33
+ configuration.
33
34
 
34
35
  | You want | Bizar provides |
35
36
  | --- | --- |
@@ -67,11 +68,16 @@ Install Bizar, then configure the repository with a running AO daemon.
67
68
 
68
69
  ```sh
69
70
  npm install -g @polderlabs/bizar
70
- bizar ao doctor
71
+ bizar ao check
72
+ bizar ao install
73
+ bizar ao update
71
74
  bizar ao setup
72
75
  ```
73
76
 
74
- `bizar ao setup` preserves AO's existing project configuration while selecting
77
+ `bizar ao check` runs AO health checks. `bizar ao install` and `bizar ao update`
78
+ open AO through its supported lifecycle command; the AO desktop app owns
79
+ installation and updates. `bizar ao setup` remains the separate, explicit
80
+ repository configuration step: it preserves AO's existing project configuration while selecting
75
81
  Codex for both AO roles and materializing Bizar's managed repository-local
76
82
  worker rules at `.ao/bizar-worker-rules.md`.
77
83
  AO remains responsible for spawning workers, messaging, PR claims, review/CI
@@ -176,10 +182,10 @@ risk. It does not create parallel workers merely to look busy.
176
182
  | --- | --- |
177
183
  | Research, planning, implementation, review, verification | Architecture, accessibility, security, tests, documentation, performance, build repair, operations, and domain analysis |
178
184
 
179
- ## Guardrails that stay out of the way
185
+ ## Evidence that stays in the loop
180
186
 
181
- Bizar is designed to be autonomous for local, reversible work and deliberate
182
- for consequential actions.
187
+ Bizar executes valid operations autonomously and records stronger evidence and
188
+ rollback context when the effect is consequential.
183
189
 
184
190
  | Category | Default behavior |
185
191
  | --- | --- |
@@ -187,12 +193,12 @@ for consequential actions.
187
193
  | Parallel code changes | Uses isolated worktrees and scoped task ownership |
188
194
  | Ambiguous material design choice | Asks one concise clarification before execution |
189
195
  | Commit | Locally allowed, with a fresh simplify review reminder |
190
- | Push, PR mutation, release, publish, deploy | Requires an explicit human decision |
191
- | Rebase, force-push, broad destructive commands | Denied or escalated by the safety floor |
196
+ | Push, PR mutation, release, publish, deploy | Executes when requested and contract-valid; records evidence |
197
+ | Rebase, force-push, broad destructive commands | Executes when requested and contract-valid; advisory context only |
192
198
 
193
- The goal is not to make Claude Code timid. It is to make its boundaries clear:
194
- Bizar works through local implementation and verification, then stops at the
195
- point where an external or difficult-to-reverse decision belongs to you.
199
+ The goal is clear request fidelity: invalid targets, schemas, signatures, and
200
+ lifecycle states fail deterministically, while explicit operator restrictions
201
+ remain binding.
196
202
 
197
203
  ## What gets installed
198
204
 
package/cli/banner.mjs CHANGED
@@ -14,7 +14,7 @@ export function showBanner() {
14
14
  console.log(chalk.hex('#6366f1').bold(RUNE_HELM));
15
15
  console.log(chalk.hex('#a855f7')(' Guarded autonomous agent system for Claude Code'));
16
16
  console.log();
17
- console.log(chalk.dim(' 84 agents · guarded autonomy · human approval gates · MCP · Skills CLI'));
17
+ console.log(chalk.dim(' 84 agents · maximum autonomy · evidence verification · MCP · Skills CLI'));
18
18
  console.log();
19
19
  }
20
20
 
@@ -10,7 +10,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { basename, dirname, resolve } from 'node:path';
12
12
 
13
- export const AO_RULES_FILE = '.ao/bizar-worker-rules.md';
13
+ export const AO_RULES_FILE = '.ao/bizar-managed-rules.md';
14
14
  export const AO_ORCHESTRATOR_RULES = 'Use AO for coordination and Bizar as the Codex worker harness. Spawn focused AO workers for implementation; workers own changes, verification, commits, and PR follow-up.';
15
15
  const WORKER_RULES_TEMPLATE = resolve(dirname(fileURLToPath(import.meta.url)), '../../config/ao/worker-rules.md');
16
16
 
@@ -21,13 +21,15 @@ function optionValue(args, flag) {
21
21
 
22
22
  export function parseAoArgs(args = []) {
23
23
  const [first, ...rest] = args;
24
- const subcommand = ['setup', 'doctor', 'status', 'sessions', 'help'].includes(first) ? first : 'forward';
24
+ const subcommand = ['check', 'install', 'update', 'setup', 'doctor', 'status', 'sessions', 'help'].includes(first) ? first : 'forward';
25
25
  return {
26
26
  subcommand,
27
27
  forward: subcommand === 'forward' ? args : rest,
28
28
  project: optionValue(args, '--project'),
29
29
  model: optionValue(args, '--model'),
30
30
  permissions: optionValue(args, '--permissions'),
31
+ workerAgent: optionValue(args, '--worker-agent'),
32
+ orchestratorAgent: optionValue(args, '--orchestrator-agent'),
31
33
  help: args.includes('--help') || args.includes('-h') || first === 'help',
32
34
  };
33
35
  }
@@ -42,8 +44,12 @@ export function defaultProjectId(cwd) {
42
44
  }
43
45
 
44
46
  export function configuredProjectConfig(current = {}, options = {}) {
45
- const worker = { ...(current.worker || {}), agent: 'codex' };
46
- const orchestrator = { ...(current.orchestrator || {}), agent: 'codex' };
47
+ const worker = { ...(current.worker || {}) };
48
+ const orchestrator = { ...(current.orchestrator || {}) };
49
+ if (!worker.agent) worker.agent = 'codex';
50
+ if (!orchestrator.agent) orchestrator.agent = 'codex';
51
+ if (options.workerAgent) worker.agent = options.workerAgent;
52
+ if (options.orchestratorAgent) orchestrator.agent = options.orchestratorAgent;
47
53
  const agentConfig = { ...(current.agentConfig || {}) };
48
54
  if (options.model) agentConfig.model = options.model;
49
55
  if (options.permissions) agentConfig.permissions = options.permissions;
@@ -116,6 +122,9 @@ function help(output = process.stdout) {
116
122
  bizar ao — Agent Orchestrator bridge (AO-primary)
117
123
 
118
124
  Usage:
125
+ bizar ao check
126
+ bizar ao install
127
+ bizar ao update
119
128
  bizar ao doctor
120
129
  bizar ao setup [--project <id>] [--model <id>] [--permissions <mode>]
121
130
  bizar ao status
@@ -123,7 +132,9 @@ Usage:
123
132
  bizar ao <any supported ao command> [args...]
124
133
 
125
134
  AO remains the sole owner of sessions, worktrees, PRs, review feedback,
126
- previews, and browser state. The setup command registers this repository with AO,
135
+ previews, and browser state. Check runs AO's health checks. Install and update
136
+ open AO through its supported lifecycle command; the AO desktop app owns updates.
137
+ The setup command registers this repository with AO,
127
138
  selects Codex for both AO roles, and preserves existing AO project settings.
128
139
  It creates and configures the repository-local ${AO_RULES_FILE} as AO worker
129
140
  rules, preserving a file that is already present.
@@ -160,7 +171,9 @@ export function setupAo(options = {}) {
160
171
  projectId ||= defaultProjectId(cwd);
161
172
 
162
173
  if (!projectDetails) {
163
- const added = runAo(['project', 'add', '--path', cwd, '--id', projectId, '--worker-agent', 'codex', '--orchestrator-agent', 'codex'], options);
174
+ const addArgs = ['project', 'add', '--path', cwd, '--id', projectId];
175
+ addArgs.push('--worker-agent', options.workerAgent || 'codex', '--orchestrator-agent', options.orchestratorAgent || 'codex');
176
+ const added = runAo(addArgs, options);
164
177
  if (!added.ok) return added;
165
178
  const details = runAo(['project', 'get', projectId, '--json'], options);
166
179
  if (!details.ok) return details;
@@ -188,6 +201,16 @@ export function doctorAo(options = {}) {
188
201
  };
189
202
  }
190
203
 
204
+ export function checkAo(options = {}) {
205
+ return runAo(['doctor', '--json'], options);
206
+ }
207
+
208
+ export function installAo(options = {}) {
209
+ return runAo(['start', '--json'], options);
210
+ }
211
+
212
+ export const updateAo = installAo;
213
+
191
214
  export function run(args = [], options = {}) {
192
215
  const parsed = parseAoArgs(args);
193
216
  const output = options.output || process;
@@ -196,9 +219,15 @@ export function run(args = [], options = {}) {
196
219
  return true;
197
220
  }
198
221
  try {
222
+ if (parsed.subcommand === 'setup' && isAoSession(options.env || process.env) && !options.allowNested) {
223
+ throw new Error('AO_OWNED_OPERATION: AO setup cannot create competing lifecycle state from an AO worker');
224
+ }
199
225
  let result;
200
226
  if (parsed.subcommand === 'setup') result = setupAo({ ...options, ...parsed });
227
+ else if (parsed.subcommand === 'check') result = checkAo(options);
201
228
  else if (parsed.subcommand === 'doctor') result = doctorAo(options);
229
+ else if (parsed.subcommand === 'install') result = installAo(options);
230
+ else if (parsed.subcommand === 'update') result = updateAo(options);
202
231
  else if (parsed.subcommand === 'status') result = runAo(['status', '--json'], options);
203
232
  else if (parsed.subcommand === 'sessions') result = runAo(['session', 'ls', ...parsed.forward], options);
204
233
  else result = runAo(parsed.forward, options);
@@ -37,7 +37,7 @@ import { createHash } from 'node:crypto';
37
37
  * originalSha256: string,
38
38
  * find: string,
39
39
  * newText: string,
40
- * verification: { command: string, cwd?: string, timeoutMs?: number, expectedExitCode?: number },
40
+ * verification: { command?: string, argv?: string[], cwd?: string, timeoutMs?: number, expectedExitCode?: number },
41
41
  * rollbackPlan: { kind: 'replace-back', note: string } | { kind: 'manual', note: string, manualCommand?: string },
42
42
  * reason: string,
43
43
  * createdAt: string,
@@ -98,8 +98,16 @@ export function validateProposal(raw) {
98
98
  if (raw.find === raw.newText) {
99
99
  throw new TypeError('Proposal.newText must differ from Proposal.find');
100
100
  }
101
- if (typeof raw.verification !== 'object' || typeof raw.verification.command !== 'string' || raw.verification.command.length === 0) {
102
- throw new TypeError('Proposal.verification.command must be a non-empty string');
101
+ if (typeof raw.verification !== 'object'
102
+ || (!Array.isArray(raw.verification.argv) && (typeof raw.verification.command !== 'string' || raw.verification.command.length === 0))) {
103
+ throw new TypeError('Proposal.verification requires a non-empty argv array or command string');
104
+ }
105
+ if (Array.isArray(raw.verification.argv)
106
+ && (raw.verification.argv.length === 0 || raw.verification.argv.some((arg) => typeof arg !== 'string' || arg.length === 0))) {
107
+ throw new TypeError('Proposal.verification.argv must contain non-empty strings');
108
+ }
109
+ if (typeof raw.verification.command === 'string' && /(?:^|[^\\])(?:;|&&|\|\||`|\$\()/.test(raw.verification.command) && !Array.isArray(raw.verification.argv)) {
110
+ throw new TypeError('Proposal.verification.command contains shell syntax; use verification.argv');
103
111
  }
104
112
  if (typeof raw.rollbackPlan !== 'object' || (raw.rollbackPlan.kind !== 'replace-back' && raw.rollbackPlan.kind !== 'manual')) {
105
113
  throw new TypeError('Proposal.rollbackPlan.kind must be "replace-back" or "manual"');
@@ -152,4 +160,4 @@ export function planRollback(proposal, currentBytes) {
152
160
  return { kind: 'replace-back', newBytes: currentBytes.replace(proposal.newText, proposal.find) };
153
161
  }
154
162
  return { kind: 'manual', manualCommand: proposal.rollbackPlan.manualCommand };
155
- }
163
+ }
@@ -56,7 +56,7 @@ import {
56
56
  statSync,
57
57
  writeFileSync,
58
58
  } from 'node:fs';
59
- import { isAbsolute, join, resolve } from 'node:path';
59
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
60
60
  import { spawnSync } from 'node:child_process';
61
61
  import { ensureSecureDir } from './secure-dir.mjs';
62
62
  import {
@@ -145,19 +145,57 @@ function readCurrentFile({ file, cwd }) {
145
145
  return { abs, bytes: readFileSync(abs, 'utf8') };
146
146
  }
147
147
 
148
+ /** Parse a deliberately small argv form without invoking a shell. */
149
+ export function parseSimpleArgv(command) {
150
+ if (typeof command !== 'string' || !command.trim()) throw new TypeError('verification command must be non-empty');
151
+ if (/(?:;|&&|\|\||`|\$\(|>|<|\n|\r)/.test(command)) {
152
+ throw new TypeError('verification command contains shell syntax; use argv');
153
+ }
154
+ const argv = [];
155
+ let token = '';
156
+ let quote = null;
157
+ let escaped = false;
158
+ for (const char of command.trim()) {
159
+ if (escaped) { token += char; escaped = false; continue; }
160
+ if (char === '\\' && quote !== "'") { escaped = true; continue; }
161
+ if (quote) {
162
+ if (char === quote) quote = null;
163
+ else token += char;
164
+ continue;
165
+ }
166
+ if (char === '"' || char === "'") { quote = char; continue; }
167
+ if (/\s/.test(char)) {
168
+ if (token) { argv.push(token); token = ''; }
169
+ continue;
170
+ }
171
+ token += char;
172
+ }
173
+ if (escaped || quote || !token && argv.length === 0) throw new TypeError('verification command has unterminated quoting');
174
+ if (token) argv.push(token);
175
+ return argv;
176
+ }
177
+
178
+ function runVerification(verification, cwd) {
179
+ const verifyCwd = verification.cwd || cwd;
180
+ const timeout = Number(verification.timeoutMs) || 60_000;
181
+ const argv = Array.isArray(verification.argv) ? verification.argv : parseSimpleArgv(verification.command);
182
+ return { result: spawnSync(argv[0], argv.slice(1), { cwd: verifyCwd, encoding: 'utf8', timeout, shell: false }), cwd: verifyCwd };
183
+ }
184
+
148
185
  function doPropose({ flags, cwd }) {
149
186
  if (!flags.file || !flags.find || !flags.new || !flags.verify) {
150
187
  process.stderr.write('usage: bizar improve propose --file <path> --find <substr> --new <text> --verify <cmd> [--reason <why>]\n');
151
188
  process.exit(2);
152
189
  }
153
190
  const { abs, bytes } = readCurrentFile({ file: flags.file, cwd });
191
+ const argv = parseSimpleArgv(flags.verify);
154
192
  const proposal = {
155
193
  id: newProposalId({ targetFile: abs, cwd }),
156
194
  targetFile: abs,
157
195
  originalSha256: sha256Text(bytes),
158
196
  find: flags.find,
159
197
  newText: flags.new,
160
- verification: { command: flags.verify, cwd, expectedExitCode: 0 },
198
+ verification: { command: flags.verify, argv, cwd, expectedExitCode: 0 },
161
199
  rollbackPlan: { kind: 'replace-back', note: `Reverse the find/newText swap on ${abs}` },
162
200
  reason: String(flags.reason ?? '(no reason supplied)'),
163
201
  createdAt: new Date().toISOString(),
@@ -218,12 +256,7 @@ function doRun({ flags, cwd }) {
218
256
  writeFileSync(abs, plan.newBytes, { mode: statSync(abs).mode & 0o777 });
219
257
 
220
258
  // Verify.
221
- const verifyCwd = proposal.verification.cwd || cwd;
222
- const verify = spawnSync('sh', ['-c', proposal.verification.command], {
223
- cwd: verifyCwd,
224
- encoding: 'utf8',
225
- timeout: Number(proposal.verification.timeoutMs) || 60_000,
226
- });
259
+ const { result: verify, cwd: verifyCwd } = runVerification(proposal.verification, cwd);
227
260
  const verifyExit = verify.status ?? -1;
228
261
  const verifyStdoutSha = sha256Text(verify.stdout ?? '');
229
262
  const verifyStderrSha = sha256Text(verify.stderr ?? '');
@@ -245,6 +278,7 @@ function doRun({ flags, cwd }) {
245
278
  rolledBackSha256: rollback.kind === 'replace-back' ? sha256Text(rollback.newBytes) : null,
246
279
  verification: {
247
280
  command: proposal.verification.command,
281
+ argv: proposal.verification.argv,
248
282
  cwd: verifyCwd,
249
283
  exitCode: verifyExit,
250
284
  stdoutSha256: verifyStdoutSha,
@@ -268,6 +302,7 @@ function doRun({ flags, cwd }) {
268
302
  afterSha256: summary.afterSha256,
269
303
  verification: {
270
304
  command: proposal.verification.command,
305
+ argv: proposal.verification.argv,
271
306
  cwd: verifyCwd,
272
307
  exitCode: verifyExit,
273
308
  stdoutSha256: verifyStdoutSha,
@@ -286,11 +321,7 @@ function doVerify({ flags, cwd }) {
286
321
  }
287
322
  const proposalPath = isAbsolute(flags.proposal) ? flags.proposal : resolve(cwd, flags.proposal);
288
323
  const proposal = validateProposal(JSON.parse(readFileSync(proposalPath, 'utf8')));
289
- const verify = spawnSync('sh', ['-c', proposal.verification.command], {
290
- cwd: proposal.verification.cwd || cwd,
291
- encoding: 'utf8',
292
- timeout: Number(proposal.verification.timeoutMs) || 60_000,
293
- });
324
+ const { result: verify } = runVerification(proposal.verification, cwd);
294
325
  process.stdout.write(JSON.stringify({
295
326
  ok: (verify.status ?? -1) === (proposal.verification.expectedExitCode ?? 0),
296
327
  exitCode: verify.status ?? -1,
@@ -197,6 +197,16 @@ export async function update(args, isHelpRequest) {
197
197
  showUpdateHelp();
198
198
  return;
199
199
  }
200
+ const versionIndex = args.indexOf('--version');
201
+ if (versionIndex >= 0) {
202
+ const version = args[versionIndex + 1];
203
+ const artifactIndex = args.indexOf('--artifact-dir');
204
+ const artifactDir = artifactIndex >= 0 ? args[artifactIndex + 1] : undefined;
205
+ const { runVerifiedUpdate } = await import('../update.mjs');
206
+ const result = await runVerifiedUpdate(args, { artifactDir });
207
+ if (!result?.ok) process.exit(1);
208
+ return;
209
+ }
200
210
  // v10.19.6 — route update through the same flag-parsing + installer
201
211
  // pipeline as install so `--dry-run`, `--force`, `--yes`, etc. do
202
212
  // what they claim. See `runUpdateWithFlags` for the testable
@@ -18,7 +18,7 @@ function help() {
18
18
  bizar openkan — default durable planning and goals surface
19
19
 
20
20
  Usage:
21
- bizar openkan install Install @polderlabs/openkan@latest from npm
21
+ bizar openkan install Install @polderlabs/openkan@0.5.0 from npm
22
22
  ok init Initialise .ok/ in this project
23
23
  ok task <add|list|show|update|...> Manage durable tasks
24
24
  ok plan <add|list|show|update> Manage plans and phases
@@ -25,7 +25,7 @@
25
25
  * The private key is operator-held. The CLI never persists it.
26
26
  */
27
27
 
28
- import { createHash, randomBytes } from 'node:crypto';
28
+ import { createHash, generateKeyPairSync } from 'node:crypto';
29
29
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
30
30
  import { execSync } from 'node:child_process';
31
31
  import { join } from 'node:path';
@@ -49,8 +49,9 @@ export const USAGE = `
49
49
  --package-json <path> Root package.json (default: ./package.json).
50
50
  --sdk-package-json <path> SDK package.json (default: ./packages/sdk/package.json).
51
51
  --private-key <path> PEM-encoded ed25519 private key. If omitted, the
52
- signature blob is generated with a deterministic
53
- test key so the operator can verify the layout.
52
+ an ephemeral fixture key is generated and returned
53
+ for tests; production releases must provide one.
54
+ --tarball <path> Exact artifact bytes to bind to provenance/signature.
54
55
  --git-sha <sha> Override the git sha (default: read from git rev-parse HEAD).
55
56
  --quiet Suppress progress output.
56
57
 
@@ -87,16 +88,17 @@ function minisignEncode(sigB64, trustedComment = `signed by bizar ${CURRENT_RELE
87
88
  `untrusted comment: ${untrustedComment}`,
88
89
  sigB64,
89
90
  `trusted comment: ${trustedComment}`,
90
- Buffer.from(trustedComment, 'utf8').toString('base64'),
91
+ Buffer.from(`trusted comment: ${trustedComment}\n`, 'utf8').toString('base64'),
91
92
  '',
92
93
  ].join('\n');
93
94
  }
94
95
 
95
- function deterministicTestKey() {
96
- // Pure-JS test key — generates a valid signature so the layout is
97
- // verifiable end-to-end. Operators MUST replace this with a
98
- // private key they control via --private-key.
99
- return Buffer.from('not-a-real-key', 'utf8').toString('base64').slice(0, 44);
96
+ function ephemeralSigningKey() {
97
+ const pair = generateKeyPairSync('ed25519');
98
+ return {
99
+ privateKeyPem: pair.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
100
+ publicKeyPem: pair.publicKey.export({ type: 'spki', format: 'pem' }).toString(),
101
+ };
100
102
  }
101
103
 
102
104
  /**
@@ -115,6 +117,8 @@ export function buildReleaseArtifacts({
115
117
  privateKeyPem,
116
118
  outDir,
117
119
  timestamp,
120
+ artifactBytes,
121
+ artifactName,
118
122
  }) {
119
123
  const sdkName = sdkPackageJson.name;
120
124
  const sdkVersion = sdkPackageJson.version;
@@ -142,9 +146,11 @@ export function buildReleaseArtifacts({
142
146
  // Use the SBOM as the artifact for provenance + signing so the
143
147
  // signed payload IS the SBOM. Operators can also sign the
144
148
  // tarball — adjust the `subject.name` accordingly.
149
+ const signedArtifact = artifactBytes ?? sbomBytes;
150
+ const signedArtifactSha256 = createHash('sha256').update(signedArtifact).digest('hex');
145
151
  const statement = buildProvenanceAttestation({
146
- artifactName: `${rootPackageJson.name}-${version}.sbom.cdx.json`,
147
- artifactSha256: sbomSha256,
152
+ artifactName: artifactName ?? `${rootPackageJson.name}-${version}.tgz`,
153
+ artifactSha256: signedArtifactSha256,
148
154
  version,
149
155
  gitSha,
150
156
  materialUri: `git+https://github.com/DrB0rk/BizarHarness@${gitSha}`,
@@ -154,20 +160,24 @@ export function buildReleaseArtifacts({
154
160
  const provenanceJsonl = JSON.stringify(statement) + '\n';
155
161
  const provenanceSha256 = createHash('sha256').update(provenanceJsonl, 'utf8').digest('hex');
156
162
 
157
- // Signature: ed25519 over the SBOM. The keyId is the
158
- // CURRENT_RELEASE_KEY_ID (8 ASCII bytes). When --private-key is
159
- // omitted, we fall back to a deterministic test signature so
160
- // operators can still see the layout.
161
- const message = sbomBytes;
162
- const keyId = Buffer.from(CURRENT_RELEASE_KEY_ID, 'utf8').subarray(0, 8);
163
+ // Signature: ed25519 over the exact artifact bytes. Production callers
164
+ // provide a private key; the generated key fallback is explicit and
165
+ // returns its public key so fixtures remain interoperable and auditable.
166
+ const trustedComment = `signed by bizar ${CURRENT_RELEASE_KEY_ID} sha256=${signedArtifactSha256}`;
167
+ // Minisign authenticates the trusted-comment envelope. Keep the exact
168
+ // bytes identical to the verifier so generated fixtures are interoperable.
169
+ const message = Buffer.from(`trusted comment: ${trustedComment}\n`, 'utf8');
170
+ const keyId = Buffer.from(CURRENT_RELEASE_KEY_ID, 'hex').subarray(0, 8);
163
171
  let sigB64;
172
+ let signingPublicKeyPem = null;
164
173
  if (privateKeyPem) {
165
174
  sigB64 = signWithEd25519(message, privateKeyPem, keyId);
166
175
  } else {
167
- // Deterministic test signature — keeps the layout byte-stable.
168
- sigB64 = deterministicTestKey();
176
+ const ephemeral = ephemeralSigningKey();
177
+ sigB64 = signWithEd25519(message, ephemeral.privateKeyPem, keyId);
178
+ signingPublicKeyPem = ephemeral.publicKeyPem;
169
179
  }
170
- const minisigText = minisignEncode(sigB64);
180
+ const minisigText = minisignEncode(sigB64, trustedComment);
171
181
 
172
182
  mkdirSync(outDir, { recursive: true, mode: 0o700 });
173
183
  const sbomPath = join(outDir, `${version}.sbom.cdx.json`);
@@ -189,6 +199,8 @@ export function buildReleaseArtifacts({
189
199
  outDir,
190
200
  sdkName,
191
201
  sdkVersion,
202
+ signedArtifactSha256,
203
+ signingPublicKeyPem,
192
204
  };
193
205
  }
194
206
 
@@ -231,6 +243,7 @@ export async function run(subargs) {
231
243
  const privateKeyPem = flags['private-key']
232
244
  ? readFileSync(String(flags['private-key']), 'utf8')
233
245
  : null;
246
+ const artifactBytes = flags.tarball ? readFileSync(String(flags.tarball)) : undefined;
234
247
 
235
248
  const artifacts = buildReleaseArtifacts({
236
249
  version,
@@ -238,6 +251,8 @@ export async function run(subargs) {
238
251
  sdkPackageJson: sdkPkg,
239
252
  gitSha,
240
253
  privateKeyPem,
254
+ artifactBytes,
255
+ artifactName: flags.tarball ? `${rootPkg.name}-${version}.tgz` : undefined,
241
256
  outDir,
242
257
  });
243
258
 
@@ -104,6 +104,9 @@ export async function run(name, args, isHelpRequest) {
104
104
  return true;
105
105
  }
106
106
  if (subcommand !== 'start') { usage(); return false; }
107
+ if (process.env.AO_SESSION_ID || process.env.AO_PROJECT_ID) {
108
+ throw new Error('AO_OWNED_OPERATION: bizar worker start is disabled inside an Agent Orchestrator worker; request AO coordination instead');
109
+ }
107
110
  const parsed = parseStart(args.slice(1));
108
111
  const plan = buildWorkerPlan({ repoRoot: process.cwd(), ...parsed });
109
112
  mkdirSync(workerRoot(), { recursive: true, mode: 0o700 });