aiden-runtime 4.6.1 → 4.8.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.
Files changed (34) hide show
  1. package/README.md +499 -265
  2. package/dist/cli/v4/aidenCLI.js +44 -5
  3. package/dist/cli/v4/callbacks.js +52 -31
  4. package/dist/cli/v4/chatSession.js +46 -1
  5. package/dist/cli/v4/commands/help.js +22 -11
  6. package/dist/cli/v4/commands/runs.js +42 -24
  7. package/dist/cli/v4/commands/skills.js +15 -17
  8. package/dist/cli/v4/commands/usage.js +17 -5
  9. package/dist/cli/v4/daemonAgentBuilder.js +13 -4
  10. package/dist/cli/v4/design/tokens.js +265 -0
  11. package/dist/cli/v4/display/framedPanel.js +116 -0
  12. package/dist/cli/v4/display/toolTrail.js +2 -2
  13. package/dist/cli/v4/display.js +446 -164
  14. package/dist/cli/v4/onboarding/disclaimer.js +42 -10
  15. package/dist/cli/v4/onboarding/loading.js +24 -1
  16. package/dist/cli/v4/onboarding/successScreen.js +17 -8
  17. package/dist/cli/v4/replyRenderer.js +74 -58
  18. package/dist/cli/v4/setupWizard.js +19 -2
  19. package/dist/cli/v4/skinEngine.js +13 -0
  20. package/dist/cli/v4/table.js +65 -8
  21. package/dist/core/v4/aidenAgent.js +42 -14
  22. package/dist/core/v4/auxiliaryClient.js +46 -13
  23. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +13 -8
  24. package/dist/core/v4/promptBuilder.js +45 -0
  25. package/dist/core/v4/sandboxFs.js +1 -1
  26. package/dist/core/v4/subagent/childBuilder.js +13 -4
  27. package/dist/core/v4/subagent/spawnSubAgent.js +7 -1
  28. package/dist/core/v4/ui/banner.js +16 -16
  29. package/dist/core/version.js +1 -1
  30. package/dist/moat/approvalEngine.js +14 -0
  31. package/dist/moat/honestyEnforcement.js +143 -241
  32. package/dist/tools/v4/index.js +54 -0
  33. package/dist/tools/v4/subagent/spawnSubAgentTool.js +23 -0
  34. package/package.json +10 -4
@@ -60,9 +60,24 @@ const readline = __importStar(require("node:readline"));
60
60
  const banner_1 = require("../../../core/v4/ui/banner");
61
61
  const theme_1 = require("../../../core/v4/ui/theme");
62
62
  const version_1 = require("../../../core/version");
63
- const DISCLAIMER_PARA = 'Aiden is a semi-autonomous AI agent that can touch your files, ' +
64
- 'browser, and shell. Open source read the code if you want. ' +
65
- 'Started as a hobby project, built solo, still rough in spots.';
63
+ // v4.8.0 Slice 10c replaced the single-paragraph prose with two
64
+ // scannable bullet lists (capability + acknowledgments). Legal terms
65
+ // surfaced as a checklist instead of buried inside prose.
66
+ const DISCLAIMER_HEAD = 'Aiden is an autonomous AI engine that runs on your machine. Aiden can:';
67
+ const CAPABILITY_BULLETS = [
68
+ 'Read, write, and modify files on your computer',
69
+ 'Execute shell commands and run code',
70
+ 'Browse the web and interact with online services',
71
+ 'Connect to AI providers using YOUR API keys (BYOK)',
72
+ 'Generate and execute new skills based on your prompts',
73
+ ];
74
+ const ACK_HEAD = 'By continuing, you acknowledge:';
75
+ const ACK_BULLETS = [
76
+ 'Aiden operates on your behalf with full local-system access',
77
+ 'You are responsible for outcomes of commands you approve',
78
+ 'Open source under AGPL-3.0 — read the code at github.com/taracodlabs/aiden',
79
+ 'This is beta software, built solo, still rough in spots',
80
+ ];
66
81
  /**
67
82
  * Word-wrap `text` to `width` columns. Preserves single spaces; does
68
83
  * not handle ANSI codes (callers pass plain text here).
@@ -97,18 +112,35 @@ function clearScreen(out) {
97
112
  out.write('\x1b[2J\x1b[H');
98
113
  }
99
114
  /**
100
- * Render the disclaimer body — banner + separator + wrapped paragraph.
101
- * Pure renderer; caller owns the write.
115
+ * Render the disclaimer body — v4.8.0 Slice 10c: banner + framed-panel
116
+ * capability list + acknowledgments. Orange `▎` bar on every line of
117
+ * the panel matches the rest of v4.8.0 chrome. `▸` bullets keep
118
+ * capability/ack items scannable rather than buried in prose.
102
119
  */
