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
package/README.md CHANGED
@@ -94,7 +94,7 @@ Windows · Linux · WSL · macOS (API Mode)
94
94
  ![Built solo](https://img.shields.io/badge/Built-solo-B8A893?style=flat-square)
95
95
  ![By Taracod](https://img.shields.io/badge/By-Taracod-FF6B35?style=flat-square)
96
96
  ![White Lotus](https://img.shields.io/badge/Brand-White_Lotus-FFB088?style=flat-square)
97
- ![v4.6.1](https://img.shields.io/badge/Latest-v4.6.1-4ADE80?style=flat-square)
97
+ ![v4.9.1](https://img.shields.io/badge/Latest-v4.9.1-4ADE80?style=flat-square)
98
98
 
99
99
  </div>
100
100
 
@@ -134,6 +134,46 @@ Drop a file in `~/Documents/inbox/anything.txt` and Aiden acts on it. The agent
134
134
 
135
135
  <br>
136
136
 
137
+ ## What's new in v4.9.0
138
+
139
+ Aiden v4.9.0 ships three new feature families:
140
+
141
+ - **Memory** — three-namespace persistent memory (memory/user/project) with CLI + auto-review
142
+ - **Hooks** — secure subprocess hook system (observe/decide/transform) with full audit trail
143
+ - **Strategic substrate** — UUIDv7 IDs, W3C trace propagation, durable runs, idempotency, crash recovery
144
+
145
+ Plus theme system, MCP integration (Claude Desktop / Cursor / VS Code), and substantial UI polish.
146
+
147
+ See [CHANGELOG.md](./CHANGELOG.md) for details.
148
+
149
+ <br>
150
+
151
+ ## What's new in v4.8
152
+
153
+ **Aiden's UI got an identity, and pasting actually works now.**
154
+
155
+ ### v4.8.1 — Paste handling + visual polish
156
+
157
+ - **Robust paste handling.** Stateful parser across stdin chunk boundaries, 800ms watchdog for missing paste-end markers, degraded marker form normalisation, universal CRLF/CR → LF, and timing-based accumulation that catches line-by-line paste delivery. Works across Windows ConPTY, SSH without `-t`, tmux/screen passthrough, and VS Code's integrated terminal. Typed prefix is preserved when paste arrives mid-input.
158
+ - **Markdown tables render as proper grids.** Proportional column-width distribution with header-floor minimums so short labels never wrap mid-word. Model nudged via prompt to prefer sectioned lists for very wide comparisons.
159
+ - **Version reporting reads runtime `package.json`.** Boot card and `/version` no longer report stale numbers after `/update install`.
160
+ - **`/update install` cleanups.** Dropped the Node 20+ deprecation warning. Install progress shown via sliding-shimmer indicator. Install-method detection broadened to Windows user-mode globals.
161
+ - **Loading indicator + spacing.** Indicator aligned at col 2 matching other surfaces; conditional erase preserves clean 1-blank rhythm between turns.
162
+ - **Status bar timer glyph (`⌛`).** Visible at all terminal widths (mid + compact tiers previously dropped it).
163
+ - **Single approval panel** per `file_write` (no duplicate event-row + panel stack).
164
+
165
+ ### v4.8.0 — Events backbone + visual identity
166
+
167
+ - **Watch the agent work.** Seven `ui_*` events (`ui_task_update`, `ui_task_done`, `ui_command_result`, `ui_test_result`, `ui_approval_request`, `ui_toast`, `ui_artifact_created`) let the model emit structured progress signals instead of writing them as prose. The REPL paints them as inline rows separate from the reply text. System prompt nudges the model to fire events during multi-step work.
168
+ - **Aiden-native visual chrome.** Orange `│` panel bars with asymmetric top-divider chrome (no closing corners). Token-sourced design system (`cli/v4/design/tokens.ts`) — palette, glyphs, spacing in one place.
169
+ - **Packed status bar.** Cyan model name · amber token ratio · semantic-tiered context bar (`●●●●●●○○○○`) · purple turn counter · teal `⌛` per-turn timer · state dot. Progressive disclosure on narrower terminals.
170
+ - **Sliding-block loading indicator.** 4-cell `█` segment slides L→R on a muted `─` track at 250ms/cell.
171
+ - **Onboarding refresh.** 24-bit ANSI AIDEN banner, capability-bullet disclaimer, 10-cell progress bar in the loading sequence, rounded heavy-frame "Built solo" card.
172
+ - **Markdown polish.** Code blocks get a top-divider with brand-orange language label. Bullet lists use `●`/`○`; task lists use `✔️`/`○`.
173
+ - **ChatGPT Plus + gpt-5 routing fix.** Auxiliary cheap-LLM calls (`risk_assess`, `compression`, `session_summary`, `skill_describe`) route through Groq + `llama-3.1-8b-instant` by default, falling back to the parent provider only when Groq isn't configured. Fixes the v4.6.x bug where every auxiliary call returned 400 from the Codex backend.
174
+
175
+ <br>
176
+
137
177
  ## What's new in v4.6
138
178
 
139
179
  **Aiden now spawns workers and learns from itself.**
@@ -296,6 +336,47 @@ export AIDEN_DEFAULT_PROVIDER=groq
296
336
  export AIDEN_DEFAULT_MODEL=llama-3.3-70b-versatile
297
337
  ```
298
338
 
339
+ ### Recommended models
340
+
341
+ | Use case | Best models |
342
+ |---|---|
343
+ | **General-purpose / quality** | Claude Sonnet 4.6 / Opus, GPT-5, Gemini 2.5 Pro |
344
+ | **Speed + free tier** | Groq Llama 3.3 70B, Cerebras Llama 3.3 70B |
345
+ | **Fully local** | Ollama with Qwen 2.5 32B, Llama 3.3 70B, or DeepSeek R1 |
346
+ | **Open weights via subscription** | Nous Portal (Hermes-3-405B) |
347
+ | **Long context** | Gemini 2.5 Pro (2M tokens) |
348
+
349
+ > 💡 **Quality scales with model capability.** Aiden's output quality depends heavily on the model you pick. For real work, prefer the highest-capability model your budget allows — Claude Sonnet 4.6, GPT-5, or Gemini 2.5 Pro. Smaller models work for simple tasks but agentic loops benefit from stronger reasoning.
350
+
351
+ > 💡 **ChatGPT Plus / Claude Pro subscribers**: use the OAuth flow instead of an API key. Pick `chatgpt-plus` or `claude-pro` as your provider during `/model` setup, then sign in via browser. No separate API costs — uses your existing subscription's model allowances.
352
+
353
+ <br>
354
+
355
+ ## Skills
356
+
357
+ Aiden ships with 74 bundled skills and supports installing more from the community registry.
358
+
359
+ ### What are skills?
360
+
361
+ Skills are composable workflows. Each skill has a `SKILL.md` prompt, optional helper scripts, and declared tool requirements. Examples: GitHub PR/issue automation, NSE/Upstox/Zerodha trading flows, Censys/Shodan lookups, Windows Defender management.
362
+
363
+ ### Managing skills
364
+
365
+ | Action | Command |
366
+ |---|---|
367
+ | List installed skills | `/skills` |
368
+ | Browse the community registry | Visit [skills.taracod.com](https://skills.taracod.com) |
369
+ | Install a skill from registry | `/skills install <name>` |
370
+ | Remove a skill | `/skills remove <name>` |
371
+ | Reload skills (after edits) | `/skills reload` |
372
+
373
+ ### Contributing skills
374
+
375
+ Skills live in [`skills/`](skills/) under Apache-2.0. PRs welcome. Each skill needs:
376
+ - A `SKILL.md` describing what it does and when to use it
377
+ - Optional `tools.json` declaring required tools
378
+ - Optional helper scripts in `lib/`
379
+
299
380
  <br>
300
381
 
301
382
  ## Configuration
@@ -485,6 +566,12 @@ Aiden can touch your files, run shell commands, and access the web. It's autonom
485
566
 
486
567
  **Built solo. Started as a hobby project. Still rough in spots. Getting better every week.**
487
568
 
569
+ ### Welcome
570
+
571
+ - **Contributors** — bug reports, skill submissions, provider adapters, and documentation PRs are all welcome. See [Contributing](#contributing) above for the workflow.
572
+ - **Sponsors / investors** — Aiden is built by one person funding it from client work. If you'd like to help Aiden grow faster, reach out at [contact@taracod.com](mailto:contact@taracod.com) or sponsor via [Razorpay](https://razorpay.me/@whitelotus9625).
573
+ - **Beta testers** — try it on your workflow, file issues, share what works and what doesn't. Honest feedback shapes the roadmap.
574
+
488
575
  ---
489
576
 
490
577
  <div align="center">
@@ -472,10 +472,37 @@ async function main(argv, opts = {}) {
472
472
  });
473
473
  process.exit(code);
474
474
  });
475
+ // v4.9.0 Slice 9 — memory CLI surface.
476
+ program
477
+ .command('memory [action] [args...]')
478
+ .description('Manage MEMORY.md + USER.md + project memory. Actions: list (default), show, add, remove, edit, backup, restore, diff, namespaces, pending, approve, reject, review.')
479
+ .allowUnknownOption()
480
+ .action(async (action, posArgs) => {
481
+ const { runMemorySubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/memory')));
482
+ const code = await runMemorySubcommand(action ?? 'list', posArgs ?? [], {
483
+ writeOut: opts.writeOut,
484
+ });
485
+ process.exit(code);
486
+ });
487
+ // v4.9.0 Slice 12b — hooks CLI surface.
488
+ program
489
+ .command('hooks [action] [args...]')
490
+ .description('Manage hook subsystem. Actions: list (default), show, trust, revoke, rescan, test, doctor, audit.')
491
+ .allowUnknownOption()
492
+ .action(async (action, posArgs) => {
493
+ const { runHooksSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/hooks')));
494
+ const code = await runHooksSubcommand(action ?? 'list', posArgs ?? [], {
495
+ writeOut: opts.writeOut,
496
+ });
497
+ process.exit(code);
498
+ });
475
499
  // v4.5 Phase 4b — daemon supervisor commands.
476
500
  program
477
501
  .command('daemon <action> [args...]')
478
- .description('Manage the v4.5 daemon. Actions: install, uninstall, start, stop, restart, status, logs.')
502
+ .description('Manage the Aiden daemon. Actions: install, uninstall, start, stop, restart, status, logs, doctor.')
503
+ // v4.9.0 Slice 8 — `aiden daemon doctor --json` / `--fix`. Allow
504
+ // unknown flags so the subcommand can parse them via posArgs.
505
+ .allowUnknownOption()
479
506
  .action(async (action, posArgs) => {
480
507
  const { runDaemonSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/daemon')));
481
508
  const code = await runDaemonSubcommand(action, posArgs ?? [], {
@@ -518,9 +545,11 @@ async function main(argv, opts = {}) {
518
545
  process.exit(code);
519
546
  });
520
547
  program
521
- .command('mcp <action>')
522
- .description('MCP server mode (Phase v4.1-mcp). Actions: serve, status, tools.')
523
- .action(async (action) => {
548
+ .command('mcp <action> [target] [extraArgs...]')
549
+ .description('MCP server mode + client config. Actions: serve | status | tools | init <client> | doctor <client> | repair <client> | uninstall <client>. ' +
550
+ 'Clients: claude (Claude Desktop), cursor, vscode.')
551
+ .allowUnknownOption()
552
+ .action(async (action, target, extraArgs = []) => {
524
553
  if (opts.runMcpHook) {
525
554
  await opts.runMcpHook(action);
526
555
  return;
@@ -531,13 +560,15 @@ async function main(argv, opts = {}) {
531
560
  const code = await runMcpSubcommand(action, {
532
561
  writeOut: opts.writeOut,
533
562
  writeErr: (t) => process.stderr.write(t),
563
+ args: extraArgs,
564
+ target,
534
565
  });
535
566
  if (code !== 0)
536
567
  process.exit(code);
537
568
  });
538
569
  program
539
570
  .command('voice [args...]')
540
- .description('Voice diagnostics + one-shot TTS / transcribe (Phase v4.1-voice-cli). ' +
571
+ .description('Voice diagnostics + one-shot TTS / transcribe. ' +
541
572
  'Usage: aiden voice doctor | tts "<text>" | transcribe <file>')
542
573
  .allowUnknownOption()
543
574
  .action(async (args) => {
@@ -580,7 +611,7 @@ async function main(argv, opts = {}) {
580
611
  // v4.1 placeholders. (`tui` graduated to a real flag in Phase 15.)
581
612
  program
582
613
  .command('cron [args...]')
583
- .description('Cron diagnostics + one-shot list / run (Phase v4.1 hardened cron). ' +
614
+ .description('Cron diagnostics + one-shot list / run. ' +
584
615
  'Usage: aiden cron status | list | run <id>')
585
616
  .allowUnknownOption()
586
617
  .action(async (args) => {
@@ -98,6 +98,8 @@ const node_path_1 = __importDefault(require("node:path"));
98
98
  const bracketedPaste_1 = require("./bracketedPaste");
99
99
  const pasteCompression_1 = require("./pasteCompression");
100
100
  const pasteIntercept_1 = require("./pasteIntercept");
101
+ // v4.9.0 Slice 1a — theme hot-reload watcher.
102
+ const themeWatcher_1 = require("../../core/v4/theme/themeWatcher");
101
103
  const shellInterpolation_1 = require("./shellInterpolation");
102
104
  const resizeGuard_1 = require("./resizeGuard");
103
105
  /**
@@ -425,6 +427,16 @@ class ChatSession {
425
427
  const restorePasteInterceptor = this.opts.promptApi
426
428
  ? () => { }
427
429
  : (0, pasteIntercept_1.installPasteInterceptor)(process.stdin);
430
+ // v4.9.0 Slice 1a — start the theme hot-reload watcher. Honours
431
+ // `~/.aiden/theme.yaml` if it exists; otherwise no-op until the
432
+ // user creates one. Disposed in the `finally` block alongside
433
+ // the paste interceptor.
434
+ if (this.opts.paths && !this.opts.promptApi) {
435
+ try {
436
+ (0, themeWatcher_1.startThemeWatcher)(require('node:path').join(this.opts.paths.root, 'theme.yaml'));
437
+ }
438
+ catch { /* watcher start failure must not crash REPL */ }
439
+ }
428
440
  // Tier-3-essentials: hard-clear the screen on terminal resize so
429
441
  // dropdown re-renders + previous prompt frames don't ghost into
430
442
  // the new viewport. No-op on non-TTY / MCP serve mode.
@@ -529,6 +541,12 @@ class ChatSession {
529
541
  // commands; the footer belongs to agent turns only.
530
542
  continue;
531
543
  }
544
+ // v4.9.0 pre-ship UI: BOTTOM rule of the prompt zone. The TOP
545
+ // rule fires at `printTurnSeparator()` above (iter > 1). This
546
+ // bottom rule fires only when actual content goes to the LLM —
547
+ // slash commands + empty input + /quit all `continue` above and
548
+ // never reach this line, matching the task's edge-case spec.
549
+ this.opts.display.write(` ${this.opts.display.rule()}\n`);
532
550
  await this.runAgentTurn(input);
533
551
  }
534
552
  }
@@ -542,6 +560,10 @@ class ChatSession {
542
560
  if (pasteEnabled)
543
561
  (0, bracketedPaste_1.disableBracketedPaste)(stdout);
544
562
  restorePasteInterceptor();
563
+ try {
564
+ (0, themeWatcher_1.stopThemeWatcher)();
565
+ }
566
+ catch { /* defensive */ }
545
567
  restoreResizeGuard();
546
568
  }
547
569
  }
@@ -1362,8 +1384,10 @@ class ChatSession {
1362
1384
  _deferredTip = null;
1363
1385
  }
1364
1386
  // Tier-3.1a: dim full-width rule between the agent reply and the
1365
- // post-turn status footer.
1366
- this.opts.display.write(` ${this.opts.display.rule()}\n`);
1387
+ // post-turn status footer. v4.9.0 pre-ship UI: prepend a blank
1388
+ // line so the response zone gets breathing room above the rule
1389
+ // — matches the `\n\n` blank already below `▎ Aiden` header.
1390
+ this.opts.display.write(`\n ${this.opts.display.rule()}\n`);
1367
1391
  this.renderStatusLine();
1368
1392
  // v4.1.5+ Path A — finalize the loop trace. No-op if the env
1369
1393
  // var is unset OR if the turn didn't trip any threshold. When
@@ -1632,6 +1656,10 @@ class ChatSession {
1632
1656
  // .update_check.json cache so subsequent boots stay quiet until
1633
1657
  // a newer release ships.
1634
1658
  try {
1659
+ // v4.9.1 — modal sits BELOW the welcome banner with a blank
1660
+ // separator. Prevents the box from visually overlapping the
1661
+ // boot card on first-paint (smoke-reported regression).
1662
+ display.write('\n');
1635
1663
  await this.maybeShowBootUpdatePrompt();
1636
1664
  }
1637
1665
  catch { /* never let the update prompt crash boot */ }
@@ -1648,15 +1676,16 @@ class ChatSession {
1648
1676
  }
1649
1677
  }
1650
1678
  catch { /* never let a missing marker crash boot */ }
1651
- // Bottom prompt hint final line of the boot card.
1652
- // v4.8.0 Slice 10d full-width muted `─` divider between the
1653
- // boot card (parchment / capability card / two-column block) and
1654
- // the input hint prevents the boot chrome from merging visually
1655
- // with the active prompt. Pattern: blank · rule · blank · hint.
1656
- display.write('\n');
1657
- display.write(` ${display.rule()}\n`);
1679
+ // v4.9.0 pre-ship UI: hint moved BEFORE the closing rule so the
1680
+ // rule sits adjacent to the active prompt (it becomes the visual
1681
+ // top of the prompt zone). New order: blank · hint · blank · rule.
1682
+ // Banner content ends with the hint; the rule below it brackets
1683
+ // the user-input zone together with the new bottom-rule emission
1684
+ // in the REPL loop.
1658
1685
  display.write('\n');
1659
1686
  display.write(display.bottomPromptHint() + '\n');
1687
+ display.write('\n');
1688
+ display.write(` ${display.rule()}\n`);
1660
1689
  }
1661
1690
  /**
1662
1691
  * v4.5 update system — orchestrates the boot prompt. Lazy-imports
@@ -1688,13 +1717,24 @@ class ChatSession {
1688
1717
  });
1689
1718
  if (choice === 'install') {
1690
1719
  if (method.inProcessInstallSupported) {
1691
- this.opts.display.write(`Installing aiden-runtime ${status.latest}…\n`);
1692
- const result = await ei.executeInstall({ packageSpec: `aiden-runtime@${status.latest}` });
1720
+ // v4.9.1 drive a live progress bar off the executor's
1721
+ // phase callback. The bar degrades cleanly on non-TTY, NO_COLOR,
1722
+ // and dumb terminals — see cli/v4/ui/progressBar.ts.
1723
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1724
+ const pb = require('./ui/progressBar');
1725
+ const bar = pb.startProgressBar({
1726
+ label: `Installing aiden-runtime ${status.latest}...`,
1727
+ phases: ['spawning', 'resolving', 'downloading', 'extracting', 'verifying', 'installed'],
1728
+ });
1729
+ const result = await ei.executeInstall({
1730
+ packageSpec: `aiden-runtime@${status.latest}`,
1731
+ onPhase: (p) => { bar.setPhase(p); bar.setPercent(pb.npmInstallPhasePercent(p)); },
1732
+ });
1693
1733
  if (result.success) {
1694
- this.opts.display.write(`aiden-runtime ${result.installedVersion ?? status.latest} installed.\n`);
1695
- this.opts.display.dim('Restart Aiden to apply: type /quit then re-run `aiden`.');
1734
+ bar.complete(`aiden-runtime ${result.installedVersion ?? status.latest} installed. Restart Aiden to apply: type /quit then re-run \`aiden\`.`);
1696
1735
  }
1697
1736
  else {
1737
+ bar.fail('Install failed.');
1698
1738
  this.opts.display.warn(result.error ?? 'Install failed (no error message).');
1699
1739
  }
1700
1740
  }
@@ -27,6 +27,39 @@
27
27
  * variance + admin requirements make the install footprint too risky
28
28
  * for an auto-installer. The docs cover pm2 + foreground patterns.
29
29
  */
30
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ var desc = Object.getOwnPropertyDescriptor(m, k);
33
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
34
+ desc = { enumerable: true, get: function() { return m[k]; } };
35
+ }
36
+ Object.defineProperty(o, k2, desc);
37
+ }) : (function(o, m, k, k2) {
38
+ if (k2 === undefined) k2 = k;
39
+ o[k2] = m[k];
40
+ }));
41
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
42
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
43
+ }) : function(o, v) {
44
+ o["default"] = v;
45
+ });
46
+ var __importStar = (this && this.__importStar) || (function () {
47
+ var ownKeys = function(o) {
48
+ ownKeys = Object.getOwnPropertyNames || function (o) {
49
+ var ar = [];
50
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
51
+ return ar;
52
+ };
53
+ return ownKeys(o);
54
+ };
55
+ return function (mod) {
56
+ if (mod && mod.__esModule) return mod;
57
+ var result = {};
58
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
59
+ __setModuleDefault(result, mod);
60
+ return result;
61
+ };
62
+ })();
30
63
  var __importDefault = (this && this.__importDefault) || function (mod) {
31
64
  return (mod && mod.__esModule) ? mod : { "default": mod };
32
65
  };
