baldart 3.35.0 → 3.35.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ All notable changes to BALDART will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.35.2] - 2026-05-30
9
+
10
+ Resolves the **3.35.1 known follow-up**: a flag introduced in a newer release (e.g. `--on-divergence`, v3.29.0) was rejected by an older global CLI with a cryptic `error: unknown option` **before** the `npx baldart@latest` auto-relaunch could self-upgrade — commander validates options during `program.parse()`, which runs *before* the action handler where the relaunch lives. The `update` command now parses **permissively**, so an unknown flag is captured instead of crashing the parser; the action then transparently relaunches under `@latest` (forwarding the raw argv so the unrecognized flag survives) or, when that isn't possible, emits an **actionable** error instead of commander's cryptic one. **No new `baldart.config.yml` keys** — the schema-propagation rule does not apply.
11
+
12
+ > **Forward-looking by nature.** This cannot retroactively fix CLIs *already* installed at a pre-3.35.2 version — their commander still throws before any code we control runs. The fix rescues flags introduced *after* this release for anyone on 3.35.2+. For older installs the only path remains `npm i -g baldart@latest` (now surfaced by the helpful error + the update notifier).
13
+
14
+ ### Fixed — self-upgrade survives flags newer than the installed CLI
15
+
16
+ - **[bin/baldart.js](bin/baldart.js)**: the `update` command gains `.allowUnknownOption()` + `.allowExcessArguments()` (scoped to `update` only — every other command keeps strict validation). Unknown tokens now land in `command.args` instead of throwing `commander.unknownOption` during parse; they are forwarded to `update(options, command.args)`.
17
+ - **[src/commands/update.js](src/commands/update.js)** (`update`): an early unknown-flag block runs **before** any work. When unknown tokens are present it relaunches under `npx baldart@latest`, forwarding `process.argv.slice(3)` (raw flags, with the leading `update` subcommand token stripped so the child isn't spawned as `update update …`). The unrecognized flag never enters `options`, so raw-argv forwarding is the only faithful path.
18
+ - **[src/commands/update.js](src/commands/update.js)** (`maybeRelaunchUnderLatest`): accepts `extra.rawArgs` (forward verbatim instead of reconstructing from parsed `options`) and `extra.unknown` (skip the interactive "proceed with current CLI" prompt — pointless when the CLI literally can't parse the flag; relaunch whenever a newer version exists). The normal stale-CLI relaunch path is unchanged (still reconstructs known flags, still forwards `--on-divergence` / `--json`).
19
+ - **[src/commands/update.js](src/commands/update.js)** (`failOnUnknownArgs`): new helpful terminal error replacing commander's cryptic message — hedges between "recently-added flag → `npm i -g baldart@latest`" and "typo", and **suppresses the upgrade suggestion** when `BALDART_RELAUNCHED=1` (we are already the `@latest` child, so a typo is the only explanation). `--json`-aware: emits a `usage-error` JSON object on stdout.
20
+ - **[src/commands/update.js](src/commands/update.js)**: `--json` mode is now **armed before** the unknown-flag block so the relaunch notice / helpful error route to STDERR — preserving the single-object STDOUT contract for `update --json --yes --<new-flag>` (the agent/CI scenario this machinery exists for).
21
+
22
+ ## [3.35.1] - 2026-05-30
23
+
24
+ Fixes a **HIGH-severity** bug where `baldart update --reset` could leave a consumer **committed-without-`.framework/`** (broken install, manual recovery only). The reset path wiped `.framework/`, committed the deletion, then handed off to `add()` **programmatically with no repository** — the bin-layer default (`antbald/BALDART`) only applies on the CLI path, so `repo` arrived `undefined`, crashing at `repo.startsWith(...)` ("Cannot read properties of undefined") *after* the wipe was already committed. Three layers now make this unreachable: (1) repo resolved **before** any destructive step, (2) reinstall failures roll back to the pre-reset backup tag, (3) defensive guards + legible errors. **No new `baldart.config.yml` keys** — the schema-propagation rule does not apply.
25
+
26
+ ### Fixed — `update --reset` never leaves the consumer without `.framework/`
27
+
28
+ - **[src/commands/update.js](src/commands/update.js)** (`runReset`): the upstream repo is now resolved via `State.resolveRepo()` **before** the backup tag / `rm -rf` / reset commit, then threaded **explicitly** into `add()` (was `addCmd(undefined, { yes: true })` — the root cause). The stale v3.28.1 comment claiming "undefined → use default from package.json" (which never existed in `add.js`) is corrected.
29
+ - **[src/commands/update.js](src/commands/update.js)**: the reinstall is wrapped in a `try/catch` that, on **any** failure (undefined repo, network, ignored path, missing hooks), auto-runs `git reset --hard <backupTag>` to restore the pre-reset `.framework/` and prints an explicit recovery command. Safe because reset is gated on a clean working tree — the backup tag holds everything but the reset commit.
30
+ - **[src/commands/add.js](src/commands/add.js)**: new `options.throwOnError` makes `add()` **re-throw** instead of `process.exit(1)` at every reachable failure point (download catch, gitignore-still-ignored, hooks-missing post-flight), so a programmatic caller can recover. CLI behavior is unchanged (flag absent → historical exit-1). Also resolves a falsy `repo` from the ledger as a last resort and fails fast **before** download with a recovery hint.
31
+ - **[src/utils/git.js](src/utils/git.js)** (`normalizeRepoUrl`): guards a falsy/non-string `repo` with a legible `Error` ("repository non specificato …") instead of the cryptic `Cannot read properties of undefined (reading 'startsWith')`.
32
+
33
+ ### Changed — shared repo resolver (de-dup)
34
+
35
+ - **[src/utils/state.js](src/utils/state.js)**: new exported `resolveRepo(cwd)` (cascade `state.framework_repo → DEFAULT_REPO`, never `undefined`, preserves a consumer's fork) + exported `DEFAULT_REPO` constant. `recordInstall` now uses the constant.
36
+ - **[src/commands/version.js](src/commands/version.js)**: drops its local `REPO_DEFAULT` duplicate in favor of the shared `State.DEFAULT_REPO`.
37
+
38
+ ### Known follow-up (separate fix)
39
+
40
+ - `--on-divergence` (v3.29.0) is rejected by an **older global CLI** with "unknown option" *before* the `npx baldart@latest` auto-relaunch can self-upgrade (commander validates options first). Tracked separately — workaround: `npm i -g baldart@latest` before using new flags.
41
+
8
42
  ## [3.35.0] - 2026-05-30
9
43
 
10
44
  De-duplicates the review chain in `/new` and adds a **Review Profile Selector** so the per-card pre-merge gate scales its *depth* by risk instead of running the full multi-agent `/codexreview` on every card — including trivial ones. The per-card gate stays **unconditional** (it always runs); only its breadth varies, and the safety-critical Codex adversarial pass + false-positive gate run on every card regardless of profile. Copertura invariata: every line is still code-reviewed once, every doc doc-reviewed once. **No new `baldart.config.yml` keys** — the profile is computed at runtime from signals already present (the Phase 3.7 high-risk-trigger detector + the QA profile), and lean mode is an internal caller→skill file contract, so the schema-propagation rule does not apply.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.35.0
1
+ 3.35.2
package/bin/baldart.js CHANGED
@@ -81,9 +81,24 @@ program
81
81
  .option('--i-know', 'Acknowledges --reset will wipe untracked/ignored files inside .framework/ (required to use --reset --yes).')
82
82
  .option('--on-divergence <strategy>', 'Non-interactive resolution when the consumer has local commits on .framework/ (for agents / CI): "scaffold-overlays" (auto-create overlay skeletons, then stop), "pull" (keep commits + merge — non-destructive), or "abort".')
83
83
  .option('--json', 'Machine-readable output (agents / CI): emit a single JSON result object on stdout, route all human output to stderr. Requires --yes (non-interactive); rejects --reset.')
84
- .action(async (options) => {
84
+ // Parse permissively so an UNKNOWN flag does not crash commander before the
85
+ // CLI self-upgrade can kick in (v3.35.2+). The whole point of `update` is to
86
+ // be the self-healing entrypoint: when a flag introduced in a newer release
87
+ // reaches an older-but-still-this-or-later CLI, commander used to throw
88
+ // `error: unknown option` during parse — i.e. BEFORE the action handler runs,
89
+ // so the auto-relaunch under `npx baldart@latest` (which lives in the action)
90
+ // never got a chance. With both guards set, unknown tokens land in
91
+ // `command.args` instead of throwing; the action forwards them to a newer CLI
92
+ // (or emits a helpful error). Scoped to `update` only — every other command
93
+ // keeps strict validation.
94
+ .allowUnknownOption()
95
+ .allowExcessArguments()
96
+ .action(async (options, command) => {
85
97
  const updateCommand = require('../src/commands/update');
86
- await updateCommand(options);
98
+ // command.args holds any tokens commander could not match to a known
99
+ // option/positional — for `update` (no positionals) that means unknown
100
+ // flags + their values. Forward them so update() can self-upgrade.
101
+ await updateCommand(options, command.args || []);
87
102
  });
88
103
 
89
104
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "3.35.0",
3
+ "version": "3.35.2",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"
@@ -14,6 +14,24 @@ async function add(repo, options) {
14
14
  // rm -rf `.framework/` themselves, so `git.frameworkExists()` is false
15
15
  // here and the destructive prompt is never reached.
16
16
  const nonInteractive = options && options.yes === true;
17
+ // When `add` is invoked programmatically (e.g. `update --reset`) the caller
18
+ // wants failures to bubble up so it can roll back, instead of add()'s default
19
+ // `process.exit(1)` which would kill the parent before it can recover.
20
+ const throwOnError = options && options.throwOnError === true;
21
+
22
+ // Repo resolution safety net. The CLI entrypoint (bin/baldart.js) defaults
23
+ // `repo` to the upstream before calling here, but programmatic callers may
24
+ // pass undefined. Resolve from the consumer's ledger (preserves forks) so
25
+ // we never reach git.addSubtree() with an undefined repo (the crash that
26
+ // broke `update --reset`: "Cannot read properties of undefined").
27
+ if (!repo) repo = State.resolveRepo();
28
+ if (!repo) {
29
+ UI.error('No framework repository to install from.');
30
+ UI.info('Expected from .baldart/state.json → framework_repo. Recover with:');
31
+ UI.info(' npx baldart add <owner/repo> --yes');
32
+ if (throwOnError) throw new Error('add: repository unresolved (state.json framework_repo empty)');
33
+ process.exit(1);
34
+ }
17
35
 
18
36
  try {
19
37
  // Step 1: Verify environment
@@ -114,10 +132,16 @@ async function add(repo, options) {
114
132
  UI.error('.framework/ is STILL ignored after auto-heal — install would fail.');
115
133
  UI.info(`Source: ${heal.recheck.source}:${heal.recheck.line} (${heal.recheck.pattern})`);
116
134
  UI.info('Resolve by removing that rule, then re-run `baldart add`.');
117
- process.exit(1);
135
+ // Deliberate abort — flag it so the local catch propagates instead of
136
+ // downgrading to a warning, and let the single outer catch decide
137
+ // throw (throwOnError) vs process.exit (CLI).
138
+ const e = new Error('.framework/ still ignored after auto-heal');
139
+ e.fatalInstall = true;
140
+ throw e;
118
141
  }
119
142
  }
120
143
  } catch (err) {
144
+ if (err && err.fatalInstall) throw err;
121
145
  UI.warning(`Could not check .gitignore for .framework: ${err.message}`);
122
146
  }
123
147
 
@@ -139,11 +163,12 @@ async function add(repo, options) {
139
163
  ' • No internet connection',
140
164
  ' • Repository not accessible',
141
165
  ` • Invalid repository: ${repo}`,
166
+ ' (resolved from .baldart/state.json → framework_repo, else default)',
142
167
  '',
143
168
  'Solution:',
144
169
  ' • Check your internet connection',
145
- ' • Verify repository exists',
146
- ' • Try again later'
170
+ ' • Verify the repository exists / is reachable',
171
+ ' • Recover with an explicit repo: npx baldart add <owner/repo> --yes'
147
172
  ]);
148
173
  throw error;
149
174
  }
@@ -293,6 +318,13 @@ async function add(repo, options) {
293
318
  : `Hooks missing after install: ${verify.missing.join(', ')}. Install is NOT complete.`
294
319
  );
295
320
  UI.info('Run `npx baldart doctor` to repair, then re-check `npx baldart status`.');
321
+ if (throwOnError) {
322
+ throw new Error(
323
+ verify.malformed
324
+ ? 'settings.json malformed — hooks unverifiable'
325
+ : `hooks missing after install: ${verify.missing.join(', ')}`
326
+ );
327
+ }
296
328
  process.exit(1);
297
329
  }
298
330
 
@@ -339,6 +371,9 @@ async function add(repo, options) {
339
371
 
340
372
  } catch (error) {
341
373
  UI.error(`Installation failed: ${error.message}`);
374
+ // Programmatic callers (update --reset) need the failure to bubble so they
375
+ // can roll back; the CLI entrypoint keeps the historical exit-1 behavior.
376
+ if (throwOnError) throw error;
342
377
  process.exit(1);
343
378
  }
344
379
  }
@@ -36,7 +36,6 @@ const UpdateNotifier = require('../utils/update-notifier');
36
36
  const cliPackageJson = require('../../package.json');
37
37
 
38
38
  const FRAMEWORK_DIR = '.framework';
39
- const REPO_DEFAULT = 'antbald/BALDART';
40
39
  const CONFIG_FILE = 'baldart.config.yml';
41
40
 
42
41
  // ──────────────────────────────────────────────────────────────────────
@@ -214,7 +213,7 @@ async function detectState(cwd, opts = {}) {
214
213
  };
215
214
  }
