acuvo-code 0.4.2 → 0.4.4
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/ENTERPRISE.md +1 -1
- package/bin/acuvo.mjs +7 -0
- package/lib/banner.mjs +155 -125
- package/lib/colour.mjs +25 -0
- package/lib/turn.mjs +71 -4
- package/package.json +1 -1
package/ENTERPRISE.md
CHANGED
|
@@ -189,7 +189,7 @@ are the numbers to quote. Counting is the first thing a reviewer does.
|
|
|
189
189
|
|
|
190
190
|
⚠️ **This said "18 shipped files", then "41", then "90", then "101", then "108", and every
|
|
191
191
|
one went stale in turn.** The package ships **120 files — 118 in `lib/`, 2 in
|
|
192
|
-
`bin/` — about
|
|
192
|
+
`bin/` — about 78091 lines**, with **237 test files** beside them (counted 2026-08-22).
|
|
193
193
|
|
|
194
194
|
⭐ **AND THE 108 WENT STALE IN THE MOST INSTRUCTIVE WAY POSSIBLE: THREE OF THE FILES IT
|
|
195
195
|
MISSED WERE REACHABLE FROM NOTHING.** `wiring-reach.test.mjs` was naming
|
package/bin/acuvo.mjs
CHANGED
|
@@ -1511,6 +1511,13 @@ ${formatBoard(listed)}
|
|
|
1511
1511
|
billing,
|
|
1512
1512
|
canRun: mode,
|
|
1513
1513
|
interactive: false,
|
|
1514
|
+
/**
|
|
1515
|
+
* ⚠️ THE PAINTER IS BUILT HERE, WHERE THE STREAM IS KNOWN. `colourEnabled`
|
|
1516
|
+
* owns NO_COLOR, FORCE_COLOR, TERM=dumb and TTY detection — so a redirected
|
|
1517
|
+
* run gets a plain banner and nobody has to remember an `if` at the call
|
|
1518
|
+
* site.
|
|
1519
|
+
*/
|
|
1520
|
+
paint: createPainter(colourEnabled()),
|
|
1514
1521
|
});
|
|
1515
1522
|
if (opts.json) process.stderr.write(banner);
|
|
1516
1523
|
else process.stdout.write(banner);
|
package/lib/banner.mjs
CHANGED
|
@@ -1,125 +1,155 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ── ⭐⭐⭐ WHAT YOU SEE WHEN YOU TYPE `acuvo` ────────────────────────────────
|
|
3
|
-
*
|
|
4
|
-
* Roman, 2026-08-22, having typed it: *"it's not opening as a typable terminal,
|
|
5
|
-
* with acuvo logo top left and details up top etc, like how claude code opens.
|
|
6
|
-
* have you actually designed the page?"* — and then: *"no we want OUR logo"*.
|
|
7
|
-
*
|
|
8
|
-
* So this is the real mark, not letters spelling the name. It is
|
|
9
|
-
* `console/public/brand/acuvo-mark.png` — the angular A — resampled to
|
|
10
|
-
* half-blocks at 14x14 and embedded as text.
|
|
11
|
-
*
|
|
12
|
-
* ── ⚠️ WHY IT IS EMBEDDED RATHER THAN DECODED AT RUNTIME ────────────────────
|
|
13
|
-
*
|
|
14
|
-
* This package has ZERO dependencies, deliberately, and Node cannot decode a
|
|
15
|
-
* PNG without one. Converting at build time and pasting the result keeps that
|
|
16
|
-
* promise and costs nothing at startup. If the mark ever changes, re-run the
|
|
17
|
-
* conversion — the logo is DERIVED from the brand asset, not drawn by hand, so
|
|
18
|
-
* it stays honest to it.
|
|
19
|
-
*
|
|
20
|
-
* ── ⚠️ THE CONSTRAINTS, WHICH ARE NOT PREFERENCES ───────────────────────────
|
|
21
|
-
*
|
|
22
|
-
* · **Half-block glyphs only** (▀ ▄ █). Braille and box-drawing render as tofu
|
|
23
|
-
* in cmd.exe and in many CI log viewers, and a logo that renders as question
|
|
24
|
-
* marks is worse than no logo — on the exact platform this is developed on.
|
|
25
|
-
* · **No colour escapes here.** The caller owns colour and honours NO_COLOR; a
|
|
26
|
-
* module that hard-codes them emits garbage the moment output is piped.
|
|
27
|
-
* · **Every line under 80 columns.** The narrowest terminal in real use is 80,
|
|
28
|
-
* and a wrapped banner does not read as dense, it reads as broken.
|
|
29
|
-
*
|
|
30
|
-
* ⭐ A NOTE ON DOING BETTER: `lib/terminal-graphics.mjs` can send a real PNG
|
|
31
|
-
* inline on Kitty and iTerm2. It is deliberately NOT used here — Windows
|
|
32
|
-
* Terminal speaks neither protocol, so the block art is what most users would
|
|
33
|
-
* see anyway, and one rendering that is the same everywhere beats two that
|
|
34
|
-
* disagree.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The Acuvo mark, resampled from the brand PNG. Seven rows: tall enough to be
|
|
39
|
-
* the mark rather than a smudge, short enough for something run forty times a
|
|
40
|
-
* day rather than opened once.
|
|
41
|
-
*/
|
|
42
|
-
const MARK = [
|
|
43
|
-
' ▄█',
|
|
44
|
-
' ▄███',
|
|
45
|
-
' ▄█▀▀██',
|
|
46
|
-
' ▄█▀ ▀██',
|
|
47
|
-
' ▄█▀ ██ ▀██',
|
|
48
|
-
' ▄████▀▀█████',
|
|
49
|
-
'█▀ █▀ ▀█',
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const MARK_WIDTH = Math.max(...MARK.map((l) => l.length));
|
|
53
|
-
const GUTTER = 2;
|
|
54
|
-
export const MAX_BANNER_COLUMNS = 80;
|
|
55
|
-
|
|
56
|
-
/** Columns left for the detail rows once the mark and gutter are placed. */
|
|
57
|
-
const TEXT_COLUMNS = MAX_BANNER_COLUMNS - MARK_WIDTH - GUTTER;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build the opening screen: mark on the left, facts on the right.
|
|
61
|
-
*
|
|
62
|
-
* @param {object} o
|
|
63
|
-
* @param {string} o.version
|
|
64
|
-
* @param {string} o.workspace already shortened by the caller
|
|
65
|
-
* @param {string} o.model
|
|
66
|
-
* @param {string} o.billing who this run will charge
|
|
67
|
-
* @param {string} o.canRun what it may execute, or that it may not
|
|
68
|
-
* @param {boolean} [o.interactive] whether a prompt follows
|
|
69
|
-
* @returns {string}
|
|
70
|
-
*/
|
|
71
|
-
export function openingScreen({ version, workspace, model, billing, canRun, interactive = false }) {
|
|
72
|
-
/**
|
|
73
|
-
* ──
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ── ⭐⭐⭐ WHAT YOU SEE WHEN YOU TYPE `acuvo` ────────────────────────────────
|
|
3
|
+
*
|
|
4
|
+
* Roman, 2026-08-22, having typed it: *"it's not opening as a typable terminal,
|
|
5
|
+
* with acuvo logo top left and details up top etc, like how claude code opens.
|
|
6
|
+
* have you actually designed the page?"* — and then: *"no we want OUR logo"*.
|
|
7
|
+
*
|
|
8
|
+
* So this is the real mark, not letters spelling the name. It is
|
|
9
|
+
* `console/public/brand/acuvo-mark.png` — the angular A — resampled to
|
|
10
|
+
* half-blocks at 14x14 and embedded as text.
|
|
11
|
+
*
|
|
12
|
+
* ── ⚠️ WHY IT IS EMBEDDED RATHER THAN DECODED AT RUNTIME ────────────────────
|
|
13
|
+
*
|
|
14
|
+
* This package has ZERO dependencies, deliberately, and Node cannot decode a
|
|
15
|
+
* PNG without one. Converting at build time and pasting the result keeps that
|
|
16
|
+
* promise and costs nothing at startup. If the mark ever changes, re-run the
|
|
17
|
+
* conversion — the logo is DERIVED from the brand asset, not drawn by hand, so
|
|
18
|
+
* it stays honest to it.
|
|
19
|
+
*
|
|
20
|
+
* ── ⚠️ THE CONSTRAINTS, WHICH ARE NOT PREFERENCES ───────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* · **Half-block glyphs only** (▀ ▄ █). Braille and box-drawing render as tofu
|
|
23
|
+
* in cmd.exe and in many CI log viewers, and a logo that renders as question
|
|
24
|
+
* marks is worse than no logo — on the exact platform this is developed on.
|
|
25
|
+
* · **No colour escapes here.** The caller owns colour and honours NO_COLOR; a
|
|
26
|
+
* module that hard-codes them emits garbage the moment output is piped.
|
|
27
|
+
* · **Every line under 80 columns.** The narrowest terminal in real use is 80,
|
|
28
|
+
* and a wrapped banner does not read as dense, it reads as broken.
|
|
29
|
+
*
|
|
30
|
+
* ⭐ A NOTE ON DOING BETTER: `lib/terminal-graphics.mjs` can send a real PNG
|
|
31
|
+
* inline on Kitty and iTerm2. It is deliberately NOT used here — Windows
|
|
32
|
+
* Terminal speaks neither protocol, so the block art is what most users would
|
|
33
|
+
* see anyway, and one rendering that is the same everywhere beats two that
|
|
34
|
+
* disagree.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The Acuvo mark, resampled from the brand PNG. Seven rows: tall enough to be
|
|
39
|
+
* the mark rather than a smudge, short enough for something run forty times a
|
|
40
|
+
* day rather than opened once.
|
|
41
|
+
*/
|
|
42
|
+
const MARK = [
|
|
43
|
+
' ▄█',
|
|
44
|
+
' ▄███',
|
|
45
|
+
' ▄█▀▀██',
|
|
46
|
+
' ▄█▀ ▀██',
|
|
47
|
+
' ▄█▀ ██ ▀██',
|
|
48
|
+
' ▄████▀▀█████',
|
|
49
|
+
'█▀ █▀ ▀█',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const MARK_WIDTH = Math.max(...MARK.map((l) => l.length));
|
|
53
|
+
const GUTTER = 2;
|
|
54
|
+
export const MAX_BANNER_COLUMNS = 80;
|
|
55
|
+
|
|
56
|
+
/** Columns left for the detail rows once the mark and gutter are placed. */
|
|
57
|
+
const TEXT_COLUMNS = MAX_BANNER_COLUMNS - MARK_WIDTH - GUTTER;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the opening screen: mark on the left, facts on the right.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} o
|
|
63
|
+
* @param {string} o.version
|
|
64
|
+
* @param {string} o.workspace already shortened by the caller
|
|
65
|
+
* @param {string} o.model
|
|
66
|
+
* @param {string} o.billing who this run will charge
|
|
67
|
+
* @param {string} o.canRun what it may execute, or that it may not
|
|
68
|
+
* @param {boolean} [o.interactive] whether a prompt follows
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function openingScreen({ version, workspace, model, billing, canRun, interactive = false, paint = null }) {
|
|
72
|
+
/**
|
|
73
|
+
* ── ⭐ THE MARK IS BRAND GREEN, AND ONLY THE MARK ────────────────────────────
|
|
74
|
+
*
|
|
75
|
+
* Roman: *"can you colour our logo green in the terminal?"* #C8E91E, sampled
|
|
76
|
+
* from the brand PNG rather than picked by eye.
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ THE PAINTER IS INJECTED, NOT IMPORTED. The caller already decided whether
|
|
79
|
+
* this stream can take colour — it owns NO_COLOR, FORCE_COLOR, TERM=dumb and
|
|
80
|
+
* TTY detection. A module that reaches for colour itself will eventually
|
|
81
|
+
* disagree with that decision and write escapes into somebody's redirected
|
|
82
|
+
* file.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ AND ONLY THE LOGO. Colouring the detail rows would make the one line that
|
|
85
|
+
* must be read as a warning — `billing: YOUR OWN OpenRouter key` — compete
|
|
86
|
+
* with decoration.
|
|
87
|
+
*/
|
|
88
|
+
const brand = paint?.brand ?? ((s) => s);
|
|
89
|
+
/**
|
|
90
|
+
* ── ⚠️⚠️ ELIDED, BECAUSE THE VALUES ARE NOT OURS ────────────────────────────
|
|
91
|
+
*
|
|
92
|
+
* Every value arrives at runtime: a deep monorepo path, a long model id, the
|
|
93
|
+
* shell-mode warning. The first version assumed they would be short and its
|
|
94
|
+
* own test caught it wrapping at 80 columns on ordinary inputs.
|
|
95
|
+
*
|
|
96
|
+
* ⭐ ELIDED IN THE MIDDLE, NOT THE END. The informative parts of a path are
|
|
97
|
+
* the drive and the leaf; chopping the tail leaves
|
|
98
|
+
* `C:\Users\somebody\Projects\a-`, which identifies nothing. Same for a model
|
|
99
|
+
* id, where the family is at the front and the variant at the back.
|
|
100
|
+
*/
|
|
101
|
+
const room = TEXT_COLUMNS - 11;
|
|
102
|
+
const fit = (v) => {
|
|
103
|
+
const s = String(v ?? '');
|
|
104
|
+
if (s.length <= room) return s;
|
|
105
|
+
const head = Math.ceil((room - 1) / 2);
|
|
106
|
+
return `${s.slice(0, head)}…${s.slice(s.length - (room - 1 - head))}`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const right = [
|
|
110
|
+
`ACUVO CODE${version ? ` ${version}` : ''}`,
|
|
111
|
+
'',
|
|
112
|
+
`workspace ${fit(workspace)}`,
|
|
113
|
+
`model ${fit(model)}`,
|
|
114
|
+
`billing ${fit(billing)}`,
|
|
115
|
+
`can run ${fit(canRun)}`,
|
|
116
|
+
'',
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* ⚠️ THE TWO COLUMNS ARE ZIPPED, NOT CONCATENATED, and the row counts are
|
|
121
|
+
* allowed to differ — whichever is shorter simply runs out. Assuming they
|
|
122
|
+
* match would break the layout the first time a row is added to either side.
|
|
123
|
+
*/
|
|
124
|
+
const rows = Math.max(MARK.length, right.length);
|
|
125
|
+
const lines = [''];
|
|
126
|
+
for (let i = 0; i < rows; i += 1) {
|
|
127
|
+
const text = right[i] ?? '';
|
|
128
|
+
/**
|
|
129
|
+
* ⚠️ PADDED FIRST, PAINTED SECOND — escape codes have no width, so padding a
|
|
130
|
+
* coloured string aligns the text against invisible bytes and the whole
|
|
131
|
+
* right-hand column drifts.
|
|
132
|
+
*
|
|
133
|
+
* ⚠️⚠️ AND PADDED ONLY WHEN SOMETHING FOLLOWS. On a mark-only row the
|
|
134
|
+
* padding sits INSIDE the colour, before the reset, where the `trimEnd`
|
|
135
|
+
* below cannot reach it — so the coloured banner carried trailing
|
|
136
|
+
* whitespace the plain one did not. Invisible, but it means colour changed
|
|
137
|
+
* the layout, which is exactly what the guard forbids.
|
|
138
|
+
*/
|
|
139
|
+
const rawMark = MARK[i] ?? '';
|
|
140
|
+
const padded = text ? rawMark.padEnd(MARK_WIDTH + GUTTER) : rawMark;
|
|
141
|
+
const mark = MARK[i] ? brand(padded) : padded;
|
|
142
|
+
lines.push(`${mark}${text}`.trimEnd());
|
|
143
|
+
}
|
|
144
|
+
lines.push('');
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* ⚠️ ONLY WHEN A PROMPT ACTUALLY FOLLOWS. Printing "type what you want done"
|
|
148
|
+
* above a one-shot run that has already been given its task is an instruction
|
|
149
|
+
* for something the user cannot do, on the screen of a thing already working.
|
|
150
|
+
*/
|
|
151
|
+
if (interactive) {
|
|
152
|
+
lines.push(' Type what you want done. /help for commands · exit to leave', '');
|
|
153
|
+
}
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
package/lib/colour.mjs
CHANGED
|
@@ -32,6 +32,21 @@ const CODES = {
|
|
|
32
32
|
green: '\x1b[32m', // it worked
|
|
33
33
|
red: '\x1b[31m', // it failed or was refused
|
|
34
34
|
cyan: '\x1b[36m', // a remote thing: MCP, a service
|
|
35
|
+
/**
|
|
36
|
+
* ── ⭐ THE BRAND GREEN, SAMPLED FROM THE MARK ITSELF ───────────────────────
|
|
37
|
+
*
|
|
38
|
+
* #C8E91E — the most common opaque pixel in
|
|
39
|
+
* `console/public/brand/acuvo-mark.png`, read rather than eyeballed, so the
|
|
40
|
+
* logo in a terminal is the same green as the one on the website.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ TRUECOLOR WITH A 256-COLOUR FALLBACK. `ESC[38;2;R;G;Bm` is exact and
|
|
43
|
+
* widely supported (Windows Terminal, iTerm2, kitty, modern VS Code) — but a
|
|
44
|
+
* terminal that does not speak it renders the sequence as LITERAL TEXT across
|
|
45
|
+
* the logo. Index 190 is the nearest lime in the 256-colour cube and is
|
|
46
|
+
* understood almost everywhere, so it ships unless truecolor is advertised.
|
|
47
|
+
*/
|
|
48
|
+
brand: '\x1b[38;5;190m',
|
|
49
|
+
brandTrue: '\x1b[38;2;200;233;30m',
|
|
35
50
|
};
|
|
36
51
|
|
|
37
52
|
/**
|
|
@@ -66,6 +81,16 @@ export function createPainter(enabled = colourEnabled()) {
|
|
|
66
81
|
green: wrap(CODES.green),
|
|
67
82
|
red: wrap(CODES.red),
|
|
68
83
|
cyan: wrap(CODES.cyan),
|
|
84
|
+
/**
|
|
85
|
+
* ⚠️ TRUECOLOR ONLY WHEN THE TERMINAL SAYS SO. `COLORTERM=truecolor` is the
|
|
86
|
+
* signal every emitter uses; without it a 24-bit sequence can print as raw
|
|
87
|
+
* text, which would smear escape codes across the first thing a user sees.
|
|
88
|
+
*/
|
|
89
|
+
brand: wrap(
|
|
90
|
+
(process.env.COLORTERM === 'truecolor' || process.env.COLORTERM === '24bit')
|
|
91
|
+
? CODES.brandTrue
|
|
92
|
+
: CODES.brand,
|
|
93
|
+
),
|
|
69
94
|
};
|
|
70
95
|
}
|
|
71
96
|
|
package/lib/turn.mjs
CHANGED
|
@@ -5135,8 +5135,54 @@ function providerPinWarning(outcome) {
|
|
|
5135
5135
|
* and no file on disk. The counted lines below come from the executor's return
|
|
5136
5136
|
* values, which is the only place that knows.
|
|
5137
5137
|
*/
|
|
5138
|
+
/**
|
|
5139
|
+
* ── ⭐⭐⭐ A CONVERSATION IS NOT A BUILD, AND MUST NOT REPORT LIKE ONE ────────
|
|
5140
|
+
*
|
|
5141
|
+
* Roman asked "hi how are you" and got back: a round header, the answer twice,
|
|
5142
|
+
* "No files changed", "⚠ NOTHING WAS RUN, so nothing here is verified", a token
|
|
5143
|
+
* count, a cache percentage and a budget ledger. *"it shouldn't say that stuff."*
|
|
5144
|
+
*
|
|
5145
|
+
* ⭐ HE IS RIGHT, AND EVERY ONE OF THOSE LINES IS CORRECT. They exist because a
|
|
5146
|
+
* BUILD that silently changed nothing, or claimed a test passed without running
|
|
5147
|
+
* it, is a real and expensive failure — those lines are the honesty machinery.
|
|
5148
|
+
* But honesty machinery aimed at a greeting is just noise, and noise is how a
|
|
5149
|
+
* user learns to stop reading the warnings that matter.
|
|
5150
|
+
*
|
|
5151
|
+
* ⚠️ THE RULE IS NOT "BE QUIET", IT IS "REPORT WHAT HAPPENED". A turn that wrote
|
|
5152
|
+
* nothing and ran nothing has nothing to verify, so the verification notice is
|
|
5153
|
+
* answering a question nobody asked. The moment a file is written or a command
|
|
5154
|
+
* runs, every line comes back — including on the same session's next turn.
|
|
5155
|
+
*
|
|
5156
|
+
* ⚠️ ERRORS, WARNINGS AND STOP REASONS ARE NEVER SUPPRESSED. Quiet applies only
|
|
5157
|
+
* to the accounting of a turn that did nothing; a turn that FAILED did something
|
|
5158
|
+
* and says so.
|
|
5159
|
+
*/
|
|
5160
|
+
export function isConversationalTurn(outcome) {
|
|
5161
|
+
if (!outcome?.ok) return false;
|
|
5162
|
+
/**
|
|
5163
|
+
* ── ⚠️ ZERO TOOL CALLS, NOT "WROTE NOTHING" — MY FIRST RULE WAS TOO BROAD ──
|
|
5164
|
+
*
|
|
5165
|
+
* It asked "did it write or run anything", which made a turn that READ a file
|
|
5166
|
+
* conversational. That is wrong: the user asked it to look at something, it
|
|
5167
|
+
* did, and "No files changed" is a real answer to a real question. An existing
|
|
5168
|
+
* guard caught it — *"a run that truly did nothing still says so plainly"*,
|
|
5169
|
+
* driven with a single `read_file`.
|
|
5170
|
+
*
|
|
5171
|
+
* ⭐ The line is between USING A TOOL and answering from the conversation. A
|
|
5172
|
+
* greeting calls nothing at all, and that is the only case with no work to
|
|
5173
|
+
* account for.
|
|
5174
|
+
*/
|
|
5175
|
+
return (outcome.executed ?? []).length === 0;
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5138
5178
|
export function formatSummary(outcome) {
|
|
5139
5179
|
const lines = [];
|
|
5180
|
+
/**
|
|
5181
|
+
* Computed once, up here, so every gate below reads the same fact — the shape
|
|
5182
|
+
* where two call sites recompute "did anything happen" and quietly disagree is
|
|
5183
|
+
* one this codebase has paid for repeatedly.
|
|
5184
|
+
*/
|
|
5185
|
+
const quiet = isConversationalTurn(outcome);
|
|
5140
5186
|
|
|
5141
5187
|
if (!outcome.ok) {
|
|
5142
5188
|
lines.push('');
|
|
@@ -5216,7 +5262,12 @@ export function formatSummary(outcome) {
|
|
|
5216
5262
|
* ours to make.
|
|
5217
5263
|
*/
|
|
5218
5264
|
const ranAProcess = outcome.executed.some((e) => PROCESS_STARTING_VERBS.has(e.name) && e.result?.ok === true);
|
|
5219
|
-
|
|
5265
|
+
/**
|
|
5266
|
+
* ⚠️ NOT ON A CONVERSATIONAL TURN. "No files changed" answers "did the build
|
|
5267
|
+
* do anything?" — a question nobody asked by saying hello. The sentence is
|
|
5268
|
+
* correct and, aimed at a greeting, it is noise.
|
|
5269
|
+
*/
|
|
5270
|
+
if (!quiet) lines.push(ranAProcess
|
|
5220
5271
|
? 'No files were written through the file tools — but a command ran, and a command can '
|
|
5221
5272
|
+ 'change files this count cannot see. Check `git status` if that matters.'
|
|
5222
5273
|
: 'No files changed.');
|
|
@@ -5350,7 +5401,17 @@ export function formatSummary(outcome) {
|
|
|
5350
5401
|
for (const f of p.findings.slice(0, 4)) lines.push(` ${f}`);
|
|
5351
5402
|
}
|
|
5352
5403
|
}
|
|
5353
|
-
} else if (outcome.allowRun && (outcome.maxRounds ?? 1) > 1) {
|
|
5404
|
+
} else if (outcome.allowRun && (outcome.maxRounds ?? 1) > 1 && !quiet) {
|
|
5405
|
+
/**
|
|
5406
|
+
* ⚠️ `!quiet` — A TURN THAT WROTE NOTHING HAS NOTHING TO VERIFY. This
|
|
5407
|
+
* warning exists because a BUILD that claims a test passed without running
|
|
5408
|
+
* it is an expensive lie. Printed under a greeting it is answering a
|
|
5409
|
+
* question nobody asked, and a warning that fires when it does not apply
|
|
5410
|
+
* is how people learn to stop reading warnings.
|
|
5411
|
+
*
|
|
5412
|
+
* ⚠️ IT COMES STRAIGHT BACK the moment a file is written or a command
|
|
5413
|
+
* runs — including on the very next turn of the same session.
|
|
5414
|
+
*/
|
|
5354
5415
|
/**
|
|
5355
5416
|
* ⚠️ IT NAMES THE CATEGORY, NOT ONE TOOL. This line used to end "the model
|
|
5356
5417
|
* never called run_command", and by the time three separate verbs could
|
|
@@ -5476,7 +5537,8 @@ export function formatSummary(outcome) {
|
|
|
5476
5537
|
}
|
|
5477
5538
|
|
|
5478
5539
|
const cost = outcome.usage?.cost;
|
|
5479
|
-
|
|
5540
|
+
// ⚠️ A greeting does not need a token count, a cache percentage and a price.
|
|
5541
|
+
if (typeof cost === 'number' && !quiet) {
|
|
5480
5542
|
const tokens = outcome.usage?.total_tokens;
|
|
5481
5543
|
const roundsPart = outcome.roundsUsed > 1 ? ` · ${outcome.roundsUsed} rounds` : '';
|
|
5482
5544
|
lines.push('');
|
|
@@ -5512,7 +5574,12 @@ export function formatSummary(outcome) {
|
|
|
5512
5574
|
* cost, so the total is an estimate" clause, and a second copy of that
|
|
5513
5575
|
* arithmetic is how the two would disagree.
|
|
5514
5576
|
*/
|
|
5515
|
-
|
|
5577
|
+
/**
|
|
5578
|
+
* ⚠️ `!quiet` — a budget ledger under a greeting is the same noise as the
|
|
5579
|
+
* token count above it. `/cost` exists for anyone who wants the number, and a
|
|
5580
|
+
* turn that spends anything real prints it unprompted.
|
|
5581
|
+
*/
|
|
5582
|
+
if (typeof outcome.budgetReport === 'string' && outcome.budgetReport && !quiet) {
|
|
5516
5583
|
lines.push('');
|
|
5517
5584
|
lines.push(outcome.budgetReport);
|
|
5518
5585
|
}
|