@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.
- package/AGENTS.md +14 -12
- package/README.md +3 -3
- package/cli/bin.mjs +64 -5
- package/cli/commands/ambiguity.mjs +473 -0
- package/cli/commands/claim.mjs +37 -267
- package/cli/commands/control.mjs +3 -1
- package/cli/commands/goal-bootstrap.mjs +33 -0
- package/cli/commands/guard.mjs +705 -0
- package/cli/commands/install.mjs +14 -11
- package/cli/commands/objective-scheduler.mjs +15 -4
- package/cli/commands/openkan.mjs +72 -0
- package/cli/commands/planning.mjs +26 -0
- package/cli/commands/spec-list.mjs +142 -7
- package/cli/commands/task.mjs +27 -234
- package/cli/commands/validate.mjs +6 -6
- package/cli/commands/workflow-gc.mjs +10 -16
- package/cli/commands/workflow.mjs +84 -7
- package/cli/control-store.mjs +7 -31
- package/cli/core/ultragoal-state.mjs +535 -0
- package/cli/openkan-store.mjs +60 -0
- package/cli/openkan.mjs +320 -0
- package/cli/provision.mjs +34 -1
- package/config/claude/CLAUDE.md +14 -12
- package/config/claude/agents/_shared/AGENT_BASELINE.md +1 -1
- package/config/claude/agents/office-greeter.md +10 -0
- package/config/claude/agents/office-manager.md +59 -0
- package/config/claude/agents/plan-architect.md +155 -0
- package/config/claude/agents/qa-reviewer.md +112 -0
- package/config/claude/commands/bizar.md +2 -2
- package/config/claude/commands/deep-interview.md +8 -0
- package/config/claude/commands/guard.md +57 -0
- package/config/claude/commands/plow-through.md +1 -1
- package/config/claude/commands/sprint.md +4 -5
- package/config/claude/commands/team.md +1 -1
- package/config/claude/commands/ultragoal.md +8 -0
- package/config/claude/hooks/goal-bootstrap.mjs +49 -0
- package/config/claude/hooks/keyword-router.mjs +3 -1
- package/config/claude/hooks/path-ownership-guard.mjs +14 -25
- package/config/claude/hooks/precompact-priorities.sh +2 -2
- package/config/claude/hooks/sessionend-recall.mjs +13 -17
- package/config/claude/hooks/sessionstart-prime.mjs +38 -51
- package/config/claude/hooks/thinking-route.mjs +1 -1
- package/config/claude/hooks/verify-deliverables.mjs +5 -11
- package/config/claude/hooks/worker-suggest.mjs +102 -0
- package/config/skills/autopilot/SKILL.md +27 -1
- package/config/skills/bizar/SKILL.md +4 -4
- package/config/skills/deep-interview/SKILL.md +166 -0
- package/config/skills/goal-bootstrap/SKILL.md +18 -0
- package/config/skills/guard/SKILL.md +16 -0
- package/config/skills/harness-engineering/SKILL.md +6 -6
- package/config/skills/openkan/SKILL.md +34 -0
- package/config/skills/ralplan/SKILL.md +92 -0
- package/config/skills/ralplan/references/pre-mortem.md +37 -0
- package/config/skills/ultragoal/SKILL.md +247 -0
- package/package.json +1 -1
- package/packages/sdk/dist/agent/goal-bootstrap.d.ts +157 -0
- package/packages/sdk/dist/agent/goal-bootstrap.js +437 -0
- package/packages/sdk/dist/agent/guard.d.ts +103 -0
- package/packages/sdk/dist/agent/guard.js +220 -0
- package/packages/sdk/dist/ambiguity/index.d.ts +9 -0
- package/packages/sdk/dist/ambiguity/index.js +9 -0
- package/packages/sdk/dist/ambiguity/score.d.ts +120 -0
- package/packages/sdk/dist/ambiguity/score.js +140 -0
- package/packages/sdk/dist/autonomy/objective-run.d.ts +12 -3
- package/packages/sdk/dist/autonomy/objective-run.js +4 -1
- package/packages/sdk/dist/handoff/ralplan.d.ts +86 -0
- package/packages/sdk/dist/handoff/ralplan.js +94 -0
- package/packages/sdk/dist/index.d.ts +5 -0
- package/packages/sdk/dist/index.js +10 -0
- package/packages/sdk/dist/mcp/server.js +93 -0
- package/packages/sdk/dist/specs/deep-interview.d.ts +112 -0
- package/packages/sdk/dist/specs/deep-interview.js +135 -0
- package/packages/sdk/dist/version.d.ts +1 -1
- package/packages/sdk/dist/version.js +1 -1
- package/packages/sdk/package.json +1 -1
- package/cli/feature-list-bridge.mjs +0 -400
- package/cli/progress-parser.mjs +0 -251
- 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.
|
|
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,
|
|
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
|
-
|
|
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**
|
|
26
|
-
- **MUST**
|
|
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
|
|
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
|
|
180
|
-
|
|
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
|
-
-
|
|
188
|
-
- `feature_list.json` —
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
- [
|
|
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
|
-
|
|
127
|
-
task <subcommand>
|
|
128
|
-
|
|
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
|
+
}
|