aiden-runtime 4.8.1 → 4.9.1

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 (100) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +37 -6
  3. package/dist/cli/v4/chatSession.js +53 -13
  4. package/dist/cli/v4/commands/daemon.js +53 -3
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +45 -26
  7. package/dist/cli/v4/commands/help.js +5 -0
  8. package/dist/cli/v4/commands/hooks.js +466 -0
  9. package/dist/cli/v4/commands/hooksSlash.js +33 -0
  10. package/dist/cli/v4/commands/index.js +13 -1
  11. package/dist/cli/v4/commands/mcp.js +89 -1
  12. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  13. package/dist/cli/v4/commands/memory.js +707 -0
  14. package/dist/cli/v4/commands/memorySlash.js +38 -0
  15. package/dist/cli/v4/commands/recovery.js +1 -1
  16. package/dist/cli/v4/commands/skin.js +7 -0
  17. package/dist/cli/v4/commands/theme.js +217 -0
  18. package/dist/cli/v4/commands/trigger.js +1 -1
  19. package/dist/cli/v4/design/tokens.js +52 -4
  20. package/dist/cli/v4/display.js +39 -26
  21. package/dist/cli/v4/replyRenderer.js +6 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/cli/v4/ui/progressBar.js +179 -0
  24. package/dist/cli/v4/util/closestAction.js +48 -0
  25. package/dist/core/v4/aidenAgent.js +45 -2
  26. package/dist/core/v4/daemon/api/runs.js +131 -0
  27. package/dist/core/v4/daemon/bootstrap.js +368 -13
  28. package/dist/core/v4/daemon/db/migrations.js +169 -0
  29. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  30. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  31. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  32. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  33. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  34. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  35. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  36. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  37. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  38. package/dist/core/v4/daemon/triggerBus.js +50 -19
  39. package/dist/core/v4/hooks/auditQuery.js +67 -0
  40. package/dist/core/v4/hooks/dispatcher.js +286 -0
  41. package/dist/core/v4/hooks/index.js +46 -0
  42. package/dist/core/v4/hooks/lifecycle.js +27 -0
  43. package/dist/core/v4/hooks/manifest.js +142 -0
  44. package/dist/core/v4/hooks/registry.js +149 -0
  45. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  46. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  47. package/dist/core/v4/hooks/trust.js +14 -0
  48. package/dist/core/v4/identity/contextManager.js +83 -0
  49. package/dist/core/v4/identity/daemonId.js +85 -0
  50. package/dist/core/v4/identity/enforcement.js +103 -0
  51. package/dist/core/v4/identity/executionContext.js +153 -0
  52. package/dist/core/v4/identity/hookExecution.js +62 -0
  53. package/dist/core/v4/identity/httpContext.js +68 -0
  54. package/dist/core/v4/identity/ids.js +185 -0
  55. package/dist/core/v4/identity/index.js +60 -0
  56. package/dist/core/v4/identity/subprocessContext.js +98 -0
  57. package/dist/core/v4/identity/traceparent.js +114 -0
  58. package/dist/core/v4/logger/index.js +3 -1
  59. package/dist/core/v4/logger/logger.js +28 -1
  60. package/dist/core/v4/logger/redact.js +149 -0
  61. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  62. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  63. package/dist/core/v4/mcp/install/backup.js +78 -0
  64. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  65. package/dist/core/v4/mcp/install/clients.js +203 -0
  66. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  67. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  68. package/dist/core/v4/mcp/install/profiles.js +109 -0
  69. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  70. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  71. package/dist/core/v4/memory/projectRoot.js +76 -0
  72. package/dist/core/v4/memory/reviewer/index.js +162 -0
  73. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  74. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  75. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  76. package/dist/core/v4/memoryManager.js +57 -10
  77. package/dist/core/v4/paths.js +2 -0
  78. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  79. package/dist/core/v4/theme/bundledThemes.js +106 -0
  80. package/dist/core/v4/theme/themeLoader.js +160 -0
  81. package/dist/core/v4/theme/themeRegistry.js +97 -0
  82. package/dist/core/v4/theme/themeWatcher.js +95 -0
  83. package/dist/core/v4/toolRegistry.js +71 -8
  84. package/dist/core/v4/update/depWarningFilter.js +76 -0
  85. package/dist/core/v4/update/executeInstall.js +41 -35
  86. package/dist/core/v4/update/platformInstructions.js +128 -0
  87. package/dist/moat/approvalEngine.js +4 -0
  88. package/dist/moat/memoryGuard.js +8 -1
  89. package/dist/providers/v4/anthropicAdapter.js +10 -4
  90. package/dist/tools/v4/backends/local.js +19 -2
  91. package/dist/tools/v4/sessions/recallSession.js +6 -1
  92. package/package.json +3 -1
  93. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  94. package/themes/default.yaml +52 -0
  95. package/themes/dracula.yaml +32 -0
  96. package/themes/light.yaml +32 -0
  97. package/themes/monochrome.yaml +31 -0
  98. package/themes/tokyo-night.yaml +32 -0
  99. package/dist/core/pluginSystem.js +0 -121
  100. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memory = exports.MEMORY_SHELL_ONLY = void 0;
