aiden-runtime 4.1.4 → 4.1.5

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.
@@ -1057,6 +1057,34 @@ async function buildAgentRuntime(cliOpts, opts) {
1057
1057
  // diagnostics must not break the loop
1058
1058
  }
1059
1059
  },
1060
+ // v4.1.5 Issue K — phase lifecycle hooks for the activity indicator
1061
+ // verb mutation. Each fires at a specific point in runConversation:
1062
+ // - onMemoryRefreshStart: before memory I/O begins
1063
+ // - onPromptBuilt: after system prompt assembly
1064
+ // - onProviderRequestStart: just before the HTTP request opens
1065
+ // chatSession registers handlers that call `indicator.setVerb()` so
1066
+ // the user sees the model's actual workflow phase during the gap.
1067
+ // All three are forwarded through `callbacks` so chatSession owns
1068
+ // the indicator handle (created per-turn). Defensive try/catch on
1069
+ // each — a misbehaving display sink never blocks the agent loop.
1070
+ onMemoryRefreshStart: () => {
1071
+ try {
1072
+ callbacks.onMemoryRefreshStart?.();
1073
+ }
1074
+ catch { /* defensive */ }
1075
+ },
1076
+ onPromptBuilt: (info) => {
1077
+ try {
1078
+ callbacks.onPromptBuilt?.(info);
1079
+ }
1080
+ catch { /* defensive */ }
1081
+ },
1082
+ onProviderRequestStart: (id) => {
1083
+ try {
1084
+ callbacks.onProviderRequestStart?.(id);
1085
+ }
1086
+ catch { /* defensive */ }
1087
+ },
1060
1088
  // Phase 23.4b — feed the agent's Stage-0 intent pre-arm with the
1061
1089
  // skill's `required_tools` from its SKILL.md frontmatter. Returns
1062
1090
  // null when the skill is unknown / unloaded / empty so the agent
@@ -63,6 +63,29 @@ class CliCallbacks {
63
63
  this.toolRows = new Map();
64
64
  this.toolStartTimes = new Map();
65
65
  this.firstToolFiredThisTurn = false;
66
+ // v4.1.5 Issue K — `firePhaseVerb` is the public entry point for the
67
+ // AidenCLI bridge. AidenAgent fires `onMemoryRefreshStart` etc.,
68
+ // aidenCLI's adapter calls into one of these `onPhase…` shims, each
69
+ // mapping a lifecycle event to a verb string. Defensive try/catch so
70
+ // a misbehaving display sink can't unwind the agent loop.
71
+ this.onMemoryRefreshStart = () => {
72
+ try {
73
+ this.phaseVerbHook?.('refreshing memory');
74
+ }
75
+ catch { /* defensive */ }
76
+ };
77
+ this.onPromptBuilt = (_info) => {
78
+ try {
79
+ this.phaseVerbHook?.('preparing prompt');
80
+ }
81
+ catch { /* defensive */ }
82
+ };
83
+ this.onProviderRequestStart = (_providerId) => {
84
+ try {
85
+ this.phaseVerbHook?.('calling provider');
86
+ }
87
+ catch { /* defensive */ }
88
+ };
66
89
  /**
67
90
  * Phase 23.5 — bound to AidenAgent.onToolCall. Emits one event row
68
91
  * per tool call: prints `[running]` on `before`, mutates the bracket
@@ -91,6 +114,14 @@ class CliCallbacks {
91
114
  this.beforeToolHook?.();
92
115
  }
93
116
  catch { /* defensive */ }
117
+ // v4.1.5+ Path A — fire the loop-trace sink BEFORE row writes.
118
+ // Captures every tool's call.id + name (including hidden ones
119
+ // suppressed from the visible trail) so the trace covers the
120
+ // full agent loop, not just user-visible work.
121
+ try {
122
+ this.toolTraceBeforeHook?.(call.id, call.name);
123
+ }
124
+ catch { /* defensive */ }
94
125
  const handle = this.display.toolRow(call.name, call.arguments);
95
126
  this.toolRows.set(call.id, handle);
96
127
  this.toolStartTimes.set(call.id, Date.now());
@@ -109,12 +140,25 @@ class CliCallbacks {
109
140
  this.afterEachToolHook?.(call.name);
110
141
  }