216
215
 
217
- state.remote = await describeRemote(git, ledger.framework_repo || REPO_DEFAULT, !!opts.offline);
216
+ state.remote = await describeRemote(git, ledger.framework_repo || State.DEFAULT_REPO, !!opts.offline);
218
217
  state.commitsAhead = await commitsAheadOfRemote(git, state.remote);
219
218
  state.workingTreeDirty = await localFrameworkChanges(git);
220
219
 
@@ -360,7 +359,9 @@ function planActions(state) {
360
359
  autoOk: false,
361
360
  run: async () => {
362
361
  const add = require('./add');
363
- await add(REPO_DEFAULT, { branch: 'main' });
362
+ // Fork-aware: resolve from the ledger (preserves a consumer's fork)
363
+ // rather than hardcoding the upstream default.
364
+ await add(State.resolveRepo(state.cwd), { branch: 'main' });
364
365
  },
365
366
  });
366
367
  return actions;
@@ -255,6 +255,15 @@ async function runReset(git, options, autoYes) {
255
255
  }
256
256
  }
257
257
 
258
+ // Resolve the upstream repo BEFORE anything destructive. The reinstall hands
259
+ // off to add() programmatically (no bin-layer default), so an unresolved repo
260
+ // would surface as "Downloading from undefined" / "Cannot read properties of
261
+ // undefined" AFTER .framework/ was already wiped and committed. Resolving here
262
+ // (state.framework_repo → default, never undefined) makes that unreachable and
263
+ // preserves a consumer's fork.
264
+ const repo = State.resolveRepo();
265
+ UI.info(`Reinstall source: ${repo}`);
266
+
258
267
  // Safety gate #3: backup tag before doing anything destructive.
