@polderlabs/bizar 10.23.23 → 10.25.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 (78) hide show
  1. package/AGENTS.md +14 -12
  2. package/README.md +3 -3
  3. package/cli/bin.mjs +64 -5
  4. package/cli/commands/ambiguity.mjs +473 -0
  5. package/cli/commands/claim.mjs +37 -267
  6. package/cli/commands/control.mjs +3 -1
  7. package/cli/commands/goal-bootstrap.mjs +33 -0
  8. package/cli/commands/guard.mjs +705 -0
  9. package/cli/commands/install.mjs +14 -11
  10. package/cli/commands/objective-scheduler.mjs +15 -4
  11. package/cli/commands/openkan.mjs +72 -0
  12. package/cli/commands/planning.mjs +26 -0
  13. package/cli/commands/spec-list.mjs +142 -7
  14. package/cli/commands/task.mjs +27 -234
  15. package/cli/commands/validate.mjs +6 -6
  16. package/cli/commands/workflow-gc.mjs +10 -16
  17. package/cli/commands/workflow.mjs +84 -7
  18. package/cli/control-store.mjs +7 -31
  19. package/cli/core/ultragoal-state.mjs +535 -0
  20. package/cli/openkan-store.mjs +60 -0
  21. package/cli/openkan.mjs +320 -0
  22. package/cli/provision.mjs +34 -1
  23. package/config/claude/CLAUDE.md +14 -12
  24. package/config/claude/agents/_shared/AGENT_BASELINE.md +1 -1
  25. package/config/claude/agents/office-greeter.md +10 -0
  26. package/config/claude/agents/office-manager.md +59 -0
  27. package/config/claude/agents/plan-architect.md +155 -0
  28. package/config/claude/agents/qa-reviewer.md +112 -0
  29. package/config/claude/commands/bizar.md +2 -2
  30. package/config/claude/commands/deep-interview.md +8 -0
  31. package/config/claude/commands/guard.md +57 -0
  32. package/config/claude/commands/plow-through.md +1 -1
  33. package/config/claude/commands/sprint.md +4 -5
  34. package/config/claude/commands/team.md +1 -1
  35. package/config/claude/commands/ultragoal.md +8 -0
  36. package/config/claude/hooks/goal-bootstrap.mjs +49 -0
  37. package/config/claude/hooks/keyword-router.mjs +3 -1
  38. package/config/claude/hooks/path-ownership-guard.mjs +14 -25
  39. package/config/claude/hooks/precompact-priorities.sh +2 -2
  40. package/config/claude/hooks/sessionend-recall.mjs +13 -17
  41. package/config/claude/hooks/sessionstart-prime.mjs +38 -51
  42. package/config/claude/hooks/thinking-route.mjs +1 -1
  43. package/config/claude/hooks/verify-deliverables.mjs +5 -11
  44. package/config/claude/hooks/worker-suggest.mjs +102 -0
  45. package/config/skills/autopilot/SKILL.md +27 -1
  46. package/config/skills/bizar/SKILL.md +4 -4
  47. package/config/skills/deep-interview/SKILL.md +166 -0
  48. package/config/skills/goal-bootstrap/SKILL.md +18 -0
  49. package/config/skills/guard/SKILL.md +16 -0
  50. package/config/skills/harness-engineering/SKILL.md +6 -6
  51. package/config/skills/openkan/SKILL.md +34 -0
  52. package/config/skills/ralplan/SKILL.md +92 -0
  53. package/config/skills/ralplan/references/pre-mortem.md +37 -0
  54. package/config/skills/ultragoal/SKILL.md +247 -0
  55. package/package.json +1 -1
  56. package/packages/sdk/dist/agent/goal-bootstrap.d.ts +157 -0
  57. package/packages/sdk/dist/agent/goal-bootstrap.js +437 -0
  58. package/packages/sdk/dist/agent/guard.d.ts +103 -0
  59. package/packages/sdk/dist/agent/guard.js +220 -0
  60. package/packages/sdk/dist/ambiguity/index.d.ts +9 -0
  61. package/packages/sdk/dist/ambiguity/index.js +9 -0
  62. package/packages/sdk/dist/ambiguity/score.d.ts +120 -0
  63. package/packages/sdk/dist/ambiguity/score.js +140 -0
  64. package/packages/sdk/dist/autonomy/objective-run.d.ts +12 -3
  65. package/packages/sdk/dist/autonomy/objective-run.js +4 -1
  66. package/packages/sdk/dist/handoff/ralplan.d.ts +86 -0
  67. package/packages/sdk/dist/handoff/ralplan.js +94 -0
  68. package/packages/sdk/dist/index.d.ts +5 -0
  69. package/packages/sdk/dist/index.js +10 -0
  70. package/packages/sdk/dist/mcp/server.js +93 -0
  71. package/packages/sdk/dist/specs/deep-interview.d.ts +112 -0
  72. package/packages/sdk/dist/specs/deep-interview.js +135 -0
  73. package/packages/sdk/dist/version.d.ts +1 -1
  74. package/packages/sdk/dist/version.js +1 -1
  75. package/packages/sdk/package.json +1 -1
  76. package/cli/feature-list-bridge.mjs +0 -400
  77. package/cli/progress-parser.mjs +0 -251
  78. package/cli/task-ledger.mjs +0 -795
