baldart 3.35.1 → 3.36.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 +32 -0
- package/VERSION +1 -1
- package/bin/baldart.js +17 -2
- package/framework/.claude/skills/new/SKILL.md +16 -1
- package/framework/.claude/skills/prd/SKILL.md +21 -2
- package/framework/.claude/skills/prd/assets/state-template.md +1 -0
- package/framework/scripts/analyze-session-tokens.js +411 -0
- package/package.json +1 -1
- package/src/commands/update.js +83 -16
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.36.0] - 2026-05-30
|
|
9
|
+
|
|
10
|
+
Adds **opt-in session telemetry** to `/new` and `/prd`: append `-stats` (or `--stats`) to a run and, at the end, the skill writes a per-**agent-role** breakdown of REAL token consumption and wall-clock time. This finally answers "what took the most time and tokens in a `/new` run" — and feeds the v3.35.0 Review Profile Selector **data gate** (still pending precisely because `docs/metrics/` carried no per-role cost data). **No new `baldart.config.yml` keys** — the opt-in is a flag, not a config feature, so the schema-propagation rule does not apply.
|
|
11
|
+
|
|
12
|
+
> **Zero model-token overhead.** The measurement is a **post-hoc Node script**, not a hook and not LLM self-reporting (the model has no reliable token counter). It reads the Claude Code session transcripts — the orchestrator's `<session>.jsonl` plus every `subagents/agent-*.jsonl` — which already carry `message.usage` (the four token counters) and per-message `timestamp`. On a real `/new` run the bulk of cost lives in the subagents (measured ~53M cumulative subagent tokens vs ~47k orchestrator output), and it is all captured. Cache-read tokens are kept **separate** from fresh input (they bill ~10×cheaper) so the breakdown doesn't mislead.
|
|
13
|
+
|
|
14
|
+
### Added — `-stats` / `--stats` opt-in session telemetry
|
|
15
|
+
|
|
16
|
+
- **[framework/scripts/analyze-session-tokens.js](framework/scripts/analyze-session-tokens.js)** (new): post-hoc aggregator. Locates the live transcripts from `$CLAUDE_CODE_SESSION_ID` + the cwd slug, sums the four `usage` counters (`fresh_input` / `cache_read` / `cache_creation` / `output`) **kept separate**, computes per-file wall-clock spans, and maps each subagent file → its role by matching the subagent's first prompt to the `Agent` tool_use `subagent_type` in the main transcript (verified 20/20 on a real `/new` batch; unmatched files bucket as `unknown`). Emits a human-readable `docs/metrics/sessions/<run_id>.md` (per-role table sorted by a weighted `cost_units` figure + totals + caveats) and a machine-readable `TELEMETRY_JSON=` line. **Fail-safe**: any error prints `stats: SKIPPED (<reason>)` and exits 0 — it can never abort a run.
|
|
17
|
+
- **[framework/.claude/skills/new/SKILL.md](framework/.claude/skills/new/SKILL.md)**: arg parser strips `-stats` / `--stats` like `-full` and sets an internal `STATS` flag (composes with `-full`). **Phase 8 — Metrics Log** gains a `STATS`-gated step that invokes the script and enriches the same `skill-runs.jsonl` row with a `"cost"` object (`by_role` + `totals` + `run_wall_ms`). Without `-stats`, Phase 8 is byte-for-byte unchanged.
|
|
18
|
+
- **[framework/.claude/skills/prd/SKILL.md](framework/.claude/skills/prd/SKILL.md)**: new **HARD RULE 18** — since `/prd` is conversational (no flag parser), the opt-in is detected once at **Step 1 (Kickoff)** from `$ARGUMENTS` and persisted as `stats_enabled` in the session state file (survives context compression / Step 0 resume). The **Step 7 Metrics Log** reads it and runs the same script (`--skill prd`), enriching its JSONL row identically.
|
|
19
|
+
- **[framework/.claude/skills/prd/assets/state-template.md](framework/.claude/skills/prd/assets/state-template.md)**: new `stats_enabled` header field.
|
|
20
|
+
|
|
21
|
+
### Notes & caveats
|
|
22
|
+
|
|
23
|
+
- **Accurate in sequential mode.** Team-mode L0/L1 orchestrators can spawn background / nested sub-orchestrators in *separate* session dirs; their subagents are not under the top-level `<session>/subagents/`. The script detects the gap (Agent spawns in main with no local subagent file) and prints an explicit caveat with the excluded count rather than silently undercounting.
|
|
24
|
+
- The per-role `wall(sum)` reflects active compute time; the headline run wall-clock is the orchestrator span and **includes human idle gaps** — both are labelled in the report.
|
|
25
|
+
|
|
26
|
+
## [3.35.2] - 2026-05-30
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
> **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).
|
|
31
|
+
|
|
32
|
+
### Fixed — self-upgrade survives flags newer than the installed CLI
|
|
33
|
+
|
|
34
|
+
- **[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)`.
|
|
35
|
+
- **[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.
|
|
36
|
+
- **[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`).
|
|
37
|
+
- **[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.
|
|
38
|
+
- **[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).
|
|
39
|
+
|
|
8
40
|
## [3.35.1] - 2026-05-30
|
|
9
41
|
|
|
10
42
|
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.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.36.0
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -37,6 +37,8 @@ Parse the card IDs from the arguments. Cards can be specified as:
|
|
|
37
37
|
|
|
38
38
|
If no card IDs are provided, ask the user which cards to implement.
|
|
39
39
|
|
|
40
|
+
**Opt-in session telemetry (`-stats` / `--stats`)**: strip `-stats` / `--stats` from the args list exactly like `-full` (it is NOT a card ID), and set an internal flag `STATS=true` for this run. When present, Phase 8 additionally measures REAL token + wall-clock cost per agent role (coder, code-reviewer, …) by post-processing the session transcripts — zero model-token overhead, runs in Bash after the work is done. When absent, Phase 8 behaves exactly as before. The flag is batch-scoped and composes with `-full` (`/new FEAT-005 -full -stats`).
|
|
41
|
+
|
|
40
42
|
---
|
|
41
43
|
|
|
42
44
|
## Context Tracking (CRITICAL)
|
|
@@ -2287,6 +2289,19 @@ and QA quality over time.
|
|
|
2287
2289
|
|
|
2288
2290
|
5. Note in tracker: `## Metrics Log: WRITTEN (run_id: batch-<FIRST-CARD-ID>)`
|
|
2289
2291
|
|
|
2292
|
+
6. **Session token telemetry (ONLY when `STATS=true`)** — if the run was invoked with `-stats` / `--stats`:
|
|
2293
|
+
|
|
2294
|
+
Measure REAL per-role token + wall-clock cost by post-processing the session transcripts (zero model-token cost — runs in Bash). Invoke:
|
|
2295
|
+
|
|
2296
|
+
```bash
|
|
2297
|
+
node .framework/framework/scripts/analyze-session-tokens.js \
|
|
2298
|
+
--skill new --run-id "batch-<FIRST-CARD-ID>" --out-dir docs/metrics
|
|
2299
|
+
```
|
|
2300
|
+
|
|
2301
|
+
The script reads `$CLAUDE_CODE_SESSION_ID` from the environment and derives the transcript paths itself (do NOT pass `--session` / `--project-dir` — those are test-only overrides). It writes a human-readable breakdown to `docs/metrics/sessions/batch-<FIRST-CARD-ID>.md` and prints a summary plus a final `TELEMETRY_JSON=<json>` line.
|
|
2302
|
+
|
|
2303
|
+
Parse the `TELEMETRY_JSON=` line and **enrich the same skill-runs.jsonl row** you wrote in step 3 by adding a `"cost"` key holding that JSON object (`by_role` + `totals` + `run_wall_ms`). If the script printed `stats: SKIPPED (...)` instead, add `"cost":{"skipped":"<reason>"}` and continue. Echo the script's summary to the user so they see the per-role breakdown.
|
|
2304
|
+
|
|
2290
2305
|
**If `docs/metrics/skill-runs.jsonl` does not exist**: create it first with `touch docs/metrics/skill-runs.jsonl`.
|
|
2291
2306
|
**If batch tracker is missing or unreadable**: log "Metrics Log: SKIPPED (tracker not found)" and proceed without blocking.
|
|
2292
|
-
**This phase is NON-BLOCKING** — if it fails for any reason, do not abort the run.
|
|
2307
|
+
**This phase is NON-BLOCKING** — if it fails for any reason, do not abort the run. The `-stats` step in particular is best-effort: the script is fail-safe (always exits 0), so never let it block the commit.
|
|
@@ -199,6 +199,16 @@ message. You ask questions, wait for answers, and iterate.
|
|
|
199
199
|
repository, or the consumer has explicitly disabled docs worktrees via
|
|
200
200
|
overlay), STOP and report the error to the user. Do NOT silently fall back
|
|
201
201
|
to writing on the main repo — that defeats the entire purpose of this rule.
|
|
202
|
+
18. **Opt-in session telemetry (`-stats` / `--stats`).** Unlike `/new`, this skill
|
|
203
|
+
has no flag parser — it is conversational. So detect the opt-in ONCE at **Step 1
|
|
204
|
+
(Kickoff)**: inspect `$ARGUMENTS` for the token `-stats` or `--stats`. If present,
|
|
205
|
+
set `stats_enabled: true` in the state-file header (the field exists in
|
|
206
|
+
`assets/state-template.md`); otherwise `false`. Persisting it in the state file is
|
|
207
|
+
what makes it survive context compression and Step 0 resume. The Metrics Log step
|
|
208
|
+
(inside Step 7) reads this field and, when `true`, post-processes the session
|
|
209
|
+
transcripts for REAL per-role token + wall-clock cost. The token itself is NOT part
|
|
210
|
+
of the feature description — strip it before recording the user's verbatim feature
|
|
211
|
+
text in `## Feature Description`.
|
|
202
212
|
|
|
203
213
|
---
|
|
204
214
|
|
|
@@ -372,6 +382,15 @@ exactly the correct semantics). No special handling needed.
|
|
|
372
382
|
{"ts":"<ISO-8601-UTC>","skill":"prd","run_id":"prd-<slug>","prd_slug":"<slug>","cards_created":0,"ac_coverage_ratio":0.0,"isa_count":0,"has_test_plan_pct":0.0,"has_db_indexes_pct":0.0,"discovery_iterations":1,"prd_path":"${paths.prd_dir}/<slug>/PRD.md"}
|
|
373
383
|
```
|
|
374
384
|
|
|
375
|
-
3.
|
|
385
|
+
3. **Session token telemetry (ONLY when `stats_enabled: true` in the state file)** — measure REAL per-role token + wall-clock cost by post-processing the session transcripts (zero model-token cost — runs in Bash). Invoke:
|
|
376
386
|
|
|
377
|
-
|
|
387
|
+
```bash
|
|
388
|
+
node .framework/framework/scripts/analyze-session-tokens.js \
|
|
389
|
+
--skill prd --run-id "prd-<slug>" --out-dir "$WORKTREE_PATH/docs/metrics"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The script reads `$CLAUDE_CODE_SESSION_ID` and derives the transcript paths itself (do NOT pass `--session` / `--project-dir` — test-only overrides). It writes `$WORKTREE_PATH/docs/metrics/sessions/prd-<slug>.md` and prints a summary plus a final `TELEMETRY_JSON=<json>` line. Parse that line and add its object under a `"cost"` key on the same JSONL row from step 2; on `stats: SKIPPED (...)` add `"cost":{"skipped":"<reason>"}`. Echo the summary to the user. Add the new `sessions/prd-<slug>.md` file to the explicit stage list too (point 4 below).
|
|
393
|
+
|
|
394
|
+
4. Add the file(s) to the explicit stage list in validation-phase.md Step 7 point 6 (the stage list is at point 6; point 5 is the invocation of this very Metrics Log step).
|
|
395
|
+
|
|
396
|
+
**This step is NON-BLOCKING** — if it fails, do not abort. Log "Metrics Log: SKIPPED" in the progress bar. The PRD merge proceeds regardless. The `-stats` script is itself fail-safe (always exits 0).
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Post-hoc session telemetry for /new and /prd (opt-in via `-stats`).
|
|
3
|
+
//
|
|
4
|
+
// Reads the live Claude Code transcripts — the orchestrator's main transcript
|
|
5
|
+
// plus every subagent transcript — and aggregates REAL token usage and wall-clock
|
|
6
|
+
// time by AGENT ROLE (coder, code-reviewer, qa-sentinel, doc-reviewer, …). The
|
|
7
|
+
// data already exists in the transcripts (`message.usage` + `timestamp`), so this
|
|
8
|
+
// costs ZERO model tokens: it runs in Node, after the run, outside the model.
|
|
9
|
+
//
|
|
10
|
+
// Transcript layout (verified):
|
|
11
|
+
// ~/.claude/projects/<slug>/<session_id>.jsonl ← orchestrator (main)
|
|
12
|
+
// ~/.claude/projects/<slug>/<session_id>/subagents/agent-*.jsonl ← one file per subagent
|
|
13
|
+
// where slug = cwd with every "/" replaced by "-".
|
|
14
|
+
//
|
|
15
|
+
// Role mapping: each `Agent` tool_use in the main transcript carries
|
|
16
|
+
// `input.subagent_type` (= role) and `input.prompt`. Every subagent file's first
|
|
17
|
+
// user message IS that prompt, so we link file→role by prompt-prefix match
|
|
18
|
+
// (verified 20/20 on a real /new run). Unlinkable files fall back to role
|
|
19
|
+
// "unknown" with a slug-derived label — their token/time data stays accurate.
|
|
20
|
+
//
|
|
21
|
+
// Cache accounting: the four usage counters are kept SEPARATE on purpose.
|
|
22
|
+
// `cache_read` is billed ~10% of fresh input; conflating them would overstate
|
|
23
|
+
// cost and mislead "what consumed the most". A single headline `cost_units`
|
|
24
|
+
// figure weights them: output + fresh_input + cache_creation×1.25 + cache_read×0.1.
|
|
25
|
+
//
|
|
26
|
+
// Usage:
|
|
27
|
+
// node analyze-session-tokens.js --skill new --run-id batch-FEAT-0001
|
|
28
|
+
// node analyze-session-tokens.js --skill prd --run-id prd-checkout --out-dir docs/metrics
|
|
29
|
+
// # test/override (bypass env + cwd derivation):
|
|
30
|
+
// node analyze-session-tokens.js --skill new --run-id X --session <id> --project-dir <path>
|
|
31
|
+
//
|
|
32
|
+
// Output:
|
|
33
|
+
// - <out-dir>/sessions/<run_id>.md — human-readable per-role breakdown + totals
|
|
34
|
+
// - stdout: same summary, PLUS a final line `TELEMETRY_JSON=<json>` carrying the
|
|
35
|
+
// totals object for the skill to embed in docs/metrics/skill-runs.jsonl.
|
|
36
|
+
//
|
|
37
|
+
// Fail-safe: ANY error prints `stats: SKIPPED (<reason>)` and exits 0. This script
|
|
38
|
+
// must NEVER abort a /new or /prd run.
|
|
39
|
+
|
|
40
|
+
'use strict';
|
|
41
|
+
|
|
42
|
+
const fs = require('fs');
|
|
43
|
+
const os = require('os');
|
|
44
|
+
const path = require('path');
|
|
45
|
+
|
|
46
|
+
// ---- arg parsing -----------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function getArg(name, fallback) {
|
|
49
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
50
|
+
return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Bail out softly — telemetry is never allowed to break a run.
|
|
54
|
+
function skip(reason) {
|
|
55
|
+
console.log(`stats: SKIPPED (${reason})`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const skill = getArg('skill', 'new');
|
|
60
|
+
const runId = getArg('run-id', null);
|
|
61
|
+
const outDir = getArg('out-dir', path.join('docs', 'metrics'));
|
|
62
|
+
const sessionId = getArg('session', process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
63
|
+
const projectDir = getArg('project-dir', null);
|
|
64
|
+
|
|
65
|
+
if (!runId) skip('no --run-id provided');
|
|
66
|
+
if (!sessionId) skip('no session id ($CLAUDE_CODE_SESSION_ID unset, no --session)');
|
|
67
|
+
|
|
68
|
+
// ---- locate transcripts ----------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function projectSlug(cwd) {
|
|
71
|
+
// Claude Code stores transcripts under a slug = cwd with "/" → "-".
|
|
72
|
+
return cwd.replace(/\//g, '-');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let baseDir;
|
|
76
|
+
if (projectDir) {
|
|
77
|
+
baseDir = projectDir;
|
|
78
|
+
} else {
|
|
79
|
+
baseDir = path.join(os.homedir(), '.claude', 'projects', projectSlug(process.cwd()));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const mainTranscript = path.join(baseDir, `${sessionId}.jsonl`);
|
|
83
|
+
const subDir = path.join(baseDir, sessionId, 'subagents');
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(mainTranscript)) skip(`main transcript not found: ${mainTranscript}`);
|
|
86
|
+
|
|
87
|
+
// ---- jsonl helpers ---------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function* readLines(file) {
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
97
|
+
if (!line.trim()) continue;
|
|
98
|
+
let obj;
|
|
99
|
+
try {
|
|
100
|
+
obj = JSON.parse(line);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
continue; // tolerate partial / malformed lines
|
|
103
|
+
}
|
|
104
|
+
yield obj;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function contentBlocks(rec) {
|
|
109
|
+
const c = rec && rec.message && rec.message.content;
|
|
110
|
+
if (Array.isArray(c)) return c;
|
|
111
|
+
if (typeof c === 'string') return [{ type: 'text', text: c }];
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function blocksText(blocks) {
|
|
116
|
+
return blocks
|
|
117
|
+
.map((b) => (typeof b === 'string' ? b : b && typeof b.text === 'string' ? b.text : ''))
|
|
118
|
+
.join('')
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Aggregate the four usage counters + wall-clock span of one transcript file.
|
|
123
|
+
function aggregateFile(file) {
|
|
124
|
+
const acc = {
|
|
125
|
+
fresh_input: 0,
|
|
126
|
+
cache_read: 0,
|
|
127
|
+
cache_creation: 0,
|
|
128
|
+
output: 0,
|
|
129
|
+
turns: 0,
|
|
130
|
+
firstTs: null,
|
|
131
|
+
lastTs: null,
|
|
132
|
+
};
|
|
133
|
+
for (const rec of readLines(file)) {
|
|
134
|
+
const ts = rec.timestamp ? Date.parse(rec.timestamp) : NaN;
|
|
135
|
+
if (!Number.isNaN(ts)) {
|
|
136
|
+
if (acc.firstTs === null || ts < acc.firstTs) acc.firstTs = ts;
|
|
137
|
+
if (acc.lastTs === null || ts > acc.lastTs) acc.lastTs = ts;
|
|
138
|
+
}
|
|
139
|
+
if (rec.type !== 'assistant') continue;
|
|
140
|
+
const u = (rec.message && rec.message.usage) || null;
|
|
141
|
+
if (!u) continue;
|
|
142
|
+
acc.fresh_input += u.input_tokens || 0;
|
|
143
|
+
acc.cache_read += u.cache_read_input_tokens || 0;
|
|
144
|
+
acc.cache_creation += u.cache_creation_input_tokens || 0;
|
|
145
|
+
acc.output += u.output_tokens || 0;
|
|
146
|
+
acc.turns += 1;
|
|
147
|
+
}
|
|
148
|
+
return acc;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function costUnits(a) {
|
|
152
|
+
return Math.round(a.output + a.fresh_input + a.cache_creation * 1.25 + a.cache_read * 0.1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function wallMs(a) {
|
|
156
|
+
return a.firstTs !== null && a.lastTs !== null ? a.lastTs - a.firstTs : 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- role map from main transcript -----------------------------------------
|
|
160
|
+
|
|
161
|
+
// Collect every Agent spawn: { role, promptPrefix } in spawn order.
|
|
162
|
+
const agentSpawns = [];
|
|
163
|
+
for (const rec of readLines(mainTranscript)) {
|
|
164
|
+
for (const blk of contentBlocks(rec)) {
|
|
165
|
+
if (blk && blk.type === 'tool_use' && blk.name === 'Agent' && blk.input) {
|
|
166
|
+
const role = blk.input.subagent_type || 'unknown';
|
|
167
|
+
const prompt = (blk.input.prompt || '').trim();
|
|
168
|
+
agentSpawns.push({ role, prompt });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function matchRole(firstPrompt) {
|
|
174
|
+
if (!firstPrompt) return null;
|
|
175
|
+
for (const s of agentSpawns) {
|
|
176
|
+
if (!s.prompt) continue;
|
|
177
|
+
// bidirectional prefix match tolerates truncation on either side
|
|
178
|
+
if (firstPrompt.startsWith(s.prompt.slice(0, 50)) || s.prompt.startsWith(firstPrompt.slice(0, 50))) {
|
|
179
|
+
return s.role;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- aggregate subagents + main --------------------------------------------
|
|
186
|
+
|
|
187
|
+
const byRole = new Map(); // role → { agg, agents }
|
|
188
|
+
function addToRole(role, agg) {
|
|
189
|
+
if (!byRole.has(role)) {
|
|
190
|
+
byRole.set(role, {
|
|
191
|
+
fresh_input: 0,
|
|
192
|
+
cache_read: 0,
|
|
193
|
+
cache_creation: 0,
|
|
194
|
+
output: 0,
|
|
195
|
+
turns: 0,
|
|
196
|
+
agents: 0,
|
|
197
|
+
wallSumMs: 0,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const r = byRole.get(role);
|
|
201
|
+
r.fresh_input += agg.fresh_input;
|
|
202
|
+
r.cache_read += agg.cache_read;
|
|
203
|
+
r.cache_creation += agg.cache_creation;
|
|
204
|
+
r.output += agg.output;
|
|
205
|
+
r.turns += agg.turns;
|
|
206
|
+
r.agents += 1;
|
|
207
|
+
r.wallSumMs += wallMs(agg);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let subFiles = [];
|
|
211
|
+
if (fs.existsSync(subDir)) {
|
|
212
|
+
try {
|
|
213
|
+
subFiles = fs
|
|
214
|
+
.readdirSync(subDir)
|
|
215
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
216
|
+
.map((f) => path.join(subDir, f));
|
|
217
|
+
} catch (err) {
|
|
218
|
+
subFiles = [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let unmatchedFiles = 0;
|
|
223
|
+
for (const f of subFiles) {
|
|
224
|
+
const agg = aggregateFile(f);
|
|
225
|
+
if (agg.turns === 0 && wallMs(agg) === 0) continue; // empty/aborted subagent
|
|
226
|
+
// role: first user prompt of the subagent file
|
|
227
|
+
let firstPrompt = '';
|
|
228
|
+
for (const rec of readLines(f)) {
|
|
229
|
+
if (rec.type === 'user') {
|
|
230
|
+
firstPrompt = blocksText(contentBlocks(rec));
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const role = matchRole(firstPrompt);
|
|
235
|
+
if (!role) unmatchedFiles += 1;
|
|
236
|
+
addToRole(role || 'unknown', agg);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// orchestrator (main transcript) as its own row
|
|
240
|
+
const mainAgg = aggregateFile(mainTranscript);
|
|
241
|
+
addToRole('orchestrator', mainAgg);
|
|
242
|
+
|
|
243
|
+
// team-mode nesting caveat: Agent calls in main with no local subagent file
|
|
244
|
+
// (background / nested orchestrators write to a different session dir).
|
|
245
|
+
const localSubCount = subFiles.length;
|
|
246
|
+
const spawnedCount = agentSpawns.length;
|
|
247
|
+
const missingNested = Math.max(0, spawnedCount - localSubCount);
|
|
248
|
+
|
|
249
|
+
// ---- totals ----------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
const totals = {
|
|
252
|
+
fresh_input: 0,
|
|
253
|
+
cache_read: 0,
|
|
254
|
+
cache_creation: 0,
|
|
255
|
+
output: 0,
|
|
256
|
+
turns: 0,
|
|
257
|
+
agents: 0,
|
|
258
|
+
};
|
|
259
|
+
for (const r of byRole.values()) {
|
|
260
|
+
totals.fresh_input += r.fresh_input;
|
|
261
|
+
totals.cache_read += r.cache_read;
|
|
262
|
+
totals.cache_creation += r.cache_creation;
|
|
263
|
+
totals.output += r.output;
|
|
264
|
+
totals.turns += r.turns;
|
|
265
|
+
totals.agents += r.agents;
|
|
266
|
+
}
|
|
267
|
+
totals.cost_units = costUnits(totals);
|
|
268
|
+
|
|
269
|
+
// run wall-clock = span of the main transcript (first→last timestamp)
|
|
270
|
+
const runWallMs = wallMs(mainAgg);
|
|
271
|
+
|
|
272
|
+
// ---- formatting helpers -----------------------------------------------------
|
|
273
|
+
|
|
274
|
+
function fmtTokens(n) {
|
|
275
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
276
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
277
|
+
return String(n);
|
|
278
|
+
}
|
|
279
|
+
function fmtDuration(ms) {
|
|
280
|
+
if (!ms || ms < 0) return '-';
|
|
281
|
+
const s = Math.round(ms / 1000);
|
|
282
|
+
const m = Math.floor(s / 60);
|
|
283
|
+
const rs = s % 60;
|
|
284
|
+
return m > 0 ? `${m}m${rs.toString().padStart(2, '0')}s` : `${rs}s`;
|
|
285
|
+
}
|
|
286
|
+
function pad(s, w) {
|
|
287
|
+
s = String(s);
|
|
288
|
+
return s.length >= w ? s : s + ' '.repeat(w - s.length);
|
|
289
|
+
}
|
|
290
|
+
function padL(s, w) {
|
|
291
|
+
s = String(s);
|
|
292
|
+
return s.length >= w ? s : ' '.repeat(w - s.length) + s;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// rows sorted by cost_units desc
|
|
296
|
+
const roleRows = [...byRole.entries()]
|
|
297
|
+
.map(([role, r]) => ({ role, ...r, cost_units: costUnits(r) }))
|
|
298
|
+
.sort((a, b) => b.cost_units - a.cost_units);
|
|
299
|
+
|
|
300
|
+
// ---- write per-run markdown summary ----------------------------------------
|
|
301
|
+
|
|
302
|
+
const lines = [];
|
|
303
|
+
lines.push(`# Session telemetry — ${runId}`);
|
|
304
|
+
lines.push('');
|
|
305
|
+
lines.push(`- **skill**: ${skill}`);
|
|
306
|
+
lines.push(`- **session**: ${sessionId}`);
|
|
307
|
+
lines.push(`- **run wall-clock** (orchestrator span, includes idle): ${fmtDuration(runWallMs)}`);
|
|
308
|
+
lines.push(`- **agents**: ${totals.agents - 1} subagent(s) + 1 orchestrator`);
|
|
309
|
+
lines.push(`- **total cost units**: ${fmtTokens(totals.cost_units)} _(output + fresh_in + cache_creation×1.25 + cache_read×0.1)_`);
|
|
310
|
+
lines.push('');
|
|
311
|
+
lines.push('## By role');
|
|
312
|
+
lines.push('');
|
|
313
|
+
const H = ['role', 'agents', 'wall(sum)', 'output', 'fresh_in', 'cache_cr', 'cache_rd', 'cost_units', '% cost'];
|
|
314
|
+
const W = [20, 7, 10, 9, 9, 9, 9, 11, 7];
|
|
315
|
+
lines.push(H.map((h, i) => pad(h, W[i])).join(' | '));
|
|
316
|
+
lines.push(W.map((w) => '-'.repeat(w)).join('-|-'));
|
|
317
|
+
for (const r of roleRows) {
|
|
318
|
+
const pct = totals.cost_units ? `${((r.cost_units / totals.cost_units) * 100).toFixed(0)}%` : '-';
|
|
319
|
+
lines.push(
|
|
320
|
+
[
|
|
321
|
+
pad(r.role, W[0]),
|
|
322
|
+
padL(r.agents, W[1]),
|
|
323
|
+
padL(fmtDuration(r.wallSumMs), W[2]),
|
|
324
|
+
padL(fmtTokens(r.output), W[3]),
|
|
325
|
+
padL(fmtTokens(r.fresh_input), W[4]),
|
|
326
|
+
padL(fmtTokens(r.cache_creation), W[5]),
|
|
327
|
+
padL(fmtTokens(r.cache_read), W[6]),
|
|
328
|
+
padL(fmtTokens(r.cost_units), W[7]),
|
|
329
|
+
padL(pct, W[8]),
|
|
330
|
+
].join(' | ')
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
lines.push(W.map((w) => '-'.repeat(w)).join('-|-'));
|
|
334
|
+
lines.push(
|
|
335
|
+
[
|
|
336
|
+
pad('TOTAL', W[0]),
|
|
337
|
+
padL(totals.agents, W[1]),
|
|
338
|
+
padL(fmtDuration(runWallMs), W[2]),
|
|
339
|
+
padL(fmtTokens(totals.output), W[3]),
|
|
340
|
+
padL(fmtTokens(totals.fresh_input), W[4]),
|
|
341
|
+
padL(fmtTokens(totals.cache_creation), W[5]),
|
|
342
|
+
padL(fmtTokens(totals.cache_read), W[6]),
|
|
343
|
+
padL(fmtTokens(totals.cost_units), W[7]),
|
|
344
|
+
padL('100%', W[8]),
|
|
345
|
+
].join(' | ')
|
|
346
|
+
);
|
|
347
|
+
lines.push('');
|
|
348
|
+
lines.push('> wall(sum) is the sum of per-agent spans and reflects ACTIVE compute time; the');
|
|
349
|
+
lines.push('> run wall-clock is the orchestrator first→last span and INCLUDES human idle gaps.');
|
|
350
|
+
lines.push('> Subagents may run in parallel, so wall(sum) can exceed the run wall-clock.');
|
|
351
|
+
lines.push('> cache_read tokens are ~10% the cost of fresh input — kept separate on purpose.');
|
|
352
|
+
if (unmatchedFiles > 0) {
|
|
353
|
+
lines.push('>');
|
|
354
|
+
lines.push(`> ⚠ ${unmatchedFiles} subagent file(s) could not be mapped to a role (bucketed as "unknown").`);
|
|
355
|
+
}
|
|
356
|
+
if (missingNested > 0) {
|
|
357
|
+
lines.push('>');
|
|
358
|
+
lines.push(
|
|
359
|
+
`> ⚠ team-mode caveat: ${missingNested} Agent spawn(s) in the main transcript have no local`
|
|
360
|
+
);
|
|
361
|
+
lines.push(
|
|
362
|
+
`> subagent file (likely background / nested orchestrators in separate sessions) — their`
|
|
363
|
+
);
|
|
364
|
+
lines.push(`> tokens are NOT included in these totals. Sequential-mode runs are fully accurate.`);
|
|
365
|
+
}
|
|
366
|
+
lines.push('');
|
|
367
|
+
|
|
368
|
+
// ---- emit ------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
const sessionsDir = path.join(outDir, 'sessions');
|
|
371
|
+
try {
|
|
372
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
373
|
+
fs.writeFileSync(path.join(sessionsDir, `${runId}.md`), lines.join('\n'));
|
|
374
|
+
} catch (err) {
|
|
375
|
+
skip(`could not write summary: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// human summary to stdout
|
|
379
|
+
console.log(lines.join('\n'));
|
|
380
|
+
|
|
381
|
+
// machine-readable totals for the skill to embed in skill-runs.jsonl
|
|
382
|
+
const jsonOut = {
|
|
383
|
+
run_wall_ms: runWallMs,
|
|
384
|
+
by_role: Object.fromEntries(
|
|
385
|
+
roleRows.map((r) => [
|
|
386
|
+
r.role,
|
|
387
|
+
{
|
|
388
|
+
agents: r.agents,
|
|
389
|
+
output: r.output,
|
|
390
|
+
fresh_input: r.fresh_input,
|
|
391
|
+
cache_creation: r.cache_creation,
|
|
392
|
+
cache_read: r.cache_read,
|
|
393
|
+
cost_units: r.cost_units,
|
|
394
|
+
wall_sum_ms: r.wallSumMs,
|
|
395
|
+
},
|
|
396
|
+
])
|
|
397
|
+
),
|
|
398
|
+
totals: {
|
|
399
|
+
agents: totals.agents,
|
|
400
|
+
output: totals.output,
|
|
401
|
+
fresh_input: totals.fresh_input,
|
|
402
|
+
cache_creation: totals.cache_creation,
|
|
403
|
+
cache_read: totals.cache_read,
|
|
404
|
+
cost_units: totals.cost_units,
|
|
405
|
+
turns: totals.turns,
|
|
406
|
+
},
|
|
407
|
+
unmatched_subagents: unmatchedFiles,
|
|
408
|
+
nested_agents_excluded: missingNested,
|
|
409
|
+
};
|
|
410
|
+
console.log(`TELEMETRY_JSON=${JSON.stringify(jsonOut)}`);
|
|
411
|
+
process.exit(0);
|
package/package.json
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -406,7 +406,16 @@ function snapshotCustomEntries(dir) {
|
|
|
406
406
|
// child inherits stdio (preserves prompts), and BALDART_RELAUNCHED=1 in env
|
|
407
407
|
// is a loop guard: if npm cache somehow serves an older version, the child
|
|
408
408
|
// sees the flag and skips its own relaunch check.
|
|
409
|
-
|
|
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 = {}) {
|
|
410
419
|
if (process.env.BALDART_RELAUNCHED === '1') return false;
|
|
411
420
|
if (process.env.BALDART_NO_RELAUNCH === '1') return false;
|
|
412
421
|
|
|
@@ -424,7 +433,13 @@ async function maybeRelaunchUnderLatest(options) {
|
|
|
424
433
|
|
|
425
434
|
const autoYes = options.yes === true;
|
|
426
435
|
let proceed = autoYes;
|
|
427
|
-
if (
|
|
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) {
|
|
428
443
|
UI.newline();
|
|
429
444
|
UI.warning(`The installed CLI is older than npm latest: v${cliVersion} → v${info.latest}.`);
|
|
430
445
|
const choice = await UI.select('How would you like to proceed?', [
|
|
@@ -440,17 +455,24 @@ async function maybeRelaunchUnderLatest(options) {
|
|
|
440
455
|
}
|
|
441
456
|
if (!proceed) return false;
|
|
442
457
|
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
+
}
|
|
454
476
|
|
|
455
477
|
const spawn = require('child_process').spawnSync;
|
|
456
478
|
UI.info(`Auto-relaunching via npx baldart@${info.latest}…`);
|
|
@@ -466,7 +488,30 @@ async function maybeRelaunchUnderLatest(options) {
|
|
|
466
488
|
process.exit(res.status || 0);
|
|
467
489
|
}
|
|
468
490
|
|
|
469
|
-
|
|
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 = []) {
|
|
470
515
|
const git = new GitUtils();
|
|
471
516
|
const symlinks = new SymlinkUtils();
|
|
472
517
|
const repo = 'antbald/BALDART'; // Default repo
|
|
@@ -478,13 +523,35 @@ async function update(options = {}) {
|
|
|
478
523
|
const autoStash = autoYes || options.autoStash === true;
|
|
479
524
|
const confirm = async (msg, def = true) => (autoYes ? def : UI.confirm(msg, def));
|
|
480
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
|
+
|
|
481
550
|
// Machine-readable mode (v3.32.0+). Must be fully non-interactive: --json
|
|
482
551
|
// requires --yes (so no UI.select/confirm can block) and rejects --reset
|
|
483
552
|
// (the nuclear path is a deliberate destructive human escape hatch, not an
|
|
484
553
|
// agent flow — see the autonomous /baldart-update skill, which never resets).
|
|
485
|
-
JSON_MODE = options.json === true;
|
|
486
554
|
if (JSON_MODE) {
|
|
487
|
-
UI.setJsonMode(true);
|
|
488
555
|
if (options.reset === true) {
|
|
489
556
|
emitUpdateJson({ ok: false, action: 'usage-error',
|
|
490
557
|
reason: '--json does not support --reset. Reset is an interactive destructive escape hatch; run it manually.' }, 2);
|