aiden-runtime 4.1.1 → 4.1.3

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 (68) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +169 -9
  3. package/dist/cli/v4/callbacks.js +20 -2
  4. package/dist/cli/v4/chatSession.js +644 -16
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/doctor.js +23 -27
  7. package/dist/cli/v4/commands/help.js +4 -0
  8. package/dist/cli/v4/commands/index.js +10 -1
  9. package/dist/cli/v4/commands/model.js +30 -1
  10. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  11. package/dist/cli/v4/commands/update.js +102 -0
  12. package/dist/cli/v4/defaultSoul.js +68 -2
  13. package/dist/cli/v4/display/capabilityCard.js +135 -0
  14. package/dist/cli/v4/display/sessionEndCard.js +127 -0
  15. package/dist/cli/v4/display/toolTrail.js +172 -0
  16. package/dist/cli/v4/display.js +492 -142
  17. package/dist/cli/v4/doctor.js +472 -58
  18. package/dist/cli/v4/doctorLiveness.js +65 -10
  19. package/dist/cli/v4/promotionPrompt.js +332 -0
  20. package/dist/cli/v4/providerBootSelector.js +144 -0
  21. package/dist/cli/v4/replyRenderer.js +311 -20
  22. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  23. package/dist/cli/v4/skinEngine.js +14 -3
  24. package/dist/cli/v4/toolPreview.js +153 -0
  25. package/dist/core/tools/nowPlaying.js +7 -15
  26. package/dist/core/v4/aidenAgent.js +91 -29
  27. package/dist/core/v4/capabilities.js +89 -0
  28. package/dist/core/v4/contextCompressor.js +25 -8
  29. package/dist/core/v4/distillationIndex.js +167 -0
  30. package/dist/core/v4/distillationStore.js +98 -0
  31. package/dist/core/v4/logger/logger.js +40 -9
  32. package/dist/core/v4/promotionCandidates.js +234 -0
  33. package/dist/core/v4/promptBuilder.js +145 -1
  34. package/dist/core/v4/sessionDistiller.js +452 -0
  35. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  36. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  37. package/dist/core/v4/subsystemHealth.js +143 -0
  38. package/dist/core/v4/toolRegistry.js +16 -1
  39. package/dist/core/v4/update/executeInstall.js +233 -0
  40. package/dist/core/version.js +1 -1
  41. package/dist/moat/memoryGuard.js +111 -0
  42. package/dist/moat/plannerGuard.js +19 -0
  43. package/dist/moat/skillTeacher.js +14 -5
  44. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  45. package/dist/providers/v4/errors.js +112 -4
  46. package/dist/providers/v4/modelDefaults.js +65 -0
  47. package/dist/providers/v4/registry.js +9 -2
  48. package/dist/providers/v4/runtimeResolver.js +6 -0
  49. package/dist/tools/v4/index.js +80 -1
  50. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  51. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  52. package/dist/tools/v4/sessions/recallSession.js +177 -0
  53. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  54. package/dist/tools/v4/system/_psHelpers.js +123 -0
  55. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  56. package/dist/tools/v4/system/appClose.js +79 -0
  57. package/dist/tools/v4/system/appInput.js +154 -0
  58. package/dist/tools/v4/system/appLaunch.js +218 -0
  59. package/dist/tools/v4/system/clipboardRead.js +54 -0
  60. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  61. package/dist/tools/v4/system/mediaKey.js +109 -0
  62. package/dist/tools/v4/system/mediaSessions.js +163 -0
  63. package/dist/tools/v4/system/mediaTransport.js +211 -0
  64. package/dist/tools/v4/system/osProcessList.js +99 -0
  65. package/dist/tools/v4/system/screenshot.js +106 -0
  66. package/dist/tools/v4/system/volumeSet.js +157 -0
  67. package/package.json +4 -1
  68. package/skills/system_control.md +185 -69
@@ -33,11 +33,6 @@ exports.LOG_LEVEL_ORDER = {
33
33
  warn: 30,
34
34
  error: 40,
35
35
  };