@@ -59,10 +92,27 @@ async function runDaemonSubcommand(action, args, opts = {}) {
59
92
  case 'restart': return runRestart({ out, err });
60
93
  case 'status': return runStatus({ out, err });
61
94
  case 'logs': return runLogs({ out, err });
62
- default:
95
+ case 'doctor': {
96
+ // v4.9.0 Slice 8 — substrate health diagnostic. Read-only by
97
+ // default; `--fix` runs safe sweeps; `--json` outputs machine-
98
+ // parseable shape.
99
+ const { runDaemonDoctor } = await Promise.resolve().then(() => __importStar(require('./daemonDoctor')));
100
+ return runDaemonDoctor({
101
+ json: args.includes('--json'),
102
+ fix: args.includes('--fix'),
103
+ writeOut: out,
104
+ writeErr: err,
105
+ });
106
+ }
107
+ default: {
63
108
  err(`Unknown daemon action: ${action}\n`);
64
- err('Actions: install, uninstall, start, stop, restart, status, logs\n');
109
+ const { closestAction } = await Promise.resolve().then(() => __importStar(require('../util/closestAction')));
110
+ const m = closestAction(action, ['install', 'uninstall', 'start', 'stop', 'restart', 'status', 'logs', 'doctor']);
111
+ if (m)
112
+ err(`Did you mean: ${m}?\n\n`);
113
+ err('Actions: install, uninstall, start, stop, restart, status, logs, doctor\n');
65
114
  return 2;
115
+ }
66
116
  }
