aiden-runtime 4.1.5 → 4.6.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 (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. package/package.json +13 -3
@@ -19,6 +19,7 @@
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.CliCallbacks = void 0;
22
+ exports.mapBlockerToCard = mapBlockerToCard;
22
23
  exports.renderApprovalBox = renderApprovalBox;
23
24
  const box_1 = require("./box");
24
25
  async function defaultPrompts() {
@@ -45,6 +46,68 @@ const DECISION_CHOICES = [
45
46
  { name: 'Deny', value: 'deny' },
46
47
  ];
47
48
  const KNOWN_TIERS = new Set(['safe', 'caution', 'dangerous']);
49
+ // ── v4.3 Phase 3 — manual-blocker card mapping ────────────────────────────
50
+ /** Best-effort hostname extraction; falls back to the raw URL. */
51
+ function blockerHostname(url) {
52
+ try {
53
+ return new URL(url).hostname || url;
54
+ }
55
+ catch {
56
+ return url;
57
+ }
58
+ }
59
+ /**
60
+ * Map a BlockerSurface to the existing CapabilityCardData chrome.
61
+ * Pure helper — same shape per `BlockerKind`, parameterised by the
62
+ * blocker's URL + optional subtype for the body text. The renderer
63
+ * (`display.capabilityCard`) handles all rendering chrome; this
64
+ * function only fills the slots semantically.
65
+ *
66
+ * Public for unit tests; chatSession + callbacks consume via the
67
+ * `renderBlockerCardIfPresent` method below.
68
+ */
69
+ function mapBlockerToCard(blocker) {
70
+ const host = blockerHostname(blocker.url);
71
+ const labels = {
72
+ captcha: {
73
+ title: `CAPTCHA challenge at ${host}`,
74
+ canStill: ['Solve the challenge in the browser tab', 'Cancel this task'],
75
+ cannot: ['Continue automatically without your action'],
76
+ fix: `I'll wait — solve the ${blocker.subtype ?? 'CAPTCHA'} challenge and tell me when ready.`,
77
+ },
78
+ login: {
79
+ title: `Sign-in required at ${host}`,
80
+ canStill: ['Sign in via the browser tab', 'Cancel this task'],
81
+ cannot: ['Continue without authentication'],
82
+ fix: `I'll wait — sign in and let me know when done.`,
83
+ },
84
+ '2fa': {
85
+ title: `Two-factor code required at ${host}`,
86
+ canStill: ['Enter the 2FA code in the browser tab', 'Cancel this task'],
87
+ cannot: ['Continue without the verification code'],
88
+ fix: `I'll wait — enter your code and tell me when complete.`,
89
+ },
90
+ verification: {
91
+ title: `Identity verification at ${host}`,
92
+ canStill: ['Complete the verification in the browser', 'Cancel this task'],
93
+ cannot: ['Continue without verification'],
94
+ fix: `I'll wait — finish the verification and tell me when done.`,
95
+ },
96
+ consent: {
97
+ title: `Consent banner at ${host}`,
98
+ canStill: ['Dismiss the banner in the browser', 'Continue if banner is dismissable'],
99
+ cannot: ['Reliably interact with content while the banner blocks it'],
100
+ fix: `Dismiss the cookie or privacy banner and I'll retry.`,
101
+ },
102
+ };
103
+ const t = labels[blocker.kind];
104
+ return {
105
+ title: `🛑 ${t.title}`,
106
+ canStill: t.canStill,
107
+ cannotReliably: t.cannot,
108
+ fix: t.fix,
109
+ };
110
+ }
48
111
  function parseRiskTier(content) {
49
112
  const head = content.trim().toLowerCase().split(/\s+/)[0] ?? '';
50
113
  // Strip punctuation that might bleed in from JSON: "safe.", "caution,"...
@@ -161,6 +224,17 @@ class CliCallbacks {
161
224
  catch { /* defensive */ }
162
225
  return;
163
226
  }
227
+ // v4.3 Phase 3 — render a structured "agent needs human help"
228
+ // card when the browser observer detected a manual blocker
229
+ // (CAPTCHA / login / 2FA / verification / consent). Renders for
230
+ // ALL trail-row outcomes below — the blocker is independent of
231
+ // the tool's own success/fail signal. Inline placement gives
232
+ // the user immediate awareness; the model's next reply will
233
+ // explain in prose. Defensive: missing fields silently skip.
234
+ try {
235
+ this.renderBlockerCardIfPresent(result);
236
+ }
237
+ catch { /* defensive */ }
164
238
  // v4.1.4 reply-quality polish — Part 1.6. Helper used by ALL
165
239
  // outcome branches below so the activity indicator gets re-armed
166
240
  // for the gap that follows this tool (next tool, or final reply).
@@ -353,12 +427,84 @@ Reply with ONE word: safe, caution, or dangerous.`;
353
427
  const label = files.length > 0 ? files.join(', ') : 'none';
354
428
  this.display.dim(`[memory] refreshed system prompt (${label})`);
355
429
  };
430
+ /**
431
+ * v4.1.6 Polish 2 — post-turn skill-proposal handler.
432
+ *
433
+ * chatSession calls this AFTER `agentTurn` has rendered the agent's
434
+ * reply on screen. Internally:
435
+ * 1. Reuses `promptSkillProposal` (the existing inquirer modal)
436
+ * to ask the user "Save this as a reusable skill? Yes/No".
437
+ * 2. If accepted AND a SkillTeacher reference is wired, calls
438
+ * `skillTeacher.handleProposal({...skipPrompt})` to build
439
+ * the markdown + persist via skillManager.
440
+ * 3. Returns the result so chatSession can surface a confirmation
441
+ * line if needed.
442
+ *
443
+ * Decoupled from the agent's `runConversation` so the inquirer no
444
+ * longer fires mid-turn (the v4.1.5 visual-smoke regression).
445
+ *
446
+ * Defensive — exceptions in any step return `{created: false,
447
+ * reason: 'error'}` so a misbehaving prompt or save can't break
448
+ * the chat loop. The inquirer modal itself catches input-stream
449
+ * exceptions via the existing try/catch in promptSkillProposal.
450
+ */
451
+ this.handleSkillProposal = async (proposal) => {
452
+ // Step 1: ask the user (reuses existing modal).
453
+ let accept;
454
+ try {
455
+ accept = await this.promptSkillProposal(proposal);
456
+ }
457
+ catch {
458
+ return { created: false, reason: 'prompt_error' };
459
+ }
460
+ if (!accept) {
461
+ return { created: false, reason: 'declined' };
462
+ }
463
+ // Step 2: persist via SkillTeacher when available. Test harnesses
464
+ // that don't wire a teacher just get the prompt without the save.
465
+ if (!this.skillTeacher) {
466
+ return { created: false, reason: 'no_skill_teacher_wired' };
467
+ }
468
+ try {
469
+ // Pass `promptUser: undefined` to bypass the modal in
470
+ // `handleProposal` (we already showed it above). The teacher
471
+ // sees tier === 'tier_3_propose' WITHOUT a prompt callback,
472
+ // which it interprets as `no_prompt_callback` and skips the
473
+ // save — so we need to call the create branch directly.
474
+ //
475
+ // SkillTeacher's tier-4 (auto) branch creates without
476
+ // prompting; we reuse that path by passing a stub that always
477
+ // returns true (user already accepted above).
478
+ const result = await this.skillTeacher.handleProposal(proposal, {
479
+ promptUser: async () => true,
480
+ });
481
+ return result;
482
+ }
483
+ catch (err) {
484
+ return {
485
+ created: false,
486
+ reason: `create_failed: ${err instanceof Error ? err.message : String(err)}`,
487
+ };
488
+ }
489
+ };
356
490
  this.display = opts.display;
357
491
  this.auxiliaryClient = opts.auxiliaryClient;
358
492
  this.verboseMode = opts.verboseMode ?? 'normal';
359
493
  this.promptsPromise = opts.promptModule
360
494
  ? Promise.resolve(opts.promptModule)
361
495
  : defaultPrompts();
496
+ this.skillTeacher = opts.skillTeacher;
497
+ }
498
+ /**
499
+ * v4.1.6 Polish 2 — late-wire the SkillTeacher reference. aidenCLI
500
+ * constructs CliCallbacks early (the approval engine needs it
501
+ * stitched in), but SkillTeacher is built later in the boot
502
+ * sequence after skillLoader / skillManager are ready. Call this
503
+ * once after both exist so `handleSkillProposal` can persist
504
+ * accepted proposals.
505
+ */
506
+ setSkillTeacher(teacher) {
507
+ this.skillTeacher = teacher;
362
508
  }
363
509
  /** Update verbose mode at runtime (wired to /verbose). */
364
510
  setVerboseMode(mode) {
@@ -415,6 +561,30 @@ Reply with ONE word: safe, caution, or dangerous.`;
415
561
  this.toolTraceBeforeHook = opts.before;
416
562
  this.toolTraceAfterHook = opts.after;
417
563
  }
564
+ /**
565
+ * v4.3 Phase 3 — render a manual-blocker card when the browser
566
+ * observer detected a CAPTCHA / login / 2FA / verification /
567
+ * consent surface on the page. Reuses the existing capabilityCard
568
+ * chrome via a `mapBlockerToCard` semantic mapping — no new layout
569
+ * code, no new dedicated card component.
570
+ *
571
+ * Defensive: silently skips when the field shape doesn't match
572
+ * (no browserState, no blocker, unrecognised kind). Never throws
573
+ * — caller wraps in try/catch defensively.
574
+ *
575
+ * The blocker info is structural data emitted by the observer
576
+ * HOC (`tools/v4/browser/_observer.ts`); the renderer reads it
577
+ * from `result.result.browserState.blocker` after every browser
578
+ * tool call. Inline placement gives users immediate awareness
579
+ * before the model's next reply lands.
580
+ */
581
+ renderBlockerCardIfPresent(result) {
582
+ const inner = (result?.result ?? null);
583
+ const blocker = inner?.browserState?.blocker;
584
+ if (!blocker)
585
+ return;
586
+ this.display.capabilityCard(mapBlockerToCard(blocker));
587
+ }
418
588
  }
419
589
  exports.CliCallbacks = CliCallbacks;
420
590
  // Tier-3.1 (v4.1-tier3.1): replaced 🟢/🟡/🔴 emoji badges with
@@ -872,6 +872,21 @@ class ChatSession {
872
872
  }
873
873
  }
