aiden-runtime 4.0.2 → 4.1.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 (113) hide show
  1. package/README.md +19 -11
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +424 -7
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +256 -55
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +102 -1
  28. package/dist/cli/v4/doctorLiveness.js +329 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/shellInterpolation.js +139 -0
  37. package/dist/cli/v4/skinEngine.js +21 -1
  38. package/dist/cli/v4/streamingPrefix.js +121 -0
  39. package/dist/cli/v4/syntaxHighlight.js +345 -0
  40. package/dist/cli/v4/table.js +216 -0
  41. package/dist/cli/v4/themeDetect.js +81 -0
  42. package/dist/cli/v4/uiBuild.js +74 -0
  43. package/dist/cli/v4/voiceCli.js +113 -0
  44. package/dist/cli/v4/voicePromptApi.js +196 -0
  45. package/dist/core/channels/discord.js +16 -10
  46. package/dist/core/channels/email.js +13 -9
  47. package/dist/core/channels/imessage.js +13 -9
  48. package/dist/core/channels/manager.js +25 -7
  49. package/dist/core/channels/pdf-extract.js +180 -0
  50. package/dist/core/channels/photo-vision.js +157 -0
  51. package/dist/core/channels/signal.js +11 -7
  52. package/dist/core/channels/slack.js +13 -10
  53. package/dist/core/channels/telegram-commands.js +154 -0
  54. package/dist/core/channels/telegram-groups.js +198 -0
  55. package/dist/core/channels/telegram-rate-limit.js +124 -0
  56. package/dist/core/channels/telegram.js +1980 -0
  57. package/dist/core/channels/twilio.js +11 -7
  58. package/dist/core/channels/webhook.js +9 -5
  59. package/dist/core/channels/whatsapp.js +15 -11
  60. package/dist/core/channels/whisper-transcribe.js +163 -0
  61. package/dist/core/cronManager.js +33 -294
  62. package/dist/core/gateway.js +29 -8
  63. package/dist/core/playwrightBridge.js +90 -0
  64. package/dist/core/v4/aidenAgent.js +35 -0
  65. package/dist/core/v4/auxiliaryClient.js +2 -2
  66. package/dist/core/v4/cron/atomicWrite.js +18 -4
  67. package/dist/core/v4/cron/cronExecute.js +300 -0
  68. package/dist/core/v4/cron/cronManager.js +502 -0
  69. package/dist/core/v4/cron/cronState.js +314 -0
  70. package/dist/core/v4/cron/cronTick.js +90 -0
  71. package/dist/core/v4/cron/diagnostics.js +104 -0
  72. package/dist/core/v4/cron/graceWindow.js +79 -0
  73. package/dist/core/v4/logger/factory.js +110 -0
  74. package/dist/core/v4/logger/index.js +22 -0
  75. package/dist/core/v4/logger/logger.js +101 -0
  76. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  77. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  78. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  79. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  80. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  81. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  82. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  83. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  84. package/dist/core/v4/platformPaths.js +105 -0
  85. package/dist/core/v4/providerFallback.js +25 -0
  86. package/dist/core/v4/skillLoader.js +21 -5
  87. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  88. package/dist/core/v4/skillMining/extractorPrompt.js +118 -0
  89. package/dist/core/v4/skillMining/proposalBuilder.js +140 -0
  90. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  91. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  92. package/dist/core/v4/subagent/budget.js +76 -0
  93. package/dist/core/v4/subagent/diagnostics.js +22 -0
  94. package/dist/core/v4/subagent/fanout.js +216 -0
  95. package/dist/core/v4/subagent/merger.js +148 -0
  96. package/dist/core/v4/subagent/providerRotation.js +54 -0
  97. package/dist/core/v4/voice/audioStream.js +373 -0
  98. package/dist/core/v4/voice/cliVoice.js +393 -0
  99. package/dist/core/v4/voice/diagnostics.js +66 -0
  100. package/dist/core/v4/voice/ttsStream.js +193 -0
  101. package/dist/core/version.js +1 -1
  102. package/dist/core/visionAnalyze.js +291 -90
  103. package/dist/core/voice/audio.js +61 -5
  104. package/dist/core/voice/audioBackend.js +134 -0
  105. package/dist/core/voice/stt.js +61 -6
  106. package/dist/core/voice/tts.js +19 -3
  107. package/dist/moat/dangerousPatterns.js +1 -1
  108. package/dist/providers/v4/codexResponsesAdapter.js +7 -2
  109. package/dist/providers/v4/errors.js +51 -1
  110. package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
  111. package/dist/tools/v4/index.js +32 -1
  112. package/dist/tools/v4/subagent/subagentFanout.js +190 -0
  113. package/package.json +11 -2