259
268
  const backupTag = await git.createBackupTag();
260
269
  UI.success(`Backup tag created: ${backupTag}`);
@@ -310,12 +319,42 @@ async function runReset(git, options, autoYes) {
310
319
  UI.newline();
311
320
  UI.info('Re-installing framework via `baldart add`…');
312
321
  const addCmd = require('./add');
313
- // `add(repo, options)` — first arg is the repo URL (undefined use default
314
- // from package.json). Pre-v3.28.1 we incorrectly passed `{ yes: true }` as
315
- // the first arg, so `options` was undefined and every interactive prompt
316
- // ("Install framework?", "Configure git aliases?", …) fired despite the
317
- // parent `update --reset --yes --i-know` having already established intent.
318
- await addCmd(undefined, { yes: true });
322
+ // `add(repo, options)` — the repo MUST be threaded explicitly: the bin-layer
323
+ // default ('antbald/BALDART') only applies when add is invoked from the CLI,
324
+ // not on this programmatic path (the v3.28.1 comment claiming "undefined
325
+ // default from package.json" was wrong and caused the undefined-repo crash).
326
+ // `throwOnError` makes add() re-throw instead of process.exit(1), so a failed
327
+ // reinstall lands in the catch below and we can roll back to the backup tag —
328
+ // the consumer is NEVER left committed-without-.framework/.
329
+ try {
330
+ await addCmd(repo, { yes: true, throwOnError: true });
331
+ } catch (err) {
332
+ UI.newline();
333
+ UI.error(`Reinstall failed: ${err.message}`);
334
+ UI.warning('Rolling back to pre-reset state…');
335
+ try {
336
+ await git.git.raw(['reset', '--hard', backupTag]);
337
+ // The clean-tree gate (hasCleanWorkingTree → status.isClean(), which counts
338
+ // untracked) guaranteed ZERO untracked files at backup-tag time, so every
339
+ // untracked file now is partial-install cruft created by the failed add()
340
+ // (symlinks, copied templates) — safe to sweep. `-d` dirs, no `-x` so
341
+ // gitignored paths are left untouched. Non-fatal: .framework/ is already
342
+ // restored by the reset above regardless.
343
+ try {
344
+ await git.git.raw(['clean', '-fd']);
345
+ } catch (cleanErr) {
346
+ UI.warning(`Could not sweep partial-install leftovers (${cleanErr.message}). .framework/ is restored; run \`git clean -fd\` to tidy.`);
347
+ }
348
+ UI.success(`Restored .framework/ from ${backupTag}. Working tree back to pre-reset state.`);
349
+ } catch (rollbackErr) {
350
+ UI.error(`Auto-rollback FAILED: ${rollbackErr.message}`);
351
+ UI.info(`Recover manually: git reset --hard ${backupTag}`);
352
+ }
353
+ UI.newline();
354
+ UI.info('Then retry, or reinstall explicitly:');
355
+ UI.info(` npx baldart add ${repo} --yes`);
356
+ process.exit(1);
357
+ }
319
358
 
320
359
  // Post-restore sanity check — confirm nothing user-owned got clobbered.
321
360
  const stillPresent = {
@@ -367,7 +406,16 @@ function snapshotCustomEntries(dir) {
367
406
  // child inherits stdio (preserves prompts), and BALDART_RELAUNCHED=1 in env
368
407
  // is a loop guard: if npm cache somehow serves an older version, the child
369
408
  // sees the flag and skips its own relaunch check.
370
- async function maybeRelaunchUnderLatest(options) {
409
+ // `extra.rawArgs` when set, forward these verbatim to the child instead of
410
+ // reconstructing flags from the parsed `options`. Used by the unknown-flag path
411
+ // (v3.35.2+): an option this CLI doesn't recognize never lands in `options`, so
412
+ // reconstruction would silently drop it — only the raw argv carries it intact.
413
+ // The caller MUST have already stripped the leading `update` subcommand token.
414
+ // `extra.unknown` — true when triggered by an unknown flag rather than a plain
415
+ // stale-CLI check. In that case the interactive "proceed with current CLI"
416
+ // branch is pointless (the current CLI literally cannot honor the flag), so we
417
+ // relaunch whenever a newer version exists and skip the prompt.
418
+ async function maybeRelaunchUnderLatest(options, extra = {}) {
371
419
  if (process.env.BALDART_RELAUNCHED === '1') return false;
372
420
  if (process.env.BALDART_NO_RELAUNCH === '1') return false;
373
421
 
@@ -385,7 +433,13 @@ async function maybeRelaunchUnderLatest(options) {
385
433
 
386
434
  const autoYes = options.yes === true;
387
435
  let proceed = autoYes;
388
- if (!autoYes) {
436
+ if (extra.unknown) {
437
+ // Unknown flag + a newer CLI exists: relaunch unconditionally. Asking
438
+ // "proceed with current CLI?" makes no sense — it can't parse the flag.
439
+ proceed = true;
440
+ UI.newline();
441
+ UI.warning(`Unknown option for the installed CLI (v${cliVersion}); a newer release (v${info.latest}) may support it.`);
442
+ } else if (!autoYes) {
389
443
  UI.newline();
390
444
  UI.warning(`The installed CLI is older than npm latest: v${cliVersion} → v${info.latest}.`);
391
445
  const choice = await UI.select('How would you like to proceed?', [
@@ -401,17 +455,24 @@ async function maybeRelaunchUnderLatest(options) {
401
455
  }
402
456
  if (!proceed) return false;
403
457
 
404
- // Reconstruct the original argv flags for the child.
405
- const flags = [];
406
- if (options.yes === true) flags.push('--yes');
407
- if (options.autoStash === true && options.yes !== true) flags.push('--auto-stash');
408
- if (options.commit === false) flags.push('--no-commit');
409
- if (options.reset === true) flags.push('--reset');
410
- if (options.iKnow === true) flags.push('--i-know');
411
- // Forward the non-interactive resolution + machine-readable flags too, else
412
- // a stale-CLI self-relaunch would silently drop them (v3.32.0+).
413
- if (options.onDivergence) flags.push('--on-divergence', options.onDivergence);
414
- if (options.json === true) flags.push('--json');
458
+ // Build the child argv. The unknown-flag path forwards the raw argv (already
459
+ // stripped of the `update` token); the normal stale-CLI path reconstructs
460
+ // from parsed options.
461
+ let flags;
462
+ if (Array.isArray(extra.rawArgs)) {
463
+ flags = extra.rawArgs;
464
+ } else {
465
+ flags = [];
466
+ if (options.yes === true) flags.push('--yes');
467
+ if (options.autoStash === true && options.yes !== true) flags.push('--auto-stash');
468
+ if (options.commit === false) flags.push('--no-commit');
469
+ if (options.reset === true) flags.push('--reset');
470
+ if (options.iKnow === true) flags.push('--i-know');
471
+ // Forward the non-interactive resolution + machine-readable flags too, else
472
+ // a stale-CLI self-relaunch would silently drop them (v3.32.0+).
473
+ if (options.onDivergence) flags.push('--on-divergence', options.onDivergence);
474
+ if (options.json === true) flags.push('--json');
475
+ }
415
476
 
416
477
  const spawn = require('child_process').spawnSync;
417
478
  UI.info(`Auto-relaunching via npx baldart@${info.latest}…`);
@@ -427,7 +488,30 @@ async function maybeRelaunchUnderLatest(options) {
427
488
  process.exit(res.status || 0);
428
489
  }
429
490
 
430
- async function update(options = {}) {
491
+ // Helpful terminal error when an unknown flag reached this CLI and a relaunch
492
+ // under a newer version was not possible (already on latest / offline / opted
493
+ // out, or we ARE the relaunched child). Replaces commander's cryptic
494
+ // `error: unknown option` with actionable guidance. (v3.35.2+)
495
+ function failOnUnknownArgs(unknownArgs, options) {
496
+ const tokens = unknownArgs.filter((a) => typeof a === 'string');
497
+ const firstOpt = tokens.find((a) => a.startsWith('-')) || tokens[0] || '(unknown)';
498
+ // In the relaunched child we are already on @latest, so "upgrade" is wrong —
499
+ // an unknown flag here means a genuine typo.
500
+ const alreadyLatest = process.env.BALDART_RELAUNCHED === '1';
501
+ const reason = alreadyLatest
502
+ ? `Unknown option ${firstOpt}. Even the latest CLI does not recognize it — check for a typo.`
503
+ : `Unknown option ${firstOpt}. If it is a recently-added flag, upgrade with \`npm i -g baldart@latest\`; otherwise check for a typo.`;
504
+ if (options.json === true) {
505
+ emitUpdateJson({ ok: false, action: 'usage-error', reason }, 2);
506
+ }
507
+ UI.error(reason);
508
+ if (!alreadyLatest) {
509
+ UI.info(`Then retry: npx baldart update ${tokens.join(' ')}`.trim());
510
+ }
511
+ process.exit(2);
512
+ }
513
+
514
+ async function update(options = {}, unknownArgs = []) {
431
515
  const git = new GitUtils();
432
516
  const symlinks = new SymlinkUtils();
433
517
  const repo = 'antbald/BALDART'; // Default repo
@@ -439,13 +523,35 @@ async function update(options = {}) {
439
523
  const autoStash = autoYes || options.autoStash === true;
440
524
  const confirm = async (msg, def = true) => (autoYes ? def : UI.confirm(msg, def));
441
525
 
526
+ // Arm machine-readable mode FIRST (v3.35.2+). The unknown-flag handling below
527
+ // can emit human lines (relaunch notice / helpful error); they must route to
528
+ // STDERR before anything reaches STDOUT, or a `--json --yes --new-flag` run
529
+ // would pollute the single-object stdout contract.
530
+ JSON_MODE = options.json === true;
531
+ if (JSON_MODE) UI.setJsonMode(true);
532
+
533
+ // Unknown-flag self-upgrade (v3.35.2+). Commander now parses `update`
534
+ // permissively (see bin/baldart.js), so a flag this CLI doesn't know lands
535
+ // in `unknownArgs` instead of crashing the parser. That flag was almost
536
+ // certainly introduced in a newer release — transparently relaunch under
537
+ // `npx baldart@latest`, forwarding the RAW argv (the unknown token never made
538
+ // it into `options`, so only raw argv carries it). If we can't relaunch
539
+ // (already latest / offline / opted out / we ARE the child), fail with an
540
+ // actionable message instead of a cryptic crash.
541
+ if (Array.isArray(unknownArgs) && unknownArgs.length > 0) {
542
+ // process.argv = [node, baldart, 'update', ...flags] — drop the first 3 so
543
+ // the child isn't spawned as `update update …` (it already gets 'update').
544
+ const rawArgs = process.argv.slice(3);
545
+ await maybeRelaunchUnderLatest(options, { rawArgs, unknown: true });
546
+ // Relaunch did not happen → this (latest-or-offline) CLI can't honor it.
547
+ failOnUnknownArgs(unknownArgs, options);
548
+ }
549
+
442
550
  // Machine-readable mode (v3.32.0+). Must be fully non-interactive: --json
443
551
  // requires --yes (so no UI.select/confirm can block) and rejects --reset
444
552
  // (the nuclear path is a deliberate destructive human escape hatch, not an
445
553
  // agent flow — see the autonomous /baldart-update skill, which never resets).
446
- JSON_MODE = options.json === true;
447
554
  if (JSON_MODE) {
448
- UI.setJsonMode(true);
449
555
  if (options.reset === true) {
450
556
  emitUpdateJson({ ok: false, action: 'usage-error',
451
557
  reason: '--json does not support --reset. Reset is an interactive destructive escape hatch; run it manually.' }, 2);
@@ -3,7 +3,6 @@ const UI = require('../utils/ui');
3
3
  const State = require('../utils/state');
4
4
  const UpdateNotifier = require('../utils/update-notifier');
5
5
 
6
- const REPO_DEFAULT = 'antbald/BALDART';
7
6
  const FRAMEWORK_DIR = '.framework';
8
7
 
9
8
  function fmtDate(iso) {
@@ -61,7 +60,7 @@ async function version(opts = {}) {
61
60
  const verbose = opts.verbose === true;
62
61
 
63
62
  // SSOT (v3.25.0+): one query, version-compare authority.
64
- const repo = state.framework_repo || REPO_DEFAULT;
63
+ const repo = state.framework_repo || State.DEFAULT_REPO;
65
64
  let status;
66
65
  if (opts.offline === true) {
67
66
  status = {
package/src/utils/git.js CHANGED
@@ -144,6 +144,17 @@ class GitUtils {
144
144
  // - "https://github.com/owner/repo" -> "https://github.com/owner/repo.git"
145
145
  // - "https://github.com/owner/repo.git" -> unchanged
146
146
 
147
+ // Fail loud and legible instead of "Cannot read properties of undefined
148
+ // (reading 'startsWith')". A falsy repo here means a caller skipped repo
149
+ // resolution (see State.resolveRepo) — surface that, don't crash cryptically.
150
+ if (!repo || typeof repo !== 'string') {
151
+ throw new Error(
152
+ `normalizeRepoUrl: repository non specificato (got ${JSON.stringify(repo)}). ` +
153
+ `Risolvilo via .baldart/state.json → framework_repo o passalo esplicito ` +
154
+ `(es. npx baldart add <owner/repo> --yes).`
155
+ );
156
+ }
157
+
147
158
  if (repo.startsWith('http://') || repo.startsWith('https://')) {
148
159
  return repo.endsWith('.git') ? repo : `${repo}.git`;
149
160
  }
@@ -35,6 +35,13 @@ const STATE_FILE = path.join(STATE_DIR, 'state.json');
35
35
  const STATE_VERSION = 1;
36
36
  const HISTORY_LIMIT = 20;
37
37
 
38
+ // Upstream framework repository. SSOT for the "where do we install from"
39
+ // default — duplicated literals in version.js / doctor.js / recordInstall
40
+ // used to drift independently. Always resolve through `resolveRepo()` so a
41
+ // consumer's fork (recorded in state.framework_repo at install time) is
42
+ // preserved instead of being silently overridden by this constant.
43
+ const DEFAULT_REPO = 'antbald/BALDART';
44
+
38
45
  function defaultState() {
39
46
  return {
40
47
  state_version: STATE_VERSION,
@@ -87,7 +94,7 @@ function recordInstall({ version, repo }, cwd = process.cwd()) {
87
94
  const now = new Date().toISOString();
88
95
  const wasFirstInstall = !state.installed_version;
89
96
  state.installed_version = version;
90
- state.framework_repo = repo || state.framework_repo || 'antbald/BALDART';
97
+ state.framework_repo = repo || state.framework_repo || DEFAULT_REPO;
91
98
  if (wasFirstInstall) state.install_date = now;
92
99
  state.history = appendHistory(state, {
93
100
  event: wasFirstInstall ? 'install' : 'reinstall',
@@ -142,6 +149,20 @@ function recordPush({ from, to, description }, cwd = process.cwd()) {
142
149
  return state;
143
150
  }
144
151
 
152
+ /**
153
+ * Resolve the upstream framework repository to install/update from. Cascade:
154
+ * `state.framework_repo` (recorded at install — preserves a consumer's fork)
155
+ * → DEFAULT_REPO. Never returns undefined, so callers that thread the result
156
+ * into git.addSubtree() / normalizeRepoUrl() can't trip the "undefined repo"
157
+ * crash that broke `update --reset` (it called add() with no repo).
158
+ *
159
+ * `baldart.config.yml` and the subtree's git remote are intentionally NOT
160
+ * consulted — neither stores the repo, so they'd be dead lookups.
161
+ */
162
+ function resolveRepo(cwd = process.cwd()) {
163
+ return load(cwd).framework_repo || DEFAULT_REPO;
164
+ }
165
+
145
166
  /**
146
167
  * Read the framework's VERSION file from .framework/VERSION. Used by version /
147
168
  * status commands to cross-check what the symlinked framework reports vs what
@@ -164,7 +185,9 @@ module.exports = {
164
185
  recordUpdate,
165
186
  recordPush,
166
187
  reconcileInstalledVersion,
188
+ resolveRepo,
167
189
  readFrameworkVersion,
190
+ DEFAULT_REPO,
168
191
  STATE_FILE,
169
192
  STATE_VERSION,
170
193
  };