@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.
Files changed (48) hide show
  1. package/cli/bin.mjs +53 -0
  2. package/cli/commands/ambiguity.mjs +473 -0
  3. package/cli/commands/goal-bootstrap.mjs +135 -0
  4. package/cli/commands/guard.mjs +850 -0
  5. package/cli/commands/objective-scheduler.mjs +15 -4
  6. package/cli/commands/spec-list.mjs +122 -4
  7. package/cli/commands/validate.mjs +6 -6
  8. package/cli/commands/workflow.mjs +84 -7
  9. package/cli/core/ultragoal-state.mjs +535 -0
  10. package/config/claude/agents/office-greeter.md +10 -0
  11. package/config/claude/agents/office-manager.md +59 -0
  12. package/config/claude/agents/plan-architect.md +155 -0
  13. package/config/claude/agents/qa-reviewer.md +112 -0
  14. package/config/claude/commands/deep-interview.md +8 -0
  15. package/config/claude/commands/guard.md +57 -0
  16. package/config/claude/commands/ultragoal.md +8 -0
  17. package/config/claude/hooks/goal-bootstrap.mjs +126 -0
  18. package/config/claude/hooks/keyword-router.mjs +3 -1
  19. package/config/claude/hooks/sessionstart-prime.mjs +39 -4
  20. package/config/claude/hooks/worker-suggest.mjs +102 -0
  21. package/config/skills/autopilot/SKILL.md +26 -0
  22. package/config/skills/deep-interview/SKILL.md +166 -0
  23. package/config/skills/goal-bootstrap/SKILL.md +86 -0
  24. package/config/skills/guard/SKILL.md +63 -0
  25. package/config/skills/ralplan/SKILL.md +92 -0
  26. package/config/skills/ralplan/references/pre-mortem.md +37 -0
  27. package/config/skills/ultragoal/SKILL.md +247 -0
  28. package/package.json +1 -1
  29. package/packages/sdk/dist/agent/goal-bootstrap.d.ts +107 -0
  30. package/packages/sdk/dist/agent/goal-bootstrap.js +288 -0
  31. package/packages/sdk/dist/agent/guard.d.ts +103 -0
  32. package/packages/sdk/dist/agent/guard.js +220 -0
  33. package/packages/sdk/dist/ambiguity/index.d.ts +9 -0
  34. package/packages/sdk/dist/ambiguity/index.js +9 -0
  35. package/packages/sdk/dist/ambiguity/score.d.ts +120 -0
  36. package/packages/sdk/dist/ambiguity/score.js +140 -0
  37. package/packages/sdk/dist/autonomy/objective-run.d.ts +12 -3
  38. package/packages/sdk/dist/autonomy/objective-run.js +4 -1
  39. package/packages/sdk/dist/handoff/ralplan.d.ts +86 -0
  40. package/packages/sdk/dist/handoff/ralplan.js +94 -0
  41. package/packages/sdk/dist/index.d.ts +5 -0
  42. package/packages/sdk/dist/index.js +10 -0
  43. package/packages/sdk/dist/mcp/server.js +93 -0
  44. package/packages/sdk/dist/specs/deep-interview.d.ts +112 -0
  45. package/packages/sdk/dist/specs/deep-interview.js +135 -0
  46. package/packages/sdk/dist/version.d.ts +1 -1
  47. package/packages/sdk/dist/version.js +1 -1
  48. 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
+ }