4
+ exports.dispatchMemorySlash = dispatchMemorySlash;
5
+ const memory_1 = require("./memory");
6
+ /** Actions that need the full CLI surface (confirmation / destructive). */
7
+ exports.MEMORY_SHELL_ONLY = new Set(['remove', 'restore']);
8
+ /**
9
+ * Pure dispatch — exported for tests + reuse. Either prints a shell
10
+ * hint OR delegates to the provided `runMemory` runner. Side effects
11
+ * confined to the supplied `write` sink.
12
+ */
13
+ async function dispatchMemorySlash(opts) {
14
+ const a = (opts.action || 'list').toLowerCase();
15
+ if (exports.MEMORY_SHELL_ONLY.has(a)) {
16
+ opts.write(`⚠ /memory ${a} not available inside chat (destructive operation)\n`);
17
+ opts.write(' Quit (/quit) and run from shell:\n\n');
18
+ const tail = opts.args.length > 0 ? ' ' + opts.args.join(' ') : '';
19
+ opts.write(` aiden memory ${a}${tail}\n`);
20
+ return;
21
+ }
22
+ await opts.runMemory(a, opts.args, { writeOut: opts.write, writeErr: opts.write });
23
+ }
24
+ exports.memory = {
25
+ name: 'memory',
26
+ description: 'Manage memory (list / show / add / namespaces / pending / approve / review).',
27
+ category: 'system',
28
+ icon: '🧠',
29
+ handler: async (ctx) => {
30
+ await dispatchMemorySlash({
31
+ action: ctx.args[0] ?? 'list',
32
+ args: ctx.args.slice(1),
33
+ write: (s) => ctx.display.write(s),
34
+ runMemory: memory_1.runMemorySubcommand,
35
+ });
36
+ return {};
37
+ },
38
+ };
@@ -35,7 +35,7 @@ function pad(value, width) {
35
35
  }