103
120
  function renderDisclaimerBody(version) {
104
121
  const w = (0, theme_1.termWidth)();
122
+ const innerW = Math.min(w - 4, 70);
105
123
  const body = [];
106
124
  body.push((0, banner_1.renderBanner)({ version }));
107
- body.push(' ' + (0, theme_1.separator)(Math.min(w - 4, 64)) + '\n');
108
- body.push('\n');
109
- const indent = ' ';
110
- for (const line of wrap(DISCLAIMER_PARA, Math.min(w - 4, 70))) {
111
- body.push(indent + theme_1.c.text(line) + '\n');
125
+ // Slice 10c framed-panel chrome. Orange bar at col 2; content + 2
126
+ // inner spaces; muted `─` divider between sections.
127
+ // v4.8.0 Slice 11c — `▎` swapped for `│` (U+2502); same swap as
128
+ // glyphs.panel.bar in the design tokens.
129
+ const bar = theme_1.c.primary('');
130
+ const divider = theme_1.c.muted('─'.repeat(innerW - 2));
131
+ const line = (s) => ` ${bar} ${s}\n`;
132
+ body.push(line(theme_1.c.text(DISCLAIMER_HEAD)));
133
+ body.push(line(''));
134
+ for (const item of CAPABILITY_BULLETS) {
135
+ body.push(line(theme_1.c.muted('▸ ') + theme_1.c.text(item)));
136
+ }
137
+ body.push(line(''));
138
+ body.push(line(divider));
139
+ body.push(line(''));
140
+ body.push(line(theme_1.c.text(ACK_HEAD)));
141
+ body.push(line(''));
142
+ for (const item of ACK_BULLETS) {
143
+ body.push(line(theme_1.c.muted('▸ ') + theme_1.c.text(item)));
112
144
  }
113
145
  body.push('\n');
114
146
  return body.join('');
@@ -86,13 +86,29 @@ async function runLoadingSequence(steps, opts = {}) {
86
86
  return { ok: results.every((r) => r.ok), steps: results };
87
87
  }
88
88
  out.write('\n ' + theme_1.c.text(heading) + '\n\n');
89
+ // v4.8.0 Slice 10c — progress bar above the step rows. 10 cells
90
+ // (●/○) split proportionally across the steps; each completed step
91
+ // fills floor(10 * (i+1) / N) cells. Uses the same hex-dot glyphs
92
+ // as the status footer's context bar for visual consistency.
93
+ const BAR_CELLS = 10;
94
+ const buildBar = (completed) => {
95
+ const filled = Math.min(BAR_CELLS, Math.floor((BAR_CELLS * completed) / steps.length));
96
+ const pct = Math.round((completed / steps.length) * 100);
97
+ const fillSeg = theme_1.c.primary('●'.repeat(filled));
98
+ const emptySeg = theme_1.c.muted('○'.repeat(BAR_CELLS - filled));
99
+ const label = completed < steps.length
100
+ ? theme_1.c.muted(steps[completed].label + '...')
101
+ : theme_1.c.muted('done');
102
+ return ` ${fillSeg}${emptySeg} ${theme_1.c.text(String(pct).padStart(3) + '%')} ${label}`;
103
+ };
104
+ out.write(buildBar(0) + '\n\n');
89
105
  // Pre-paint placeholder rows so the spinner overwrites in place.
90
106
  for (const step of steps) {
91
107
  const line = ' ' + theme_1.c.muted('·') + ' ' + rpad(step.label, labelCol) +
92
108
  ' ' + theme_1.c.muted(lpad('—', statusCol));
93
109
  out.write(line + '\n');
94
110
  }
95
- // Walk back up to the top of the block.
111
+ // Walk back up to the top of the step block.
96
112
  out.write(`\x1b[${steps.length}A`);
97
113
  for (let i = 0; i < steps.length; i++) {
98
114
  const step = steps[i];
@@ -127,6 +143,13 @@ async function runLoadingSequence(steps, opts = {}) {
127
143
  ' ' + lpad(statusText, statusCol) + ' ' + timing;
128
144
  out.write('\x1b[2K\r' + row + '\n');
129
145
  results.push({ label: step.label, ok, status, ms });
146
+ // v4.8.0 Slice 10c — repaint the progress bar above the step
147
+ // block after each step completes. Cursor is currently on the
148
+ // line below the just-completed step; walk up to the bar line
149
+ // (steps.length - i - 1 rows of remaining steps + 1 blank line
150
+ // separator + the bar itself), rewrite, then walk back down.
151
+ const upCount = (steps.length - i - 1) + 2;
152
+ out.write(`\x1b[${upCount}A\x1b[2K\r${buildBar(i + 1)}\x1b[${upCount}B\r`);
130
153
  }
131
154
  out.write('\n ' + (0, theme_1.separator)(Math.min(w - 4, 64)) + '\n');
132
155
  return { ok: results.every((r) => r.ok), steps: results };
@@ -47,22 +47,31 @@ function renderSuccessScreen(opts = {}) {
47
47
  out.write('setup-complete\n');
48
48
  return;
49
49
  }
50
+ // v4.8.0 Slice 10b — Aiden-native framed panel chrome. Each row
51
+ // carries the orange bar; content (title + examples + closing
52
+ // hint) preserved verbatim so content-level test assertions hold.
53
+ // v4.8.0 Slice 11c — `▎` swapped for `│` (U+2502); same swap as
54
+ // glyphs.panel.bar in the design tokens.
50
55
  const w = (0, theme_1.termWidth)();
51
56
  const sepW = Math.min(w - 4, 64);
52
57
  const narrow = w < 60;
53
- out.write('\n ' + (0, theme_1.separator)(sepW) + '\n');
54
- out.write('\n ' + (0, theme_1.bold)(theme_1.c.primary('All set!')) + '\n');
55
- out.write('\n ' + theme_1.c.text('Aiden is ready. Try these to start:') + '\n');
58
+ const bar = theme_1.c.primary('');
59
+ const divider = theme_1.c.muted(''.repeat(sepW - 2));
60
+ const line = (s) => ` ${bar} ${s}`;
56
61
  out.write('\n');
62
+ out.write(line((0, theme_1.bold)(theme_1.c.primary('All set!'))) + '\n');
63
+ out.write(line(divider) + '\n');
64
+ out.write(line(theme_1.c.text('Aiden is ready. Try these to start:')) + '\n');
65
+ out.write(line('') + '\n');
57
66
  if (narrow) {
58
- // Compact: a single suggestion line + the muted hello fallback.
59
- out.write(' ' + theme_1.c.muted('▸ ') + theme_1.c.accent(examples[0]) + '\n');
67
+ out.write(line(theme_1.c.muted('▸ ') + theme_1.c.accent(examples[0])) + '\n');
60
68
  }
61
69
  else {
62
70
  for (const ex of examples) {
63
- out.write(' ' + theme_1.c.muted('▸ ') + theme_1.c.accent(ex) + '\n');
71
+ out.write(line(theme_1.c.muted('▸ ') + theme_1.c.accent(ex)) + '\n');
64
72
  }
65
73
  }
66
- out.write('\n ' + theme_1.c.muted('Or just say hi.') + '\n');
67
- out.write('\n ' + (0, theme_1.separator)(sepW) + '\n\n');
74
+ out.write(line('') + '\n');
75
+ out.write(line(theme_1.c.muted('Or just say hi.')) + '\n');
76
+ out.write('\n');
68
77
  }
@@ -32,6 +32,8 @@ exports._resetForTests = _resetForTests;
32
32
  const marked_1 = require("marked");
33
33
  const skinEngine_1 = require("./skinEngine");
34
34
  const syntaxHighlight_1 = require("./syntaxHighlight");
35
+ // v4.8.0 Slice 8 — token-sourced bullet glyphs + task-list markers.
36
+ const tokens_1 = require("./design/tokens");
35
37
  // v4.1.4 reply-quality polish: single source of truth for frame math.
36
38
  // Replaces 3 inline `Math.min(process.stdout.columns ?? 80, 100) - 4`
37
39
  // callsites in this file with `getBodyWidth()` and adds soft-wrap for
@@ -131,61 +133,45 @@ function paintBold(kind) {
131
133
  const CODE_BG_ON = '\x1b[48;2;50;50;60m';
132
134
  const CODE_BG_OFF = '\x1b[49m';
133
135
  function renderCodeBlock(code, lang) {
136
+ // v4.8.0 Slice 9 hotfix — top-divider asymmetric chrome.
137
+ //
138
+ // ── python ─────────────────────────────────────────
139
+ // print("Hello, world!")
140
+ // greet("Aiden")
141
+ //
142
+ // The earlier Slice 9 version used `▎` left-rail on every line and
143
+ // visually competed with the dark-bg syntax highlighting. This
144
+ // revision drops the rail, keeps a single muted `──` top divider
145
+ // with the language label in brand orange, indents body content,
146
+ // and omits the bottom border (Slice 4 asymmetric signature).
147
+ // CODE_BG_ON/OFF envelope preserved.
134
148
  const sk = (0, skinEngine_1.getSkinEngine)();
135
- // v4.1.4 reply-quality polish: width sourced from frame.ts. Same
136
- // visual budget as the v4.1.3 formula (cols capped at 100, minus
137
- // gutter+2) — but expressed via the shared helper so it tracks any
138
- // future width-policy change in one place.
139
149
  const width = (0, frame_1.getBodyWidth)();
140
150
  const langLabel = (lang ?? '').trim();
141
- // v4.1.3-essentials reply-polish: language tag on the top rule
142
- // already shipped; keep it. Bottom rule unlabeled (closing fence).
143
- const top = langLabel
144
- ? `── ${langLabel} ${'─'.repeat(Math.max(0, width - langLabel.length - 4))}`
145
- : '─'.repeat(width);
146
- const bot = '─'.repeat(width);
147
151
  const body = (0, syntaxHighlight_1.isSupportedLang)(langLabel)
148
152
  ? (0, syntaxHighlight_1.highlightCode)(code, langLabel)
149
153
  : code;
150
- // v4.1.4 reply-quality polish: per-line soft wrap. The rail + bg
151
- // chrome adds 4 visible columns (` │ `, padding spaces around the
152
- // line). Subtract those so wrap math targets the actual content
153
- // budget. `hard: true` ensures even pathological long tokens
154
- // (minified JS, hashes) break instead of escaping the frame.
155
- //
156
- // Width inside the body of a code line:
157
- // gutter (3) + `│ ` (2) + leading-space (1) + CONTENT + trailing-space (1)
158
- // → content budget = width - gutter - 4. We further cap at width to
159
- // keep the fence rule aligned with the body's right margin.
160
- const contentBudget = Math.max(8, width - frame_1.GUTTER - 4);
161
- // v4.1.3-essentials reply-polish (preserved): each body line gets:
162
- // - frame gutter (was 2-space outer indent; now uses shared GUTTER)
163
- // - left rail `│ ` painted muted (mirrors blockquote's `┃ ` rail
164
- // with a different glyph so they're visually distinct)
165
- // - 24-bit dark background wrapping the rail + content (subtle
166
- // "this is code" affordance without going full TUI box-frame)
167
- const rail = sk.applyColors('│', 'muted');
168
- const gutter = (0, frame_1.getIndent)(0);
169
- // Wrap each source line independently — code-block semantics demand
170
- // that a "logical line" remains visible as one continued unit even
171
- // when soft-wrapped. The CODE_BG painting closes per VISUAL line so
172
- // a wrap break doesn't bleed bg across the rail of the next row.
154
+ const indent = ' ';
155
+ const hLine = tokens_1.glyphs.chrome.hLine;
156
+ // Top divider: `── <lang> ─────` (lang in brand) OR full-width
157
+ // `────────────` when no language declared.
158
+ const top = langLabel
159
+ ? `${indent}${sk.applyColors(`${hLine.repeat(2)} `, 'muted')}` +
160
+ `${sk.applyColors(langLabel, 'brand')}` +
161
+ ` ${sk.applyColors(hLine.repeat(Math.max(1, width - langLabel.length - 4)), 'muted')}`
162
+ : `${indent}${sk.applyColors(hLine.repeat(width), 'muted')}`;
163
+ // Body content lands at col 4 (4-space indent inside the divider).
164
+ // Width budget: leave room for body indent + CODE_BG envelope spaces.
165
+ const bodyIndent = ' ';
166
+ const contentBudget = Math.max(8, width - 6);
173
167
  const wrappedLines = [];
174
168
  for (const srcLine of body.split('\n')) {
175
169
  const wrapped = (0, frame_1.wrap)(srcLine, contentBudget, { trim: false, hard: true });
176
170
  for (const visualLine of wrapped.split('\n')) {
177
- wrappedLines.push(`${gutter}${rail} ${CODE_BG_ON} ${visualLine} ${CODE_BG_OFF}`);
171
+ wrappedLines.push(`${bodyIndent}${CODE_BG_ON} ${visualLine} ${CODE_BG_OFF}`);
178
172
  }
179
173
  }
180
- const indented = wrappedLines.join('\n');
181
- // Top + bottom fence rules sit at the gutter too — visually anchors
182
- // the block as a unit inside the assistant frame.
183
- return [
184
- `${gutter}${sk.applyColors(top, 'muted')}`,
185
- indented,
186
- `${gutter}${sk.applyColors(bot, 'muted')}`,
187
- '',
188
- ].join('\n') + '\n';
174
+ return [top, ...wrappedLines].join('\n') + '\n';
189
175
  }
190
176
  /**
191
177
  * Render a block quote with a `┃` left rail in muted colour.
@@ -337,7 +323,16 @@ function renderListItemTokens(it, parser) {
337
323
  // block parser handles these via the normal dispatch (which calls
338
324
  // back into our own `renderer.list` override for nested lists —
339
325
  // depth counter is already incremented before we got here).
326
+ //
327
+ // v4.8.0 Slice 8 hotfix — ensure inline text and following block
328
+ // tokens are separated by a newline. Without this, a tight-list
329
+ // item like `- Python` followed by a nested `- Django` collapses
330
+ // to `● Python ○ Django` on a single line because head/tail
331
+ // split in renderer.list takes only the first line as `head`.
340
332
  if (parser.parse) {
333
+ if (out.length > 0 && !out[out.length - 1].endsWith('\n')) {
334
+ out.push('\n');
335
+ }
341
336
  out.push(parser.parse([tk]));
342
337
  continue;
343
338
  }
@@ -535,6 +530,10 @@ function getReplyRenderer() {
535
530
  let isOrdered = false;
536
531
  let startNum = 1;
537
532
  let items;
533
+ // v4.8.0 Slice 8 — task/checked flags collected alongside items so
534
+ // the marker dispatch below can pick ✔ (checked) or ○ (unchecked).
535
+ // Default false (not a task) so the bullet path stays unchanged.
536
+ let itemTasks = [];
538
537
  // CRITICAL: increment depth BEFORE walking items. Item walking via
539
538
  // `parser.parse(it.tokens)` recurses into our own override for any
540
539
  // nested list tokens — those nested calls need to see the parent's
@@ -570,10 +569,14 @@ function getReplyRenderer() {
570
569
  // Confirmed against marked v15 token shapes from `marked.lexer`
571
570
  // (see scripts/smoke-issue-c-tokens.ts).
572
571
  const parser = this.parser;
573
- items = (tok.items ?? []).map((it) => {
574
- if (!parser)
575
- return it.text ?? '';
576
- return renderListItemTokens(it, parser);
572
+ // v4.8.0 Slice 8 capture GFM task/checked flags alongside the
573
+ // rendered text so the marker dispatch below can pick the right
574
+ // glyph (✔ checked / ○ unchecked) for task-list items.
575
+ const rawItems = tok.items ?? [];
576
+ items = rawItems.map((it) => parser ? renderListItemTokens(it, parser) : (it.text ?? ''));
577
+ itemTasks = rawItems.map((it) => {
578
+ const itx = it;
579
+ return { task: itx.task === true, checked: itx.checked === true };
577
580
  });
578
581
  }
579
582
  else {
@@ -587,20 +590,33 @@ function getReplyRenderer() {
587
590
  items = raw.split('\n').filter((ln) => ln.trim().length > 0);
588
591
  }
589
592
  const indent = ' '.repeat(depth);
590
- // Top-level bullet `•` (filled); nested `▸` (arrow-like) for
591
- // visual depth differentiation. Numbered lists override with
592
- // `N.` regardless of depth.
593
- const bulletGlyph = depth === 1 ? '•' : '▸';
593
+ // v4.8.0 Slice 8 — token-sourced bullet glyphs. Top-level (depth 1)
594
+ // uses filled `●`, nested (depth 2) uses hollow `○`. Both painted
595
+ // brand orange to give lists visual identity (was `muted` grey).
596
+ const bulletGlyph = depth === 1 ? tokens_1.glyphs.util.bullet : tokens_1.glyphs.util.bulletOpen;
594
597
  const lines = [];
595
598
  for (let i = 0; i < items.length; i += 1) {
596
599
  const item = items[i];
597
- // Each item may itself contain newlines (nested list output,
598
- // multi-line paragraph). Indent every line of the rendered
599
- // item AFTER the first the first line takes the bullet, the
600
- // continuation lines align under the bullet's content column.
601
- const marker = isOrdered
602
- ? paint('muted')(`${startNum + i}.`)
603
- : paint('muted')(bulletGlyph);
600
+ const task = itemTasks[i] ?? { task: false, checked: false };
601
+ // v4.8.0 Slice 8 marker dispatch:
602
+ // GFM checked task in semantic success (green)
603
+ // GFM unchecked task in tertiary dim (looks incomplete)
604
+ // • Ordered list → `N.` right-padded to 3 cols, brand orange
605
+ // • Bullet ●/○ by depth, brand orange
606
+ let marker;
607
+ if (task.task && task.checked) {
608
+ marker = paint('success')(tokens_1.glyphs.util.check);
609
+ }
610
+ else if (task.task) {
611
+ marker = paint('tertiary')(tokens_1.glyphs.util.bulletOpen);
612
+ }
613
+ else if (isOrdered) {
614
+ const numStr = `${startNum + i}.`.padStart(3);
615
+ marker = paint('brand')(numStr);
616
+ }
617
+ else {
618
+ marker = paint('brand')(bulletGlyph);
619
+ }
604
620
  const itemLines = item.split('\n');
605
621
  const head = itemLines[0] ?? '';
606
622
  const tail = itemLines.slice(1);
@@ -47,6 +47,8 @@ const box_1 = require("./box");
47
47
  // the cost of broken unit tests under the test runtime.
48
48
  const successScreen_1 = require("./onboarding/successScreen");
49
49
  const providerPicker_1 = require("./onboarding/providerPicker");
50
+ // v4.8.0 Slice 10b — bar + chrome tokens for step headers.
51
+ const tokens_1 = require("./design/tokens");
50
52
  const modelFetch_1 = require("../../core/v4/providers/modelFetch");
51
53
  const probe_1 = require("../../core/v4/providers/probe");
52
54
  // Phase 30.2.1 — provider order optimised for new-user time-to-first-chat.
@@ -537,8 +539,19 @@ async function runSetupWizard(opts = {}) {
537
539
  if (opts.prompts) {
538
540
  display.printBanner();
539
541
  }
540
- display.write('\nWelcomelet\'s pick a provider.\n');
541
- display.write(`${kleur_1.default.dim('(Press Enter to accept Groq free + fastest setup.)')}\n\n`);
542
+ // v4.8.0 Slice 10b step-header helper. Each major wizard step
543
+ // starts with ` ▎ Set up Aiden step N` painted with the orange
544
+ // panel bar so the flow visually consistent with /help and the
545
+ // approval panel. Inquirer widgets render below unchanged.
546
+ const stepHeader = (n) => {
547
+ const bar = display.applyColors(tokens_1.glyphs.panel.bar, 'brand');
548
+ const title = display.applyColors('Set up Aiden', 'heading');
549
+ const sub = display.applyColors(`step ${n}`, 'muted');
550
+ return `\n ${bar} ${title} ${sub}\n`;
551
+ };
552
+ display.write(stepHeader(1));
553
+ display.write(' Welcome — let\'s pick a provider.\n');
554
+ display.write(` ${kleur_1.default.dim('(Press Enter to accept Groq — free + fastest setup.)')}\n\n`);
542
555
  // Phase 30.2.1 — Groq is the new recommended default for first-time
543
556
  // users: free tier, fastest signup, and avoids the surprise charge
544
557
  // path of paid providers. Together AI moved to position [8] paid.
@@ -695,6 +708,9 @@ async function runSetupWizard(opts = {}) {
695
708
  // anyway, so the reorder bought nothing there. Existing test
696
709
  // fixtures provide inputs in legacy order; preserving custom's
697
710
  // order keeps them green.
711
+ if (provider.kind === 'key' || provider.kind === 'subscription' || provider.kind === 'local') {
712
+ display.write(stepHeader(2));
713
+ }
698
714
  let apiKey;
699
715
  let baseUrl;
700
716
  if (provider.kind === 'local') {
@@ -713,6 +729,7 @@ async function runSetupWizard(opts = {}) {
713
729
  }
714
730
  // provider.kind === 'custom' — defer credential prompts until AFTER
715
731
  // the model picker below.
732
+ display.write(stepHeader(3));
716
733
  // Step 3: live model fetch + pick.
717
734
  //
718
735
  // Test-harness gate: when the caller injected `opts.prompts` (only
@@ -66,6 +66,13 @@ const DEFAULT_SKIN = {
66
66
  // (which shares the colour) so callers can differentiate in code
67
67
  // even though they render identically.
68
68
  degraded: [0xff, 0xc1, 0x07],
69
+ // v4.8.0 Slice 7 hotfix #2 — purple accent for the turn-counter
70
+ // segment (⌘) in the status footer. #a48be0 reads as a soft
71
+ // lavender that doesn't compete with brand orange.
72
+ metric_turn: [0xa4, 0x8b, 0xe0],
73
+ // v4.8.0 Slice 8 — tertiary dim grey, dimmer than `muted` (warm
74
+ // tint) for lowest-priority text like unchecked task markers.
75
+ tertiary: [0x6a, 0x6a, 0x6a],
69
76
  },
70
77
  glyphs: {
71
78
  bullet: '•',
@@ -99,6 +106,10 @@ const LIGHT_SKIN = {
99
106
  heading: [0xc4, 0x42, 0x10],
100
107
  session: [0x00, 0x55, 0x88],
101
108
  degraded: [0x80, 0x60, 0x00],
109
+ // Slice 7 hotfix #2 — deeper purple on light bg keeps contrast budget.
110
+ metric_turn: [0x6e, 0x50, 0xaa],
111
+ // Slice 8 — lighter grey on light bg keeps the dim-but-readable feel.
112
+ tertiary: [0x9a, 0x9a, 0x9a],
102
113
  },
103
114
  glyphs: { ...DEFAULT_SKIN.glyphs },
104
115
  };
@@ -118,6 +129,8 @@ const MONOCHROME_SKIN = {
118
129
  heading: null,
119
130
  session: null,
120
131
  degraded: null,
132
+ metric_turn: null,
133
+ tertiary: null,
121
134
  },
122
135
  glyphs: {
123
136
  bullet: '*',
@@ -23,6 +23,7 @@ exports.renderTable = renderTable;
23
23
  const stringWidth = require('string-width');
24
24
  const skinEngine_1 = require("./skinEngine");
25
25
  const box_1 = require("./box");
26
+ const tokens_1 = require("./design/tokens");
26
27
  /**
27
28
  * Visible (post-ANSI-strip) column width. Falls back to
28
29
  * `visibleLength` from box.ts when string-width is unavailable
@@ -167,13 +168,32 @@ function renderTable(rows, cols, opts = {}) {
167
168
  }
168
169
  }
169
170
  }
170
- // Border characters (sharp ASCII).
171
- const TL = '┌', TR = '┐', BL = '└', BR = '┘';
172
- const T = '┬', B = '┴', L = '├', R = '┤';
173
- const X = '┼', H = '─', V = '│';
171
+ // Border characters — token-sourced from design/tokens.ts (v4.8.0 Slice 3).
172
+ const { topLeft: TL, topRight: TR, botLeft: BL, botRight: BR } = tokens_1.glyphs.chrome;
173
+ const { teeDown: T, teeUp: B, teeRight: L, teeLeft: R } = tokens_1.glyphs.chrome;
174
+ const { cross: X, hLine: H, vLine: V } = tokens_1.glyphs.chrome;
174
175
  const ind = ' '.repeat(indent);
175
- // Top border.
176
- const top = TL + widths.map((w) => H.repeat(w + 2)).join(T) + TR;
176
+ // Total inner content width across all cells + inner separators.
177
+ // Used by title-embedded top border + page footer.
178
+ const innerWidth = widths.reduce((s, w) => s + w + 2, 0) + (numCols - 1);
179
+ // v4.8.0 Slice 3 — top border with optional embedded title + count.
180
+ // Format: `┌─ title ──────────── totalCount ──┐`
181
+ // Pads the centre with `─` so the right edge stays aligned regardless
182
+ // of title / count length. Falls back to the legacy plain top border
183
+ // when neither field is supplied.
184
+ let top;
185
+ if (opts.title || opts.totalCount) {
186
+ const titleText = opts.title ? ` ${opts.title} ` : '';
187
+ const countText = opts.totalCount ? ` ${opts.totalCount} ` : '';
188
+ const fixed = vWidth(titleText) + vWidth(countText);
189
+ const filler = Math.max(0, innerWidth - fixed);
190
+ const titlePainted = opts.title ? skin.applyColors(titleText, 'heading') : '';
191
+ const countPainted = opts.totalCount ? skin.applyColors(countText, 'muted') : '';
192
+ top = TL + H + titlePainted + H.repeat(filler) + countPainted + H + TR;
193
+ }
194
+ else {
195
+ top = TL + widths.map((w) => H.repeat(w + 2)).join(T) + TR;
196
+ }
177
197
  // Header row — heading colour, padded. Truncate first if the
178
198
  // header itself is wider than the allocated width (rare, but
179
199
  // keeps borders aligned under aggressive narrow-width pressure).
@@ -203,13 +223,50 @@ function renderTable(rows, cols, opts = {}) {
203
223
  });
204
224
  bodyLines.push(V + cells.join(V) + V);
205
225
  });
206
- // Bottom border.
207
- const bot = BL + widths.map((w) => H.repeat(w + 2)).join(B) + BR;
226
+ // v4.8.0 Slice 3 — pagination footer above the bottom border. Renders
227
+ // `← prev · page X/Y · next →` centred inside the inner width. Side
228
+ // arrows are dim when the page is at the edge so users can read
229
+ // "at-end" cleanly. Caller wires hotkeys; we just paint the chrome.
230
+ let pageFooter = null;
231
+ if (opts.page) {
232
+ const { current, total } = opts.page;
233
+ const atStart = current <= 1;
234
+ const atEnd = current >= total;
235
+ const leftKind = atStart ? 'muted' : 'session';
236
+ const rightKind = atEnd ? 'muted' : 'session';
237
+ const left = skin.applyColors('← prev', leftKind);
238
+ const mid = skin.applyColors(`page ${current}/${total}`, 'muted');
239
+ const right = skin.applyColors('next →', rightKind);
240
+ const sep = skin.applyColors(' · ', 'muted');
241
+ const body = `${left}${sep}${mid}${sep}${right}`;
242
+ const bodyW = vWidth('← prev') + vWidth(` · page ${current}/${total} · `) + vWidth('next →');
243
+ const padW = Math.max(0, innerWidth - bodyW);
244
+ const lpad = Math.floor(padW / 2);
245
+ const rpad = padW - lpad;
246
+ pageFooter = V + ' '.repeat(lpad) + body + ' '.repeat(rpad) + V;
247
+ }
248
+ // Bottom border. Skip the inner tees when the title-style top was
249
+ // used (legacy plain bottom keeps column alignment for un-titled
250
+ // tables; a title-only border on top reads cleanest with a plain
251
+ // bottom mirror).
252
+ const bot = (opts.title || opts.totalCount)
253
+ ? BL + H.repeat(innerWidth) + BR
254
+ : BL + widths.map((w) => H.repeat(w + 2)).join(B) + BR;
255
+ // v4.8.0 Slice 3 — empty-state path. Borders stay so the layout
256
+ // weight matches a populated table; the body is one centered line.
257
+ if (rows.length === 0 && opts.emptyMessage) {
258
+ const msg = skin.applyColors(opts.emptyMessage, 'muted');
259
+ const pad = Math.max(0, innerWidth - vWidth(opts.emptyMessage));
260
+ const lpad = Math.floor(pad / 2);
261
+ const emptyRow = V + ' '.repeat(lpad) + msg + ' '.repeat(pad - lpad) + V;
262
+ return [top, emptyRow, bot].map((l) => ind + l).join('\n') + '\n';
263
+ }
208
264
  const allLines = [
209
265
  top,
210
266
  headerRow,
211
267
  ...(showRule ? [rule] : []),
212
268
  ...bodyLines,
269
+ ...(pageFooter ? [pageFooter] : []),
213
270
  bot,
214
271
  ].map((l) => ind + l);
215
272
  return allLines.join('\n') + '\n';
@@ -166,6 +166,7 @@ class AidenAgent {
166
166
  this.resolveVerifiedFlag = opts.resolveVerifiedFlag;
167
167
  this.resolveToolset = opts.resolveToolset;
168
168
  this.resolveMutates = opts.resolveMutates;
169
+ this.resolveUiOnly = opts.resolveUiOnly;
169
170
  this.promptBuilder = opts.promptBuilder;
170
171
  this.promptBuilderOptions = opts.promptBuilderOptions;
171
172
  this.contextCompressor = opts.contextCompressor;
@@ -372,25 +373,21 @@ class AidenAgent {
372
373
  // 8. Run the tool-calling loop.
373
374
  const loopResult = await this.runTurnLoop(messages, narrowedTools, trackers, options);
374
375
  // 9. Honesty post-loop scan (only if loop ended with a normal stop).
376
+ //
377
+ // v4.7.0 Phase 2.3 — the verifier now records deterministic
378
+ // outcome events from `toolCallTrace` (not regex over the
379
+ // assistant's text). When `findings.length > 0` AND mode is
380
+ // `enforce`, it returns an append-only `footer` we concatenate
381
+ // to `finalContent`. The model's text is NEVER rewritten —
382
+ // that was the v4.6.x failure mode this verifier replaces.
375
383
  let honestyFindings;
376
384
  let finalContent = loopResult.finalContent;
377
385
  if (this.honestyEnforcement && loopResult.finishReason === 'stop') {
378
386
  try {
379
387
  const scan = await this.honestyEnforcement.check(finalContent, loopResult.messages, loopResult.toolCallTrace);
380
- if (!scan.passed) {
381
- honestyFindings = scan.findings;
382
- if (scan.correctedResponse) {
383
- finalContent = scan.correctedResponse;
384
- // Reflect the corrected text in the message history too so
385
- // /debug-prompt and /usage agree on the final string.
386
- for (let i = loopResult.messages.length - 1; i >= 0; i--) {
387
- const m = loopResult.messages[i];
388
- if (m.role === 'assistant' && (!m.toolCalls || m.toolCalls.length === 0)) {
389
- loopResult.messages[i].content = finalContent;
390
- break;
391
- }
392
- }
393
- }
388
+ honestyFindings = scan.findings;
389
+ if (scan.footer) {
390
+ finalContent = `${finalContent}\n\n${scan.footer}`;
394
391
  }
395
392
  }
396
393
  catch {
@@ -843,6 +840,28 @@ class AidenAgent {
843
840
  finalContent = '';
844
841
  break;
845
842
  }
843
+ // v4.8.0 — uiOnly tools are signal channels, not executable
844
+ // tools. The model calls them to communicate render-time
845
+ // state. Dispatch loop skips execute / iteration / mutation
846
+ // marking / verifier / trace / observability hooks and fires
847
+ // onUiEvent on the caller. A '(no output)' tool_result is
848
+ // pushed to satisfy the provider protocol (every tool_call_id
849
+ // needs a matching tool_result). Listener exceptions are
850
+ // swallowed so a bad UI handler cannot break the turn.
851
+ if (this.resolveUiOnly?.(call.name) === true) {
852
+ turnToolMessages.push({
853
+ role: 'tool',
854
+ toolCallId: call.id,
855
+ content: '(no output)',
856
+ });
857
+ try {
858
+ runOptions.onUiEvent?.(call.name, call.arguments);
859
+ }
860
+ catch {
861
+ // defensive — UI listener faults must never break dispatch
862
+ }
863
+ continue;
864
+ }
846
865
  this.onToolCall?.(call, 'before');
847
866
  // v4.2 Phase 4 — mark any active checkpoints as containing a
848
867
  // mutating call BEFORE dispatch. Done pre-dispatch (not post)
@@ -970,6 +989,15 @@ class AidenAgent {
970
989
  result: result.result,
971
990
  error: result.error,
972
991
  verified: this.resolveVerifiedFlag?.(result),
992
+ // v4.7.0 Phase 2.3 — stamp the handler's `mutates` flag
993
+ // at dispatch time so the post-loop honesty verifier can
994
+ // distinguish mutating vs read-only failures without
995
+ // needing a registry handle. Defaults to `false` for
996
+ // unknown tools (the resolver returns undefined) — read-
997
+ // only tools that error are surfaced via the tool-trail
998
+ // row already; the verifier deliberately stays quiet
999
+ // about them.
1000
+ handlerMutates: this.resolveMutates?.(call.name) ?? false,
973
1001
  // v4.2 Phase 1 — verification surfaces alongside the trace
974
1002
  // entry for downstream callers (chatSession, loopTrace,
975
1003
  // future RecoveryReport). Undefined when TCE is off.