36
- /**
37
- * Default `Logger` implementation. Holds a list of sinks and the
38
- * current scope; child loggers share the same sink list (so updating
39
- * the level / detaching at the root affects everything).
40
- */
41
36
  class CoreLogger {
42
37
  /**
43
38
  * Construct a root logger. Use `child(segment)` for sub-loggers.
@@ -47,7 +42,11 @@ class CoreLogger {
47
42
  this.scope = opts.scope ?? '';
48
43
  this.sinks = opts.sinks;
49
44
  this.level = opts.level ?? 'debug';
50
- this.sinksOwner = { sinks: this.sinks, level: this.level };
45
+ this.sinksOwner = {
46
+ sinks: this.sinks,
47
+ level: this.level,
48
+ counters: opts.sinks.map(() => ({ totalWrites: 0, failures: 0 })),
49
+ };
51
50
  }
52
51
  /** Internal — used by `child()` to share state with the root. */
53
52
  static childOf(parent, segment) {
@@ -73,6 +72,21 @@ class CoreLogger {
73
72
  }
74
73
  detachAll() {
75
74
  this.sinksOwner.sinks.length = 0;
75
+ this.sinksOwner.counters.length = 0;
76
+ }
77
+ getSinkHealth() {
78
+ const out = [];
79
+ for (let i = 0; i < this.sinksOwner.sinks.length; i += 1) {
80
+ const sink = this.sinksOwner.sinks[i];
81
+ const counter = this.sinksOwner.counters[i] ?? { totalWrites: 0, failures: 0 };
82
+ out.push({
83
+ name: sink.name ?? `sink:${i}`,
84
+ totalWrites: counter.totalWrites,
85
+ failures: counter.failures,
86
+ ...(counter.lastError ? { lastError: counter.lastError } : {}),
87
+ });
88
+ }
89
+ return out;
76
90
  }
77
91
  debug(msg, ctx) { this.write('debug', msg, ctx); }
78
92
  info(msg, ctx) { this.write('info', msg, ctx); }
@@ -89,12 +103,29 @@ class CoreLogger {
89
103
  ctx,
90
104
  };
91
105
  // Sinks must not throw — the helpers in ./sinks/* all wrap their
92
- // I/O in try/catch. Be defensive anyway.
93
- for (const s of this.sinksOwner.sinks) {
106
+ // I/O in try/catch. Be defensive anyway. Phase v4.1.2-slice3:
107
+ // bump the per-sink counter and capture the most recent failure
108
+ // message so `aiden doctor` can render it. The counter itself is
109
+ // never logged through this logger (would recurse).
110
+ for (let i = 0; i < this.sinksOwner.sinks.length; i += 1) {
111
+ const s = this.sinksOwner.sinks[i];
112
+ const c = this.sinksOwner.counters[i];
113
+ if (c)
114
+ c.totalWrites += 1;
94
115
  try {
95
116
  s.write(record);
96
117
  }
97
- catch { /* logging must not break callers */ }
118
+ catch (err) {
119
+ if (c) {
120
+ c.failures += 1;
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ c.lastError = {
123
+ message: msg.length > 200 ? msg.slice(0, 197) + '...' : msg,
124
+ at: new Date(),
125
+ };
126
+ }
127
+ /* logging must not break callers */
128
+ }
98
129
  }
99
130
  }
100
131
  }
@@ -0,0 +1,234 @@
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
+ * core/v4/promotionCandidates.ts — Phase v4.1.2-memory-D.
10
+ *
11
+ * Pure module that builds the list of "should we promote this to
12
+ * MEMORY.md `## Durable facts`?" candidates at session-end. The CLI
13
+ * surface (`cli/v4/promotionPrompt.ts`) reads the candidates, asks
14
+ * the user, and writes the approved subset.
15
+ *
16
+ * Sources combined (per Phase D's Q1 decision — A + B, defer C):
17
+ *
18
+ * A. Explicit user signals — regex over `history` user messages.
19
+ * "remember that X", "save this", "for next time", "don't forget"
20
+ * → the captured phrase becomes the candidate text. The
21
+ * surrounding user message is kept as `context` so the user can
22
+ * verify what they're promoting before approving.
23
+ *
24
+ * B. Distillation `decisions[]` + `open_items[]` — Phase A+B's
25
+ * structured output. Decisions are "X was chosen over Y"; open
26
+ * items are unfinished work / next-time prompts. Both are
27
+ * durable-worthy.
28
+ *
29
+ * C. Recurring facts across sessions — DEFERRED. Substring matching
30
+ * alone produces false positives ("any session mentioning Aiden"
31
+ * matches every other one). Semantic similarity belongs in Phase E
32
+ * alongside embeddings; lands when that slice ships.
33
+ *
34
+ * Priority ordering (drives the rendered list AND dedup-precedence):
35
+ * 1 — explicit (user EXPLICITLY asked to remember)
36
+ * 2 — decision (model identified as a settled decision)
37
+ * 3 — open_item (unfinished work — actionable next time)
38
+ *
39
+ * Dedup rules:
40
+ * - Within the candidate list: same-text (case-fold substring)
41
+ * candidates from multiple sources fold to highest priority.
42
+ * - Against existing durable body: substring-match every candidate
43
+ * against the caller's `existingDurableBody` (case-fold). Skipped
44
+ * candidates count toward `dedupedAgainstExisting` so the caller
45
+ * can surface the dim "N candidates already in durable facts"
46
+ * line per Phase D's Q5 first-run UX.
47
+ *
48
+ * Output cap: 10 candidates max (per Q3). Forces intentionality —
49
+ * sessions with 20+ durable-worthy items signal the user should
50
+ * reconsider what's actually durable.
51
+ */
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.MAX_CANDIDATES = void 0;
54
+ exports.extractExplicitSignals = extractExplicitSignals;
55
+ exports.extractDistillationCandidates = extractDistillationCandidates;
56
+ exports.extractCandidates = extractCandidates;
57
+ exports.MAX_CANDIDATES = 10;
58
+ // ── Source A: explicit signals ────────────────────────────────────────────
59
+ /**
60
+ * Regex set for explicit promotion signals. Each pattern's capture
61
+ * group `[1]` is the phrase the user wants remembered. Anchored on
62
+ * word boundaries so partial-word matches don't fire ("remembering" ≠
63
+ * "remember").
64
+ *
65
+ * Capture semantics — Phase v4.1.2-bug-Z:
66
+ *
67
+ * Previous: capture terminated at any `[.!?\n]` (sentence boundary).
68
+ * That truncated common payloads with periods:
69
+ * - version strings: "gpt-5.5" → captured as "gpt-5"
70
+ * - URLs: "https://api.example.com/v1" → "https://api"
71
+ * - semver: "v1.2.3" → "v1"
72
+ * - filenames: "config.test.ts" → "config"
73
+ *
74
+ * Current: capture rest-of-payload until BLANK-LINE BOUNDARY
75
+ * (`\n\s*\n`) or end-of-string. Regex detects intent (the user said
76
+ * "remember"), not meaning (what counts as a sentence). The `s` flag
77
+ * makes `.` match newlines so multi-line payloads survive a single
78
+ * capture.
79
+ *
80
+ * Trade-off: when multiple markers fire within one sentence-run
81
+ * (no blank-line boundaries), the first-marker capture extends to
82
+ * end-of-payload and the second marker's narrower capture is
83
+ * dedup-folded into it. One candidate covers both facts. Cleanly
84
+ * paragraph-separated multi-markers still split because the
85
+ * blank-line boundary terminates the first capture before the
86
+ * second marker's position. The promotion prompt shows the full
87
+ * capture so the user approves knowingly.
88
+ */
89
+ // Separator tolerance: between the verb-phrase ("remember that",
90
+ // "save this", "don't forget to") and the fact, accept whitespace
91
+ // AND optional punctuation (`:`, `,`). Some users naturally write
92
+ // "remember that: the port is 4200" or "save this — we use X".
93
+ const SEP = '[\\s:,—-]+';
94
+ // Capture-end: blank line (`\n` + optional whitespace + `\n`) OR
95
+ // end-of-string. The `s` flag lets `.` match newlines so single-marker
96
+ // multi-line payloads stay in one capture.
97
+ const END = '(?:\\n\\s*\\n|$)';
98
+ const EXPLICIT_SIGNAL_PATTERNS = Object.freeze([
99
+ new RegExp(`\\bremember${SEP}(?:that|this)${SEP}(.+?)${END}`, 'gis'),
100
+ new RegExp(`\\bsave${SEP}(?:this|that)${SEP}(?:to memory${SEP})?(.+?)${END}`, 'gis'),
101
+ new RegExp(`\\bfor next time${SEP}(.+?)${END}`, 'gis'),
102
+ new RegExp(`\\bdon'?t forget${SEP}(?:that|to)${SEP}(.+?)${END}`, 'gis'),
103
+ ]);
104
+ /**
105
+ * Strip leading filler that often slips past the regex's "that|this"
106
+ * anchor ("that the", "this — "), and trim. Empty / too-short results
107
+ * are signalled by returning the empty string; caller drops them.
108
+ */
109
+ function cleanCandidateText(raw) {
110
+ let s = raw.trim();
111
+ // Drop leading "that ", "this ", "to " (the regex caught them
112
+ // sometimes when they sat between the verb and the fact).
113
+ s = s.replace(/^(?:that|this|to)\s+/i, '').trim();
114
+ // Drop trailing punctuation noise.
115
+ s = s.replace(/[\s,;:]+$/, '');
116
+ return s;
117
+ }
118
+ function extractExplicitSignals(history) {
119
+ const out = [];
120
+ for (const msg of history) {
121
+ if (msg.role !== 'user')
122
+ continue;
123
+ const text = typeof msg.content === 'string' ? msg.content : '';
124
+ if (!text)
125
+ continue;
126
+ for (const pat of EXPLICIT_SIGNAL_PATTERNS) {
127
+ // Recreate per-message so the global flag resets.
128
+ const re = new RegExp(pat.source, pat.flags);
129
+ let m;
130
+ while ((m = re.exec(text)) !== null) {
131
+ const cleaned = cleanCandidateText(m[1] ?? '');
132
+ if (cleaned.length < 4)
133
+ continue; // 1-3 char hits are noise
134
+ out.push({
135
+ text: cleaned,
136
+ source: 'explicit',
137
+ context: text.trim(),
138
+ priority: 1,
139
+ });
140
+ }
141
+ }
142
+ }
143
+ return out;
144
+ }
145
+ // ── Source B: distillation decisions + open_items ─────────────────────────
146
+ function extractDistillationCandidates(dist) {
147
+ const out = [];
148
+ for (const d of dist.decisions) {
149
+ const t = d.trim();
150
+ if (t.length >= 4) {
151
+ out.push({ text: t, source: 'decision', priority: 2 });
152
+ }
153
+ }
154
+ for (const o of dist.open_items) {
155
+ const t = o.trim();
156
+ if (t.length >= 4) {
157
+ out.push({ text: t, source: 'open_item', priority: 3 });
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+ // ── Dedup + ranking ───────────────────────────────────────────────────────
163
+ function normalize(s) {
164
+ return s.toLowerCase().replace(/\s+/g, ' ').trim();
165
+ }
166
+ /**
167
+ * Within-session dedup: when the same fact surfaces from multiple
168
+ * sources, keep the highest-priority one. Substring-match in both
169
+ * directions so "Aiden runs on port 4200" and "Port 4200 for Aiden"
170
+ * collide on the longer-containing case.
171
+ */
172
+ function dedupWithinSession(input) {
173
+ const sorted = [...input].sort((a, b) => a.priority - b.priority);
174
+ const kept = [];
175
+ let dropped = 0;
176
+ for (const c of sorted) {
177
+ const normC = normalize(c.text);
178
+ const collision = kept.some((k) => {
179
+ const normK = normalize(k.text);
180
+ return normK.includes(normC) || normC.includes(normK);
181
+ });
182
+ if (collision) {
183
+ dropped += 1;
184
+ continue;
185
+ }
186
+ kept.push(c);
187
+ }
188
+ return { kept, dropped };
189
+ }
190
+ /**
191
+ * Dedup against existing `## Durable facts` body. Substring-match
192
+ * each candidate against the body (case-fold). Skipped candidates
193
+ * count toward the returned `dropped` so the caller can render the
194
+ * "N already in durable facts" dim line.
195
+ */
196
+ function dedupAgainstExisting(input, existing) {
197
+ if (!existing.trim())
198
+ return { kept: [...input], dropped: 0 };
199
+ const normExisting = normalize(existing);
200
+ const kept = [];
201
+ let dropped = 0;
202
+ for (const c of input) {
203
+ if (normExisting.includes(normalize(c.text))) {
204
+ dropped += 1;
205
+ continue;
206
+ }
207
+ kept.push(c);
208
+ }
209
+ return { kept, dropped };
210
+ }
211
+ // ── Public entry point ────────────────────────────────────────────────────
212
+ /**
213
+ * Build the full candidate list. Combines source A + source B,
214
+ * dedups within session, dedups against existing durable body, sorts
215
+ * by priority (stable within priority by source order — explicit
216
+ * signals before decisions before open items), and caps at 10.
217
+ */
218
+ function extractCandidates(history, distillation, existingDurableBody) {
219
+ const rawA = extractExplicitSignals(history);
220
+ const rawB = extractDistillationCandidates(distillation);
221
+ const totalBeforeDedup = rawA.length + rawB.length;
222
+ const within = dedupWithinSession([...rawA, ...rawB]);
223
+ const against = dedupAgainstExisting(within.kept, existingDurableBody);
224
+ // Stable sort by priority — within a priority tier, preserve insertion
225
+ // order so explicit signals land in chronological message order and
226
+ // decisions land in distillation order.
227
+ const sorted = [...against.kept].sort((a, b) => a.priority - b.priority);
228
+ return {
229
+ candidates: sorted.slice(0, exports.MAX_CANDIDATES),
230
+ dedupedAgainstExisting: against.dropped,
231
+ dedupedWithinSession: within.dropped,
232
+ totalBeforeDedup,
233
+ };
234
+ }
@@ -38,11 +38,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.PromptBuilder = void 0;
40
40
  exports.shouldInjectLlama33ToolHint = shouldInjectLlama33ToolHint;
41
+ exports.shouldInjectExecutionDiscipline = shouldInjectExecutionDiscipline;
41
42
  const node_fs_1 = require("node:fs");
42
43
  const node_os_1 = __importDefault(require("node:os"));
43
44
  // When SOUL.md is missing or whitespace-only the bundled default takes
44
45
  // over so a fresh install still has a working identity.
45
46
  const defaultSoul_1 = require("../../cli/v4/defaultSoul");
47
+ // Phase v4.1.2-followup: runtime-injected version + capabilities slot.
48
+ const capabilities_1 = require("./capabilities");
46
49
  // ── Section header / sentinel string contract ─────────────────────────
47
50
  //
48
51
  // Every literal here is part of the API contract pinned by tests. Header
@@ -59,6 +62,74 @@ const NOTE_USER_LIVE = '[System note: Treat as live identity, not past conversat
59
62
  const NOTE_MEMORY_LIVE = '[System note: Treat as live working memory, not past conversation.]';
60
63
  const SKILLS_LOAD_NOTE = 'You MUST load it first via the `skill_view` tool before invoking ' +
61
64
  'the underlying capability. Skills carry the procedure the tools alone don\'t.';
65
+ /**
66
+ * Phase v4.1.2 alive-core: when the user has authored a real SOUL.md
67
+ * (not the bundled default), prepend a one-line embodiment directive
68
+ * to its content. The directive tells the model to *be* the identity,
69
+ * not narrate about it — closes the most common "stiff generic reply"
70
+ * failure mode where the model paraphrases SOUL.md back at the user.
71
+ *
72
+ * Intentionally suppressed when the identity slot falls back to
73
+ * DEFAULT_SOUL_MD: that text is generic by design and the directive
74
+ * would coach the model to perform a flat persona.
75
+ */
76
+ const EMBODIMENT_DIRECTIVE = 'Embody this identity and tone. Speak as Aiden, not about Aiden. ' +
77
+ 'Avoid generic, stiff replies.';
78
+ /**
79
+ * Phase v4.1.2 alive-core: tool-conditional guidance blocks. Each one
80
+ * is injected only when the corresponding toolset tag is in
81
+ * `opts.toolsetsLoaded`. Replaces the "fixed slot order regardless of
82
+ * capability" assumption — persona shape-shifts per available
83
+ * capability (prior-art pattern surfaced during v4.2 recon).
84
+ *
85
+ * Key match strings:
86
+ * - 'memory' → MEMORY_GUIDANCE
87
+ * - 'session-search' → SESSION_SEARCH_GUIDANCE
88
+ * - 'skills' → SKILLS_GUIDANCE
89
+ *
90
+ * Match the strings in `ToolHandler.toolset` on the registered tools
91
+ * (tools/v4/memory/*.ts ships `toolset: 'memory'`,
92
+ * tools/v4/sessions/sessionSearch.ts ships `toolset: 'session-search'`,
93
+ * skill tools ship `toolset: 'skills'`).
94
+ */
95
+ const MEMORY_GUIDANCE = [
96
+ '## Persistent memory',
97
+ '',
98
+ 'You have persistent memory across sessions. Save durable facts using `memory_add`:',
99
+ 'user preferences, environment details, stable conventions. Memory is injected into',
100
+ 'every turn; keep it compact and focused on facts that will still matter later.',
101
+ 'Prioritize what reduces future user steering.',
102
+ ].join('\n');
103
+ const SESSION_SEARCH_GUIDANCE = [
104
+ '## Session recall',
105
+ '',
106
+ 'When the user references something from a past conversation or you suspect',
107
+ 'relevant cross-session context exists, use `session_search` to recall it before',
108
+ 'asking them to repeat themselves.',
109
+ ].join('\n');
110
+ const SKILLS_GUIDANCE = [
111
+ '## Skill upkeep',
112
+ '',
113
+ 'After completing a complex task (5+ tool calls), fixing a tricky error, or',
114
+ 'discovering a non-trivial workflow, save it as a skill so you can reuse it next',
115
+ 'time. When using an existing skill and finding it outdated, patch it immediately',
116
+ '— don\'t wait to be asked.',
117
+ ].join('\n');
118
+ /**
119
+ * Phase v4.1.2 alive-core: execution-discipline prose. Counters the
120
+ * "I'll run the tests" → no tool call → end-of-turn failure mode by
121
+ * making the contract explicit. Injected when
122
+ * `shouldInjectExecutionDiscipline(modelId)` is true (currently always).
123
+ */
124
+ const EXECUTION_DISCIPLINE_PROSE = [
125
+ '## Tool use enforcement',
126
+ '',
127
+ 'When you say you will perform an action ("I\'ll run the tests", "let me check the',
128
+ 'file"), you MUST immediately make the corresponding tool call in the same response.',
129
+ 'Never end your turn with a promise of future action — execute it now. Every',
130
+ 'response should either contain tool calls that make progress, or deliver a final',
131
+ 'result. Responses that only describe intentions without acting are not acceptable.',
132
+ ].join('\n');
62
133
  /**
63
134
  * Llama-3.3-specific tool-call format guard. Adapter-side recovery picks
64
135
  * up failures, but we'd rather avoid the 400 round-trip.
@@ -73,6 +144,16 @@ function shouldInjectLlama33ToolHint(modelId) {
73
144
  return false;
74
145
  return /llama-?3\.3/i.test(modelId);
75
146
  }
147
+ /**
148
+ * Phase v4.1.2: predicate for the execution-discipline prose slot.
149
+ * Currently always-on — the "act, don't narrate" directive helps every
150
+ * tool-using model we route through. Narrow this if a specific model
151
+ * proves counter-productive; better to over-apply a useful prompt than
152
+ * guess incorrectly which models need it.
153
+ */
154
+ function shouldInjectExecutionDiscipline(_modelId) {
155
+ return true;
156
+ }
76
157
  // ── Internal helpers ──────────────────────────────────────────────────
77
158
  function detectPlatform() {
78
159
  const p = node_os_1.default.platform();
@@ -171,9 +252,16 @@ class PromptBuilder {
171
252
  if (!opts.skipFilesystem) {
172
253
  identity = await readNonEmpty(opts.paths.soulMd);
173
254
  }
255
+ // Phase v4.1.2: track whether the identity came from a real
256
+ // user-authored SOUL.md so the embodiment directive only fires
257
+ // when there's a meaningful persona to embody.
258
+ const identityFromDisk = identity !== null;
174
259
  if (!identity)
175
260
  identity = defaultSoul_1.DEFAULT_SOUL_MD;
176
- slots.push({ name: 'identity', content: identity.trim(), optional: false });
261
+ const identityContent = identityFromDisk
262
+ ? `${EMBODIMENT_DIRECTIVE}\n\n${identity.trim()}`
263
+ : identity.trim();
264
+ slots.push({ name: 'identity', content: identityContent, optional: false });
177
265
  // ── 2. Personality overlay ────────────────────────────────────────
178
266
  const overlay = opts.personalityOverlay?.trim();
179
267
  if (overlay) {
@@ -197,6 +285,51 @@ class PromptBuilder {
197
285
  optional: true,
198
286
  });
199
287
  }
288
+ // ── 4.25. Runtime manifest (self-awareness) ───────────────────────
289
+ // High-signal facts about what Aiden actually has loaded right now:
290
+ // version, tool count, skill count, channel/surface list, current
291
+ // provider/model. Always present so "what version are you" /
292
+ // "what tools do you have" answers come from facts in context,
293
+ // not from whatever stale text used to live in SOUL.md.
294
+ const runtimeManifest = (0, capabilities_1.buildRuntimeManifest)({
295
+ toolCount: opts.toolCount ?? 0,
296
+ skillCount: opts.skillsList?.length ?? 0,
297
+ providerId: opts.providerId,
298
+ modelId: opts.modelId,
299
+ });
300
+ slots.push({
301
+ name: 'runtime',
302
+ content: (0, capabilities_1.renderRuntimeSlot)(runtimeManifest),
303
+ optional: false,
304
+ });
305
+ // ── 4.5. Tool-conditional guidance ────────────────────────────────
306
+ // Each block fires only when its corresponding toolset is loaded.
307
+ // Order is deterministic so the prefix cache stays stable across
308
+ // turns with the same toolset set.
309
+ const toolsets = opts.toolsetsLoaded;
310
+ if (toolsets && toolsets.size > 0) {
311
+ if (toolsets.has('memory')) {
312
+ slots.push({
313
+ name: 'guidance.memory',
314
+ content: MEMORY_GUIDANCE,
315
+ optional: true,
316
+ });
317
+ }
318
+ if (toolsets.has('session-search')) {
319
+ slots.push({
320
+ name: 'guidance.sessionSearch',
321
+ content: SESSION_SEARCH_GUIDANCE,
322
+ optional: true,
323
+ });
324
+ }
325
+ if (toolsets.has('skills')) {
326
+ slots.push({
327
+ name: 'guidance.skills',
328
+ content: SKILLS_GUIDANCE,
329
+ optional: true,
330
+ });
331
+ }
332
+ }
200
333
  // ── 5. Skills ─────────────────────────────────────────────────────
201
334
  if (opts.skillsList && opts.skillsList.length > 0) {
202
335
  slots.push({
@@ -213,6 +346,17 @@ class PromptBuilder {
213
346
  optional: true,
214
347
  });
215
348
  }
349
+ // ── 6.5. Execution discipline ─────────────────────────────────────
350
+ // Phase v4.1.2: closes the "promise without acting" failure mode.
351
+ // Model-conditional via shouldInjectExecutionDiscipline so we can
352
+ // narrow later if a specific model proves counter-productive.
353
+ if (shouldInjectExecutionDiscipline(opts.modelId)) {
354
+ slots.push({
355
+ name: 'executionDiscipline',
356
+ content: EXECUTION_DISCIPLINE_PROSE,
357
+ optional: true,
358
+ });
359
+ }
216
360
  // ── 7. Iteration budget ───────────────────────────────────────────
217
361
  if (opts.initialBudget) {
218
362
  const { used, max } = opts.initialBudget;