67
117
  }
68
118
  const SYSTEMD_UNIT_NAME = 'aiden.service';
@@ -280,7 +330,7 @@ async function runInstallMacOS(io) {
280
330
  async function runInstallWindows(io) {
281
331
  io.out((0, daemon_1.windowsServiceGuidance)());
282
332
  io.out('\n');
283
- io.out('See docs/v4.5/daemon-windows.md for the full walkthrough.\n');
333
+ io.out('See docs/daemon-windows.md for the full walkthrough.\n');
284
334
  return 0;
285
335
  }
286
336
  // ── uninstall ───────────────────────────────────────────────────────────────
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * cli/v4/commands/daemonDoctor.ts — v4.9.0 Slice 8.
10
+ *
11
+ * `aiden daemon doctor` — read-only diagnostic of substrate health.
12
+ * `aiden daemon doctor --json` — machine-parseable shape.
13
+ * `aiden daemon doctor --fix` — runs sweeps for safely-fixable issues.
14
+ *
15
+ * Checks (in display order):
16
+ * 1. daemon_id file exists + parses
17
+ * 2. Schema at LATEST_SCHEMA_VERSION
18
+ * 3. Recent incarnation row present + heartbeat fresh
19
+ * 4. Recent crashes (24h) — warn if more than 3
20
+ * 5. Stuck attempts present (sweep candidates)
21
+ * 6. Orphan spans present (sweep candidates)
22
+ * 7. Idempotency table bounded (warn if > 10000 rows)
23
+ * 8. Pending-but-stale trigger events (claim lease expired)
24
+ */
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.collectDoctorChecks = collectDoctorChecks;
30
+ exports.runDaemonDoctor = runDaemonDoctor;
31
+ const node_fs_1 = __importDefault(require("node:fs"));
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
34
+ const paths_1 = require("../../../core/v4/paths");
35
+ const identity_1 = require("../../../core/v4/identity");
36
+ const migrations_1 = require("../../../core/v4/daemon/db/migrations");
37
+ const stuckAttemptWatchdog_1 = require("../../../core/v4/daemon/runs/stuckAttemptWatchdog");
38
+ const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
39
+ /** Compute overall status: error if any error, warn if any warn, else ok. */
40
+ function overall(results) {
41
+ if (results.some((r) => r.status === 'error'))
42
+ return 'error';
43
+ if (results.some((r) => r.status === 'warn'))
44
+ return 'warn';
45
+ return 'ok';
46
+ }
47
+ function tableExists(db, name) {
48
+ const row = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`).get(name);
49
+ return !!row?.name;
50
+ }
51
+ /**
52
+ * Run all checks against the daemon DB at `<root>/daemon/daemon.db`.
53
+ * Pure-data path; no terminal I/O. Renders happen in `runDaemonDoctor`.
54
+ */
55
+ function collectDoctorChecks(rootDir) {
56
+ const checks = [];
57
+ // 1. daemon_id file
58
+ const idPath = (0, identity_1.daemonIdFilePath)(rootDir);
59
+ if (!node_fs_1.default.existsSync(idPath)) {
60
+ checks.push({ name: 'daemon_id file', status: 'error',
61
+ detail: `missing: ${idPath} (daemon never booted in this root)`,
62
+ fixable: false });
63
+ }
64
+ else {
65
+ const content = node_fs_1.default.readFileSync(idPath, 'utf8').trim();
66
+ checks.push({ name: 'daemon_id file', status: content.startsWith('dmn_') ? 'ok' : 'error',
67
+ detail: `${idPath} → ${content.slice(0, 40)}...`,
68
+ fixable: false });
69
+ }
70
+ // Open DB. If unavailable, remaining checks short-circuit.
71
+ const dbPath = node_path_1.default.join(rootDir, 'daemon', 'daemon.db');
72
+ if (!node_fs_1.default.existsSync(dbPath)) {
73
+ checks.push({ name: 'daemon DB', status: 'error',
74
+ detail: `missing: ${dbPath}`, fixable: false });
75
+ return checks;
76
+ }
77
+ const db = new better_sqlite3_1.default(dbPath, { readonly: false });
78
+ db.pragma('foreign_keys = ON');
79
+ try {
80
+ // 2. Schema version
81
+ const verRow = db.prepare(`SELECT version FROM schema_version WHERE id = 1`)
82
+ .get();
83
+ const ver = verRow?.version ?? 0;
84
+ checks.push({ name: 'schema version', status: ver === migrations_1.LATEST_SCHEMA_VERSION ? 'ok' : 'warn',
85
+ detail: `current=${ver} latest=${migrations_1.LATEST_SCHEMA_VERSION}`,
86
+ fixable: false });
87
+ // 3. Recent incarnation
88
+ const inc = db.prepare(`SELECT incarnation_id, started_at, ended_at, exit_reason FROM daemon_incarnations
89
+ ORDER BY started_at DESC LIMIT 1`).get();
90
+ if (!inc) {
91
+ checks.push({ name: 'recent incarnation', status: 'warn',
92
+ detail: 'no daemon_incarnations rows (daemon never started in this root)',
93
+ fixable: false });
94
+ }
95
+ else {
96
+ const closedDetail = inc.ended_at
97
+ ? `last incarnation ${inc.incarnation_id.slice(0, 20)}... ended_at=${inc.ended_at} reason=${inc.exit_reason}`
98
+ : `latest incarnation ${inc.incarnation_id.slice(0, 20)}... still open (started_at=${inc.started_at})`;
99
+ checks.push({ name: 'recent incarnation', status: 'ok', detail: closedDetail, fixable: false });
100
+ }
101
+ // 4. Recent crashes (24h)
102
+ const sinceIso = new Date(Date.now() - TWENTY_FOUR_HOURS_MS).toISOString();
103
+ const crashes = db.prepare(`SELECT COUNT(*) AS c FROM daemon_incarnations
104
+ WHERE exit_reason = 'crash' AND started_at > ?`).get(sinceIso);
105
+ checks.push({ name: 'recent crashes (24h)',
106
+ status: crashes.c === 0 ? 'ok' : crashes.c > 3 ? 'warn' : 'ok',
107
+ detail: `${crashes.c} crash(es) in last 24h`,
108
+ fixable: false });
109
+ // 5. Stuck attempts (require Slice 5 + 8 tables)
110
+ let stuckAttempts = 0;
111
+ if (tableExists(db, 'run_attempts')) {
112
+ const currentInc = inc?.incarnation_id ?? '';
113
+ const cutoffIso = new Date(Date.now() - 30 * 60 * 1000).toISOString();
114
+ stuckAttempts = db.prepare(`SELECT COUNT(*) AS c FROM run_attempts
115
+ WHERE status='running' AND incarnation_id != ? AND started_at < ?`).get(currentInc, cutoffIso).c;
116
+ }
117
+ checks.push({ name: 'stuck attempts',
118
+ status: stuckAttempts === 0 ? 'ok' : 'warn',
119
+ detail: `${stuckAttempts} stuck attempt(s) eligible for sweep`,
120
+ fixable: stuckAttempts > 0,
121
+ fix: stuckAttempts > 0 && inc
122
+ ? () => { (0, stuckAttemptWatchdog_1.sweepStuckAttempts)(db, { currentIncarnationId: inc.incarnation_id }); }
123
+ : undefined });
124
+ // 6. Orphan spans (open + non-current incarnation)
125
+ let orphanSpans = 0;
126
+ if (tableExists(db, 'spans')) {
127
+ const currentInc = inc?.incarnation_id ?? '';
128
+ orphanSpans = db.prepare(`SELECT COUNT(*) AS c FROM spans
129
+ WHERE status IS NULL AND ended_at IS NULL AND incarnation_id != ?`).get(currentInc).c;
130
+ }
131
+ checks.push({ name: 'orphan spans',
132
+ status: orphanSpans === 0 ? 'ok' : 'warn',
133
+ detail: `${orphanSpans} orphan span(s) eligible for sweep`,
134
+ fixable: orphanSpans > 0,
135
+ fix: orphanSpans > 0 && inc
136
+ ? () => { (0, stuckAttemptWatchdog_1.sweepStuckAttempts)(db, { currentIncarnationId: inc.incarnation_id }); }
137
+ : undefined });
138
+ // 7. Idempotency tables size
139
+ let idemRows = 0;
140
+ if (tableExists(db, 'run_idempotency_keys')) {
141
+ idemRows = db.prepare(`SELECT COUNT(*) AS c FROM run_idempotency_keys`).get().c;
142
+ }
143
+ checks.push({ name: 'run_idempotency_keys size',
144
+ status: idemRows < 10000 ? 'ok' : 'warn',
145
+ detail: `${idemRows} row(s)` + (idemRows >= 10000 ? ' — consider sweepExpired' : ''),
146
+ fixable: false });
147
+ // 8. Stale-claimed trigger events
148
+ let staleClaimed = 0;
149
+ if (tableExists(db, 'trigger_events')) {
150
+ staleClaimed = db.prepare(`SELECT COUNT(*) AS c FROM trigger_events
151
+ WHERE status='claimed' AND claim_expires_at IS NOT NULL AND claim_expires_at < ?`).get(Date.now()).c;
152
+ }
153
+ checks.push({ name: 'stale-claimed trigger events',
154
+ status: staleClaimed === 0 ? 'ok' : 'warn',
155
+ detail: `${staleClaimed} trigger_events past claim_expires_at`,
156
+ fixable: false });
157
+ }
158
+ finally {
159
+ try {
160
+ db.close();
161
+ }
162
+ catch { /* noop */ }
163
+ }
164
+ return checks;
165
+ }
166
+ /**
167
+ * CLI entry. Returns the desired process exit code (0=ok, 1=error,
168
+ * 0 for warn-only since warnings shouldn't break scripts).
169
+ */
170
+ function runDaemonDoctor(opts = {}) {
171
+ const root = opts.rootDir ?? (0, paths_1.resolveAidenRoot)();
172
+ const out = opts.writeOut ?? ((s) => { process.stdout.write(s); });
173
+ const err = opts.writeErr ?? ((s) => { process.stderr.write(s); });
174
+ const checks = collectDoctorChecks(root);
175
+ const fixed = [];
176
+ if (opts.fix) {
177
+ for (const c of checks) {
178
+ if (c.fixable && c.fix) {
179
+ try {
180
+ c.fix();
181
+ fixed.push(c.name);
182
+ }
183
+ catch (e) {
184
+ err(`[doctor] fix '${c.name}' failed: ${e instanceof Error ? e.message : String(e)}\n`);
185
+ }
186
+ }
187
+ }
188
+ }
189
+ const report = {
190
+ overall: overall(checks),
191
+ checks: checks.map((c) => ({ name: c.name, status: c.status, detail: c.detail, fixable: c.fixable })),
192
+ ...(opts.fix ? { fixed } : {}),
193
+ };
194
+ if (opts.json) {
195
+ out(JSON.stringify(report, null, 2) + '\n');
196
+ }
197
+ else {
198
+ const sym = (s) => s === 'ok' ? '[ok] ' : s === 'warn' ? '[warn]' : '[err] ';
199
+ out(`aiden daemon doctor — ${report.overall.toUpperCase()}\n`);
200
+ out(`root: ${root}\n\n`);
201
+ for (const c of report.checks) {
202
+ out(` ${sym(c.status)} ${c.name}: ${c.detail}\n`);
203
+ }
204
+ if (opts.fix && fixed.length > 0) {
205
+ out(`\nfixed: ${fixed.join(', ')}\n`);
206
+ }
207
+ else if (opts.fix) {
208
+ out(`\nno fixable issues found\n`);
209
+ }
210
+ }
211
+ return report.overall === 'error' ? 1 : 0;
212
+ }