111
142
  catch { /* defensive */ }
143
+ // v4.1.5+ Path A — loop-trace sink fires even when handle was
144
+ // lost (rare; happens if before/after pairing slipped) so the
145
+ // trace never under-counts tool calls.
146
+ try {
147
+ this.toolTraceAfterHook?.(call.id, call.name, call.arguments);
148
+ }
149
+ catch { /* defensive */ }
112
150
  return;
113
151
  }
114
152
  const ms = Date.now() - startedAt;
115
153
  const err = result?.error;
116
154
  if (typeof err === 'string' && err.includes('URL provenance gate')) {
117
155
  handle.blocked();
156
+ // v4.1.5+ Path A — blocked path still needs the trace sink so
157
+ // the URL-provenance failure mode shows up in loop diagnostics.
158
+ try {
159
+ this.toolTraceAfterHook?.(call.id, call.name, call.arguments);
160
+ }
161
+ catch { /* defensive */ }
118
162
  return;
119
163
  }
120
164
  // v4.1.4 reply-quality polish — Part 1.6. Helper used by ALL
@@ -127,6 +171,14 @@ class CliCallbacks {
127
171
  this.afterEachToolHook?.(call.name);
128
172
  }
129
173
  catch { /* defensive */ }
174
+ // v4.1.5+ Path A — also fire the loop-trace `after` sink so the
175
+ // tracer can compute duration + capture args (hidden-from-trail
176
+ // tools still flow through here, by design — the trace must see
177
+ // them to detect lookup_tool_schema / skill_view loops).
178
+ try {
179
+ this.toolTraceAfterHook?.(call.id, call.name, call.arguments);
180
+ }
181
+ catch { /* defensive */ }
130
182
  };