36
36
  exports.recovery = {
37
37
  name: 'recovery',
38
- description: 'Inspect recurring failure patterns + recoveries (v4.6 Phase 3b).',
38
+ description: 'Inspect recurring failure patterns + recoveries.',
39
39
  category: 'system',
40
40
  icon: '🩹',
41
41
  handler: async (ctx) => {
@@ -7,6 +7,13 @@ exports.skin = {
7
7
  category: 'system',
8
8
  icon: '🎨',
9
9
  handler: async (ctx) => {
10
+ // v4.9.0 Slice 1a — /skin is now an alias for the new /theme system.
11
+ // The legacy color skins (~/.aiden/skins/*.yaml, RGB-tuple format)
12
+ // continue to work alongside the new theme system; /skin still
13
+ // manages the SkinEngine palette. Theme tokens (panel chrome,
14
+ // status footer glyphs, shimmer) are controlled by /theme.
15
+ ctx.display.warn('/skin is deprecated in v4.9 — use /theme for full visual customisation. ' +
16
+ '/skin continues to work for legacy colour skins.');
10
17
  const engine = ctx.skin;
11
18
  if (!engine) {
12
19
  ctx.display.warn('Skin engine not wired in this context.');
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.theme = void 0;
7
+ /**
8
+ * Copyright (c) 2026 Shiva Deore (Taracod).
9
+ * Licensed under AGPL-3.0. See LICENSE for details.
10
+ *
11
+ * Aiden — local-first agent.
12
+ */
13
+ /**
14
+ * cli/v4/commands/theme.ts — v4.9.0 Slice 1a.
15
+ *
16
+ * `/theme` show current theme name + path + hint
17
+ * `/theme reload` force re-read of ~/.aiden/theme.yaml
18
+ * `/theme reset` delete ~/.aiden/theme.yaml, restore baseline
19
+ * `/theme edit` print ~/.aiden/theme.yaml path
20
+ *
21
+ * `/theme list` and `/theme set <name>` ship in Slice 1b alongside
22
+ * the remaining 4 bundled themes.
23
+ */
24
+ const node_fs_1 = require("node:fs");
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ const themeLoader_1 = require("../../../core/v4/theme/themeLoader");
27
+ const themeRegistry_1 = require("../../../core/v4/theme/themeRegistry");
28
+ const bundledThemes_1 = require("../../../core/v4/theme/bundledThemes");
29
+ function themeFilePath(ctx) {
30
+ const root = ctx.paths?.root;
31
+ if (!root)
32
+ return null;
33
+ return node_path_1.default.join(root, 'theme.yaml');
34
+ }
35
+ function userThemesDir(ctx) {
36
+ const root = ctx.paths?.root;
37
+ if (!root)
38
+ return null;
39
+ return node_path_1.default.join(root, 'themes');
40
+ }
41
+ function listUserThemes(dir) {
42
+ if (!dir || !(0, node_fs_1.existsSync)(dir))
43
+ return [];
44
+ try {
45
+ return (0, node_fs_1.readdirSync)(dir)
46
+ .filter((f) => f.endsWith('.yaml'))
47
+ .map((f) => {
48
+ const name = f.replace(/\.yaml$/, '');
49
+ let description = '';
50
+ try {
51
+ const yaml = (0, node_fs_1.readFileSync)(node_path_1.default.join(dir, f), 'utf8');
52
+ const { parsed } = (0, themeLoader_1.parseThemeYaml)(yaml);
53
+ description = parsed?.description ?? '';
54
+ }
55
+ catch { /* tolerate unreadable */ }
56
+ return { name, description };
57
+ });
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ exports.theme = {
64
+ name: 'theme',
65
+ description: 'Show, reload, reset, or open the user theme.yaml.',
66
+ category: 'system',
67
+ icon: '🎨',
68
+ handler: async (ctx) => {
69
+ const sub = ctx.rawArgs.trim();
70
+ const themePath = themeFilePath(ctx);
71
+ if (sub === '' || sub === 'show') {
72
+ const current = (0, themeRegistry_1.getCurrentName)();
73
+ const active = (0, themeRegistry_1.getActivePath)();
74
+ ctx.display.info(`Active theme: ${current}`);
75
+ if (active) {
76
+ ctx.display.info(`Source: ${active}`);
77
+ }
78
+ else if (themePath) {
79
+ ctx.display.info(`No user theme.yaml at ${themePath} — using bundled default.`);
80
+ }
81
+ ctx.display.info('Use /theme list, /theme set <name>, /theme reload, /theme reset, or /theme edit.');
82
+ return {};
83
+ }
84
+ if (sub === 'list') {
85
+ const current = (0, themeRegistry_1.getCurrentName)();
86
+ const bundled = (0, bundledThemes_1.listBundled)();
87
+ const userDir = userThemesDir(ctx);
88
+ const userList = listUserThemes(userDir);
89
+ ctx.display.info(`Active theme: ${current}`);
90
+ ctx.display.info('Available themes:');
91
+ const labelW = Math.max(...bundled.map((b) => b.name.length), ...userList.map((u) => u.name.length), 4);
92
+ for (const b of bundled) {
93
+ const marker = b.name === current ? '●' : '○';
94
+ const padded = b.name.padEnd(labelW);
95
+ ctx.display.info(` ${marker} ${padded} (bundled) ${b.description}`);
96
+ }
97
+ for (const u of userList) {
98
+ const marker = u.name === current ? '●' : '○';
99
+ const padded = u.name.padEnd(labelW);
100
+ ctx.display.info(` ${marker} ${padded} (user) ${u.description}`);
101
+ }
102
+ if (bundled.length === 0 && userList.length === 0) {
103
+ ctx.display.warn('No themes found. Reinstall aiden-runtime or use /theme edit to author one.');
104
+ }
105
+ return {};
106
+ }
107
+ if (sub.startsWith('set ') || sub === 'set') {
108
+ const name = sub.replace(/^set\s*/, '').trim();
109
+ if (!name) {
110
+ ctx.display.printError('Usage: /theme set <name>', `Try one of: ${bundledThemes_1.BUNDLED_NAMES.join(', ')} or /theme list for full options.`);
111
+ return {};
112
+ }
113
+ if (!themePath) {
114
+ ctx.display.warn('/theme set needs Aiden user-data paths — try in a real session.');
115
+ return {};
116
+ }
117
+ // Resolution order: bundled → user themes/ dir.
118
+ let yamlText = null;
119
+ let sourceLabel = '';
120
+ if ((0, bundledThemes_1.isBundled)(name)) {
121
+ yamlText = (0, bundledThemes_1.getYaml)(name);
122
+ sourceLabel = 'bundled';
123
+ }
124
+ if (!yamlText) {
125
+ const userDir = userThemesDir(ctx);
126
+ if (userDir) {
127
+ const userFile = node_path_1.default.join(userDir, `${name}.yaml`);
128
+ if ((0, node_fs_1.existsSync)(userFile)) {
129
+ try {
130
+ yamlText = (0, node_fs_1.readFileSync)(userFile, 'utf8');
131
+ sourceLabel = 'user';
132
+ }
133
+ catch { /* fall through to not-found */ }
134
+ }
135
+ }
136
+ }
137
+ if (!yamlText) {
138
+ ctx.display.printError(`Theme not found: "${name}"`, `Available bundled: ${bundledThemes_1.BUNDLED_NAMES.join(', ')}. Or create ~/.aiden/themes/${name}.yaml.`);
139
+ return {};
140
+ }
141
+ // Copy to ~/.aiden/theme.yaml and apply immediately (don't wait
142
+ // for the chokidar watcher debounce — slash-command-driven
143
+ // changes should feel instant).
144
+ try {
145
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(themePath), { recursive: true });
146
+ (0, node_fs_1.writeFileSync)(themePath, yamlText, 'utf8');
147
+ }
148
+ catch (err) {
149
+ ctx.display.printError(`Could not write ${themePath}: ${err.message}`, 'Check filesystem permissions and try again.');
150
+ return {};
151
+ }
152
+ const { parsed, warnings } = (0, themeLoader_1.parseThemeYaml)(yamlText);
153
+ for (const w of warnings)
154
+ ctx.display.warn(`theme: ${w}`);
155
+ if (parsed) {
156
+ (0, themeRegistry_1.applyTheme)(parsed, themePath);
157
+ ctx.display.success(`✓ Theme set to ${name} (${sourceLabel}). Run /theme reset to revert to default.`);
158
+ }
159
+ else {
160
+ ctx.display.printError(`Theme "${name}" parsed empty; current theme retained.`, 'Check the warnings above for the specific YAML / hex error.');
161
+ }
162
+ return {};
163
+ }
164
+ if (sub === 'reload') {
165
+ if (!themePath) {
166
+ ctx.display.warn('/theme reload needs Aiden user-data paths — try in a real session.');
167
+ return {};
168
+ }
169
+ if (!(0, node_fs_1.existsSync)(themePath)) {
170
+ ctx.display.warn(`No theme file at ${themePath}. Use /theme edit to create one.`);
171
+ return {};
172
+ }
173
+ const { parsed, warnings } = (0, themeLoader_1.loadThemeFile)(themePath);
174
+ for (const w of warnings)
175
+ ctx.display.warn(`theme: ${w}`);
176
+ if (parsed) {
177
+ (0, themeRegistry_1.applyTheme)(parsed, themePath);
178
+ ctx.display.success(`Theme reloaded: ${parsed.name}`);
179
+ }
180
+ else {
181
+ ctx.display.printError('Theme parse failed; current theme retained.', 'Check the warnings above for the specific YAML / hex error.');
182
+ }
183
+ return {};
184
+ }
185
+ if (sub === 'reset') {
186
+ if (!themePath) {
187
+ ctx.display.warn('/theme reset needs Aiden user-data paths — try in a real session.');
188
+ return {};
189
+ }
190
+ if ((0, node_fs_1.existsSync)(themePath)) {
191
+ try {
192
+ (0, node_fs_1.unlinkSync)(themePath);
193
+ }
194
+ catch (err) {
195
+ ctx.display.warn(`Could not delete ${themePath}: ${err.message}`);
196
+ return {};
197
+ }
198
+ }
199
+ (0, themeRegistry_1.resetToDefault)();
200
+ ctx.display.success('Theme reset to bundled default.');
201
+ return {};
202
+ }
203
+ if (sub === 'edit') {
204
+ if (!themePath) {
205
+ ctx.display.warn('/theme edit needs Aiden user-data paths — try in a real session.');
206
+ return {};
207
+ }
208
+ ctx.display.info(`Theme file: ${themePath}`);
209
+ if (!(0, node_fs_1.existsSync)(themePath)) {
210
+ ctx.display.info('File does not exist yet. Create it with your editor; Aiden hot-reloads on save.');
211
+ }
212
+ return {};
213
+ }
214
+ ctx.display.printError(`Unknown /theme subcommand: ${sub}`, 'Available: /theme | /theme list | /theme set <name> | /theme reload | /theme reset | /theme edit');
215
+ return {};
216
+ },
217
+ };
@@ -369,7 +369,7 @@ async function runAddEmail(db, argv, out, err) {
369
369
  out(`poll interval: ${spec.pollIntervalMs}ms\n`);
370
370
  out(`allow-senders: ${spec.allowedSenders.join(', ')}\n`);
371
371
  out(`⚠ Password stored in plaintext in daemon.db (chmod 600 on POSIX,\n`);
372
- out(` user-private on Windows). Encryption-at-rest is deferred to v4.6+.\n`);
372
+ out(` user-private on Windows). Encryption-at-rest is deferred to a future release.\n`);
373
373
  out(`Restart the daemon to activate the trigger.\n`);
374
374
  // Note: runAddEmail returns a Promise<number>, so the outer switch must
375
375
  // await it. (Already handled — runTriggerSubcommand is async.)
@@ -16,7 +16,8 @@
16
16
  * isVerbose() reads env at call time (Ollama-options precedent).
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.VERBOSE_MODE_ENV = exports.spacing = exports.glyphs = exports.colors = exports.TRAIL_PIPE = void 0;
19
+ exports.VERBOSE_MODE_ENV = exports.spacing = exports.BASELINE_GLYPHS = exports.BASELINE_COLORS = exports.glyphs = exports.colors = exports.TRAIL_PIPE = void 0;
20
+ exports._restoreBaselineForTokens = _restoreBaselineForTokens;
20
21
  exports.isVerbose = isVerbose;
21
22
  // Re-export so consumers can import the trail gutter from tokens
22
23
  // alongside the rest of the design system. The original constant
@@ -24,6 +25,20 @@ exports.isVerbose = isVerbose;
24
25
  // that imports from `display/toolTrail` directly.
25
26
  var toolTrail_1 = require("../display/toolTrail");
26
27
  Object.defineProperty(exports, "TRAIL_PIPE", { enumerable: true, get: function () { return toolTrail_1.TRAIL_PIPE; } });
28
+ /**
29
+ * v4.9.0 Slice 1a — `colors` and `glyphs` were previously exported with
30
+ * `as const` (frozen literal-typed object). They are now mutable
31
+ * singletons so the ThemeRegistry can swap values at runtime when a
32
+ * user theme is loaded from `~/.aiden/theme.yaml`. Every existing
33
+ * consumer (`import { colors, glyphs } from '../design/tokens'`)
34
+ * continues to work because property reads now reflect the LIVE
35
+ * current values.
36
+ *
37
+ * `BASELINE_COLORS` / `BASELINE_GLYPHS` are deep-frozen snapshots
38
+ * captured at module load. The registry's `resetToDefault()` restores
39
+ * from these. Consumers that need the unmodified default (e.g. tests)
40
+ * can import these directly.
41
+ */
27
42
  // ── Colors ────────────────────────────────────────────────────────────────
28
43
  /**
29
44
  * Hex color tokens. Mirrors skinEngine RGB tuples for existing kinds;
@@ -119,9 +134,9 @@ exports.glyphs = {
119
134
  dotOpen: '○',
120
135
  /** Status footer column separator. */
121
136
  sep: '│',
122
- /** Slice 7 turn counter prefix. Slice 9 hotfix: glyph dropped
123
- * entirely. The bare colored number (purple `metric_turn` kind)
124
- * carries the meaning, matching the timer pattern. */
137
+ /** Turn counter retired per v4.9.0 pre-ship UX feedback
138
+ * value-to-pixel ratio too low. Field kept (empty) so the
139
+ * token-table shape stays stable for downstream consumers. */
125
140
  turn: '',
126
141
  /** Slice 7 — session timer prefix. Slice 9 hotfix: `⌛` (U+231B
127
142
  * HOURGLASS WITH FLOWING SAND) restored. The font set Shiva
@@ -224,6 +239,39 @@ exports.glyphs = {
224
239
  track: '─',
225
240
  },
226
241
  };
242
+ /**
243
+ * Deep-frozen snapshots of the baseline values, captured at module
244
+ * load. The ThemeRegistry uses these to reset to defaults via
245
+ * `resetToDefault()`, and tests can import them as the source of
246
+ * truth even after a user theme has been applied to the mutable
247
+ * `colors` / `glyphs` exports above.
248
+ */
249
+ function deepFreeze(o) {
250
+ if (o === null || typeof o !== 'object')
251
+ return o;
252
+ for (const k of Object.keys(o)) {
253
+ deepFreeze(o[k]);
254
+ }
255
+ return Object.freeze(o);
256
+ }
257
+ function deepClone(o) {
258
+ if (o === null || typeof o !== 'object')
259
+ return o;
260
+ if (Array.isArray(o))
261
+ return o.map(deepClone);
262
+ const out = {};
263
+ for (const k of Object.keys(o)) {
264
+ out[k] = deepClone(o[k]);
265
+ }
266
+ return out;
267
+ }
268
+ exports.BASELINE_COLORS = deepFreeze(deepClone(exports.colors));
269
+ exports.BASELINE_GLYPHS = deepFreeze(deepClone(exports.glyphs));
270
+ /** Used by ThemeRegistry to restore baseline values into the live singletons. */
271
+ function _restoreBaselineForTokens() {
272
+ Object.assign(exports.colors, deepClone(exports.BASELINE_COLORS));
273
+ Object.assign(exports.glyphs, deepClone(exports.BASELINE_GLYPHS));
274
+ }
227
275
  // ── Spacing ───────────────────────────────────────────────────────────────
228
276
  /**
229
277
  * Integer spacing tokens. 0-indexed column counts; subagent depth
@@ -23,6 +23,8 @@ exports.iconForTool = iconForTool;
23
23
  exports.detectConfiguredChannels = detectConfiguredChannels;
24
24
  exports.summarizeConfiguredChannels = summarizeConfiguredChannels;
25
25
  exports.summarizeChannelState = summarizeChannelState;
26
+ exports.computeContextBarFill = computeContextBarFill;
27
+ exports.renderContextBar = renderContextBar;
26
28
  exports.voiceIndicator = voiceIndicator;
27
29
  exports.makeNoOpToolRowHandle = makeNoOpToolRowHandle;
28
30
  exports.previewToolArgs = previewToolArgs;
@@ -214,6 +216,22 @@ const AIDEN_BANNER = String.raw `
214
216
  ██║ ██║██║██████╔╝███████╗██║ ╚████║
215
217
  ╚═╝ ╚═╝╚═╝╚═════╝ ╚══════╝╚═╝ ╚═══╝
216
218
  `;
219
+ /**
220
+ * v4.9.0 pre-ship UI hotfix — pure context-bar helpers. Extracted
221
+ * for testability + to fix the "always empty" symptom. Scale:
222
+ * 0% → 0 cells, 1-19% → 1, 20-39% → 2, 40-59% → 3, 60-79% → 4,
223
+ * 80-100% → 5. `renderContextBar` returns the glyph array.
224
+ */
225
+ function computeContextBarFill(pct, barW = 5) {
226
+ if (pct <= 0)
227
+ return 0;
228
+ if (pct >= 100)
229
+ return barW;
230
+ return Math.min(barW, Math.floor(pct / 20) + 1);
231
+ }
232
+ function renderContextBar(filled, barW = 5) {
233
+ return Array.from({ length: barW }, (_, i) => i < filled ? tokens_1.glyphs.bar.filled : tokens_1.glyphs.bar.empty);
234
+ }
217
235
  class Display {
218
236
  constructor(opts = {}) {
219
237
  // ── Phase 16c: streaming surface ─────────────────────────────────────
@@ -605,23 +623,23 @@ class Display {
605
623
  statusFooter(args) {
606
624
  const sk = this.skin;
607
625
  const SEP = sk.applyColors(' │ ', 'muted');
608
- const tri = this.triangle();
609
- // v4.8.0 Slice 7 hotfix #2 per-metric accent palette.
610
- // Model: cyan (tool kind). Token counts: amber (warn). Bar/pct:
611
- // semantic tier. Turn: purple (metric_turn). Timer: teal (success).
612
- const provModel = `${tri} ${sk.applyColors(args.provider, 'muted')}` +
626
+ // v4.9.0 pre-ship UI hotfix — dropped the leading `▲` from
627
+ // provModel (prompt `▲` owns the marker; footer `▲` read as
628
+ // a duplicate orphan). Slice 7 per-metric palette preserved.
629
+ const provModel = `${sk.applyColors(args.provider, 'muted')}` +
613
630
  `${sk.applyColors(' · ', 'muted')}` +
614
631
  sk.applyColors(args.model, 'tool');
615
632
  const pct = args.ctxMax > 0
616
633
  ? Math.min(100, Math.round((args.ctxUsed / args.ctxMax) * 100))
617
634
  : 0;
618
- // 5-cell bar with single-space separators reads as discrete dots
619
- // rather than a continuous line. Total visible width ≈ 9 cells,
620
- // similar to the prior 10-cell solid bar so 80-col tier stays tight.
635
+ // v4.9.0 pre-ship UI hotfixbar math extracted to
636
+ // `computeContextBarFill` (1 cell per 20% bucket, ≥1 when any
637
+ // context used). Old `Math.round(pct/100 * barW)` floored to 0
638
+ // below ~10% so the bar stayed empty all session at typical use.
621
639
  const barW = 5;
622
- const filled = Math.round((pct / 100) * barW);
640
+ const filled = computeContextBarFill(pct, barW);
623
641
  const ctxKind = pct < 60 ? 'success' : pct < 85 ? 'warn' : 'error';
624
- const cells = Array.from({ length: barW }, (_, i) => i < filled ? tokens_1.glyphs.bar.filled : tokens_1.glyphs.bar.empty);
642
+ const cells = renderContextBar(filled, barW);
625
643
  const bar = sk.applyColors(cells.join(' '), ctxKind);
626
644
  const ctxRatio = sk.applyColors(`${formatCompactTokens(args.ctxUsed)}/${formatCompactTokens(args.ctxMax)}`, 'warn');
627
645
  const ctxPctText = sk.applyColors(`${pct}%`, ctxKind);
@@ -638,12 +656,10 @@ class Display {
638
656
  const stateDot = args.state
639
657
  ? sk.applyColors(tokens_1.glyphs.status.dot, this.stateKind(args.state))
640
658
  : '';
641
- // v4.8.0 Slice 9 hotfix — turn glyph dropped; bare colored number
642
- // matches the timer pattern. Color alone (purple metric_turn)
643
- // carries the semantic.
644
- const turnSeg = args.turnCount !== undefined
645
- ? sk.applyColors(String(args.turnCount), 'metric_turn')
646
- : '';
659
+ // v4.9.0 pre-ship UI: turn counter retired entirely value-to-pixel
660
+ // ratio too low. `args.turnCount` stays in the signature for caller
661
+ // back-compat; ignored here.
662
+ void args.turnCount;
647
663
  // v4.8.0 Slice 9 hotfix — ⌛ restored ahead of the bare elapsed
648
664
  // string. Wider font support than the retired ⏱. `sessionMs` arg
649
665
  // stays plumbed-but-unused for backward compat with the field name.
@@ -654,17 +670,14 @@ class Display {
654
670
  const ctxSegFull = `${ctxRatio} ${bar} ${ctxPctText}`;
655
671
  const ctxSegCompact = `${bar} ${ctxPctText}`;
656
672
  let segments;
657
- if (cols >= 120 && stateDot && turnSeg && sessionSeg) {
658
- segments = [provModel, ctxSegFull, turnSeg, sessionSeg, stateDot];
673
+ if (cols >= 120 && stateDot && sessionSeg) {
674
+ segments = [provModel, ctxSegFull, sessionSeg, stateDot];
659
675
  }
660
- else if (cols >= 100 && turnSeg) {
661
- // v4.8.1 Slice 2 hotfix — was `elapsed` (bare); now uses
662
- // `sessionSeg` which includes the timer glyph. The previous
663
- // mid-tier dropped the glyph for "denser" packing, but Shiva's
664
- // smoke at 80–110 cols showed only ` 5.1s` (leading space, no
665
- // glyph). The glyph is single-cell, cheap, and load-bearing as
666
- // the timer's identity affordance.
667
- segments = [provModel, ctxSegFull, turnSeg, sessionSeg || elapsed];
676
+ else if (cols >= 100) {
677
+ // v4.8.1 Slice 2 hotfix — sessionSeg keeps the identity glyph
678
+ // (single-cell, cheap) even at this tier. v4.9.0 pre-ship UI:
679
+ // turn counter retired; mid tier collapses to 2 separators.
680
+ segments = [provModel, ctxSegFull, sessionSeg || elapsed];
668
681
  }
669
682
  else {
670
683
  segments = [provModel, ctxSegCompact, sessionSeg || elapsed];
@@ -183,7 +183,7 @@ function renderBlockquote(quote) {
183
183
  return quote
184
184
  .split('\n')
185
185
  .map((ln) => (ln.length === 0 ? rail.trimEnd() : `${rail}${ln}`))
186
- .join('\n') + '\n';
186
+ .join('\n') + '\n\n'; // v4.9.0 pre-ship UI: blank line after blockquote
187
187
  }
188
188
  /**
189
189
  * v4.1.3-essentials reply-polish: 4-tier heading hierarchy using the
@@ -633,11 +633,12 @@ function getReplyRenderer() {
633
633
  }
634
634
  }
635
635
  proto._listDepth -= 1;
636
- // Top-level list closes with a trailing newline to separate from
637
- // the next block; nested lists return without extra padding so
638
- // they nest cleanly inside their parent item.
636
+ // v4.9.0 pre-ship UI: top-level list closes with a BLANK LINE
637
+ // (`\n\n`) so a following paragraph / heading / table reads with
638
+ // breathing room. Nested lists stay tight (`\n`) so they nest
639
+ // cleanly under their parent item.
639
640
  const out = lines.join('\n');
640
- return proto._listDepth === 0 ? out + '\n' : out + '\n';
641
+ return proto._listDepth === 0 ? out + '\n\n' : out + '\n';
641
642
  };
642
643
  // ── v4.8.1 Slice 2 — markdown table override ──────────────────────────
643
644
  //
@@ -32,10 +32,58 @@ const node_fs_1 = require("node:fs");
32
32
  const node_path_1 = __importDefault(require("node:path"));
33
33
  const node_os_1 = __importDefault(require("node:os"));
34
34
  const js_yaml_1 = __importDefault(require("js-yaml"));
35
+ // v4.9.0 Slice 1a hotfix — read live theme overrides from tokens.ts.
36
+ const tokens_1 = require("./design/tokens");
37
+ const themeRegistry_1 = require("../../core/v4/theme/themeRegistry");
35
38
  /** Wrap text with a 24-bit ANSI foreground colour. */
36
39
  function ansiRgb(text, r, g, b) {
37
40
  return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
38
41
  }
42
+ /**
43
+ * v4.9.0 Slice 1a hotfix — map each SkinEngine `ColorKind` to a dotted
44
+ * path inside the v4.8 `tokens.ts` colour tree so the legacy paint API
45
+ * (`applyColors(text, kind)`) can resolve user-theme overrides without
46
+ * touching the legacy skin YAML cache. Each kind picks the closest
47
+ * semantic equivalent from the new tree; kinds without a natural fit
48
+ * (e.g. `agent`, `user`) fall through to the skin's own RGB tuple.
49
+ *
50
+ * The lookup is only consulted when a user theme is active (i.e.
51
+ * `getActiveThemePath() !== null`). When no theme is loaded, the
52
+ * legacy skin path runs unchanged — preserves /skin custom-palette
53
+ * users from being silently overridden by tokens.ts baselines.
54
+ */
55
+ const COLOR_KIND_TO_TOKEN_PATH = {
56
+ brand: 'brand.primary',
57
+ accent: 'brand.primary',
58
+ heading: 'brand.primary',
59
+ tool: 'metrics.model',
60
+ session: 'metrics.model',
61
+ error: 'semantic.error',
62
+ warn: 'semantic.warn',
63
+ success: 'semantic.success',
64
+ muted: 'content.secondary',
65
+ tertiary: 'content.tertiary',
66
+ metric_turn: 'metrics.turnCount',
67
+ degraded: 'semantic.warn',
68
+ };
69
+ function hexToRgb(hex) {
70
+ const m3 = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/.exec(hex);
71
+ if (m3)
72
+ return [parseInt(m3[1] + m3[1], 16), parseInt(m3[2] + m3[2], 16), parseInt(m3[3] + m3[3], 16)];
73
+ const m6 = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(hex);
74
+ if (m6)
75
+ return [parseInt(m6[1], 16), parseInt(m6[2], 16), parseInt(m6[3], 16)];
76
+ return null;
77
+ }
78
+ function readDottedPath(root, dotted) {
79
+ let node = root;
80
+ for (const seg of dotted.split('.')) {
81
+ if (node === null || typeof node !== 'object')
82
+ return undefined;
83
+ node = node[seg];
84
+ }
85
+ return node;
86
+ }
39
87
  const BRAND_ORANGE = [0xff, 0x6b, 0x35];
40
88
  const DEFAULT_SKIN = {
41
89
  name: 'default',
@@ -250,6 +298,25 @@ class SkinEngine {
250
298
  applyColors(text, kind) {
251
299
  if (this.forceMono)
252
300
  return text;
301
+ // v4.9.0 Slice 1a hotfix — when a user theme is active, resolve
302
+ // the colour from the live tokens.ts tree FIRST. This lets a
303
+ // ~/.aiden/theme.yaml override every paint surface that routes
304
+ // through SkinEngine (Aiden reply chrome, panel bars, status
305
+ // footer text, tool rows) without requiring users to also
306
+ // re-author a parallel ~/.aiden/skins/<name>.yaml. When no user
307
+ // theme is active, the legacy skin RGB path runs unchanged —
308
+ // preserves /skin custom-palette users from regression.
309
+ if ((0, themeRegistry_1.getActivePath)() !== null) {
310
+ const dotted = COLOR_KIND_TO_TOKEN_PATH[kind];
311
+ if (dotted) {
312
+ const hex = readDottedPath(tokens_1.colors, dotted);
313
+ if (typeof hex === 'string') {
314
+ const rgb = hexToRgb(hex);
315
+ if (rgb)
316
+ return ansiRgb(text, rgb[0], rgb[1], rgb[2]);
317
+ }
318
+ }
319
+ }
253
320
  const rgb = this.current.colors[kind];
254
321
  if (!rgb)
255
322
  return text;