874
874
  async runAgentTurn(userInput) {
875
+ // v4.5 Phase 8b — daemon-scheduling intent check on the user's
876
+ // initial message. Classifies regex hits like "every day at",
877
+ // "watch this folder", "when an email arrives" — and queues a
878
+ // tip to render at the END of the agent's response (so it
879
+ // doesn't crowd the agent's actual reply). Engine handles
880
+ // budget + dismissal.
881
+ let _deferredTip = null;
882
+ try {
883
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
884
+ const { getSuggestionEngine } = require('../../core/v4/suggestionEngine');
885
+ const t = getSuggestionEngine().checkInitialMessage(userInput);
886
+ if (t)
887
+ _deferredTip = t;
888
+ }
889
+ catch { /* defensive — never block a turn on a suggestion */ }
875
890
  // Phase 30.2.1 — explore mode: short-circuit BEFORE building the
876
891
  // turn-status spinner / agent call. The wizard skipped, so there's
877
892
  // no real provider to talk to. Print a friendly redirect to /setup
@@ -907,6 +922,43 @@ class ChatSession {
907
922
  const baseHistory = newHistory.length > 0
908
923
  ? [...this.history, ...newHistory, userMsg]
909
924
  : [...this.history, userMsg];
925
+ // v4.6 Phase 2Q-B — REPL parent-run row (best-effort).
926
+ //
927
+ // Insert a `runs` row tagged with this REPL session BEFORE the
928
+ // agent loop dispatches. Capture the row id into the shared
929
+ // `replParentRunRef` so any `spawn_sub_agent` / `subagent_fanout`
930
+ // child this turn produces can link back via
931
+ // `spawned_from_run_id`. The ref is cleared in the catch /
932
+ // success paths below regardless of outcome.
933
+ //
934
+ // Defensive: a runStore write failure (locked DB, schema drift,
935
+ // etc.) must NOT crash the REPL — every persistence call here is
936
+ // wrapped in try/catch and reduces to a logged warning. The
937
+ // user-facing turn still runs.
938
+ let replRunId = null;
939
+ const replRunStore = this.opts.replRunStore;
940
+ const replInstanceId = this.opts.replInstanceId;
941
+ const replParentRunRef = this.opts.replParentRunRef;
942
+ if (replRunStore && replInstanceId && this.sessionId) {
943
+ try {
944
+ replRunId = replRunStore.create({
945
+ sessionId: this.sessionId,
946
+ instanceId: replInstanceId,
947
+ status: 'running',
948
+ startedAt: turnStartedAt,
949
+ });
950
+ if (replParentRunRef) {
951
+ replParentRunRef.runId = replRunId;
952
+ replParentRunRef.sessionId = this.sessionId;
953
+ }
954
+ }
955
+ catch (err) {
956
+ // Logged once per turn; the user's chat is not interrupted.
957
+ // eslint-disable-next-line no-console
958
+ console.warn('[runs] failed to write REPL parent-run row:', err instanceof Error ? err.message : String(err));
959
+ replRunId = null;
960
+ }
961
+ }
910
962
  // Phase 16c: streaming gated on display.streaming config.
911
963
  // v4.1.4 Part 1.6: PRODUCTION DEFAULT FLIPPED FROM FALSE TO TRUE.
912
964
  // Streaming delivers the activity indicator, tool-row live tick,
@@ -1164,6 +1216,35 @@ class ChatSession {
1164
1216
  this.history = result.messages;
1165
1217
  this.totalUsage.inputTokens += result.totalUsage.inputTokens;
1166
1218
  this.totalUsage.outputTokens += result.totalUsage.outputTokens;
1219
+ // v4.6 Phase 2Q-B — finalize the REPL parent-run row on success.
1220
+ // `finishReason` from the agent loop maps directly into our DB
1221
+ // status: `stop` → completed; `interrupted` / `tool_loop` →
1222
+ // surface as 'interrupted' so it's visible in `runs list`;
1223
+ // `budget_exhausted` / `error` → failed. Wrapped in try/catch
1224
+ // so even a runStore write failure here can't crash the REPL.
1225
+ if (replRunStore && replRunId !== null) {
1226
+ try {
1227
+ const dbStatus = result.finishReason === 'stop' ? 'completed' :
1228
+ result.finishReason === 'interrupted' ? 'interrupted' :
1229
+ result.finishReason === 'tool_loop' ? 'interrupted' :
1230
+ 'failed';
1231
+ replRunStore.setStatus(replRunId, dbStatus, {
1232
+ finishReason: result.finishReason,
1233
+ completedAt: Date.now(),
1234
+ });
1235
+ }
1236
+ catch (err) {
1237
+ // eslint-disable-next-line no-console
1238
+ console.warn('[runs] failed to finalize REPL parent-run row:', err instanceof Error ? err.message : String(err));
1239
+ }
1240
+ }
1241
+ // Clear the shared ref so a subsequent turn (or stray
1242
+ // spawn/fanout dispatched between turns from a slash command
1243
+ // handler) doesn't see a stale parent id.
1244
+ if (replParentRunRef) {
1245
+ replParentRunRef.runId = null;
1246
+ replParentRunRef.sessionId = null;
1247
+ }
1167
1248
  // Phase 16d: surface inline confirmations for verified memory writes.
1168
1249
  // We MUST gate on verified=true (the post-write read flag from
1169
1250
  // MemoryGuard) — HonestyEnforcement uses the same flag to catch
@@ -1178,9 +1259,25 @@ class ChatSession {
1178
1259
  if (result.toolCallTrace && result.toolCallTrace.length > 0) {
1179
1260
  this.sessionToolTrace.push(...result.toolCallTrace);
1180
1261
  }
1181
- // When streaming was active and emitted the final content already,
1182
- // skip the markdown re-render we'd otherwise duplicate text.
1183
- if (result.finalContent && !streamingActive) {
1262
+ // v4.1.6 spike (TCE) tool-loop terminal surface. When the
1263
+ // agent ended the turn via the recovery controller's surface
1264
+ // stage, render a structured-failure card instead of the
1265
+ // (empty) reply. Same chrome as auth / platform capability
1266
+ // cards — fits the established Aiden UX language for
1267
+ // "the action you wanted didn't happen, here's why and what
1268
+ // you can do." Surface BEFORE the tool→reply separator path
1269
+ // below because there's no agent reply to introduce.
1270
+ if (result.finishReason === 'tool_loop' && result.toolLoopCard) {
1271
+ // Emit the muted rule so the card visually separates from
1272
+ // the tool trail above it.
1273
+ emitToolReplySeparator();
1274
+ this.opts.display.capabilityCard(result.toolLoopCard);
1275
+ }
1276
+ else if (result.finalContent && !streamingActive) {
1277
+ // When streaming was active and emitted the final content
1278
+ // already, skip the markdown re-render — we'd otherwise
1279
+ // duplicate text.
1280
+ //
1184
1281
  // v4.1.5 Issue O — non-streaming reply path. Emit the muted
1185
1282
  // rule between the tool trail and the agent header before
1186
1283
  // the one-shot reply lands. Idempotent + tool-gated by
@@ -1188,6 +1285,30 @@ class ChatSession {
1188
1285
  emitToolReplySeparator();
1189
1286
  this.opts.display.write(this.opts.display.agentTurn(result.finalContent));
1190
1287
  }
1288
+ // v4.1.6 Polish 2 — post-render skill-proposal handler.
1289
+ // The agent loop now SKIPS the inquirer prompt when a
1290
+ // prompt callback is wired, surfacing the SkillProposal
1291
+ // here instead. We fire the prompt AFTER the agent reply
1292
+ // has rendered so the user sees the answer before being
1293
+ // asked "save this as a reusable skill?" — fixing the
1294
+ // v4.1.5 visual-smoke regression where the prompt fired
1295
+ // mid-turn and clobbered the reply.
1296
+ //
1297
+ // Wrapped in try/catch so a buggy proposal flow never
1298
+ // breaks the chat loop. A successful save surfaces a
1299
+ // dim confirmation line that fits the established
1300
+ // memory-confirmation chrome.
1301
+ if (result.skillProposal && this.opts.callbacks?.handleSkillProposal) {
1302
+ try {
1303
+ const saveResult = await this.opts.callbacks.handleSkillProposal(result.skillProposal);
1304
+ if (saveResult?.created && saveResult.skillName) {
1305
+ this.opts.display.dim(` ✓ Saved as skill: ${saveResult.skillName}`);
1306
+ }
1307
+ }
1308
+ catch {
1309
+ /* defensive — never let proposal flow break the chat loop */
1310
+ }
1311
+ }
1191
1312
  if (this.sessionId) {
1192
1313
  // Only persist the new tail of messages — what got added this turn.
1193
1314
  const newSlice = this.history.slice(turnStart);
@@ -1195,6 +1316,20 @@ class ChatSession {
1195
1316
  }
1196
1317
  this.setStatusState({ kind: 'ready' });
1197
1318
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
1319
+ // v4.5 Phase 8b — surface a deferred daemon-scheduling tip
1320
+ // queued at turn start. Renders AFTER the agent's response per
1321
+ // Q-P8b-3(b) — the user reads the answer first, then sees the
1322
+ // ambient capability hint.
1323
+ if (_deferredTip) {
1324
+ try {
1325
+ this.opts.display.dim(_deferredTip.message);
1326
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1327
+ const { getSuggestionEngine } = require('../../core/v4/suggestionEngine');
1328
+ getSuggestionEngine().recordFired(_deferredTip.slot);
1329
+ }
1330
+ catch { /* defensive */ }
1331
+ _deferredTip = null;
1332
+ }
1198
1333
  // Tier-3.1a: dim full-width rule between the agent reply and the
1199
1334
  // post-turn status footer.
1200
1335
  this.opts.display.write(` ${this.opts.display.rule()}\n`);
@@ -1220,6 +1355,26 @@ class ChatSession {
1220
1355
  progressBar?.hide();
1221
1356
  if (streamingActive)
1222
1357
  this.opts.display.streamComplete();
1358
+ // v4.6 Phase 2Q-B — finalize REPL parent-run row on error.
1359
+ // Visible in `aiden runs list` as a failed top-level row so
1360
+ // operators can correlate a chat error with whatever children
1361
+ // it had already kicked off this turn.
1362
+ if (replRunStore && replRunId !== null) {
1363
+ try {
1364
+ replRunStore.setStatus(replRunId, 'failed', {
1365
+ finishReason: 'error',
1366
+ completedAt: Date.now(),
1367
+ });
1368
+ }
1369
+ catch (e2) {
1370
+ // eslint-disable-next-line no-console
1371
+ console.warn('[runs] failed to mark REPL parent-run failed:', e2 instanceof Error ? e2.message : String(e2));
1372
+ }
1373
+ }
1374
+ if (replParentRunRef) {
1375
+ replParentRunRef.runId = null;
1376
+ replParentRunRef.sessionId = null;
1377
+ }
1223
1378
  const msg = err?.message ?? String(err);
1224
1379
  // v4.1.3-prebump: classify the error so the suggestion below
1225
1380
  // points at the actual fix instead of the generic "/model or
@@ -1349,6 +1504,30 @@ class ChatSession {
1349
1504
  providerOk: !this.opts.unconfigured,
1350
1505
  version: version_1.VERSION,
1351
1506
  }) + '\n');
1507
+ // v4.6 Phase 3A — operator kill-switch indicator. Lands ABOVE
1508
+ // the blank-line + provider-source annotation so an operator
1509
+ // who paused in a prior session sees the state immediately on
1510
+ // boot, alongside the standard status pills. Single dim
1511
+ // warning line; no special chrome — the message itself is the
1512
+ // visual signal.
1513
+ try {
1514
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1515
+ const { getSpawnPause } = require('../../core/v4/subagent/spawnPause');
1516
+ const s = getSpawnPause().status();
1517
+ if (s.paused) {
1518
+ const reasonSuffix = s.reason ? ` · ${s.reason}` : '';
1519
+ const durationSuffix = typeof s.durationMs === 'number'
1520
+ ? ` · ${formatDuration(s.durationMs)}`
1521
+ : '';
1522
+ display.warn(`spawn-pause: ON${reasonSuffix}${durationSuffix} — use /spawn-pause off to resume`);
1523
+ }
1524
+ }
1525
+ catch {
1526
+ // Singleton not initialised (test stubs, etc.) — silently skip.
1527
+ }
1528
+ // v4.5 TUI polish — blank line so the status pills row doesn't
1529
+ // crowd the muted source annotation right beneath it.
1530
+ display.write('\n');
1352
1531
  // v4.1.3-prebump: dim source annotation under the pills row so the
1353
1532
  // user can see WHY this provider/model was chosen — closes the
1354
1533
  // information gap that made Case 3 (persisted-config) look like a
@@ -1408,10 +1587,73 @@ class ChatSession {
1408
1587
  }
1409
1588
  // Scroll footer (parchment at ≥80 cols, single-line credits below).
1410
1589
  display.write(display.scrollFooter() + '\n');
1590
+ // v4.5 update system — boxed three-option prompt rendered AFTER
1591
+ // the boot card / status pills (Q-U5b less-intrusive position),
1592
+ // BEFORE the bottomPromptHint. Fires only when:
1593
+ // - update check came back with `updateAvailable && !skipped`
1594
+ // - stdin is a TTY (non-interactive boots short-circuit to 'later')
1595
+ // 5-second timeout defaults to 'later' so a user away from
1596
+ // keyboard isn't held up. Skip-on-'n' writes the version to the
1597
+ // .update_check.json cache so subsequent boots stay quiet until
1598
+ // a newer release ships.
1599
+ try {
1600
+ await this.maybeShowBootUpdatePrompt();
1601
+ }
1602
+ catch { /* never let the update prompt crash boot */ }
1411
1603
  // Bottom prompt hint — final line of the boot card.
1412
1604
  display.write('\n');
1413
1605
  display.write(display.bottomPromptHint() + '\n');
1414
1606
  }
1607
+ /**
1608
+ * v4.5 update system — orchestrates the boot prompt. Lazy-imports
1609
+ * the update modules so non-boot code paths (e.g. test harness
1610
+ * sessions constructed without paths wired) don't pay the cost.
1611
+ */
1612
+ async maybeShowBootUpdatePrompt() {
1613
+ if (!this.opts.paths)
1614
+ return;
1615
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1616
+ const cu = require('../../core/v4/update/checkUpdate');
1617
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1618
+ const md = require('../../core/v4/update/installMethodDetect');
1619
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1620
+ const ss = require('../../core/v4/update/skipState');
1621
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1622
+ const bp = require('./updateBootPrompt');
1623
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1624
+ const ei = require('../../core/v4/update/executeInstall');
1625
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1626
+ const ver = require('../../core/version');
1627
+ const status = await cu.checkForUpdate({ paths: this.opts.paths, installedVersion: ver.VERSION });
1628
+ if (!status.updateAvailable || !status.latest || status.skipped)
1629
+ return;
1630
+ const method = md.detectInstallMethod();
1631
+ const choice = await bp.showBootUpdatePrompt({
1632
+ status, method,
1633
+ display: { write: (s) => this.opts.display.write(s), dim: (s) => this.opts.display.dim(s) },
1634
+ });
1635
+ if (choice === 'install') {
1636
+ if (method.inProcessInstallSupported) {
1637
+ this.opts.display.write(`Installing aiden-runtime ${status.latest}…\n`);
1638
+ const result = await ei.executeInstall({ packageSpec: `aiden-runtime@${status.latest}` });
1639
+ if (result.success) {
1640
+ this.opts.display.write(` ✓ aiden-runtime ${result.installedVersion ?? status.latest} installed.\n`);
1641
+ this.opts.display.dim('Restart Aiden to apply: type /quit then re-run `aiden`.');
1642
+ }
1643
+ else {
1644
+ this.opts.display.warn(result.error ?? 'Install failed (no error message).');
1645
+ }
1646
+ }
1647
+ else {
1648
+ this.opts.display.write(`To update, run:\n ${method.updateCommand(status.latest)}\n`);
1649
+ }
1650
+ }
1651
+ else if (choice === 'skip') {
1652
+ await cu.updateCacheFile(this.opts.paths, (current) => ss.applySkip(current, status.latest));
1653
+ this.opts.display.dim(` skipped ${status.latest}. Boot prompt resumes when a newer version ships.`);
1654
+ }
1655
+ // 'later' = no-op; prompt fires again next session.
1656
+ }
1415
1657
  /** Phase 22 Task 4: state transitions for the right-most segment. */
1416
1658
  setStatusState(state) {
1417
1659
  this.statusState = state;
@@ -0,0 +1,94 @@
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/_runtimeToggleHelpers.ts — v4.5 Phase 8a.
10
+ *
11
+ * Shared `flip` + `printStatus` helpers used by /sandbox, /tce, and
12
+ * /browser-depth. Each subsystem command is a tiny wrapper over
13
+ * these — same on/off/status surface, same status format.
14
+ *
15
+ * The persist path goes through `ConfigManager.set()` + `save()` so
16
+ * the runtime_toggles section is written verbatim to config.yaml.
17
+ * The runtimeToggles singleton's `set()` then fires onChange
18
+ * callbacks for cached consumers (sandboxConfig's singleton).
19
+ *
20
+ * Test seam: `flip` and `printStatus` accept a `ctx` shaped like
21
+ * SlashCommandContext (just `display`, optionally `config`). Tests
22
+ * pass a minimal stub.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.flip = flip;
26
+ exports.printStatus = printStatus;
27
+ exports.parseSubcommand = parseSubcommand;
28
+ const runtimeToggles_1 = require("../../../core/v4/runtimeToggles");
29
+ const LABEL = {
30
+ sandbox: 'Sandbox',
31
+ tce: 'TCE',
32
+ browser_depth: 'Browser depth',
33
+ suggestions: 'Suggestions',
34
+ planner_guard: 'Planner-Guard',
35
+ };
36
+ const CONFIG_DOTTED = {
37
+ sandbox: 'runtime_toggles.sandbox',
38
+ tce: 'runtime_toggles.tce',
39
+ browser_depth: 'runtime_toggles.browser_depth',
40
+ suggestions: 'runtime_toggles.suggestions',
41
+ planner_guard: 'runtime_toggles.planner_guard',
42
+ };
43
+ /**
44
+ * Apply a toggle change. When `ctx.config` is wired, persists to
45
+ * config.yaml. Otherwise the flip is in-process only (current
46
+ * session sees the new value; next process boot doesn't).
47
+ *
48
+ * Returns nothing — prints status via ctx.display.
49
+ */
50
+ async function flip(key, value, ctx) {
51
+ const rt = (0, runtimeToggles_1.getRuntimeToggles)();
52
+ // Persist when a ConfigManager is wired.
53
+ if (ctx.config) {
54
+ try {
55
+ ctx.config.set(CONFIG_DOTTED[key], value);
56
+ await ctx.config.save();
57
+ }
58
+ catch (e) {
59
+ ctx.display.warn(`[${key}] config.yaml save failed (${e instanceof Error ? e.message : String(e)}); ` +
60
+ `flip applies to this session only.`);
61
+ }
62
+ }
63
+ await rt.set(key, value, { persist: false });
64
+ printStatus(key, ctx);
65
+ }
66
+ /**
67
+ * Print the current state of one toggle. One-line output per
68
+ * Q-P8a-2(a):
69
+ *
70
+ * `Sandbox: ON (source: config)`
71
+ *
72
+ * `source` reveals which precedence layer provided the value —
73
+ * critical for debugging "why is it ON when my .env says 0".
74
+ */
75
+ function printStatus(key, ctx) {
76
+ const snap = (0, runtimeToggles_1.getRuntimeToggles)().snapshot()[key];
77
+ const label = LABEL[key];
78
+ const state = snap.value ? 'ON' : 'OFF';
79
+ ctx.display.write(`${label}: ${state} (source: ${snap.source})\n`);
80
+ }
81
+ /**
82
+ * Parse the on/off/status subcommand. Returns null when the input
83
+ * is unrecognised (caller prints usage).
84
+ */
85
+ function parseSubcommand(raw) {
86
+ const s = (raw ?? 'status').toLowerCase();
87
+ if (s === 'on' || s === 'enable' || s === '1' || s === 'true')
88
+ return 'on';
89
+ if (s === 'off' || s === 'disable' || s === '0' || s === 'false')
90
+ return 'off';
91
+ if (s === 'status' || s === '' || s === undefined)
92
+ return 'status';
93
+ return null;
94
+ }
@@ -0,0 +1,45 @@
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/browserDepth.ts — v4.5 Phase 8a.
10
+ *
11
+ * `/browser-depth on|off|status` — flip the v4.3 state-aware
12
+ * browser observer (URL/DOM/iframe-tree capture, stale-ref retry,
13
+ * manual-blocker detection) without restart. Persists to
14
+ * config.yaml. Env var AIDEN_BROWSER_DEPTH always wins.
15
+ *
16
+ * Q-P8a-5(a): named `/browser-depth` to mirror the env var
17
+ * exactly. Reserves `/browser` for future browser-navigation
18
+ * commands so the namespace stays unambiguous.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.browserDepth = void 0;
22
+ const _runtimeToggleHelpers_1 = require("./_runtimeToggleHelpers");
23
+ exports.browserDepth = {
24
+ name: 'browser-depth',
25
+ description: 'Toggle the v4.3 state-aware browser observer.',
26
+ category: 'system',
27
+ icon: '🌐',
28
+ handler: async (ctx) => {
29
+ const sub = (0, _runtimeToggleHelpers_1.parseSubcommand)(ctx.args[0]);
30
+ if (sub === 'on') {
31
+ await (0, _runtimeToggleHelpers_1.flip)('browser_depth', true, ctx);
32
+ return {};
33
+ }
34
+ if (sub === 'off') {
35
+ await (0, _runtimeToggleHelpers_1.flip)('browser_depth', false, ctx);
36
+ return {};
37
+ }
38
+ if (sub === 'status') {
39
+ (0, _runtimeToggleHelpers_1.printStatus)('browser_depth', ctx);
40
+ return {};
41
+ }
42
+ ctx.display.printError('Usage: /browser-depth on|off|status');
43
+ return {};
44
+ },
45
+ };