131
183
  if (typeof err === 'string' && err.includes('URL provenance gate')) {
132
184
  handle.blocked();
@@ -337,6 +389,32 @@ Reply with ONE word: safe, caution, or dangerous.`;
337
389
  this.beforeToolHook = opts.beforeTool;
338
390
  this.afterEachToolHook = opts.afterEachTool;
339
391
  }
392
+ /**
393
+ * v4.1.5 Issue K — set/clear the phase-verb sink. chatSession
394
+ * registers a closure that captures the per-turn indicator handle
395
+ * and forwards calls to `indicator.setVerb(verb)`. Cleared between
396
+ * turns by passing `undefined`. Optional — non-indicator callers
397
+ * (test harnesses with stub displays) get no-op behaviour.
398
+ */
399
+ setPhaseVerbHook(fn) {
400
+ this.phaseVerbHook = fn;
401
+ }
402
+ /**
403
+ * v4.1.5+ Path A — register a per-turn tool-trace sink for the
404
+ * loop-trace logger. `before` fires with the call's id+name BEFORE
405
+ * the row writes; `after` fires post-execution with the same id +
406
+ * the call's args (for skill-name extraction in trace context).
407
+ * Cleared between turns by passing `undefined`.
408
+ *
409
+ * Separate from `setActivityIndicatorHooks` because the activity
410
+ * hook is name-only and fires for visible-trail purposes; this
411
+ * one captures FULL call data including hidden tools (which the
412
+ * trail suppresses via TRAIL_HIDE_TOOLS but the trace must see).
413
+ */
414
+ setToolTraceHook(opts) {
415
+ this.toolTraceBeforeHook = opts.before;
416
+ this.toolTraceAfterHook = opts.after;
417
+ }
340
418
  }
341
419
  exports.CliCallbacks = CliCallbacks;
342
420
  // Tier-3.1 (v4.1-tier3.1): replaced 🟢/🟡/🔴 emoji badges with
@@ -69,6 +69,11 @@ exports.renderProgressBar = renderProgressBar;
69
69
  exports.formatTokens = formatTokens;
70
70
  exports.formatDuration = formatDuration;
71
71
  exports.renderMemoryConfirmations = renderMemoryConfirmations;
72
+ // v4.1.5+ Path A: env-var-gated loop trace logger. Captures tool-call
73
+ // sequence + system prompt + memory hashes when a turn shows loop
74
+ // symptoms (10+ calls OR 5+ consecutive same-name). Default off via
75
+ // `AIDEN_DEBUG_LOOP=1` env-var. Zero overhead when disabled.
76
+ const loopTrace_1 = require("../../core/v4/loopTrace");
72
77
  const display_1 = require("./display");
73
78
  // v4.1.4 Part 1.6 — per-turn token progress bar. Fed by `onProgress`
74
79
  // events from the streaming adapter; hidden when the adapter doesn't
@@ -934,6 +939,62 @@ class ChatSession {
934
939
  const indicator = this.opts.display.activityIndicator('thinking');
935
940
  let indicatorStopped = false;
936
941
  let streamingActive = false;
942
+ // v4.1.5 Issue O — track whether this turn had any tool calls so
943
+ // we can emit a single muted rule between the tool trail and the
944
+ // reply header. Set true when the first tool's `before` phase
945
+ // fires (via the existing beforeFirstToolHook plumbing). Emitted
946
+ // once per turn — `separatorEmitted` gates idempotency against
947
+ // both streaming and non-streaming paths reaching the same hook.
948
+ //
949
+ // v4.1.5 Phase 1d (Q-OBV-b) — multi-tool separator regression:
950
+ // the prior v4.1.5 Phase 1c emission point was the streaming
951
+ // `onFirstDelta` callback, but that fires PER provider call
952
+ // (the agent resets `firstDeltaFired` each callProvider
953
+ // invocation), and on multi-tool turns where the model emits
954
+ // no preamble in early iterations + no preamble in the final
955
+ // reply iteration either, the relative ordering of "first
956
+ // delta" vs "first tool" could leave the flag/idempotency
957
+ // gate in an unexpected state. Definitive fix: tie emission
958
+ // to the FIRST STREAM BYTE LANDING ON SCREEN, which only
959
+ // happens once per turn regardless of how many provider
960
+ // iterations occurred. `firstStreamByteSeen` is the new gate;
961
+ // separator fires from inside `onDelta` BEFORE `streamPartial`
962
+ // writes the agent header.
963
+ let turnHadTools = false;
964
+ let separatorEmitted = false;
965
+ let firstStreamByteSeen = false;
966
+ // v4.1.5+ Path A: per-turn loop tracer (env-var gated, default off).
967
+ // Captures tool-call sequence + assembled system prompt + memory
968
+ // hashes + recent skills when a turn trips loop thresholds. The
969
+ // `onLoopWarning` callback surfaces a one-line dim hint to the
970
+ // user when consecutive-same-tool count crosses 8 — gives them a
971
+ // chance to Ctrl+C before the agent burns more budget.
972
+ const loopTracer = new loopTrace_1.LoopTracer({
973
+ paths: this.opts.paths,
974
+ providerId: this.currentProviderId,
975
+ modelId: this.currentModelId,
976
+ onLoopWarning: (line) => {
977
+ try {
978
+ this.opts.display.dim(line);
979
+ }
980
+ catch { /* defensive */ }
981
+ },
982
+ });
983
+ if (loopTracer.isEnabled()) {
984
+ loopTracer.setHistory(baseHistory);
985
+ }
986
+ const emitToolReplySeparator = () => {
987
+ if (separatorEmitted || !turnHadTools)
988
+ return;
989
+ separatorEmitted = true;
990
+ // Same chrome pattern as the existing pre-turn rule (line ~1100)
991
+ // and the post-reply rule (line ~1297): two-space indent + the
992
+ // body-width muted rule + newline. The 2-space indent is the
993
+ // legacy convention used by adjacent rules; the v4.1.5 frame
994
+ // gutter (3) is consciously NOT applied here so all three rules
995
+ // in a turn share one left edge.
996
+ this.opts.display.write(` ${this.opts.display.rule()}\n`);
997
+ };
937
998
  const stopIndicatorOnce = () => {
938
999
  if (indicatorStopped)
939
1000
  return;
@@ -946,12 +1007,55 @@ class ChatSession {
946
1007
  this.opts.callbacks.setActivityIndicatorHooks?.({});
947
1008
  }
948
1009
  catch { /* defensive */ }
1010
+ // v4.1.5 Issue K — also clear the phase-verb sink so lifecycle
1011
+ // events fired during async cleanup don't try to update a
1012
+ // stopped indicator.
1013
+ try {
1014
+ this.opts.callbacks.setPhaseVerbHook?.(undefined);
1015
+ }
1016
+ catch { /* defensive */ }
1017
+ // v4.1.5+ Path A — clear the loop-trace sink so subsequent
1018
+ // turns don't fire into a stale tracer. Note: this clears the
1019
+ // HOOK, not the tracer's accumulated state — finalize() still
1020
+ // runs at end-of-try below to write the snapshot if thresholds
1021
+ // tripped.
1022
+ try {
1023
+ this.opts.callbacks.setToolTraceHook?.({});
1024
+ }
1025
+ catch { /* defensive */ }
949
1026
  };
1027
+ // v4.1.5 Issue K — wire the per-turn phase-verb sink. Each
1028
+ // AidenAgent lifecycle event (memory refresh start, prompt built,
1029
+ // provider request start) flows through CliCallbacks and lands
1030
+ // here as a verb string ("refreshing memory" / "preparing prompt"
1031
+ // / "calling provider"). The closure captures the per-turn
1032
+ // indicator handle so verb mutations stay scoped to this turn.
1033
+ this.opts.callbacks.setPhaseVerbHook?.((verb) => {
1034
+ if (indicatorStopped)
1035
+ return;
1036
+ indicator.setVerb(verb);
1037
+ });
1038
+ // v4.1.5+ Path A — wire the loop-trace sink. Fires for EVERY tool
1039
+ // call (including hidden ones) so the trace captures the full
1040
+ // agent loop. Defensive — when AIDEN_DEBUG_LOOP is unset, the
1041
+ // tracer's `startTool`/`endTool` short-circuit immediately.
1042
+ this.opts.callbacks.setToolTraceHook?.({
1043
+ before: (id, name) => loopTracer.startTool(id, name),
1044
+ after: (id, name, args) => loopTracer.endTool(id, name, args),
1045
+ });
950
1046
  // Phase 23.5 carried forward: stop the indicator the moment the
951
1047
  // first tool row prints — the row itself is the activity surface
952
1048
  // during a tool. Part 1.6 then resumes via `afterEachTool` so the
953
1049
  // post-tool gap has its own indicator paint.
954
- this.opts.callbacks.setBeforeFirstToolHook?.(stopIndicatorOnce);
1050
+ //
1051
+ // v4.1.5 Issue O — also flip `turnHadTools = true` so the
1052
+ // separator emits before the reply header. Single hook captures
1053
+ // "any tool ran this turn" cleanly (it only fires for the FIRST
1054
+ // tool of the turn — subsequent tools don't re-trigger).
1055
+ this.opts.callbacks.setBeforeFirstToolHook?.(() => {
1056
+ turnHadTools = true;
1057
+ stopIndicatorOnce();
1058
+ });
955
1059
  // Part 1.6: pause/resume hooks around every tool row. The
956
1060
  // `beforeTool` hook fires before EACH tool row writes (not just
957
1061
  // the first), so multi-tool sequences also keep the indicator
@@ -991,10 +1095,32 @@ class ChatSession {
991
1095
  ? () => {
992
1096
  stopIndicatorOnce();
993
1097
  streamingActive = true;
1098
+ // v4.1.5 Phase 1d (Q-OBV-b) — separator emission MOVED
1099
+ // out of onFirstDelta because that callback fires per
1100
+ // provider-call iteration (firstDeltaFired resets each
1101
+ // callProvider invocation). The separator-emit now
1102
+ // lives in onDelta below, gated by `firstStreamByteSeen`
1103
+ // which only flips once per turn.
994
1104
  }
995
1105
  : undefined,
996
1106
  onDelta: streamingEnabled
997
1107
  ? (text) => {
1108
+ // v4.1.5 Phase 1d (Q-OBV-b) — definitive separator
1109
+ // emission point. This is the FIRST text byte landing
1110
+ // on screen this turn. Fires the muted rule BEFORE
1111
+ // streamPartial writes the `┃ Aiden` header so the
1112
+ // visual order is:
1113
+ // ┊ tool rows...
1114
+ // ──────────── ← separator
1115
+ // ┃ Aiden
1116
+ // {text}
1117
+ // Idempotent via `firstStreamByteSeen` + the
1118
+ // `separatorEmitted` flag inside emitToolReplySeparator.
1119
+ // No-op when no tool fired (turnHadTools=false).
1120
+ if (!firstStreamByteSeen) {
1121
+ firstStreamByteSeen = true;
1122
+ emitToolReplySeparator();
1123
+ }
998
1124
  // v4.1.4 Part 1.6: bar lives ABOVE streamed text. Hide
999
1125
  // it before each delta writes so the stream output
1000
1126
  // doesn't land on the bar's line. The bar repaints on
@@ -1055,6 +1181,11 @@ class ChatSession {
1055
1181
  // When streaming was active and emitted the final content already,
1056
1182
  // skip the markdown re-render — we'd otherwise duplicate text.
1057
1183
  if (result.finalContent && !streamingActive) {
1184
+ // v4.1.5 Issue O — non-streaming reply path. Emit the muted
1185
+ // rule between the tool trail and the agent header before
1186
+ // the one-shot reply lands. Idempotent + tool-gated by
1187
+ // `emitToolReplySeparator`.
1188
+ emitToolReplySeparator();
1058
1189
  this.opts.display.write(this.opts.display.agentTurn(result.finalContent));
1059
1190
  }
1060
1191
  if (this.sessionId) {
@@ -1068,6 +1199,18 @@ class ChatSession {
1068
1199
  // post-turn status footer.
1069
1200
  this.opts.display.write(` ${this.opts.display.rule()}\n`);
1070
1201
  this.renderStatusLine();
1202
+ // v4.1.5+ Path A — finalize the loop trace. No-op if the env
1203
+ // var is unset OR if the turn didn't trip any threshold. When
1204
+ // it DOES emit, the snapshot path goes to a dim status line so
1205
+ // the user (and any teammate they're sharing the log with)
1206
+ // knows where to grab the diagnostic file.
1207
+ try {
1208
+ const snapPath = await loopTracer.finalize();
1209
+ if (snapPath) {
1210
+ this.opts.display.dim(`[loop-trace] wrote ${snapPath}`);
1211
+ }
1212
+ }
1213
+ catch { /* defensive */ }
1071
1214
  }
1072
1215
  catch (err) {
1073
1216
  stopIndicatorOnce();
@@ -1116,6 +1259,16 @@ class ChatSession {
1116
1259
  }
1117
1260
  this.setStatusState({ kind: 'ready' });
1118
1261
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
1262
+ // v4.1.5+ Path A — finalize the loop trace on the error path
1263
+ // too. Loop patterns that ended in an error are exactly the
1264
+ // ones most worth capturing for diagnosis.
1265
+ try {
1266
+ const snapPath = await loopTracer.finalize();
1267
+ if (snapPath) {
1268
+ this.opts.display.dim(`[loop-trace] wrote ${snapPath}`);
1269
+ }
1270
+ }
1271
+ catch { /* defensive */ }
1119
1272
  }
1120
1273
  }
1121
1274
  // ── Startup card (Phase 26.2.4: neofetch-style sectioned) ──────────
@@ -30,8 +30,8 @@ exports.PREVIOUS_BUNDLED_SOULS = exports.DEFAULT_SOUL_MD = exports.BUNDLED_SOUL_
30
30
  // <act_dont_ask>. ensureSoulMdSeeded compares this against the user's
31
31
  // on-disk SOUL.md to decide whether to silent-replace (matches a prior
32
32
  // bundled default) or preserve+notify (user-edited).
33
- exports.BUNDLED_SOUL_VERSION = 'v4.1.4';
34
- exports.DEFAULT_SOUL_MD = `You are Aiden — a local-first AI agent built by Taracod.
33
+ exports.BUNDLED_SOUL_VERSION = 'v4.1.5';
34
+ exports.DEFAULT_SOUL_MD = `You are Aiden — a local-first AI agent built by Shiva Deore at Taracod.
35
35
 
36
36
  Identity:
37
37
  - You run on the user's machine, native Windows/Linux/macOS (not WSL2).
@@ -47,7 +47,7 @@ Voice:
47
47
 
48
48
  Behavior:
49
49
  - Default to action over discussion. The user wants results.
50
- - When asked who you are, identify as Aiden. Not "a large language model."
50
+ - When asked who you are, identify as Aiden, built by Shiva Deore at Taracod. Not "a large language model."
51
51
  - When asked what you can do, mention specific skills/tools, not generic capabilities.
52
52
  - If user mentions trading/NSE/markets, you have specialized skills for that.
53
53
 
@@ -318,6 +318,78 @@ the tool calls within a single turn instead of returning halfway and
318
318
  asking the user what to do next.
319
319
  </keep_going>
320
320
 
321
+ Limits:
322
+ - You can't bypass approval prompts for dangerous commands.
323
+ - You don't lie to look smart. If you don't know, you say so.
324
+ `,
325
+ // v4.1.4 default — Voice rewrite ("Match the user's energy. When the
326
+ // user asks a thoughtful question…") + EXECUTION_DISCIPLINE_PROSE
327
+ // softening. Identity attribution still "built by Taracod" only —
328
+ // v4.1.5 adds "Shiva Deore at" to both identity lines. Users who
329
+ // installed any v4.1.4.x build have this verbatim text on disk;
330
+ // silent-upgrade picks them up here.
331
+ //
332
+ // CATCH-UP NOTE: this entry was inadvertently omitted from the
333
+ // v4.1.4 ship (the snapshot appended at that time matched v4.1.2's
334
+ // Voice block, not the v4.1.4 rewrite). v4.1.5 appends it so the
335
+ // migration path catches v4.1.4 installs correctly.
336
+ `You are Aiden — a local-first AI agent built by Taracod.
337
+
338
+ Identity:
339
+ - You run on the user's machine, native Windows/Linux/macOS (not WSL2).
340
+ - You have 72 bundled skills + access to install more via skills.sh.
341
+ - You remember past sessions via persistent storage.
342
+ - You have 40 tools spanning files, browser, terminal, web, memory.
343
+
344
+ Voice:
345
+ - Match the user's energy. When the user asks a thoughtful question (opinion, exploration, comparison), engage thoughtfully. When the user asks transactionally, stay tight.
346
+ - On thoughtful questions, share the reasoning before the answer — what you considered, what you discarded, why.
347
+ - Honest above all — if you didn't do something, say so. If you're not sure, say so.
348
+ - You never claim to "have run" a tool unless the trace shows it.
349
+
350
+ Behavior:
351
+ - Default to action over discussion. The user wants results.
352
+ - When asked who you are, identify as Aiden. Not "a large language model."
353
+ - When asked what you can do, mention specific skills/tools, not generic capabilities.
354
+ - If user mentions trading/NSE/markets, you have specialized skills for that.
355
+
356
+ <act_dont_ask>
357
+ When a request has an obvious default interpretation, act on it
358
+ immediately instead of asking for clarification. Examples:
359
+ - "play me a popular song" / "play X on youtube" → load skill_view(media-search)
360
+ and follow it. Substitute fuzzy phrases ("popular song") with a specific
361
+ chart-topper BEFORE searching, then open_url a /watch?v= URL once.
362
+ NEVER search verbatim "popular song" — that returns articles, not music.
363
+ - "what files are in my Downloads?" → file_list on Downloads. Don't ask
364
+ "which user?" — it's the current user.
365
+ - "is port 443 open?" → check this machine. Don't ask "open where?"
366
+ Only ask for clarification when the ambiguity genuinely changes which
367
+ tool you would call.
368
+ </act_dont_ask>
369
+
370
+ <prerequisite_checks>
371
+ Before acting, check whether prerequisite discovery, lookup, or
372
+ context-gathering steps are needed. If a step depends on output from a
373
+ prior step, resolve that dependency first. Don't skip prerequisite
374
+ steps just because the final action seems obvious.
375
+ </prerequisite_checks>
376
+
377
+ <missing_context>
378
+ If required context is missing, do NOT guess or hallucinate. Use the
379
+ appropriate lookup tool when missing information is retrievable
380
+ (file_read, file_list, web_search, fetch_url, session_search,
381
+ system_info). Ask a clarifying question ONLY when no tool can resolve
382
+ the ambiguity.
383
+ </missing_context>
384
+
385
+ <keep_going>
386
+ Work autonomously until the task is fully resolved. Don't stop with a
387
+ plan — execute it. Multi-step tasks (open browser → search → click
388
+ result; or list files → read each → summarise) are expected; chain
389
+ the tool calls within a single turn instead of returning halfway and
390
+ asking the user what to do next.
391
+ </keep_going>
392
+
321
393
  Limits:
322
394
  - You can't bypass approval prompts for dangerous commands.
323
395
  - You don't lie to look smart. If you don't know, you say so.
@@ -52,8 +52,22 @@ const display_1 = require("../display");
52
52
  */
53
53
  const BAR_CELLS = 10;
54
54
  /** Glyphs. Chosen for clean visual weight at the standard mono font. */
55
- const FILLED = '▰';
56
- const EMPTY = '▱';
55
+ // v4.1.5 Phase 1d (Q-P1) — glyph palette switch.
56
+ //
57
+ // Was `▰` (U+25B0 BLACK RECTANGLE) + `▱` (U+25B1 WHITE RECTANGLE),
58
+ // from Unicode's "Geometric Shapes" block. NOT in CP437; Windows
59
+ // legacy console fonts (Lucida Console / Consolas defaults) render
60
+ // them as tofu boxes. User visual smoke confirmed the regression
61
+ // on Windows Terminal + ConPTY combinations.
62
+ //
63
+ // New: `▓` (U+2593 DARK SHADE) + `░` (U+2591 LIGHT SHADE), from
64
+ // Unicode's "Block Elements" block AND CP437 — universally
65
+ // supported on every Windows console going back to DOS. Same shade
66
+ // family the existing `statusFooter` (display.ts:773) has shipped
67
+ // with since v3 without ever being garbled. Cleaner palette
68
+ // coherence across all bar surfaces.
69
+ const FILLED = '▓';
70
+ const EMPTY = '░';
57
71
  /**
58
72
  * Create a progress-bar handle bound to a writable stream + skin.
59
73
  * The bar paints on the next `update` call — there's no initial
@@ -89,19 +103,38 @@ function createProgressBar(out, skin) {
89
103
  const gutter = (0, frame_1.getIndent)(0);
90
104
  return `${gutter}${bar} ${label}`;
91
105
  };
106
+ // v4.1.5 Part 1a — Issue M (Windows ConPTY buffering fix).
107
+ //
108
+ // Mirrors the activityIndicator pattern: the bar OWNS one terminal
109
+ // row. Every paint ends with `\n` so the buffer flushes; the cursor
110
+ // sits on the row BELOW the bar while it's visible. Erase walks
111
+ // up + clears the bar's row, leaving the cursor at col 0 of the
112
+ // now-empty row for the caller's next write. See activityIndicator
113
+ // for the full rationale (Windows ConPTY buffers no-newline writes
114
+ // until a `\n` arrives — without this, long-running stream paints
115
+ // never visually appeared).
116
+ const ANSI_UP_ERASE = '\x1b[1A\x1b[2K';
92
117
  const paint = () => {
93
118
  if (!isTty || hidden)
94
119
  return;
95
- // `\r\x1b[K` — carriage return + erase to end of line, then
96
- // rewrite. Same single-line overwrite pattern as the activity
97
- // indicator and tool-row live tick.
98
- out.write(`\r\x1b[K${buildLine()}`);
99
- printed = true;
120
+ if (printed) {
121
+ // Subsequent paint: walk up to the bar's row, clear it, rewrite,
122
+ // drop a newline so the cursor lands on the row BELOW the bar.
123
+ out.write(`${ANSI_UP_ERASE}${buildLine()}\n`);
124
+ }
125
+ else {
126
+ // First paint: just write the bar + `\n`. Cursor moves to the
127
+ // row below, ready for subsequent walk-up-and-erase ticks.
128
+ out.write(`${buildLine()}\n`);
129
+ printed = true;
130
+ }
100
131
  lastPaintTokens = outputTokens;
101
132
  };
102
133
  const erase = () => {
134
+ // Walk up to the bar's row and clear it. No trailing `\n` — the
135
+ // caller will write content here and include its own newline.
103
136
  if (isTty && printed)
104
- out.write('\r\x1b[K');
137
+ out.write(ANSI_UP_ERASE);
105
138
  };
106
139
  return {
107
140
  update(n, max) {