@@ -0,0 +1,118 @@
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/skillMining/extractorPrompt.ts — Phase v4.1-skill-mining
10
+ *
11
+ * Optional LLM refinement pass on top of `proposalBuilder.draft()`.
12
+ * The skeleton SKILL.md the builder produces is already a valid
13
+ * v4 skill; the refiner only polishes prose. Two hard rules:
14
+ *
15
+ * 1. Structural fields are sacred — name, description, version,
16
+ * metadata.aiden.* must round-trip unchanged. We re-parse the
17
+ * refined output and discard it if any required field drifts.
18
+ *
19
+ * 2. Never write attribution tokens or "portions adapted from..." /
20
+ * "original copyright" strings. The permanent attribution
21
+ * sweep validates this; if a refined output contains any
22
+ * forbidden token, we fall back to the skeleton.
23
+ *
24
+ * If the auxiliary client is unavailable, the call times out, or
25
+ * the refined output fails validation, the function returns the
26
+ * skeleton unchanged. Mining works fully offline — refinement is
27
+ * best-effort polish.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.refine = refine;
31
+ const skillSpec_1 = require("../skillSpec");
32
+ const BANNED_TOKENS = [
33
+ 'portions adapted from',
34
+ 'original copyright',
35
+ 'derived from',
36
+ 'based on the',
37
+ 'adapted from',
38
+ ];
39
+ const FORBIDDEN_TOKENS_RE = new RegExp(`\\b(${BANNED_TOKENS.join('|')})\\b`, 'i');
40
+ const REFINER_SYSTEM_PROMPT = `
41
+ You polish auto-generated skill markdown for a local-first AI agent.
42
+
43
+ Your job is to improve the WORDING ONLY of an already-valid SKILL.md
44
+ file. The frontmatter (everything between the leading "---" markers)
45
+ must round-trip BYTE-FOR-BYTE unchanged. The "# <name>" heading must
46
+ stay first in the body. The numbered "## Steps" list must remain
47
+ numbered and in the same order; you may rephrase step descriptions
48
+ but must NOT add or remove steps.
49
+
50
+ Hard rules:
51
+ - Output the COMPLETE SKILL.md, not a diff.
52
+ - Do not add boilerplate, citations, or attribution to any other
53
+ agent or codebase. The skill is 100% the user's own.
54
+ - Do not introduce mock/fake values into commands.
55
+ - Keep total length under 6000 characters.
56
+ `.trim();
57
+ /**
58
+ * Refine `skeleton` via the auxiliary client. Always returns a
59
+ * valid SKILL.md string — either the refined output (if it passes
60
+ * round-trip + no-attribution validation) or the original skeleton.
61
+ */
62
+ async function refine(skeleton, opts = {}) {
63
+ const client = opts.client;
64
+ if (!client || client.isUnavailable()) {
65
+ return skeleton;
66
+ }
67
+ // Parse the skeleton up front to lock the canonical frontmatter
68
+ // shape we'll require the refined output to match.
69
+ let skeletonParsed;
70
+ try {
71
+ skeletonParsed = (0, skillSpec_1.parseSkillContent)(skeleton);
72
+ }
73
+ catch {
74
+ // If the skeleton itself is invalid, refining can only make it
75
+ // worse; bubble the skeleton out and let the caller decide.
76
+ return skeleton;
77
+ }
78
+ const prompt = `${REFINER_SYSTEM_PROMPT}\n\n` +
79
+ `INPUT SKILL.md:\n\`\`\`\n${skeleton}\n\`\`\`\n\n` +
80
+ `Output the refined SKILL.md only — no commentary, no code fences.`;
81
+ let refined;
82
+ try {
83
+ const result = await client.call({
84
+ purpose: 'skill_describe',
85
+ prompt,
86
+ maxTokens: opts.maxTokens ?? 1500,
87
+ timeoutMs: opts.timeoutMs ?? 20000,
88
+ });
89
+ refined = (result.content ?? '').trim();
90
+ }
91
+ catch {
92
+ return skeleton;
93
+ }
94
+ if (refined.length === 0)
95
+ return skeleton;
96
+ // Strip a wrapping ```...``` if the model returned one.
97
+ refined = refined.replace(/^```(?:markdown|md)?\s*\n/, '').replace(/\n```\s*$/, '');
98
+ // Validation 1 — attribution sweep. Refined output must not
99
+ // contain any forbidden token. The permanent sweep would catch
100
+ // this at ship time; we catch it earlier so a single noisy
101
+ // refinement doesn't pollute the candidate queue.
102
+ if (FORBIDDEN_TOKENS_RE.test(refined))
103
+ return skeleton;
104
+ // Validation 2 — round-trip through parseSkillContent and verify
105
+ // the canonical fields match the skeleton.
106
+ let refinedParsed;
107
+ try {
108
+ refinedParsed = (0, skillSpec_1.parseSkillContent)(refined);
109
+ }
110
+ catch {
111
+ return skeleton;
112
+ }
113
+ if (refinedParsed.frontmatter.name !== skeletonParsed.frontmatter.name ||
114
+ refinedParsed.frontmatter.version !== skeletonParsed.frontmatter.version) {
115
+ return skeleton;
116
+ }
117
+ return refined;
118
+ }
@@ -0,0 +1,140 @@
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/skillMining/proposalBuilder.ts — Phase v4.1-skill-mining
10
+ *
11
+ * Pure-function skeleton SKILL.md generator from a successful tool-
12
+ * call trace. Used by skillMiner BEFORE optional LLM refinement —
13
+ * the skeleton must already be a valid v4 skill (parseSkillContent
14
+ * round-trips), so that mining works offline when the auxiliary
15
+ * client is unavailable.
16
+ *
17
+ * Invariants:
18
+ * - emits required fields `name`, `description`, `version`
19
+ * - emits `metadata.aiden` with the mining provenance fields
20
+ * - body is numbered tool-call steps in markdown
21
+ * - never writes attribution strings — the banned-token regex
22
+ * strips them at extraction time and the permanent attribution
23
+ * sweep validates the result
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.deriveName = deriveName;
27
+ exports.deriveDescription = deriveDescription;
28
+ exports.draft = draft;
29
+ const STOP_WORDS = new Set([
30
+ 'the', 'a', 'an', 'to', 'for', 'of', 'on', 'at', 'in', 'by', 'and', 'or', 'but', 'with',
31
+ 'from', 'please', 'can', 'you', 'will', 'do', 'does', 'my', 'our', 'this', 'that', 'these',
32
+ 'is', 'are', 'be', 'was', 'were', 'it', 'its', 'as', 'if', 'then', 'else', 'some', 'any',
33
+ 'me', 'i', 'your', 'their', 'his', 'her', 'him',
34
+ ]);
35
+ /**
36
+ * Derive a kebab-case skill-name from the first user message.
37
+ * Conservative — keeps only [a-z0-9-], collapses runs, max 40 chars.
38
+ * Falls back to `learned-skill-<short-fingerprint>` if extraction
39
+ * yields nothing usable.
40
+ */
41
+ function deriveName(firstUserPrompt, fingerprint) {
42
+ const lowered = firstUserPrompt.toLowerCase();
43
+ // Pull out non-stopword tokens, max 5 of them.
44
+ const tokens = lowered
45
+ .split(/[^a-z0-9]+/)
46
+ .filter((t) => t.length >= 3 && !STOP_WORDS.has(t))
47
+ .slice(0, 5);
48
+ let stem = tokens.join('-');
49
+ // Strict alphanum-and-dash, collapse multiple dashes, trim leading/trailing.
50
+ stem = stem.replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
51
+ if (stem.length === 0) {
52
+ return `learned-skill-${fingerprint.slice(0, 8)}`;
53
+ }
54
+ return stem.slice(0, 40);
55
+ }
56
+ /**
57
+ * Build the description line — single sentence summarising what the
58
+ * trace did. Keep it under 200 chars (the skill loader caps the
59
+ * displayed description in /skills list).
60
+ */
61
+ function deriveDescription(trace) {
62
+ if (trace.length === 0)
63
+ return 'Learned workflow.';
64
+ const tools = trace.map((e) => e.name).filter(Boolean);
65
+ const distinct = Array.from(new Set(tools));
66
+ if (distinct.length === 1) {
67
+ return `Learned workflow: ${distinct[0]} (${tools.length}x).`;
68
+ }
69
+ const head = distinct.slice(0, 4).join(' → ');
70
+ const more = distinct.length > 4 ? ` (+${distinct.length - 4} more)` : '';
71
+ return `Learned workflow: ${head}${more}.`;
72
+ }
73
+ /**
74
+ * Render the body as numbered tool-call steps. Args are JSON-stringified
75
+ * and clipped at 120 chars per step so a chatty trace doesn't bloat
76
+ * SKILL.md to multi-MB.
77
+ */
78
+ function renderBody(trace) {
79
+ if (trace.length === 0) {
80
+ return '## Steps\n\n_(empty trace)_\n';
81
+ }
82
+ const out = ['## Steps', ''];
83
+ trace.forEach((entry, i) => {
84
+ const idx = i + 1;
85
+ const argSummary = (() => {
86
+ if (entry.args == null || typeof entry.args !== 'object')
87
+ return '';
88
+ try {
89
+ const json = JSON.stringify(entry.args);
90
+ if (json.length <= 120)
91
+ return ` \\\`${json}\\\``;
92
+ return ` \\\`${json.slice(0, 117)}…\\\``;
93
+ }
94
+ catch {
95
+ return '';
96
+ }
97
+ })();
98
+ out.push(`${idx}. **${entry.name}**${argSummary}`);
99
+ });
100
+ out.push('');
101
+ out.push('## Notes', '');
102
+ out.push('Mined from a successful turn — review the steps above before relying on it.');
103
+ out.push('');
104
+ return out.join('\n');
105
+ }
106
+ /**
107
+ * Build a full SKILL.md (frontmatter + body) ready for parseSkillContent.
108
+ * The output is deterministic for a given (trace, context) input —
109
+ * smokes assert this for stable round-trip behaviour.
110
+ */
111
+ function draft(trace, ctx) {
112
+ const name = ctx.nameOverride?.trim() || deriveName(ctx.firstUserPrompt, ctx.traceFingerprint);
113
+ const description = deriveDescription(trace);
114
+ const version = '0.1.0';
115
+ const createdAt = new Date().toISOString();
116
+ // YAML frontmatter — keep field order stable so smokes can pin
117
+ // shape without depending on YAML library output ordering.
118
+ const frontmatter = [
119
+ '---',
120
+ `name: ${name}`,
121
+ `description: ${JSON.stringify(description)}`,
122
+ `version: ${version}`,
123
+ 'category: learned',
124
+ 'metadata:',
125
+ ' aiden:',
126
+ ' learned: true',
127
+ ` sourceSessionId: ${JSON.stringify(ctx.sourceSessionId)}`,
128
+ ` sourceTurnIdx: ${ctx.sourceTurnIdx}`,
129
+ ` createdAt: ${JSON.stringify(createdAt)}`,
130
+ ` traceFingerprint: ${JSON.stringify(ctx.traceFingerprint)}`,
131
+ ` candidateConfidence: ${ctx.candidateConfidence.toFixed(3)}`,
132
+ '---',
133
+ '',
134
+ `# ${name}`,
135
+ '',
136
+ description,
137
+ '',
138
+ ].join('\n');
139
+ return frontmatter + renderBody(trace);
140
+ }
@@ -0,0 +1,191 @@
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/skillMining/skillMiner.ts — Phase v4.1-skill-mining
10
+ *
11
+ * Orchestrator for the skill-mining post-turn observation. Sits next
12
+ * to `moat/skillTeacher.ts:observeTurn` in the agent loop hook
13
+ * (`core/v4/aidenAgent.ts:440-468`); the two are complementary.
14
+ * SkillTeacher proposes inline (immediate accept/reject); SkillMiner
15
+ * stages a candidate for `/skills review` so the user can audit
16
+ * before any disk-mutation lands in the live skills directory.
17
+ *
18
+ * Programmatic gates (in order — first failure short-circuits):
19
+ * 1. trace length < 3 → skip
20
+ * 2. any tool errored → skip
21
+ * 3. finishReason !== 'stop' → skip
22
+ * 4. session candidate count >= SESSION_SKILL_LIMIT → skip
23
+ * 5. user opt-out phrase in conversation → skip (reuse OPT_OUT_RE)
24
+ * 6. fingerprint matches pending candidate → skip (dedup)
25
+ * 7. fingerprint matches rejected list → skip (dedup)
26
+ *
27
+ * Pass-through pipeline:
28
+ * - compute confidence score from programmatic features
29
+ * - draft skeleton via proposalBuilder
30
+ * - refine via extractorPrompt (best-effort; falls back to
31
+ * skeleton)
32
+ * - validate via parseSkillContent round-trip
33
+ * - append to candidateStore
34
+ * - return ObservationResult so chatSession can notify
35
+ *
36
+ * MCP serve mode: caller (aidenAgent) MUST gate on
37
+ * !isMcpServeMode() before invoking observeTurn — the mining
38
+ * subsystem itself doesn't write to stdout but a candidate
39
+ * notification would, and serve mode owns stdout for JSON-RPC.
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.SkillMiner = exports.SESSION_SKILL_LIMIT = void 0;
43
+ exports.computeConfidence = computeConfidence;
44
+ const node_crypto_1 = require("node:crypto");
45
+ const skillSpec_1 = require("../skillSpec");
46
+ const candidateStore_1 = require("./candidateStore");
47
+ const traceFingerprint_1 = require("./traceFingerprint");
48
+ const proposalBuilder_1 = require("./proposalBuilder");
49
+ const extractorPrompt_1 = require("./extractorPrompt");
50
+ /** Per-session candidate cap — port from v3's `SESSION_SKILL_LIMIT`. */
51
+ exports.SESSION_SKILL_LIMIT = 2;
52
+ /** Minimum trace length to consider mining at all. */
53
+ const MIN_TRACE_LENGTH = 3;
54
+ /**
55
+ * Opt-out phrases that suppress mining for the current turn. Match
56
+ * the SkillTeacher pattern at `moat/skillTeacher.ts:110` so a user
57
+ * who silences SkillTeacher also silences mining.
58
+ */
59
+ const OPT_OUT_RE = /\b(stop|don['']?t|no|never)\s+(suggest|propose|create|save|learn|remember)\w*\b/i;
60
+ /**
61
+ * Compute a 0..1 confidence score from programmatic trace features.
62
+ * Used to sort `/skills review` so the most promising candidates
63
+ * surface first.
64
+ *
65
+ * Components:
66
+ * - lengthScore : trace length sweet spot is 5..15 (peaks at 10)
67
+ * - errorRate : already gated to 0 (no errors in trace) but the
68
+ * feature is computed for forward-compat with
69
+ * future "soft" mining that admits some errors
70
+ * - distinctSet : more distinct tools = more reusable workflow
71
+ * - distinctTools: simple toolset diversity
72
+ */
73
+ function computeConfidence(trace) {
74
+ if (trace.length === 0)
75
+ return 0;
76
+ // Length score — peaks at 10, falls off at extremes.
77
+ const len = trace.length;
78
+ const lengthScore = len < 3 ? 0 :
79
+ len <= 10 ? len / 10 :
80
+ len <= 15 ? 1 - (len - 10) * 0.04 :
81
+ Math.max(0.4, 1 - (len - 10) * 0.05);
82
+ // Error rate (0 = best, 1 = worst).
83
+ const errors = trace.filter((e) => e.error != null).length;
84
+ const errorRate = errors / trace.length;
85
+ // Distinct tool diversity.
86
+ const distinctTools = new Set(trace.map((e) => e.name)).size;
87
+ const diversityScore = Math.min(1, distinctTools / 4);
88
+ // Distinct toolsets diversity (some traces tag entries).
89
+ const distinctSets = new Set(trace.map((e) => e.toolset ?? '').filter(Boolean)).size;
90
+ const setBonus = Math.min(0.2, distinctSets * 0.1);
91
+ // Weighted average — length and diversity dominate, error penalty
92
+ // proportional to rate.
93
+ const raw = 0.55 * lengthScore + 0.35 * diversityScore + setBonus - errorRate;
94
+ return Math.max(0, Math.min(1, Number(raw.toFixed(3))));
95
+ }
96
+ /** Single-trace orchestrator. Stateless except for the per-session counter. */
97
+ class SkillMiner {
98
+ constructor(opts = {}) {
99
+ this.perSessionCount = new Map();
100
+ this.store = opts.store ?? new candidateStore_1.CandidateStore();
101
+ this.auxiliaryClient = opts.auxiliaryClient;
102
+ this.sessionCap = opts.sessionCap ?? exports.SESSION_SKILL_LIMIT;
103
+ this.skeletonOnly = opts.skeletonOnly ?? false;
104
+ }
105
+ /** Test/reset hook. */
106
+ _resetForTests() {
107
+ this.perSessionCount.clear();
108
+ }
109
+ /** Returns the count of pending candidates already attributed to a session. */
110
+ countForSession(sessionId) {
111
+ return this.perSessionCount.get(sessionId) ?? 0;
112
+ }
113
+ async observeTurn(obs) {
114
+ // Gate 1 — short trace.
115
+ if (!obs.trace || obs.trace.length < MIN_TRACE_LENGTH) {
116
+ return { status: 'short-trace' };
117
+ }
118
+ // Gate 2 — any tool errored.
119
+ if (obs.trace.some((e) => e.error != null)) {
120
+ return { status: 'tool-error' };
121
+ }
122
+ // Gate 3 — turn was aborted.
123
+ if (obs.finishReason !== 'stop') {
124
+ return { status: 'abort' };
125
+ }
126
+ // Gate 4 — session cap.
127
+ if (this.countForSession(obs.sessionId) >= this.sessionCap) {
128
+ return { status: 'session-cap' };
129
+ }
130
+ // Gate 5 — user opt-out anywhere in this turn's conversation.
131
+ for (const msg of obs.history) {
132
+ if (msg.role === 'user' && typeof msg.content === 'string' && OPT_OUT_RE.test(msg.content)) {
133
+ return { status: 'opt-out' };
134
+ }
135
+ }
136
+ // Fingerprint + dedup.
137
+ const fpEntries = obs.trace.map((e) => ({ name: e.name, args: e.args }));
138
+ const fingerprint = (0, traceFingerprint_1.traceFingerprint)(fpEntries);
139
+ const pending = await this.store.list();
140
+ if (pending.some((c) => c.fingerprint === fingerprint)) {
141
+ return { status: 'dedup-pending' };
142
+ }
143
+ const rejected = await this.store.loadRejected();
144
+ if (rejected.has(fingerprint)) {
145
+ return { status: 'dedup-rejected' };
146
+ }
147
+ // Compute confidence + first user prompt for skeleton seeding.
148
+ const confidence = computeConfidence(obs.trace);
149
+ const firstUserPrompt = (() => {
150
+ for (const m of obs.history) {
151
+ if (m.role === 'user' && typeof m.content === 'string' && m.content.trim().length > 0) {
152
+ return m.content.trim();
153
+ }
154
+ }
155
+ return '';
156
+ })();
157
+ const ctx = {
158
+ firstUserPrompt,
159
+ sourceSessionId: obs.sessionId,
160
+ sourceTurnIdx: obs.sourceTurnIdx,
161
+ traceFingerprint: fingerprint,
162
+ candidateConfidence: confidence,
163
+ };
164
+ let skill = (0, proposalBuilder_1.draft)(obs.trace, ctx);
165
+ if (!this.skeletonOnly) {
166
+ skill = await (0, extractorPrompt_1.refine)(skill, { client: this.auxiliaryClient });
167
+ }
168
+ // Final validation — must round-trip through parseSkillContent
169
+ // (the canonical loader parser). If it doesn't, drop the
170
+ // candidate rather than poison the queue.
171
+ try {
172
+ (0, skillSpec_1.parseSkillContent)(skill);
173
+ }
174
+ catch {
175
+ return { status: 'invalid-skill', confidence };
176
+ }
177
+ const candidate = {
178
+ id: (0, node_crypto_1.randomUUID)(),
179
+ fingerprint,
180
+ sourceSessionId: obs.sessionId,
181
+ sourceTurnIdx: obs.sourceTurnIdx,
182
+ createdAt: new Date().toISOString(),
183
+ candidateConfidence: confidence,
184
+ skillContent: skill,
185
+ };
186
+ await this.store.append(candidate);
187
+ this.perSessionCount.set(obs.sessionId, this.countForSession(obs.sessionId) + 1);
188
+ return { status: 'queued', candidate, confidence };
189
+ }
190
+ }
191
+ exports.SkillMiner = SkillMiner;
@@ -0,0 +1,51 @@
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/skillMining/traceFingerprint.ts — Phase v4.1-skill-mining
10
+ *
11
+ * Deterministic content-addressing for tool-call traces.
12
+ *
13
+ * The fingerprint is the sha256 hex of the normalized
14
+ * (toolName, sorted-arg-keys) sequence joined by `|`.
15
+ *
16
+ * Properties (verified by smoke):
17
+ * - identical traces produce identical hashes
18
+ * - traces that differ only in arg *values* (same arg keys)
19
+ * produce identical hashes — this is the desired behavior:
20
+ * "search github for X" and "search github for Y" should
21
+ * dedup to one candidate
22
+ * - traces with different tool sequences or arg keys produce
23
+ * different hashes
24
+ * - deterministic across runs (no salt, no time)
25
+ *
26
+ * The candidateStore + skillMiner use this to dedup proposals;
27
+ * the rejected list also tracks fingerprints so a user-rejected
28
+ * workflow doesn't get re-proposed on the next run.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.traceFingerprint = traceFingerprint;
32
+ const node_crypto_1 = require("node:crypto");
33
+ /** Normalize a single trace entry to its fingerprint contribution. */
34
+ function normalizeEntry(entry) {
35
+ const name = String(entry.name ?? '').trim().toLowerCase();
36
+ let argKeys = [];
37
+ if (entry.args && typeof entry.args === 'object' && !Array.isArray(entry.args)) {
38
+ argKeys = Object.keys(entry.args).sort();
39
+ }
40
+ return `${name}(${argKeys.join(',')})`;
41
+ }
42
+ /**
43
+ * Fingerprint a trace. Returns a 64-char lowercase sha256 hex.
44
+ * Empty trace is a valid input (returns the hash of the empty
45
+ * normalized string) — callers should reject empty traces before
46
+ * fingerprinting to keep the pending queue free of useless entries.
47
+ */
48
+ function traceFingerprint(trace) {
49
+ const normalized = trace.map(normalizeEntry).join('|');
50
+ return (0, node_crypto_1.createHash)('sha256').update(normalized, 'utf8').digest('hex');
51
+ }
@@ -0,0 +1,76 @@
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/subagent/budget.ts — Phase v4.1-subagent
10
+ *
11
+ * Per-subagent timeouts and iteration caps. Two budgets layered:
12
+ *
13
+ * - `perSubagentTimeoutMs` — hard wall-clock cap on a single
14
+ * subagent. Fired via AbortController; AidenAgent's provider
15
+ * adapter receives the abort and the in-flight HTTP call is
16
+ * cancelled (the v3 lesson — flag-only cancellation leaks
17
+ * tokens, AbortController plumbed through is the v4 fix).
18
+ *
19
+ * - `wallClockCapMs` — outer cap on the whole fanout. Defaults
20
+ * to 5× the per-subagent timeout because parallel subagents
21
+ * should finish faster than 5× one-at-a-time, but variance
22
+ * (provider rate limits, retry backoff) can extend the tail.
23
+ *
24
+ * - `maxIterations` — fresh per subagent. v3 starved nested
25
+ * spawns by dividing a global budget; v4 hands each subagent a
26
+ * full fresh budget and relies on the wall-clock cap for the
27
+ * outer bound.
28
+ *
29
+ * Read at fanout start. Each subagent gets its own AbortSignal
30
+ * derived from the timeout; abort propagates from parent down via
31
+ * `parentAbort.aborted`.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.DEFAULT_FANOUT_N = exports.MAX_FANOUT_N = exports.WALL_CLOCK_CAP_MULT = exports.DEFAULT_SUBAGENT_MAX_ITERATIONS = exports.DEFAULT_SUBAGENT_TIMEOUT_MS = void 0;
35
+ exports.resolveBudget = resolveBudget;
36
+ exports.validateN = validateN;
37
+ /** Default per-subagent timeout (ms). Override via env
38
+ * `AIDEN_SUBAGENT_TIMEOUT_MS` or `timeoutMs` argument on the tool. */
39
+ exports.DEFAULT_SUBAGENT_TIMEOUT_MS = 90000;
40
+ /** Default per-subagent iteration cap. Fresh per subagent. */
41
+ exports.DEFAULT_SUBAGENT_MAX_ITERATIONS = 20;
42
+ /** Wall-clock cap multiplier — outer cap = `perSubagentTimeoutMs * MULT`. */
43
+ exports.WALL_CLOCK_CAP_MULT = 5;
44
+ /** Max N — hard refuse beyond. */
45
+ exports.MAX_FANOUT_N = 5;
46
+ /** Default N when the caller doesn't specify. */
47
+ exports.DEFAULT_FANOUT_N = 3;
48
+ /** Resolve the live budget. Tool-call argument > env > module default. */
49
+ function resolveBudget(opts = {}) {
50
+ const env = opts.env ?? process.env;
51
+ const envTimeoutRaw = env.AIDEN_SUBAGENT_TIMEOUT_MS;
52
+ const envTimeout = envTimeoutRaw && /^\d+$/.test(envTimeoutRaw)
53
+ ? Number.parseInt(envTimeoutRaw, 10)
54
+ : null;
55
+ const perSubagentTimeoutMs = opts.timeoutMs ?? envTimeout ?? exports.DEFAULT_SUBAGENT_TIMEOUT_MS;
56
+ return {
57
+ perSubagentTimeoutMs,
58
+ wallClockCapMs: perSubagentTimeoutMs * exports.WALL_CLOCK_CAP_MULT,
59
+ maxIterations: exports.DEFAULT_SUBAGENT_MAX_ITERATIONS,
60
+ };
61
+ }
62
+ /** Validate the requested N — throws with a clear message when out of
63
+ * bounds. Caller surfaces the error as a tool-result error string. */
64
+ function validateN(n) {
65
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
66
+ throw new Error(`subagent_fanout: n must be an integer, got ${n}`);
67
+ }
68
+ if (n < 1) {
69
+ throw new Error(`subagent_fanout: n must be >= 1, got ${n}`);
70
+ }
71
+ if (n > exports.MAX_FANOUT_N) {
72
+ throw new Error(`subagent_fanout: n=${n} exceeds hard cap ${exports.MAX_FANOUT_N}. ` +
73
+ `Higher concurrency hits provider RPM limits and increases tail latency variance.`);
74
+ }
75
+ return n;
76
+ }
@@ -0,0 +1,22 @@
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/subagent/diagnostics.ts — Phase v4.1-subagent
10
+ *
11
+ * Build fingerprint + counts surfaced by `aiden subagent status` and
12
+ * the per-fanout result envelope. The fingerprint follows the same
13
+ * convention every Aiden phase since v4.1-3.2 has used: a constant
14
+ * string the user can grep for to verify the running build matches
15
+ * the phase they expected. Bump on every shipped phase. Format:
16
+ * `v4.1-subagent[+suffix]`.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.AIDEN_SUBAGENT_BUILD = void 0;
20
+ /** Build fingerprint — bump per phase. Surfaced in `aiden subagent
21
+ * status` and the post-fanout summary line. */
22
+ exports.AIDEN_SUBAGENT_BUILD = 'v4.1-subagent.2';