package/AGENTS.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # AGENTS.md — Bizar Harness
2
2
 
3
- Bizar Harness is a Claude Code-native, guarded-autonomy harness. It ships project and user-level agents, skills, slash commands, hooks, an MCP server, CLI utilities, and verification scripts. It has no embedded web control plane and no Bizar note-vault subsystem; OpenKan can consume the explicit `bizar control` CLI boundary as an optional external UI.
3
+ Bizar Harness is a Claude Code-native, guarded-autonomy harness. It ships project and user-level agents, skills, slash commands, hooks, an MCP server, CLI utilities, and verification scripts. OpenKan is bundled as Bizar’s default durable planning, progression, task, and PRD-goal system; Bizar integrates with it only through the `.ok/` workspace and its supported CLI boundary.
4
4
 
5
- If you are an agent: read this file, read `PROGRESS.md`, inspect `feature_list.json`, then run `make check` before changing code.
5
+ If you are an agent: read this file, inspect `.ok/` with `bizar task list` and `bizar goals list`, then run `make check` before changing code.
6
6
 
7
7
  ## Commands
8
8
 
@@ -15,15 +15,17 @@ make check-arch # architectural and removed-surface checks
15
15
  make verify-removed-surfaces # prove deleted subsystems are absent
16
16
  make verify-repo-structure # prove tracked/package paths are clean
17
17
  make clean-check # debug-artifact/static hygiene gate
18
- make vcr # feature-ledger reality ratio
18
+ bizar task list # OpenKan task progression
19
+ bizar plan list # OpenKan plans
20
+ bizar goals list # OpenKan PRD goals
19
21
  make session-start # lifecycle compatibility target
20
22
  make session-end # lifecycle compatibility target
21
23
  ```
22
24
 
23
25
  ## Hard constraints
24
26
 
25
- - **MUST** update `PROGRESS.md` before and after each logical code change.
26
- - **MUST** keep WIP=1 in `feature_list.json`.
27
+ - **MUST** keep the scoped OpenKan task current in `.ok/`: claim before implementation, update status/evidence at each durable handoff, and complete only with verification evidence.
28
+ - **MUST** use OpenKan PRDs and plans for durable goals and progression; `PROGRESS.md` and `feature_list.json` are legacy historical records, not live control state.
27
29
  - **MUST** keep one logical operation per commit and keep its docs in the same commit.
28
30
  - **MUST** run targeted tests, then `make check`; run `make e2e` for cross-component changes.
29
31
  - **MUST** verify evidence before claiming completion.
@@ -52,7 +54,7 @@ Agents execute clear, local, reversible work autonomously — they inspect,
52
54
  edit, test, and iterate without pausing for routine decisions. Routine
53
55
  decisions (file layout, naming, scope of a single commit, choosing between
54
56
  two equivalent stdlib calls, picking a verification command from the Makefile,
55
- or marking a task `passing` after `make check` is green) do NOT require
57
+ or completing an OpenKan task after `make check` is green) do NOT require
56
58
  human approval and MUST NOT trigger a permission handoff. PreToolUse hooks
57
59
  still deny prohibited actions and escalate externally visible or irreversible
58
60
  actions with `permissionDecision: "ask"`; that escalation list is the
@@ -176,16 +178,16 @@ isolation.
176
178
  - `scripts/` + `.harness/` + `templates/` — verification, feature/eval state, audit output, and reusable contracts.
177
179
 
178
180
  The harness has no embedded browser/server UI layer or local web editor.
179
- `bizar control` is a machine-readable subprocess boundary for optional OpenKan
180
- integration; OpenKan owns HTTP, WebSocket, and presentation concerns. Session
181
+ `bizar control` is a machine-readable subprocess boundary over the default OpenKan
182
+ workspace; OpenKan owns durable task/plan/PRD state, HTTP, WebSocket, and presentation concerns. Session
181
183
  handoff, control inbox, and learning logs are bounded operational records for
182
184
  autonomy; they are not a general note vault, semantic search service, or
183
185
  knowledge-base API.
184
186
 
185
187
  ## State and evidence
186
188
 
187
- - `PROGRESS.md`current objective, evidence, next actions, blockers.
188
- - `feature_list.json` — WIP=1 feature state.
189
+ - `.ok/`authoritative OpenKan tasks, plans, PRDs, progression, evidence, and scoped ownership.
190
+ - `PROGRESS.md` and `feature_list.json` — legacy historical records; do not use them for new work.
189
191
  - `DECISIONS.md` and `docs/decisions/` — current architecture decisions.
190
192
  - `.harness/evals/` — feature evaluation records.
191
193
  - `~/.config/bizar/telemetry/` — local correlation and rejected-action feedback.
@@ -194,7 +196,7 @@ knowledge-base API.
194
196
  ## Definition of done
195
197
 
196
198
  1. Behavior is implemented with a regression test.
197
- 2. Documentation and feature state describe the actual code.
199
+ 2. Documentation and OpenKan task/plan/PRD state describe the actual code.
198
200
  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.
199
- 4. `PROGRESS.md` records fresh evidence and no required work remains.
201
+ 4. The OpenKan task records fresh verification evidence and no required work remains.
200
202
  5. `/simplify` reviews the staged diff before the approval-gated commit.
package/README.md CHANGED
@@ -52,8 +52,8 @@ bizar install
52
52
  bizar models
53
53
  ```
