baldart 3.30.0 → 3.32.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/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ 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.32.0] - 2026-05-30
9
+
10
+ Makes the CLI **agent-native at the output layer**: `npx baldart version --json` and `npx baldart update --json --yes` now emit a single **machine-readable JSON object** on stdout (schemas `baldart.version/1` / `baldart.update/1`), with every human line routed to stderr. This closes the last fragile seam in the autonomous-update path — the `/baldart-update` skill previously had to **regex-scrape human-readable prose** (the v3.26.0 wording change had already forced a dual-format tolerant parser in Step 0). An agent now reads a field instead of matching a sentence. Borrowed from the [CLI-Anything](https://github.com/HKUDS/CLI-Anything) philosophy (`--json` on every command) — but BALDART is already a headless CLI, so this is a thin, hand-rolled output layer, not a generated wrapper. **No new `baldart.config.yml` keys** — `--json` is a CLI flag, not a config key, so the schema-propagation rule does not apply.
11
+
12
+ ### Added — `--json` structured output
13
+
14
+ - **[src/commands/version.js](src/commands/version.js)**: `--json` emits `{schema:"baldart.version/1", installed, installed_version, remote_version, aligned, offline, fetched, uncommitted_files, commits_ahead/behind, cli_version, cli_latest, cli_update_available, repository, last_update_date, last_pushed_version, last_push_date}` — exactly the facts the human box shows. Framework-not-installed and error paths emit `{installed:false|null, error, …}` so the agent always gets parseable stdout.
15
+ - **[src/commands/update.js](src/commands/update.js)**: `--json` emits a single `{schema:"baldart.update/1", ok, action, …}` result at **every** terminal point through one `emitUpdateJson` chokepoint (all-or-nothing: a missed exit would leave stdout empty). `ok:true` ⟺ the framework is now at the remote version (`action` `updated` / `already-current`); every refusal/failure is `ok:false` with `action` (`divergence-refused`, `divergence-scaffolded`, `aborted`, `failed`, `framework-missing`, `usage-error`), `reason`, and a `next_command` the human can re-run. `--json` **requires `--yes`** (so no prompt can block) and **rejects `--reset`** (the nuclear path stays an interactive human escape hatch).
16
+ - **[src/utils/ui.js](src/utils/ui.js)**: new `UI.setJsonMode(on)` redirects every `console.log` the UI helpers emit to **stderr** (reversible), keeping stdout clean for the JSON. `ora` spinners already write to stderr, so they need no change. The JSON itself is written with `process.stdout.write`, never `console.log`, so it survives the redirect.
17
+
18
+ ### Changed — autonomous `/baldart-update` reads JSON, not prose
19
+
20
+ - **[framework/.claude/skills/baldart-update/SKILL.md](framework/.claude/skills/baldart-update/SKILL.md)**: Step 0 now **prefers** `version --json` (field reads) and **falls back** to the dual-format text parser when the global CLI predates v3.32.0 (`--json` unknown / non-JSON stdout) — the same "new skill + old global CLI" transition window the text parser already guards. Autonomous mode runs `update --json --yes`, parses the single result object (`ok` / `action` / `installed_after` / `backup_tag` / `next_command`), and keeps the **exit code as the safety backstop**. The post-flight assert cross-checks `installed_after` against on-disk `.framework/VERSION` and the Step 0 `remote_version`. `last_verified` v3.31.0 → v3.32.0; added a **JSON-OUTPUT NOTE** recording that `--json` is a flag, not a config key.
21
+
22
+ ### Fixed — self-relaunch flag forwarding
23
+
24
+ - **[src/commands/update.js](src/commands/update.js)** `maybeRelaunchUnderLatest`: the stale-CLI self-relaunch now forwards `--json` **and** `--on-divergence <strategy>` to the `npx baldart@latest` child. The latter was a pre-existing latent drop (an agent's non-interactive divergence strategy would silently vanish on relaunch); both are fixed together.
25
+ - **[bin/baldart.js](bin/baldart.js)**: the CLI self-update notifier's stdout `announce` is suppressed under `--json` so it cannot pollute the single JSON object.
26
+
27
+ ## [3.31.0] - 2026-05-30
28
+
29
+ Gives `/baldart-update` an **autonomous mode** so agents and scheduled routines can bring the framework up-to-date with **zero human interaction** — closing the gap that the skill, as shipped, was *purely* human-in-the-loop (it replicated every CLI decision point as a chat confirmation, which an unattended agent cannot answer). The new mode is **auto-detected** from a positive automation signal and keeps every one of the CLI's safety gates: it drives `npx baldart update --yes` and **stops hard** (never forces a merge or reset) the moment the CLI refuses on custom divergence or a conflict — exactly the calls a human should own. **No new `baldart.config.yml` keys** — `BALDART_AUTONOMOUS` is a runtime env var, not a config flag, so the schema-propagation rule does not apply.
30
+
31
+ ### Added — autonomous (unattended) mode for `/baldart-update`
32
+
33
+ - **[framework/.claude/skills/baldart-update/SKILL.md](framework/.claude/skills/baldart-update/SKILL.md)**: new **Mode detection** first step (`printenv | grep -qE '^(BALDART_AUTONOMOUS|CI|GITHUB_ACTIONS)='`) branches the skill into INTERACTIVE (a human is present → existing chat-confirmation flow, unchanged) or AUTONOMOUS (no human → new flow). Detection is **positive-signal-only**: absence of every signal defaults to INTERACTIVE so a human never silently loses control. Documents *why* the TTY test (`[ -t 0 ]`) cannot be used — Claude's Bash tool never has a TTY on stdin, even with a human present, so it can't tell the two situations apart.
34
+ - **`## Autonomous mode`** section: pre-flight (shared Step 0 + CLI version drift gate) → `npx baldart update --yes` (bare, `timeout: 600000`) → exit-code interpretation → shared post-flight assert → structured log-friendly report. **Hard guardrail**: on a non-zero exit (custom-overlay-able / real-custom divergence, subtree-pull conflict, stale-CLI halt) the skill STOPS and reports the exact flag a human would re-run with — it **never** retries with `--on-divergence pull|scaffold-overlays` or `--reset` to force past a refusal. A refusal is a *correct* outcome that needs a human.
35
+
36
+ ### Added — automation marker injected by the routine-adapters
37
+
38
+ - **[src/utils/routine-adapters/github-actions.js](src/utils/routine-adapters/github-actions.js)**: the generated workflow's run step now sets `BALDART_AUTONOMOUS: '1'` in its `env:` block (alongside `ANTHROPIC_API_KEY`), inherited by the `claude` process and every child Bash it spawns.
39
+ - **[src/utils/routine-adapters/cron.js](src/utils/routine-adapters/cron.js)**: the generated shell wrapper `export`s `BALDART_AUTONOMOUS=1` before invoking `claude`.
40
+ - **[src/utils/routine-adapters/claude-code-cloud.js](src/utils/routine-adapters/claude-code-cloud.js)**: the generated routine config now carries `env: { BALDART_AUTONOMOUS: '1' }` (best-effort — relies on the RemoteTrigger surfacing `env` into the run environment; github-actions / cron inject it directly).
41
+
42
+ ### Changed — drift-check counter + honest framing
43
+
44
+ - **[framework/.claude/skills/baldart-update/SKILL.md](framework/.claude/skills/baldart-update/SKILL.md)**: `last_verified` v3.26.0 → v3.31.0; added an **AUTONOMY NOTE** recording that `BALDART_AUTONOMOUS` is a runtime env var (not a config key) so the schema-propagation rule does not apply. Hard rules gain a rule #0 (run mode detection first; default-on-absence is INTERACTIVE); rule #2 now qualifies the chat-replication mandate as INTERACTIVE-only. The closing "both keep the user in the loop" claim is corrected to reflect that autonomous mode drops confirmations while keeping the safety gates.
45
+
8
46
  ## [3.30.0] - 2026-05-29
9
47
 
10
48
  Hardens the **overlay** system — the layer that turns generic framework skills into project-specific knowledge. An audit surfaced a structural asymmetry: agent/command overlays are *compile-time merged* (the overlay is materially fused into the generated file → guaranteed delivery), but skill overlays use *runtime concatenation* (a line of prose tells the agent to read `.baldart/overlays/<skill>.md` → best-effort, nothing verifies it loaded). The most important overlays for knowledge transfer had the weakest guarantee. This release ships the high-certainty authoring/DX fixes now and adds **observation-only telemetry** to gather real data on whether skill overlays actually load — before deciding whether a stronger enforcement mechanism is worth building. **No new `baldart.config.yml` keys** — the telemetry hook is always-on (like `framework-edit-gate` / `agent-discovery-gate`), so the schema-propagation rule does not apply.
package/README.md CHANGED
@@ -456,6 +456,14 @@ Update framework to latest version.
456
456
  - Updates via Git subtree
457
457
  - Verifies symlinks
458
458
 
459
+ **For agents / CI (v3.32.0+):** `npx baldart update --json --yes` emits a single
460
+ machine-readable result object on stdout (`{schema:"baldart.update/1", ok,
461
+ action, installed_after, backup_tag, next_command, …}`) with all human output on
462
+ stderr — no prose to scrape. `ok:true` ⟺ the framework is now at the remote
463
+ version. `--json` requires `--yes` and rejects `--reset`. Pair with
464
+ `--on-divergence pull|scaffold-overlays|abort` for non-interactive divergence
465
+ resolution.
466
+
459
467
  ### `npx baldart` (no args) / `doctor` (v3.2.0+)
460
468
 
461
469
  Smart diagnostic that detects the install state and proposes the next sensible
@@ -474,7 +482,10 @@ npx baldart doctor # explicit alias (same behaviour)
474
482
 
475
483
  Show installed framework version, install date, drift from remote (commits
476
484
  ahead/behind), uncommitted-files count in `.framework/`, and last-push info.
477
- Use `--offline` to skip the upstream fetch when offline.
485
+ Use `--offline` to skip the upstream fetch when offline. Use `--json`
486
+ (v3.32.0+) for a machine-readable object (`schema:"baldart.version/1"`) on
487
+ stdout — agents read `installed_version` / `remote_version` / `aligned`
488
+ instead of scraping the human box.
478
489
 
479
490
  ### `npx baldart push` / `/baldart-push` (v3.1.0+)
480
491
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.30.0
1
+ 3.32.0
package/bin/baldart.js CHANGED
@@ -43,10 +43,16 @@ const CLI_VERSION = resolveVersion();
43
43
  // command on a network call.
44
44
  const rawArgsForNotifier = process.argv.slice(2);
45
45
  const offlineForNotifier = rawArgsForNotifier.includes('--offline');
46
- updateNotifier.announceAndRefresh({
47
- currentVersion: CLI_VERSION,
48
- offline: offlineForNotifier,
49
- });
46
+ // In --json mode STDOUT must carry exactly one JSON object — the notifier hint
47
+ // would pollute it. Suppress the announce entirely (the refresh is harmless but
48
+ // the announce prints to stdout). (v3.32.0+)
49
+ const jsonForNotifier = rawArgsForNotifier.includes('--json');
50
+ if (!jsonForNotifier) {
51
+ updateNotifier.announceAndRefresh({
52
+ currentVersion: CLI_VERSION,
53
+ offline: offlineForNotifier,
54
+ });
55
+ }
50
56
 
51
57
  const program = new Command();
52
58
 
@@ -73,6 +79,7 @@ program
73
79
  .option('--reset', 'Nuclear option: rm -rf .framework/ + fresh install, preserving baldart.config.yml + .baldart/ + custom .claude/{agents,skills,commands}/. Requires clean working tree.')
74
80
  .option('--i-know', 'Acknowledges --reset will wipe untracked/ignored files inside .framework/ (required to use --reset --yes).')
75
81
  .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".')
82
+ .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.')
76
83
  .action(async (options) => {
77
84
  const updateCommand = require('../src/commands/update');
78
85
  await updateCommand(options);
@@ -90,9 +97,10 @@ program
90
97
  .command('version')
91
98
  .description('Show installed framework version + drift from remote + last-push info')
92
99
  .option('--offline', 'Skip the upstream fetch (no remote drift report)')
100
+ .option('--json', 'Machine-readable output: emit a single JSON object on stdout, route human output to stderr (for agents / CI).')
93
101
  .action(async (options) => {
94
102
  const versionCommand = require('../src/commands/version');
95
- await versionCommand({ offline: !!options.offline });
103
+ await versionCommand({ offline: !!options.offline, json: !!options.json });
96
104
  });
97
105
 
98
106
  program
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: baldart-update
3
- description: "Guidance for updating BALDART framework to the latest version in a consumer repo. Use when the user says /baldart-update, 'aggiorna baldart', 'aggiorna framework', 'update del framework', 'pull baldart latest', or wants to bring the framework up-to-date. Replicates the native CLI's user-decision points (preview diff, working-tree stash) as chat-side confirmations, lets the CLI auto-resolve mechanical noise (hook backfill, subtree-merge commits, schema migration), and asserts post-flight that .framework/VERSION actually changed. For non-update drift, defers to `npx baldart` (smart doctor)."
3
+ description: "Guidance for updating BALDART framework to the latest version in a consumer repo. Use when the user says /baldart-update, 'aggiorna baldart', 'aggiorna framework', 'update del framework', 'pull baldart latest', or wants to bring the framework up-to-date. Two modes, auto-detected: INTERACTIVE (a human is present) replicates the native CLI's user-decision points (preview diff, working-tree stash) as chat-side confirmations; AUTONOMOUS (unattended — CI / scheduled routine, signalled by BALDART_AUTONOMOUS / CI / GITHUB_ACTIONS env) runs `npx baldart update --yes` with zero prompts and hard-STOP guardrails (never forces a merge or reset on custom divergence / conflicts). Both let the CLI auto-resolve mechanical noise (hook backfill, subtree-merge commits, schema migration) and assert post-flight that .framework/VERSION actually changed. For non-update drift, defers to `npx baldart` (smart doctor)."
4
4
  contamination_scan: skip
5
5
  ---
6
6
 
@@ -12,10 +12,30 @@ pull_request). When any of the numbers below changes against the actual
12
12
  sources, a warning is surfaced in the workflow run. The decision whether
13
13
  this skill needs an update is left to a human reviewer.
14
14
 
15
- update_js_prompts: 7
15
+ update_js_prompts: 6
16
16
  hook_registry_entries: 4
17
17
  config_template_top_level_keys: features,git,identity,lsp,paths,stack,tools,version
18
- last_verified: v3.26.0
18
+ last_verified: v3.32.0
19
+ -->
20
+
21
+ <!--
22
+ JSON-OUTPUT NOTE (v3.32.0)
23
+ `npx baldart version --json` and `npx baldart update --json --yes` emit a single
24
+ machine-readable object on stdout (schemas `baldart.version/1` /
25
+ `baldart.update/1`), with all human output on stderr. The skill PREFERS these
26
+ over regex-scraping prose, but keeps the text parser as a fallback for the
27
+ "new skill + old global CLI" transition window. `--json` is a CLI FLAG, not a
28
+ baldart.config.yml key — the schema-change propagation rule does NOT apply.
29
+ -->
30
+
31
+ <!--
32
+ AUTONOMY NOTE (v3.31.0)
33
+ `BALDART_AUTONOMOUS` is a RUNTIME ENV VAR, not a baldart.config.yml key — the
34
+ schema-change propagation rule does NOT apply (no template/configure/update
35
+ detector change). It is injected by the routine-adapters (github-actions: env
36
+ block; cron: `export`; claude-code-cloud: best-effort `env` field in the run
37
+ config) so scheduled routines run unattended. Any orchestrator that wants a
38
+ child agent to update autonomously can set it the same way.
19
39
  -->
20
40
 
21
41
 
@@ -42,7 +62,31 @@ CLI would normally ask in the terminal — so the CLI can be driven with
42
62
  ╚══════════════════════════════════════════════════════════════════════╝
43
63
  ```
44
64
 
45
- ## Execution modelIMPORTANT
65
+ ## Mode detectionFIRST STEP, BLOCKING (v3.31.0+)
66
+
67
+ Before anything else, decide which of two modes you are in. This gates the
68
+ entire rest of the skill.
69
+
70
+ ```bash
71
+ # A single positive signal of automation means "no human is in the loop".
72
+ # Absence of ALL signals means "human present" → stay interactive.
73
+ printenv | grep -qE '^(BALDART_AUTONOMOUS|CI|GITHUB_ACTIONS)=' && echo AUTONOMOUS || echo INTERACTIVE
74
+ ```
75
+
76
+ - **`AUTONOMOUS`** → no human can answer chat prompts (scheduled routine, CI,
77
+ or an orchestrator that exported the marker). Follow **`## Autonomous mode`**
78
+ below. Do NOT ask any chat questions — there is no one to read them.
79
+ - **`INTERACTIVE`** → a human is present. Follow the interactive flow
80
+ (Execution model + Workflow steps), which replicates every CLI decision
81
+ point as a chat confirmation.
82
+
83
+ > **Why not `[ -t 0 ]`?** When Claude runs a command via the Bash tool, stdin
84
+ > is never a TTY — not even with a human present. The TTY test cannot tell the
85
+ > two situations apart. Only a *positive* automation signal (env var) is
86
+ > reliable, and we default to INTERACTIVE on its absence so a human never
87
+ > silently loses control.
88
+
89
+ ## Execution model (INTERACTIVE mode) — IMPORTANT
46
90
 
47
91
  This is an **agent-driven skill with full chat-side replication** of every
48
92
  decision point the CLI would pose interactively. The flow is:
@@ -64,6 +108,79 @@ pure-guidance (the user runs the CLI themselves). Here the user has asked
64
108
  for an agent that drives the update, so the skill drives it — but it
65
109
  replicates every confirmation so nothing is silently auto-accepted.
66
110
 
111
+ ## Autonomous mode (AUTONOMOUS) — unattended, zero prompts (v3.31.0+)
112
+
113
+ When mode detection returned `AUTONOMOUS`, there is no human to confirm
114
+ anything. The skill drives the update end-to-end with NO chat questions, and
115
+ leans entirely on the CLI's own safety gates — which already **refuse** to do
116
+ anything destructive under bare `--yes`.
117
+
118
+ **The contract: autonomy over mechanical work, hard STOP on anything that
119
+ touches the user's custom content.**
120
+
121
+ ### Autonomous workflow
122
+
123
+ 1. **Pre-flight (read-only)** — same as interactive Step 0: run
124
+ `npx baldart version`, parse the `Installed:` and `Remote:` lines with the
125
+ tolerant matcher. Apply the same **CLI version drift gate** (a too-old
126
+ global CLI crossing the v3.27.0 payload boundary → STOP and report; do not
127
+ launch).
128
+ - Aligned / already up-to-date → report "already current", exit success.
129
+ 2. **Launch directly — no preview/stash confirmation. Prefer `--json`
130
+ (v3.32.0+) so the result is a parseable object, not prose:**
131
+ ```bash
132
+ npx baldart update --json --yes
133
+ ```
134
+ Use the Bash tool with `timeout: 600000`. `--yes` implies `--auto-stash`,
135
+ so a dirty tree is handled (stashed + re-applied) without asking. `--json`
136
+ requires `--yes` (it refuses interactively) and routes all human output to
137
+ stderr — **stdout carries exactly one JSON object**.
138
+ **Do NOT add `--on-divergence pull` or `--reset`.** Bare `--yes` is the
139
+ safe ceiling: the CLI auto-resolves only mechanical noise and *refuses*
140
+ (exits non-zero) on real custom divergence.
141
+
142
+ **Fallback:** if the CLI predates v3.32.0 it rejects `--json` (unknown
143
+ option, non-zero exit, empty/garbled stdout). Re-run without it
144
+ (`npx baldart update --yes`) and fall back to exit-code + stdout-grep
145
+ interpretation. Never let a missing `--json` block the update.
146
+ 3. **Interpret the result — JSON first, exit code as the safety backstop:**
147
+ - With `--json`, parse the single stdout object. `ok === true` (action
148
+ `updated` or `already-current`) → success, go to post-flight using
149
+ `installed_after` / `backup_tag`. `ok === false` → the CLI refused or
150
+ failed; read `action` (`divergence-refused`, `divergence-scaffolded`,
151
+ `failed`, `framework-missing`, `usage-error`), `reason`, and
152
+ `next_command`. Report them verbatim.
153
+ - The **exit code is still the contract** regardless of `--json`:
154
+ - **Exit 0** → update applied (or already aligned). Go to post-flight.
155
+ - **Exit non-zero** → the CLI hit a wall it will not cross unattended:
156
+ custom-overlay-able / real-custom divergence, a subtree-pull conflict, or
157
+ a stale-CLI halt. **STOP. Do NOT retry with `--on-divergence` or
158
+ `--reset`.** Surface the JSON `reason` + `next_command` (or, in fallback
159
+ mode, the verbatim stderr) so the human who reads the routine log knows
160
+ exactly what needs a hands-on decision. Forcing a merge or a reset
161
+ unattended is precisely the silent destruction this mode forbids.
162
+ 4. **Post-flight assert (MANDATORY)** — identical to interactive Step 4:
163
+ `cat .framework/VERSION`, compare to the pre-update version. If the CLI
164
+ exited 0 but VERSION did not change (and stdout is not "Already up to
165
+ date"), **FAIL LOUD** — do not report success. With `--json` this is a
166
+ direct check: the result's `installed_after` MUST equal both
167
+ `.framework/VERSION` and the Step 0 `remote_version`; any mismatch (e.g.
168
+ `ok:true` but `installed_after` ≠ on-disk VERSION) is the same FAIL LOUD.
169
+ 5. **Report** — emit a structured, log-friendly summary (no questions):
170
+ version delta, backup tag + rollback command, any agent-layout migration
171
+ (v3.27.0+), and — on a non-zero exit — the verbatim CLI stderr plus the
172
+ exact flag a human would re-run with (`--on-divergence pull` /
173
+ `scaffold-overlays` / `--reset --yes --i-know`). The skill names the
174
+ options; it never picks one autonomously.
175
+
176
+ ### What autonomous mode must NEVER do
177
+
178
+ - Ask a chat question (no one is there; a blocked prompt hangs the routine).
179
+ - Pass `--on-divergence pull|scaffold-overlays` or `--reset` to "get past" a
180
+ refusal. A refusal is a *correct* outcome that needs a human.
181
+ - Report success when the post-flight assert fails.
182
+ - Auto-resolve a stash-pop or merge conflict with `--hard` / `--no-verify`.
183
+
67
184
  ## When to use
68
185
 
69
186
  - The user invokes `/baldart-update`.
@@ -100,13 +217,20 @@ for the protocol.
100
217
 
101
218
  ## Hard rules
102
219
 
220
+ 0. **MUST run mode detection FIRST** (the `printenv | grep` probe above) and
221
+ branch accordingly. Defaulting to AUTONOMOUS when no signal is present —
222
+ or to INTERACTIVE when a signal IS present — are both wrong. The default on
223
+ *absence of signal* is INTERACTIVE (human keeps control).
103
224
  1. **MUST NOT re-implement the logic of `update.js`.** Same rule as
104
225
  `/baldart-push`: the CLI is the source of truth. The skill only
105
226
  replicates prompts and parses output.
106
- 2. **MUST replicate in chat all 5 decision points** before passing `--yes`:
107
- preview, working-tree stash, hooks drift, schema config drift,
108
- auto-commit of BALDART-managed files. Silencing one is dishonestthe
109
- user thinks they confirmed everything, but they didn't see that prompt.
227
+ 2. **(INTERACTIVE mode) MUST replicate in chat the 2 remaining decision
228
+ points** before passing `--yes`: preview and working-tree stash (the other
229
+ three hooks drift, schema config drift, BALDART-managed auto-commit are
230
+ auto-resolved silently by the CLI since v3.26.0). Silencing a real prompt
231
+ is dishonest — the user thinks they confirmed everything, but they didn't
232
+ see that prompt. **(AUTONOMOUS mode) MUST NOT ask any chat question** — see
233
+ `## Autonomous mode`; a blocked prompt hangs the routine.
110
234
  3. **MUST pass `timeout: 600000` (10 minutes)** to the `Bash` call running
111
235
  `npx baldart update --yes`. The default 120s is not enough for subtree
112
236
  pull + symlink reconcile + post-update wizard + hooks register +
@@ -150,7 +274,11 @@ post-flight assert:
150
274
  > preview. The repo URL is exposed by `npx baldart version` in the
151
275
  > `Repository:` line.
152
276
 
153
- ## Workflow
277
+ ## Workflow (INTERACTIVE mode)
278
+
279
+ > In AUTONOMOUS mode follow `## Autonomous mode` instead. Step 0 (pre-flight
280
+ > probe + CLI version drift gate) and Step 4 (post-flight assert) are shared
281
+ > by both modes; Steps 1–3 (chat confirmations) are interactive-only.
154
282
 
155
283
  ### Step 0 — Pre-flight (read-only)
156
284
 
@@ -161,7 +289,36 @@ post-flight assert:
161
289
  npx baldart version
162
290
  ```
163
291
 
164
- **Parser tollerante** the output format changed in v3.26.0 (version-compare
292
+ #### Preferred: structured `--json` (v3.32.0+) — NO regex
293
+
294
+ As of v3.32.0 the CLI emits a machine-readable object — read fields instead
295
+ of scraping prose:
296
+
297
+ ```bash
298
+ npx baldart version --json 2>/dev/null
299
+ # → {"schema":"baldart.version/1","installed":true,"installed_version":"3.32.0",
300
+ # "remote_version":"3.33.0","aligned":false,"offline":false,"fetched":true,
301
+ # "uncommitted_files":0,"cli_version":"3.32.0","cli_latest":null,
302
+ # "cli_update_available":false,"repository":"https://github.com/antbald/BALDART",...}
303
+ ```
304
+
305
+ Decision logic on the JSON:
306
+ - `installed === false` → framework not installed → STOP, instruct `npx baldart add`.
307
+ - `aligned === true` → already up-to-date (`installed_version`). STOP.
308
+ - `aligned === false` AND `remote_version !== "unknown"` → continue (save
309
+ `installed_version` for the post-flight assert, `remote_version` as target).
310
+ - `offline === true` / `fetched === false` → ask the user how to proceed.
311
+ - Still apply the **CLI version drift gate** below using `cli_version` /
312
+ `cli_latest` (same semver logic).
313
+
314
+ **Fallback (BLOCKING for the transition window):** if `version --json` errors
315
+ (non-zero exit, unknown-option message, or stdout that is NOT valid JSON), the
316
+ global CLI predates v3.32.0. Do **not** abort — fall back to the **Parser
317
+ tollerante** below, which scrapes the human-readable output. This is the same
318
+ "new skill + old global CLI" case the dual-format text parser already exists
319
+ for. Never remove the text parser.
320
+
321
+ **Parser tollerante (fallback)** — the output format changed in v3.26.0 (version-compare
165
322
  authority instead of subtree-merge commit count). The skill must accept BOTH
166
323
  the new and legacy formats so it works during the transition window where
167
324
  the consumer has the new skill but an older global CLI.
@@ -391,5 +548,9 @@ spine of BALDART maintenance:
391
548
  - `/baldart-update` — pull from upstream.
392
549
  - `/baldart-push` — push to upstream.
393
550
 
394
- Both wrap the CLI without re-implementing it; both keep the user in the
395
- loop. The CLI remains the source of truth.
551
+ Both wrap the CLI without re-implementing it; the CLI remains the source of
552
+ truth. `/baldart-push` always keeps the user in the loop. `/baldart-update`
553
+ does too **when a human is present** (interactive mode); when run unattended
554
+ (autonomous mode — CI / scheduled routine) it drops the confirmations but
555
+ keeps the CLI's safety gates, stopping hard rather than forcing any
556
+ destructive resolution a human would otherwise own.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "3.30.0",
3
+ "version": "3.32.0",
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"
@@ -28,6 +28,20 @@ function readEnabledTools(cwd = process.cwd()) {
28
28
  // agent-overlay feedback that prompted this).
29
29
  const DIVERGENCE_STRATEGIES = ['abort', 'pull', 'scaffold-overlays'];
30
30
 
31
+ // Machine-readable result channel (v3.32.0+). When --json is active, STDOUT
32
+ // must carry exactly ONE JSON object and nothing else — UI.setJsonMode() has
33
+ // already redirected every human line to STDERR. Every terminal point in the
34
+ // standard flow routes through `emitUpdateJson` so the contract holds: one
35
+ // object, then exit. `ok: true` means "framework is now at the remote version"
36
+ // (action `updated` or `already-current`); everything else is `ok: false`.
37
+ let JSON_MODE = false;
38
+ function emitUpdateJson(obj, exitCode) {
39
+ if (JSON_MODE) {
40
+ process.stdout.write(JSON.stringify({ schema: 'baldart.update/1', ...obj }) + '\n');
41
+ }
42
+ process.exit(exitCode);
43
+ }
44
+
31
45
  // Map a touched framework path to an `overlay scaffold` target string.
32
46
  // .framework/framework/.claude/agents/coder.md → 'agents/coder'
33
47
  // .framework/framework/.claude/commands/check.md → 'commands/check'
@@ -394,6 +408,10 @@ async function maybeRelaunchUnderLatest(options) {
394
408
  if (options.commit === false) flags.push('--no-commit');
395
409
  if (options.reset === true) flags.push('--reset');
396
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');
397
415
 
398
416
  const spawn = require('child_process').spawnSync;
399
417
  UI.info(`Auto-relaunching via npx baldart@${info.latest}…`);
@@ -421,10 +439,31 @@ async function update(options = {}) {
421
439
  const autoStash = autoYes || options.autoStash === true;
422
440
  const confirm = async (msg, def = true) => (autoYes ? def : UI.confirm(msg, def));
423
441
 
442
+ // Machine-readable mode (v3.32.0+). Must be fully non-interactive: --json
443
+ // requires --yes (so no UI.select/confirm can block) and rejects --reset
444
+ // (the nuclear path is a deliberate destructive human escape hatch, not an
445
+ // agent flow — see the autonomous /baldart-update skill, which never resets).
446
+ JSON_MODE = options.json === true;
447
+ if (JSON_MODE) {
448
+ UI.setJsonMode(true);
449
+ if (options.reset === true) {
450
+ emitUpdateJson({ ok: false, action: 'usage-error',
451
+ reason: '--json does not support --reset. Reset is an interactive destructive escape hatch; run it manually.' }, 2);
452
+ }
453
+ if (!autoYes) {
454
+ emitUpdateJson({ ok: false, action: 'usage-error',
455
+ reason: '--json requires --yes (non-interactive). Re-run: npx baldart update --json --yes [--on-divergence pull|scaffold-overlays]' }, 2);
456
+ }
457
+ }
458
+
424
459
  // Non-interactive divergence strategy (since v3.29.0). Validated up-front so
425
460
  // a typo fails fast instead of mid-flow after the network fetch.
426
461
  const divStrategy = options.onDivergence;
427
462
  if (divStrategy !== undefined && !DIVERGENCE_STRATEGIES.includes(divStrategy)) {
463
+ if (JSON_MODE) {
464
+ emitUpdateJson({ ok: false, action: 'usage-error',
465
+ reason: `Invalid --on-divergence "${divStrategy}". Use one of: ${DIVERGENCE_STRATEGIES.join(', ')}.` }, 1);
466
+ }
428
467
  UI.error(`Invalid --on-divergence "${divStrategy}". Use one of: ${DIVERGENCE_STRATEGIES.join(', ')}.`);
429
468
  process.exit(1);
430
469
  }
@@ -443,7 +482,9 @@ async function update(options = {}) {
443
482
  'Solution:',
444
483
  ' Install first: npx baldart add'
445
484
  ]);
446
- process.exit(1);
485
+ emitUpdateJson({ ok: false, action: 'framework-missing',
486
+ reason: 'Framework directory .framework/ not found.',
487
+ next_command: 'npx baldart add' }, 1);
447
488
  }
448
489
 
449
490
  // v3.26.0+ NUCLEAR OPTION: --reset rips out .framework/ and reinstalls
@@ -480,7 +521,8 @@ async function update(options = {}) {
480
521
  UI.error('.framework/ is STILL ignored after auto-heal — likely a parent-dir .gitignore outside this repo.');
481
522
  UI.info(`Source: ${heal.recheck.source}:${heal.recheck.line} (${heal.recheck.pattern})`);
482
523
  UI.info('Resolve by removing that rule, then re-run `baldart update`.');
483
- process.exit(1);
524
+ emitUpdateJson({ ok: false, action: 'failed',
525
+ reason: `.framework/ is still ignored after auto-heal (${heal.recheck.source}:${heal.recheck.line}). Remove that .gitignore rule and re-run.` }, 1);
484
526
  }
485
527
  }
486
528
  } catch (err) {
@@ -512,19 +554,24 @@ async function update(options = {}) {
512
554
  ' • Check internet connection',
513
555
  ' • Try again later'
514
556
  ]);
515
- process.exit(1);
557
+ emitUpdateJson({ ok: false, action: 'connect-failed',
558
+ reason: 'Cannot connect to repository (network / access).' }, 1);
516
559
  }
517
560
 
518
561
  if (!status.fetched || !status.hasFetchHead) {
519
562
  UI.error('Could not fetch upstream — try again with network access.');
520
- process.exit(1);
563
+ emitUpdateJson({ ok: false, action: 'fetch-failed',
564
+ reason: 'Could not fetch upstream — try again with network access.' }, 1);
521
565
  }
522
566
 
523
567
  // VERSION-compare authority. Subtree-merge commit count is noise; never
524
568
  // use behind > 0 as the up-to-date signal (root cause of v3.24.x bug).
525
569
  if (status.isAligned) {
526
570
  UI.success(`Already up to date! (v${status.installedVersion})`);
527
- process.exit(0);
571
+ emitUpdateJson({ ok: true, action: 'already-current',
572
+ installed_before: status.installedVersion,
573
+ installed_after: status.installedVersion,
574
+ remote_version: status.remoteVersion }, 0);
528
575
  }
529
576
 
530
577
  UI.warning(`Update available: v${status.installedVersion} → v${status.remoteVersion}`);
@@ -556,9 +603,18 @@ async function update(options = {}) {
556
603
  'Then complete the update with `npx baldart update --reset --yes --i-know` — this discards the now-redundant `.framework/` edits and reinstalls clean; your `.baldart/overlays/` are preserved and re-applied.',
557
604
  'Alternatively, push the edits upstream with `/baldart-push` if they are generic improvements.',
558
605
  ], 'cyan');
559
- process.exit(0);
606
+ emitUpdateJson({ ok: false, action: 'divergence-scaffolded',
607
+ divergence_class: status.divergenceClass,
608
+ installed_before: status.installedVersion, remote_version: status.remoteVersion,
609
+ reason: 'Scaffolded overlay skeleton(s); fill them in, then re-run.',
610
+ next_command: 'npx baldart update --reset --yes --i-know' }, 0);
611
+ }
612
+ if (divStrategy === 'abort') {
613
+ UI.info('Update cancelled (--on-divergence abort).');
614
+ emitUpdateJson({ ok: false, action: 'aborted',
615
+ divergence_class: status.divergenceClass,
616
+ reason: 'Update cancelled (--on-divergence abort).' }, 0);
560
617
  }
561
- if (divStrategy === 'abort') { UI.info('Update cancelled (--on-divergence abort).'); process.exit(0); }
562
618
  if (divStrategy === 'pull') {
563
619
  UI.info('Keeping the custom commits (--on-divergence pull). The subtree pull will attempt a merge.');
564
620
  // fall through
@@ -572,7 +628,11 @@ async function update(options = {}) {
572
628
  '--on-divergence pull → keep the commits and merge the update (non-destructive)',
573
629
  '--reset --yes --i-know → discard .framework/ edits and reinstall clean (destructive)',
574
630
  ], 'cyan');
575
- process.exit(1);
631
+ emitUpdateJson({ ok: false, action: 'divergence-refused',
632
+ divergence_class: status.divergenceClass,
633
+ installed_before: status.installedVersion, remote_version: status.remoteVersion,
634
+ reason: 'Custom edits present — refusing to resolve them unattended under --yes.',
635
+ next_command: 'npx baldart update --json --yes --on-divergence scaffold-overlays' }, 1);
576
636
  } else {
577
637
  const choice = await UI.select('How would you like to proceed?', [
578
638
  { name: 'Scaffold overlay skeleton(s) now (recommended — then fill them in & re-run)', value: 'overlay' },
@@ -605,16 +665,29 @@ async function update(options = {}) {
605
665
  if (overlayCommits.length === 0) {
606
666
  UI.error('No overlay-able commits to scaffold — every custom commit touches non-overlayable paths (src/, CHANGELOG, …).');
607
667
  UI.info('Use `/baldart-push` to contribute them upstream, or `--on-divergence pull` to merge.');
608
- process.exit(1);
668
+ emitUpdateJson({ ok: false, action: 'divergence-refused',
669
+ divergence_class: status.divergenceClass,
670
+ installed_before: status.installedVersion, remote_version: status.remoteVersion,
671
+ reason: 'No overlay-able commits to scaffold — every custom commit touches non-overlayable paths.',
672
+ next_command: 'npx baldart update --json --yes --on-divergence pull' }, 1);
609
673
  }
610
674
  UI.newline();
611
675
  scaffoldOverlaysForCommits(overlayCommits);
612
676
  UI.newline();
613
677
  UI.warning('Scaffolded overlays for the overlay-able commits, but custom-other commit(s) remain — NOT pulling (would be destructive to those edits).');
614
678
  UI.info('Resolve the remaining commits (push upstream with `/baldart-push`, or revert), then re-run `npx baldart update`.');
615
- process.exit(0);
679
+ emitUpdateJson({ ok: false, action: 'divergence-scaffolded',
680
+ divergence_class: status.divergenceClass,
681
+ installed_before: status.installedVersion, remote_version: status.remoteVersion,
682
+ reason: 'Scaffolded overlay-able subset; custom-other commit(s) still need handling before pulling.',
683
+ next_command: 'npx baldart update --json --yes' }, 0);
684
+ }
685
+ if (divStrategy === 'abort') {
686
+ UI.info('Update cancelled (--on-divergence abort).');
687
+ emitUpdateJson({ ok: false, action: 'aborted',
688
+ divergence_class: status.divergenceClass,
689
+ reason: 'Update cancelled (--on-divergence abort).' }, 0);
616
690
  }
617
- if (divStrategy === 'abort') { UI.info('Update cancelled (--on-divergence abort).'); process.exit(0); }
618
691
  if (divStrategy === 'pull') {
619
692
  UI.info('Pulling over the custom commits (--on-divergence pull). A merge commit will be created — resolve any conflicts that surface.');
620
693
  // fall through
@@ -627,7 +700,11 @@ async function update(options = {}) {
627
700
  '--on-divergence abort → do nothing',
628
701
  ], 'cyan');
629
702
  UI.info('Best practice: contribute generic improvements upstream first with `/baldart-push`.');
630
- process.exit(1);
703
+ emitUpdateJson({ ok: false, action: 'divergence-refused',
704
+ divergence_class: status.divergenceClass,
705
+ installed_before: status.installedVersion, remote_version: status.remoteVersion,
706
+ reason: 'Custom commits present — refusing to pull over them unattended under --yes.',
707
+ next_command: 'npx baldart update --json --yes --on-divergence pull' }, 1);
631
708
  } else {
632
709
  const choice = await UI.select('How would you like to proceed?', [
633
710
  { name: 'Push these commits upstream first (recommended — `/baldart-push`)', value: 'push' },
@@ -669,6 +746,7 @@ async function update(options = {}) {
669
746
  // "fatal: working tree has modifications. Cannot add." Offer to stash
670
747
  // and re-apply after the update, so the user doesn't have to abort mid-flow.
671
748
  let stashRef = null;
749
+ let stashConflict = false; // surfaced in the --json result (v3.32.0+)
672
750
  const isClean = await git.hasCleanWorkingTree();
673
751
  if (!isClean) {
674
752
  UI.newline();
@@ -703,7 +781,8 @@ async function update(options = {}) {
703
781
  UI.success(`Stashed working tree as "${stashRef}".`);
704
782
  } catch (err) {
705
783
  UI.error(`Could not stash: ${err.message}`);
706
- process.exit(1);
784
+ emitUpdateJson({ ok: false, action: 'failed',
785
+ reason: `Could not stash working tree before update: ${err.message}` }, 1);
707
786
  }
708
787
  }
709
788
 
@@ -756,6 +835,7 @@ async function update(options = {}) {
756
835
  await git.git.stash(['pop']);
757
836
  UI.success(`Re-applied pre-update stash "${stashRef}".`);
758
837
  } catch (err) {
838
+ stashConflict = true;
759
839
  UI.newline();
760
840
  UI.warning(`Could not auto-re-apply stash "${stashRef}" — conflicts with the framework update.`);
761
841
  UI.box('STASH CONFLICT', [
@@ -1006,7 +1086,15 @@ async function update(options = {}) {
1006
1086
  UI.newline();
1007
1087
  UI.success('Framework updated successfully!');
1008
1088
 
1089
+ emitUpdateJson({ ok: true, action: 'updated',
1090
+ installed_before: currentVersion, installed_after: newVersion,
1091
+ remote_version: status.remoteVersion, backup_tag: backupTag,
1092
+ stash_conflict: stashConflict }, 0);
1093
+
1009
1094
  } catch (error) {
1095
+ if (JSON_MODE) {
1096
+ emitUpdateJson({ ok: false, action: 'failed', reason: error.message }, 1);
1097
+ }
1010
1098
  UI.error(`Update failed: ${error.message}`);
1011
1099
  process.exit(1);
1012
1100
  }
@@ -30,12 +30,25 @@ async function describeDrift(git, status) {
30
30
  };
31
31
  }
32
32
 
33
+ // Emit a structured JSON object on stdout (v3.32.0+). Used by agents / CI to
34
+ // avoid scraping the human-readable box with fragile regex. Schema is
35
+ // versioned (`baldart.version/1`) so consumers can detect breaking changes.
36
+ function emitVersionJson(obj) {
37
+ process.stdout.write(JSON.stringify({ schema: 'baldart.version/1', ...obj }) + '\n');
38
+ }
39
+
33
40
  async function version(opts = {}) {
34
41
  const git = new GitUtils();
42
+ const jsonMode = opts.json === true;
43
+ if (jsonMode) UI.setJsonMode(true);
35
44
 
36
45
  try {
37
46
  const exists = await git.frameworkExists();
38
47
  if (!exists) {
48
+ if (jsonMode) {
49
+ emitVersionJson({ installed: false, error: 'framework_not_installed' });
50
+ process.exit(0);
51
+ }
39
52
  UI.warning('Framework not installed');
40
53
  UI.info('Install with: npx baldart add');
41
54
  process.exit(0);
@@ -63,6 +76,31 @@ async function version(opts = {}) {
63
76
  }
64
77
  const drift = await describeDrift(git, status);
65
78
 
79
+ // Machine-readable path (v3.32.0+) — emit the structured object and stop.
80
+ // Mirrors exactly the facts the human box shows below, so the autonomous
81
+ // /baldart-update skill can read fields instead of regex-scraping prose.
82
+ if (jsonMode) {
83
+ emitVersionJson({
84
+ installed: true,
85
+ installed_version: frameworkVersion,
86
+ remote_version: drift.remoteVersion, // string version, or 'unknown'
87
+ aligned: drift.isAligned,
88
+ offline: opts.offline === true || !drift.hasFetchHead,
89
+ fetched: drift.hasFetchHead,
90
+ uncommitted_files: drift.uncommittedFiles,
91
+ commits_ahead: drift.ahead,
92
+ commits_behind: drift.behind,
93
+ cli_version: cliVersion,
94
+ cli_latest: cliUpdate ? cliUpdate.latest : null,
95
+ cli_update_available: !!cliUpdate,
96
+ repository: `https://github.com/${repo}`,
97
+ last_update_date: state.last_update_date || null,
98
+ last_pushed_version: state.last_pushed_version || null,
99
+ last_push_date: state.last_push_date || null,
100
+ });
101
+ return;
102
+ }
103
+
66
104
  // Messaging based on `isAligned` (not commit count, which is subtree noise).
67
105
  let remoteLine;
68
106
  if (!drift.hasFetchHead) {
@@ -103,6 +141,10 @@ async function version(opts = {}) {
103
141
  UI.box('BALDART VERSION', lines.filter((l) => l !== ''));
104
142
  UI.newline();
105
143
  } catch (error) {
144
+ if (jsonMode) {
145
+ emitVersionJson({ installed: null, error: 'version_failed', reason: error.message });
146
+ process.exit(1);
147
+ }
106
148
  UI.error(`Failed to get version: ${error.message}`);
107
149
  process.exit(1);
108
150
  }
@@ -43,6 +43,13 @@ class ClaudeCodeCloudAdapter {
43
43
  prompt: spec.prompt,
44
44
  output: spec.output,
45
45
  backend: this.name,
46
+ // Marks this as an unattended (no-human) run. Autonomy-aware skills
47
+ // (e.g. /baldart-update) auto-detect this and drop their chat
48
+ // confirmations, with safe guardrails (STOP on custom divergence /
49
+ // conflicts, never forces a destructive merge). Best-effort for this
50
+ // backend: RemoteTrigger must surface `env` into the run environment for
51
+ // the runtime probe to see it; github-actions / cron inject it directly.
52
+ env: { BALDART_AUTONOMOUS: '1' },
46
53
  installed_at: new Date().toISOString()
47
54
  };
48
55
  fs.writeFileSync(this.configPath(spec.name), JSON.stringify(config, null, 2) + '\n');
@@ -101,6 +101,13 @@ if ! command -v claude >/dev/null 2>&1; then
101
101
  fi
102
102
  : "\${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY must be set (export it from the cron env or your shell profile)}"
103
103
 
104
+ # Marks this as an unattended (no-human) run. The /baldart-update skill (and
105
+ # any other autonomy-aware skill) auto-detects this and switches to
106
+ # non-interactive mode — no chat confirmations, safe guardrails (STOP on
107
+ # custom divergence / conflicts, never forces a destructive merge). Exported
108
+ # so the \`claude\` process and every child Bash it spawns inherit it.
109
+ export BALDART_AUTONOMOUS=1
110
+
104
111
  DATE=$(date -u +%Y%m%d)
105
112
  OUT_PATH="${outputPath}"
106
113
  OUT_PATH="\${OUT_PATH//\\{\\{YYYYMMDD\\}\\}/$DATE}"
@@ -105,6 +105,13 @@ jobs:
105
105
  - name: Run ${spec.name}
106
106
  env:
107
107
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
108
+ # Marks this as an unattended (no-human) run. The /baldart-update
109
+ # skill (and any other autonomy-aware skill) auto-detects this and
110
+ # switches to non-interactive mode — no chat confirmations, safe
111
+ # guardrails (STOP on custom divergence / conflicts, never forces a
112
+ # destructive merge). Inherited by the \`claude\` process and every
113
+ # child Bash it spawns.
114
+ BALDART_AUTONOMOUS: '1'
108
115
  run: |
109
116
  DATE=$(date -u +%Y%m%d)
110
117
  OUT_PATH="${outputPath}"
package/src/utils/ui.js CHANGED
@@ -3,6 +3,22 @@ const ora = require('ora');
3
3
  const inquirer = require('inquirer');
4
4
 
5
5
  class UI {
6
+ // Machine-readable mode (v3.32.0+). When on, every human-facing line that
7
+ // these helpers emit via `console.log` is redirected to STDERR, leaving
8
+ // STDOUT free for a single JSON object the caller (agent / CI) can parse.
9
+ // The JSON itself MUST be written with `process.stdout.write` directly —
10
+ // never through `console.log` — so it survives this redirect. Reversible;
11
+ // `ora` spinners already write to stderr, so they need no special handling.
12
+ static setJsonMode(on) {
13
+ if (on && !UI._origLog) {
14
+ UI._origLog = console.log;
15
+ console.log = (...args) => process.stderr.write(args.join(' ') + '\n');
16
+ } else if (!on && UI._origLog) {
17
+ console.log = UI._origLog;
18
+ UI._origLog = null;
19
+ }
20
+ }
21
+
6
22
  static header(text) {
7
23
  console.log('');
8
24
  console.log(chalk.cyan('━'.repeat(60)));