@polderlabs/bizar 10.23.23 → 10.24.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/cli/bin.mjs +53 -0
- package/cli/commands/ambiguity.mjs +473 -0
- package/cli/commands/goal-bootstrap.mjs +135 -0
- package/cli/commands/guard.mjs +850 -0
- package/cli/commands/objective-scheduler.mjs +15 -4
- package/cli/commands/spec-list.mjs +122 -4
- package/cli/commands/validate.mjs +6 -6
- package/cli/commands/workflow.mjs +84 -7
- package/cli/core/ultragoal-state.mjs +535 -0
- 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/deep-interview.md +8 -0
- package/config/claude/commands/guard.md +57 -0
- package/config/claude/commands/ultragoal.md +8 -0
- package/config/claude/hooks/goal-bootstrap.mjs +126 -0
- package/config/claude/hooks/keyword-router.mjs +3 -1
- package/config/claude/hooks/sessionstart-prime.mjs +39 -4
- package/config/claude/hooks/worker-suggest.mjs +102 -0
- package/config/skills/autopilot/SKILL.md +26 -0
- package/config/skills/deep-interview/SKILL.md +166 -0
- package/config/skills/goal-bootstrap/SKILL.md +86 -0
- package/config/skills/guard/SKILL.md +63 -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 +107 -0
- package/packages/sdk/dist/agent/goal-bootstrap.js +288 -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/bin.mjs
CHANGED
|
@@ -117,6 +117,9 @@ 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
|
|
@@ -527,6 +530,56 @@ async function main() {
|
|
|
527
530
|
break;
|
|
528
531
|
}
|
|
529
532
|
|
|
533
|
+
case 'guard': {
|
|
534
|
+
// F-206 — `/guard` progress-guarding loop. The CLI is read-only
|
|
535
|
+
// with respect to the repo (plan + PROGRESS.md + feature_list +
|
|
536
|
+
// checks.jsonl). See cli/commands/guard.mjs for the verdict
|
|
537
|
+
// semantics and side-effect rules.
|
|
538
|
+
const mod = await importCommand('guard');
|
|
539
|
+
if (!mod) {
|
|
540
|
+
console.error(chalk.red(` ✗ Could not load guard command module`));
|
|
541
|
+
process.exit(EXIT_ERROR);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
dbg('loaded command module:', 'guard');
|
|
545
|
+
const code = await mod.run(cmdArgs);
|
|
546
|
+
if (typeof code === 'number') process.exit(code);
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case 'goal-bootstrap': {
|
|
551
|
+
// F-207 — Mike autonomous goal / ultragoal bootstrap. Single-action
|
|
552
|
+
// CLI: read feature_list.json + docs/specs/ and emit a
|
|
553
|
+
// discriminated-union verdict (resume | bootstrap | idle). The
|
|
554
|
+
// bootstrap action writes exactly one durable artifact (the
|
|
555
|
+
// charter) under docs/specs/. See cli/commands/goal-bootstrap.mjs.
|
|
556
|
+
const mod = await importCommand('goal-bootstrap');
|
|
557
|
+
if (!mod) {
|
|
558
|
+
console.error(chalk.red(` ✗ Could not load goal-bootstrap command module`));
|
|
559
|
+
process.exit(EXIT_ERROR);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
dbg('loaded command module:', 'goal-bootstrap');
|
|
563
|
+
const code = await mod.run(cmdArgs);
|
|
564
|
+
if (typeof code === 'number') process.exit(code);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
case 'ambiguity': {
|
|
569
|
+
// Phase 3 OMX adoption — score a deep-interview spec's
|
|
570
|
+
// clarity breakdown. Read-only with respect to docs/specs/.
|
|
571
|
+
const mod = await importCommand('ambiguity');
|
|
572
|
+
if (!mod) {
|
|
573
|
+
console.error(chalk.red(` ✗ Could not load ambiguity command module`));
|
|
574
|
+
process.exit(EXIT_ERROR);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
dbg('loaded command module:', 'ambiguity');
|
|
578
|
+
const code = await mod.run(cmdArgs);
|
|
579
|
+
if (typeof code === 'number') process.exit(code);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
|
|
530
583
|
case 'bench': {
|
|
531
584
|
const mod = await importCommand('bench');
|
|
532
585
|
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
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli/commands/goal-bootstrap.mjs
|
|
4
|
+
*
|
|
5
|
+
* `bizar goal-bootstrap` — F-207 Mike autonomous goal / ultragoal
|
|
6
|
+
* bootstrap. Reads `feature_list.json` + `docs/specs/ultragoal-*.md`
|
|
7
|
+
* and runs the SDK helper. On `bootstrap` it writes the new charter
|
|
8
|
+
* to disk; on `resume` and `idle` it touches nothing.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* bizar goal-bootstrap [--feature-list <path>] [--specs-dir <path>] [--json]
|
|
12
|
+
*
|
|
13
|
+
* Defaults:
|
|
14
|
+
* --feature-list ./feature_list.json
|
|
15
|
+
* --specs-dir ./docs/specs
|
|
16
|
+
*
|
|
17
|
+
* Output is JSON on stdout. Exit codes:
|
|
18
|
+
* 0 resume | bootstrap | idle (with optional warning)
|
|
19
|
+
* 1 malformed input / I/O failure (GoalBootstrapError surfaces here)
|
|
20
|
+
*
|
|
21
|
+
* The CLI MUST NOT auto-commit, auto-push, or auto-publish. Only the
|
|
22
|
+
* `bootstrap` action writes a single durable artifact (the charter)
|
|
23
|
+
* under `docs/specs/`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
27
|
+
import {
|
|
28
|
+
bootstrapGoalFromFile,
|
|
29
|
+
GoalBootstrapError,
|
|
30
|
+
} from '../../packages/sdk/dist/agent/goal-bootstrap.js';
|
|
31
|
+
|
|
32
|
+
const EXIT_OK = 0;
|
|
33
|
+
const EXIT_ERROR = 1;
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
console.log(`
|
|
37
|
+
bizar goal-bootstrap — F-207 Mike autonomous goal seeding
|
|
38
|
+
Usage:
|
|
39
|
+
bizar goal-bootstrap [--feature-list <path>] [--specs-dir <path>] [--json]
|
|
40
|
+
What it does:
|
|
41
|
+
On every SessionStart, Mike reads feature_list.json and any
|
|
42
|
+
existing docs/specs/ultragoal-*.md charter. If a charter exists
|
|
43
|
+
whose feature is non-passing, the bootstrap RESUMES that goal
|
|
44
|
+
and writes nothing. Otherwise, if any feature is not_started,
|
|
45
|
+
the bootstrap PICKS the smallest F-ID and writes a fresh
|
|
46
|
+
aggregate-mode charter to docs/specs/ultragoal-<id>.md. If
|
|
47
|
+
neither condition holds, the bootstrap returns IDLE.
|
|
48
|
+
Defaults:
|
|
49
|
+
--feature-list ./feature_list.json
|
|
50
|
+
--specs-dir ./docs/specs (resolved to absolute path)
|
|
51
|
+
Output:
|
|
52
|
+
JSON on stdout:
|
|
53
|
+
{ action: "resume", id, source }
|
|
54
|
+
{ action: "bootstrap", id, charterPath }
|
|
55
|
+
{ action: "idle" }
|
|
56
|
+
Plus an optional "warning" field when the feature_list.json is
|
|
57
|
+
missing or malformed (verdict falls back to idle).
|
|
58
|
+
Exit codes:
|
|
59
|
+
0 resume | bootstrap | idle
|
|
60
|
+
1 GoalBootstrapError (malformed features or write failure)
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseFlags(argv) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (let i = 0; i < argv.length; i++) {
|
|
67
|
+
const a = argv[i];
|
|
68
|
+
if (typeof a !== 'string') continue;
|
|
69
|
+
if (!a.startsWith('--')) {
|
|
70
|
+
if (!out._positional) out._positional = [];
|
|
71
|
+
out._positional.push(a);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const key = a.slice(2);
|
|
75
|
+
const next = argv[i + 1];
|
|
76
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
77
|
+
out[key] = next;
|
|
78
|
+
i += 1;
|
|
79
|
+
} else {
|
|
80
|
+
out[key] = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveFeatureList(featureListRaw) {
|
|
87
|
+
return typeof featureListRaw === 'string' && featureListRaw.length > 0
|
|
88
|
+
? resolve(process.cwd(), featureListRaw)
|
|
89
|
+
: resolve(process.cwd(), 'feature_list.json');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveSpecsDir(specsDirRaw, featureListPath) {
|
|
93
|
+
if (typeof specsDirRaw === 'string' && specsDirRaw.length > 0) {
|
|
94
|
+
return isAbsolute(specsDirRaw)
|
|
95
|
+
? specsDirRaw
|
|
96
|
+
: resolve(dirname(featureListPath), specsDirRaw);
|
|
97
|
+
}
|
|
98
|
+
return resolve(dirname(featureListPath), 'docs', 'specs');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function run(subargs) {
|
|
102
|
+
const args = Array.isArray(subargs) ? subargs : [];
|
|
103
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
104
|
+
usage();
|
|
105
|
+
return EXIT_OK;
|
|
106
|
+
}
|
|
107
|
+
const flags = parseFlags(args);
|
|
108
|
+
const featureListPath = resolveFeatureList(flags['feature-list']);
|
|
109
|
+
const specsDir = resolveSpecsDir(
|
|
110
|
+
typeof flags['specs-dir'] === 'string' ? flags['specs-dir'] : null,
|
|
111
|
+
featureListPath,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
result = bootstrapGoalFromFile({ featureListPath, specsDir });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof GoalBootstrapError) {
|
|
119
|
+
console.error(`goal-bootstrap: ${err.message}`);
|
|
120
|
+
return EXIT_ERROR;
|
|
121
|
+
}
|
|
122
|
+
console.error(`goal-bootstrap: unexpected error: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
return EXIT_ERROR;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const payload = result.warning
|
|
127
|
+
? { ...result.verdict, warning: result.warning }
|
|
128
|
+
: result.verdict;
|
|
129
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
130
|
+
return EXIT_OK;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
134
|
+
run(process.argv.slice(2)).then((code) => process.exit(code ?? 0));
|
|
135
|
+
}
|