54
54
 
55
- Restart Claude Code after installation. The installer adds Bizar's agents,
56
- skills, commands, hooks, and settings to your user-level Claude configuration.
55
+ Restart Claude Code after installation. The installer adds Bizar's agents, skills, commands, hooks, settings, and the
56
+ default OpenKan planning runtime to your user-level Claude configuration.
57
57
  It preserves your configured gateway endpoint and credential values during a
58
58
  clean reinstall.
59
59
 
@@ -242,7 +242,7 @@ make check
242
242
  - [Documentation index](docs/INDEX.md)
243
243
  - [Architecture](docs/architecture.md)
244
244
  - [Model routing decisions](docs/decisions/)
245
- - [Current progress and evidence](PROGRESS.md)
245
+ - [Durable project progress, plans, and goals](.ok/)
246
246
  - [MIT license](LICENSE)
247
247
 
248
248
  ## License
package/cli/bin.mjs CHANGED
@@ -117,15 +117,21 @@ function showHelp() {
117
117
  release-provenance Generate SBOM + provenance + minisig for a release (audit #83)
118
118
  verify-release Verify a release artifact set against the pinned allowlist
119
119
  spec-list List SDK schemas, policy docs, and mirror sync status (audit #84)
120
+ ambiguity Score a deep-interview spec's clarity breakdown (Phase 3 OMX)
121
+ guard <subcommand> F-206 progress-guarding loop (start/check/status/stop/list)
122
+ goal-bootstrap F-207 Mike autonomous goal seeding (resume | bootstrap | idle)
120
123
  bench Efficiency benchmarks + auto-fan-out rule (audit #85)
121
124
  team Run the office-manager orchestration agent
122
125
  subagent Run a named agent in read-only plan mode
123
126
  run Run Claude Code once (optionally --bg)
124
127
  rca Analyze a GitHub issue (Claude Code CLI sample)
125
128
  cost <subcommand> Atomic cost gate (SQLite-backed room budget tracker)
126
- claim <subcommand> GitHub-style claim protocol over feature_list.json
127
- task <subcommand> Durable dependency/worktree/path task coordination
128
- control <subcommand> Machine-readable agents/tasks/sessions/messages API
129
+ openkan <subcommand> Install, initialise, and operate the default OpenKan workspace
130
+ task <subcommand> OpenKan-backed durable task lifecycle
131
+ plan <subcommand> OpenKan plans and phases
132
+ goals <subcommand> OpenKan PRDs, goals, and milestones
133
+ claim <subcommand> OpenKan task lease shortcut
134
+ control <subcommand> Machine-readable agents/tasks/plans/goals/sessions/messages API
129
135
  workflow <subcommand> Session-bound autopilot workflow state
130
136
  hook <name> Run a portable Claude Code hook
131
137
  worktree-merge <branch> Merge a feature branch with archive tag (no work lost)
@@ -384,8 +390,6 @@ async function main() {
384
390
  }
385
391
 
386
392
  case 'claim': {
387
- // F-035 MetaHarness — GitHub-style claim protocol over feature_list.json.
388
- // Subcommands: <featureId> | release | handoff | steal | status | list | transition
389
393
  const mod = await importCommand('claim');
390
394
  if (!mod) {
391
395
  console.error(chalk.red(` ✗ Could not load claim command module`));
@@ -417,6 +421,21 @@ async function main() {
417
421
  break;
418
422
  }
419
423
 
424
+ case 'openkan': {
425
+ const mod = await importCommand('openkan');
426
+ if (!mod) { process.exit(EXIT_ERROR); return; }
427
+ await mod.run(cmd, cmdArgs, isHelpRequest);
428
+ break;
429
+ }
430
+
431
+ case 'plan':
432
+ case 'goals': {
433
+ const mod = await importCommand('planning');
434
+ if (!mod) { process.exit(EXIT_ERROR); return; }
435
+ await mod.run(cmd, cmdArgs, isHelpRequest);
436
+ break;
437
+ }
438
+
420
439
  case 'control': {
421
440
  const mod = await importCommand('control');
422
441
  if (!mod) {
@@ -527,6 +546,46 @@ async function main() {
527
546
  break;
528
547
  }
529
548
 
549
+ case 'guard': {
550
+ // F-206 — `/guard` progress-guarding loop. The CLI is read-only
551
+ // with respect to the repo (plan + OpenKan .ok state + checks.jsonl).
552
+ // See cli/commands/guard.mjs for the verdict
553
+ // semantics and side-effect rules.
554
+ const mod = await importCommand('guard');
555
+ if (!mod) {
556
+ console.error(chalk.red(` ✗ Could not load guard command module`));
557
+ process.exit(EXIT_ERROR);
558
+ return;
559
+ }
560
+ dbg('loaded command module:', 'guard');
561
+ const code = await mod.run(cmdArgs);
562
+ if (typeof code === 'number') process.exit(code);
563
+ break;
564
+ }
565
+
566
+ case 'goal-bootstrap': {
567
+ console.error(chalk.yellow(' ! goal-bootstrap is retired; OpenKan PRDs are the default goal system.'));
568
+ const mod = await importCommand('planning');
569
+ if (!mod) { process.exit(EXIT_ERROR); return; }
570
+ await mod.run('goals', cmdArgs, isHelpRequest);
571
+ break;
572
+ }
573
+
574
+ case 'ambiguity': {
575
+ // Phase 3 OMX adoption — score a deep-interview spec's
576
+ // clarity breakdown. Read-only with respect to docs/specs/.
577
+ const mod = await importCommand('ambiguity');
578
+ if (!mod) {
579
+ console.error(chalk.red(` ✗ Could not load ambiguity command module`));
580
+ process.exit(EXIT_ERROR);
581
+ return;
582
+ }
583
+ dbg('loaded command module:', 'ambiguity');
584
+ const code = await mod.run(cmdArgs);
585
+ if (typeof code === 'number') process.exit(code);
586
+ break;
587
+ }
588
+
530
589
  case 'bench': {
531
590
  const mod = await importCommand('bench');
532
591
  if (!mod) {
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli/commands/ambiguity.mjs
4
+ *
5
+ * `bizar ambiguity` — Phase 3 OMX adoption operator surface
6
+ * (docs/plans/2026-09-03-omx-features.md §1).
7
+ *
8
+ * Thin wrapper over the SDK ambiguity module
9
+ * (`packages/sdk/src/ambiguity/score.ts`) that loads a deep-interview
10
+ * spec artifact from `docs/specs/deep-interview-<slug>.md`, extracts
11
+ * the embedded `## Ambiguity breakdown` section, re-computes the score
12
+ * via `computeAmbiguity`, and prints the result.
13
+ *
14
+ * CLI shape (matches the spec-list / bench thin-wrapper pattern):
15
+ *
16
+ * bizar ambiguity default: most recent
17
+ * docs/specs/deep-interview-*.md
18
+ * bizar ambiguity <path> load a specific spec file
19
+ * bizar ambiguity --format json machine-readable output
20
+ * bizar ambiguity --breakdown show per-dimension contributions
21
+ * bizar ambiguity --allow-high permit score > 0.10 (operator override)
22
+ * bizar ambiguity --kind greenfield|brownfield override the inferred kind
23
+ * bizar ambiguity --help usage banner
24
+ *
25
+ * Exit codes:
26
+ * 0 — score ≤ 0.10 (closure threshold per deep-interview skill)
27
+ * 1 — score > 0.10 AND --allow-high not set
28
+ * 2 — usage / input error (bad path, missing breakdown, etc.)
29
+ *
30
+ * DEC-022 compliance: the command is **read-only** with respect to
31
+ * `docs/specs/`. The deep-interview skill owns writes to that
32
+ * directory; this command only reads and reports.
33
+ */
34
+
35
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
36
+ import { basename, isAbsolute, join, resolve } from 'node:path';
37
+
38
+ import {
39
+ AMBIGUITY_SCHEMA_VERSION,
40
+ AMBIGUITY_WEIGHTS,
41
+ computeAmbiguity,
42
+ } from '../../packages/sdk/dist/ambiguity/index.js';
43
+
44
+ const REPO_ROOT = process.cwd();
45
+ const CLOSURE_THRESHOLD = 0.10;
46
+ const DEFAULT_SPECS_DIR = 'docs/specs';
47
+
48
+ /**
49
+ * Default depth profile. The plan calls out `greenfield` (six
50
+ * dimensions) and `brownfield` (five). When the spec does not
51
+ * declare its kind, default to `greenfield` so callers see the
52
+ * strictest dimension set first; operators can override with `--kind`.
53
+ */
54
+ const DEFAULT_KIND = 'greenfield';
55
+
56
+ /**
57
+ * Find the most-recently-modified `deep-interview-*.md` file under
58
+ * `docs/specs/`. Returns an absolute path or `null` when the
59
+ * directory is missing / empty.
60
+ *
61
+ * "Most recent" is defined by mtime — the spec-list / deep-interview
62
+ * skill uses atomic temp-file rename, so mtime order matches
63
+ * closure-order in practice.
64
+ */
65
+ export function findMostRecentSpec({ cwd = REPO_ROOT, specsDir = DEFAULT_SPECS_DIR } = {}) {
66
+ const absDir = isAbsolute(specsDir) ? specsDir : join(cwd, specsDir);
67
+ if (!existsSync(absDir)) return null;
68
+ const entries = readdirSync(absDir)
69
+ .filter((name) => name.startsWith('deep-interview-') && name.endsWith('.md'))
70
+ .map((name) => {
71
+ const abs = join(absDir, name);
72
+ return { name, abs, mtime: statSync(abs).mtimeMs };
73
+ })
74
+ .sort((a, b) => b.mtime - a.mtime);
75
+ return entries.length > 0 ? entries[0].abs : null;
76
+ }
77
+
78
+ /**
79
+ * Resolve the spec path from CLI arguments. Accepts an explicit
80
+ * positional path; otherwise falls back to the most recent
81
+ * `docs/specs/deep-interview-*.md`. Throws a `TypeError` with a
82
+ * clear message when no spec can be located.
83
+ */
84
+ export function resolveSpecPath(args, { cwd = REPO_ROOT } = {}) {
85
+ // Skip flag tokens; the first non-flag positional wins.
86
+ const positional = args.find((a) => !a.startsWith('--'));
87
+ if (positional) {
88
+ const abs = isAbsolute(positional) ? positional : resolve(cwd, positional);
89
+ if (!existsSync(abs)) {
90
+ throw new TypeError(`ambiguity: spec file not found: ${abs}`);
91
+ }
92
+ return abs;
93
+ }
94
+ const recent = findMostRecentSpec({ cwd });
95
+ if (!recent) {
96
+ throw new TypeError(
97
+ `ambiguity: no positional path supplied and no ${DEFAULT_SPECS_DIR}/deep-interview-*.md files found under ${cwd}`,
98
+ );
99
+ }
100
+ return recent;
101
+ }
102
+
103
+ /**
104
+ * Parse the CLI flags. Recognises:
105
+ * --format <json|human> output format (default: human)
106
+ * --breakdown show per-dimension contributions
107
+ * --allow-high permit score > 0.10 (override closure gate)
108
+ * --kind <greenfield|brownfield>
109
+ * override the inferred kind
110
+ * --help, -h show usage banner
111
+ */
112
+ export function parseFlags(args) {
113
+ /** @type {Record<string, string | boolean>} */
114
+ const flags = {};
115
+ for (let i = 0; i < args.length; i++) {
116
+ const arg = args[i];
117
+ if (arg === '--help' || arg === '-h') {
118
+ flags.help = true;
119
+ continue;
120
+ }
121
+ if (!arg.startsWith('--')) continue;
122
+ const key = arg.slice(2);
123
+ const next = args[i + 1];
124
+ if (next !== undefined && !next.startsWith('--')) {
125
+ flags[key] = next;
126
+ i++;
127
+ } else {
128
+ flags[key] = true;
129
+ }
130
+ }
131
+ return flags;
132
+ }
133
+
134
+ /**
135
+ * Parse a markdown spec and extract the `## Ambiguity breakdown`
136
+ * section. Returns:
137
+ * {
138
+ * clarityBreakdown: { [dimension]: number },
139
+ * kind: 'greenfield' | 'brownfield',
140
+ * explicitScore?: number, // only when the spec embeds one
141
+ * sectionText: string, // raw section text for debug
142
+ * }
143
+ *
144
+ * Two embedded formats are supported:
145
+ *
146
+ * 1. JSON code block (preferred — round-trips the SDK object):
147
+ * ```json
148
+ * {
149
+ * "kind": "greenfield",
150
+ * "clarityBreakdown": { "intent": 0.95, "outcome": 0.92, ... },
151
+ * "score": 0.08
152
+ * }
153
+ * ```
154
+ *
155
+ * 2. Markdown table with a parallel dimension / clarity column:
156
+ * | Dimension | Clarity | ... |
157
+ * |---|---|---|
158
+ * | intent | 0.95 | ... |
159
+ *
160
+ * Throws `TypeError` with a precise message on malformed input.
161
+ */
162
+ export function parseAmbiguitySection(specText) {
163
+ const headingRe = /^#{1,6}\s*Ambiguity breakdown\s*$/im;
164
+ const headingMatch = specText.match(headingRe);
165
+ if (!headingMatch) {
166
+ throw new TypeError(
167
+ 'parseAmbiguitySection: no "## Ambiguity breakdown" heading found in spec',
168
+ );
169
+ }
170
+ const sectionStart = headingMatch.index + headingMatch[0].length;
171
+ // Section ends at the next heading of any level.
172
+ const restOfDoc = specText.slice(sectionStart);
173
+ const nextHeading = restOfDoc.match(/^#{1,6}\s/m);
174
+ const sectionText = nextHeading ? restOfDoc.slice(0, nextHeading.index) : restOfDoc;
175
+
176
+ // ── Format 1: JSON code block ─────────────────────────────────────────
177
+ const jsonMatch = sectionText.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
178
+ if (jsonMatch) {
179
+ let parsed;
180
+ try {
181
+ parsed = JSON.parse(jsonMatch[1]);
182
+ } catch (err) {
183
+ throw new TypeError(`parseAmbiguitySection: malformed JSON block: ${err.message}`);
184
+ }
185
+ if (parsed == null || typeof parsed !== 'object') {
186
+ throw new TypeError('parseAmbiguitySection: JSON block must be an object');
187
+ }
188
+ // Accept either {kind, clarityBreakdown} or a full {kind, score, breakdown}.
189
+ // As of AMBIGUITY_SCHEMA_VERSION 2.0.0 the `breakdown` map stores
190
+ // ambiguity contributions `w_i · (1 − clarity_i)` (low-is-good),
191
+ // not clarity contributions. Recover the raw clarity per dimension
192
+ // by inverting: `clarity_i = 1 − contribution / w_i`.
193
+ let clarityBreakdown;
194
+ if (parsed.clarityBreakdown && typeof parsed.clarityBreakdown === 'object') {
195
+ clarityBreakdown = parsed.clarityBreakdown;
196
+ } else if (parsed.breakdown && typeof parsed.breakdown === 'object') {
197
+ clarityBreakdown = {};
198
+ const weights = AMBIGUITY_WEIGHTS[parsed.kind ?? DEFAULT_KIND];
199
+ for (const [dim, contribution] of Object.entries(parsed.breakdown)) {
200
+ const w = weights[dim];
201
+ if (typeof w !== 'number' || w === 0) {
202
+ throw new TypeError(
203
+ `parseAmbiguitySection: cannot invert breakdown for dimension "${dim}" (missing weight)`,
204
+ );
205
+ }
206
+ const recovered = 1 - contribution / w;
207
+ if (!Number.isFinite(recovered) || recovered < 0 || recovered > 1) {
208
+ throw new TypeError(
209
+ `parseAmbiguitySection: breakdown contribution for "${dim}" implies clarity outside [0,1] (got ${recovered})`,
210
+ );
211
+ }
212
+ clarityBreakdown[dim] = recovered;
213
+ }
214
+ } else {
215
+ throw new TypeError(
216
+ 'parseAmbiguitySection: JSON block must contain either "clarityBreakdown" or "breakdown"',
217
+ );
218
+ }
219
+ return {
220
+ clarityBreakdown,
221
+ kind: parsed.kind ?? DEFAULT_KIND,
222
+ explicitScore: typeof parsed.score === 'number' ? parsed.score : undefined,
223
+ sectionText,
224
+ };
225
+ }
226
+
227
+ // ── Format 2: markdown table ──────────────────────────────────────────
228
+ const tableRows = [];
229
+ for (const line of sectionText.split('\n')) {
230
+ const trimmed = line.trim();
231
+ if (!trimmed.startsWith('|')) continue;
232
+ const cells = trimmed
233
+ .replace(/^\|/, '')
234
+ .replace(/\|$/, '')
235
+ .split('|')
236
+ .map((c) => c.trim());
237
+ if (cells.length < 2) continue;
238
+ // Skip the header row and the dashed separator row.
239
+ if (/^-+$/.test(cells[0])) continue;
240
+ const dim = cells[0].toLowerCase();
241
+ const clarityRaw = cells[1];
242
+ if (dim === 'dimension' || dim === '') continue;
243
+ if (!/^[a-z]+$/.test(dim)) continue;
244
+ const clarity = Number(clarityRaw);
245
+ if (!Number.isFinite(clarity)) continue;
246
+ tableRows.push([dim, clarity]);
247
+ }
248
+ if (tableRows.length === 0) {
249
+ throw new TypeError(
250
+ 'parseAmbiguitySection: no JSON block and no markdown table found in section',
251
+ );
252
+ }
253
+ const clarityBreakdown = Object.fromEntries(tableRows);
254
+ // Look for an explicit `**Kind:**` line anywhere in the section.
255
+ const kindMatch = sectionText.match(/\*\*Kind:\*\*\s*`?([a-z]+)`?/i);
256
+ const scoreMatch = sectionText.match(/\*\*AmbiguityScore:\*\*\s*([0-9.]+)/i);
257
+ return {
258
+ clarityBreakdown,
259
+ kind: kindMatch ? kindMatch[1].toLowerCase() : DEFAULT_KIND,
260
+ explicitScore: scoreMatch ? Number(scoreMatch[1]) : undefined,
261
+ sectionText,
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Validate the parsed breakdown against the chosen kind's weight set.
267
+ * Throws `TypeError` when a required dimension is missing or out of
268
+ * range; the SDK would catch this too but we want a CLI-friendly error
269
+ * before the SDK call so the operator sees a precise message.
270
+ */
271
+ export function validateBreakdown(clarityBreakdown, kind) {
272
+ const weights = AMBIGUITY_WEIGHTS[kind];
273
+ if (!weights) {
274
+ throw new TypeError(`validateBreakdown: unknown kind "${kind}"`);
275
+ }
276
+ for (const [dim, weight] of Object.entries(weights)) {
277
+ const value = clarityBreakdown[dim];
278
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {
279
+ throw new TypeError(
280
+ `validateBreakdown: dimension "${dim}" must be a finite number in [0, 1] (got ${String(value)})`,
281
+ );
282
+ }
283
+ if (typeof weight !== 'number' || weight <= 0) {
284
+ throw new TypeError(
285
+ `validateBreakdown: weight for "${dim}" must be a positive number (got ${String(weight)})`,
286
+ );
287
+ }
288
+ }
289
+ }
290
+
291
+ export const USAGE = `
292
+ bizar ambiguity — score a deep-interview spec's clarity breakdown
293
+
294
+ Usage:
295
+ bizar ambiguity [<spec-path>] [--format=json|human] [--breakdown]
296
+ [--allow-high] [--kind greenfield|brownfield]
297
+
298
+ Arguments:
299
+ <spec-path> Path to a deep-interview spec. Defaults to the
300
+ most recent docs/specs/deep-interview-*.md.
301
+
302
+ Flags:
303
+ --format <fmt> Output format. 'human' (default) prints a clean
304
+ aligned table; 'json' emits the raw score object.
305
+ --breakdown Include per-dimension contributions in human
306
+ output and JSON.
307
+ --allow-high Permit AmbiguityScore > 0.10 without exiting
308
+ non-zero. Required for the closure threshold
309
+ gate when a high score is intentional.
310
+ --kind <kind> Override the inferred weight preset
311
+ (greenfield | brownfield). Default: greenfield.
312
+ --help, -h Show this help banner.
313
+
314
+ Exit codes:
315
+ 0 score ≤ 0.10 (closure threshold met)
316
+ 1 score > 0.10 and --allow-high was NOT supplied
317
+ 2 usage error / malformed spec
318
+
319
+ Notes:
320
+ Per DEC-022, this command NEVER writes to docs/specs/. The
321
+ deep-interview skill owns the artifact writes; this command is
322
+ read-only and reports the score.
323
+ `;
324
+
325
+ /** Format a number for the human table — clamp at 4 decimal places. */
326
+ function fmt(value, width = 7) {
327
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 'n/a'.padStart(width);
328
+ return value.toFixed(4).padStart(width);
329
+ }
330
+
331
+ /** Render the score as an aligned human table. */
332
+ export function renderHuman({ specPath, kind, score, breakdown, explicitScore, showBreakdown }) {
333
+ const lines = [];
334
+ lines.push(`bizar ambiguity — ${basename(specPath)}`);
335
+ lines.push('');
336
+ lines.push(` kind: ${kind}`);
337
+ if (typeof explicitScore === 'number') {
338
+ lines.push(` spec-stored score: ${explicitScore.toFixed(4)}`);
339
+ }
340
+ lines.push(` computed score: ${fmt(score)}`);
341
+ lines.push(` closure threshold: ${CLOSURE_THRESHOLD.toFixed(2)} (${score <= CLOSURE_THRESHOLD ? 'PASS' : 'ABOVE'})`);
342
+ if (showBreakdown) {
343
+ lines.push('');
344
+ // Post-v2.0.0: `breakdown[dim]` is the ambiguity contribution
345
+ // `w_i · (1 − clarity_i)` (low-is-good). Recover the clarity for
346
+ // display by inverting: `clarity_i = 1 − breakdown[dim] / w_i`.
347
+ lines.push(` ${'dimension'.padEnd(14)}${'weight'.padStart(8)}${'ambiguity'.padStart(12)}${'clarity'.padStart(10)}`);
348
+ const weights = AMBIGUITY_WEIGHTS[kind];
349
+ for (const dim of Object.keys(weights)) {
350
+ const w = weights[dim];
351
+ const contrib = breakdown[dim] ?? 0;
352
+ const clarity = w > 0 ? 1 - contrib / w : 0;
353
+ lines.push(` ${dim.padEnd(14)}${fmt(w, 8)}${fmt(contrib, 12)}${fmt(clarity, 10)}`);
354
+ }
355
+ }
356
+ lines.push('');
357
+ lines.push(` schema: ${AMBIGUITY_SCHEMA_VERSION}`);
358
+ return lines.join('\n');
359
+ }
360
+
361
+ /** Render the score as JSON (always includes breakdown). */
362
+ export function renderJson({ specPath, kind, score, breakdown, explicitScore, showBreakdown }) {
363
+ const out = {
364
+ spec: specPath,
365
+ kind,
366
+ score,
367
+ schemaVersion: AMBIGUITY_SCHEMA_VERSION,
368
+ closureThreshold: CLOSURE_THRESHOLD,
369
+ pass: score <= CLOSURE_THRESHOLD,
370
+ };
371
+ if (typeof explicitScore === 'number') {
372
+ out.specScore = explicitScore;
373
+ out.scoreMatchesSpec = Math.abs(explicitScore - score) < 1e-3;
374
+ }
375
+ if (showBreakdown) {
376
+ out.breakdown = { ...breakdown };
377
+ } else {
378
+ // Always include a slim breakdown so callers can render a table
379
+ // without recomputing the contributions.
380
+ out.breakdown = { ...breakdown };
381
+ }
382
+ return out;
383
+ }
384
+
385
+ /**
386
+ * Run the ambiguity command.
387
+ *
388
+ * @param {string[]} subargs remaining CLI tokens after `binar ambiguity`
389
+ * @returns {Promise<number>} process exit code (0, 1, or 2)
390
+ */
391
+ export async function run(subargs) {
392
+ const flags = parseFlags(subargs);
393
+ if (flags.help) {
394
+ console.log(USAGE);
395
+ return 0;
396
+ }
397
+
398
+ const format = String(flags.format ?? 'human');
399
+ if (format !== 'human' && format !== 'json') {
400
+ console.error(`ambiguity: --format must be 'human' or 'json' (got '${format}')`);
401
+ return 2;
402
+ }
403
+ const allowHigh = Boolean(flags['allow-high']);
404
+ const showBreakdown = Boolean(flags.breakdown);
405
+ const kindOverride = flags.kind ? String(flags.kind) : null;
406
+
407
+ let specPath;
408
+ try {
409
+ specPath = resolveSpecPath(subargs);
410
+ } catch (err) {
411
+ console.error(`ambiguity: ${err.message}`);
412
+ return 2;
413
+ }
414
+
415
+ let parsed;
416
+ try {
417
+ const specText = readFileSync(specPath, 'utf8');
418
+ parsed = parseAmbiguitySection(specText);
419
+ } catch (err) {
420
+ console.error(`ambiguity: ${err.message}`);
421
+ return 2;
422
+ }
423
+
424
+ const kind = kindOverride ?? parsed.kind;
425
+ try {
426
+ validateBreakdown(parsed.clarityBreakdown, kind);
427
+ } catch (err) {
428
+ console.error(`ambiguity: ${err.message}`);
429
+ return 2;
430
+ }
431
+
432
+ const computed = computeAmbiguity(parsed.clarityBreakdown, kind);
433
+
434
+ if (format === 'json') {
435
+ console.log(
436
+ JSON.stringify(
437
+ renderJson({
438
+ specPath,
439
+ kind,
440
+ score: computed.score,
441
+ breakdown: computed.breakdown,
442
+ explicitScore: parsed.explicitScore,
443
+ showBreakdown,
444
+ }),
445
+ null,
446
+ 2,
447
+ ),
448
+ );
449
+ } else {
450
+ console.log(
451
+ renderHuman({
452
+ specPath,
453
+ kind,
454
+ score: computed.score,
455
+ breakdown: computed.breakdown,
456
+ explicitScore: parsed.explicitScore,
457
+ showBreakdown,
458
+ }),
459
+ );
460
+ }
461
+
462
+ if (computed.score > CLOSURE_THRESHOLD && !allowHigh) {
463
+ console.error(
464
+ `ambiguity: score ${computed.score.toFixed(4)} exceeds closure threshold ${CLOSURE_THRESHOLD.toFixed(2)}; pass --allow-high to override`,
465
+ );
466
+ return 1;
467
+ }
468
+ return 0;
469
+ }
470
+
471
+ if (import.meta.url === `file://${process.argv[1]}`) {
472
+ run(process.argv.slice(2)).then((code) => process.exit(code));
473
+ }