aiden-runtime 4.0.1 → 4.1.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.
- package/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +513 -14
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +269 -52
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +19 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/setup.js +34 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +300 -14
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/setupWizard.js +466 -232
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/firstRun/providerDetection.js +287 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/providers/v4/nullAdapter.js +58 -0
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- package/package.json +11 -2
|
@@ -412,6 +412,31 @@ class FallbackAdapter {
|
|
|
412
412
|
}
|
|
413
413
|
this.activeSlotId = slotId;
|
|
414
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Phase v4.1-subagent — clone the adapter with FRESH mutable state
|
|
417
|
+
* but SHARED slot configs. Subagent fanout uses this so each
|
|
418
|
+
* subagent gets its own rate-limit bookkeeping (slotState,
|
|
419
|
+
* cooldownUntil, requestCount, activeSlotId all reset) without
|
|
420
|
+
* paying for slot-config rebuilds. The slot list and the cooldownMs
|
|
421
|
+
* / clock / observer callbacks are read-only and safely shared
|
|
422
|
+
* across clones.
|
|
423
|
+
*
|
|
424
|
+
* One subagent hitting a 429 marks ITS clone's slot as cooled-down;
|
|
425
|
+
* the parent and sibling clones still see the slot as available.
|
|
426
|
+
* That's a deliberate tradeoff: tighter contention on the same
|
|
427
|
+
* provider key, but isolation prevents one slow subagent from
|
|
428
|
+
* starving siblings via parent-side cooldown state.
|
|
429
|
+
*/
|
|
430
|
+
clone() {
|
|
431
|
+
return new FallbackAdapter({
|
|
432
|
+
apiMode: this.apiMode,
|
|
433
|
+
slots: this.slots,
|
|
434
|
+
cooldownMs: this.cooldownMs,
|
|
435
|
+
now: this.clock,
|
|
436
|
+
onRateLimit: this.onRateLimit,
|
|
437
|
+
onFallback: this.onFallback,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
415
440
|
/**
|
|
416
441
|
* Diagnostic snapshot for `/providers`. Per-slot cooldown is reported
|
|
417
442
|
* in seconds remaining (0 when the slot is fresh) so the slash command
|
|
@@ -74,15 +74,31 @@ class SkillLoader {
|
|
|
74
74
|
return { ...this.lastCounts, skippedPaths: [...this.lastCounts.skippedPaths] };
|
|
75
75
|
}
|
|
76
76
|
async load(name) {
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
77
|
+
// Phase v4.1-cross-platform: case-insensitive cache lookup so a
|
|
78
|
+
// skill registered as `WebSearch` resolves the same on Linux
|
|
79
|
+
// (case-sensitive FS) as on Windows (case-insensitive FS). The
|
|
80
|
+
// ON-DISK directory name still has to match in case for the
|
|
81
|
+
// disk-fallback path to work, so we ALSO loadAll first when the
|
|
82
|
+
// case-insensitive cache hit succeeds — that gives us the actual
|
|
83
|
+
// file-system path and the loader can serve it from cache.
|
|
84
|
+
const target = name.toLowerCase();
|
|
85
|
+
if (!this.cache) {
|
|
86
|
+
// Trigger a one-time scan so case-insensitive lookup has data.
|
|
87
|
+
try {
|
|
88
|
+
await this.loadAll();
|
|
89
|
+
}
|
|
90
|
+
catch { /* ignore — fall through to disk */ }
|
|
91
|
+
}
|
|
81
92
|
if (this.cache) {
|
|
82
|
-
const hit = this.cache.find((s) => s.frontmatter.name ===
|
|
93
|
+
const hit = this.cache.find((s) => (s.frontmatter.name ?? '').toLowerCase() === target);
|
|
83
94
|
if (hit)
|
|
84
95
|
return hit;
|
|
85
96
|
}
|
|
97
|
+
// Disk fallback: try the verbatim name, then a lowercase variant
|
|
98
|
+
// (covers case where the cache failed to populate but the on-disk
|
|
99
|
+
// dir uses lowercase). On case-sensitive filesystems neither will
|
|
100
|
+
// hit if the directory case doesn't match the requested name —
|
|
101
|
+
// that's fine, the cache lookup above is the authoritative path.
|
|
86
102
|
const dirSkill = node_path_1.default.join(this.paths.skillsDir, name, 'SKILL.md');
|
|
87
103
|
const fileSkill = node_path_1.default.join(this.paths.skillsDir, `${name}.md`);
|
|
88
104
|
return (await this.tryParse(dirSkill)) ?? (await this.tryParse(fileSkill));
|
|
@@ -0,0 +1,164 @@
|
|
|
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/candidateStore.ts — Phase v4.1-skill-mining
|
|
10
|
+
*
|
|
11
|
+
* Atomic JSON queue for mined skill candidates and rejection
|
|
12
|
+
* fingerprints. Two files under `<aidenHome>/skills/learned/`:
|
|
13
|
+
*
|
|
14
|
+
* - `.candidates.json` — pending review queue, append-only via
|
|
15
|
+
* this module. `/skills review` reads it; `/skills accept`
|
|
16
|
+
* and `/skills reject` mutate it.
|
|
17
|
+
*
|
|
18
|
+
* - `.rejected.json` — fingerprint set the dedup gate consults
|
|
19
|
+
* so a user-rejected workflow doesn't get re-proposed on the
|
|
20
|
+
* next matching turn. Keyed by fingerprint, not id, so the
|
|
21
|
+
* dedup survives across the candidate's lifecycle.
|
|
22
|
+
*
|
|
23
|
+
* Concurrency: a per-process write queue serialises every mutation
|
|
24
|
+
* (mirrors the BundledManifest pattern at
|
|
25
|
+
* `core/v4/skillBundledManifest.ts:50-53`). Cross-process safety
|
|
26
|
+
* is non-goal — the agent is a single-user CLI.
|
|
27
|
+
*
|
|
28
|
+
* Atomicity: writes go to a sibling `.tmp` file then `rename` over
|
|
29
|
+
* the live path. Windows rename is atomic on the same volume so a
|
|
30
|
+
* crash mid-write never leaves a partial JSON file.
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CandidateStore = void 0;
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
39
|
+
const paths_1 = require("../paths");
|
|
40
|
+
const ENVELOPE_VERSION = 1;
|
|
41
|
+
class CandidateStore {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.writeQueue = Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
/** `<aidenHome>/skills/learned/`. */
|
|
46
|
+
dir() {
|
|
47
|
+
return node_path_1.default.join((0, paths_1.resolveAidenPaths)().skillsDir, 'learned');
|
|
48
|
+
}
|
|
49
|
+
candidatesPath() {
|
|
50
|
+
return node_path_1.default.join(this.dir(), '.candidates.json');
|
|
51
|
+
}
|
|
52
|
+
rejectedPath() {
|
|
53
|
+
return node_path_1.default.join(this.dir(), '.rejected.json');
|
|
54
|
+
}
|
|
55
|
+
async ensureDir() {
|
|
56
|
+
await node_fs_1.promises.mkdir(this.dir(), { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
async readJson(p, fallback) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await node_fs_1.promises.readFile(p, 'utf8');
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Atomic `tmp` + rename. */
|
|
68
|
+
async writeJsonAtomic(p, data) {
|
|
69
|
+
await this.ensureDir();
|
|
70
|
+
const tmp = `${p}.tmp`;
|
|
71
|
+
await node_fs_1.promises.writeFile(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
72
|
+
await node_fs_1.promises.rename(tmp, p);
|
|
73
|
+
}
|
|
74
|
+
/** Append a candidate to the pending queue. Returns the assigned id. */
|
|
75
|
+
async append(candidate) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const env = await this.readJson(this.candidatesPath(), {
|
|
80
|
+
version: ENVELOPE_VERSION,
|
|
81
|
+
candidates: [],
|
|
82
|
+
});
|
|
83
|
+
env.version = ENVELOPE_VERSION;
|
|
84
|
+
env.candidates.push(candidate);
|
|
85
|
+
await this.writeJsonAtomic(this.candidatesPath(), env);
|
|
86
|
+
resolve(candidate);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
reject(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Read the full pending queue, newest last. */
|
|
95
|
+
async list() {
|
|
96
|
+
const env = await this.readJson(this.candidatesPath(), {
|
|
97
|
+
version: ENVELOPE_VERSION,
|
|
98
|
+
candidates: [],
|
|
99
|
+
});
|
|
100
|
+
return env.candidates ?? [];
|
|
101
|
+
}
|
|
102
|
+
/** Fetch a single candidate by id, or undefined. */
|
|
103
|
+
async get(id) {
|
|
104
|
+
const all = await this.list();
|
|
105
|
+
return all.find((c) => c.id === id);
|
|
106
|
+
}
|
|
107
|
+
/** Remove a candidate by id. No-op if missing. */
|
|
108
|
+
async remove(id) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
111
|
+
try {
|
|
112
|
+
const env = await this.readJson(this.candidatesPath(), {
|
|
113
|
+
version: ENVELOPE_VERSION,
|
|
114
|
+
candidates: [],
|
|
115
|
+
});
|
|
116
|
+
env.version = ENVELOPE_VERSION;
|
|
117
|
+
env.candidates = env.candidates.filter((c) => c.id !== id);
|
|
118
|
+
await this.writeJsonAtomic(this.candidatesPath(), env);
|
|
119
|
+
resolve();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
reject(err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** Append a rejection fingerprint (with optional reason). */
|
|
128
|
+
async recordRejection(fingerprint, reason) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
131
|
+
try {
|
|
132
|
+
const env = await this.readJson(this.rejectedPath(), {
|
|
133
|
+
version: ENVELOPE_VERSION,
|
|
134
|
+
rejected: [],
|
|
135
|
+
});
|
|
136
|
+
env.version = ENVELOPE_VERSION;
|
|
137
|
+
env.rejected.push({
|
|
138
|
+
fingerprint,
|
|
139
|
+
reason,
|
|
140
|
+
rejectedAt: new Date().toISOString(),
|
|
141
|
+
});
|
|
142
|
+
await this.writeJsonAtomic(this.rejectedPath(), env);
|
|
143
|
+
resolve();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
reject(err);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Return the set of rejected fingerprints (for dedup). */
|
|
152
|
+
async loadRejected() {
|
|
153
|
+
const env = await this.readJson(this.rejectedPath(), {
|
|
154
|
+
version: ENVELOPE_VERSION,
|
|
155
|
+
rejected: [],
|
|
156
|
+
});
|
|
157
|
+
return new Set((env.rejected ?? []).map((r) => r.fingerprint));
|
|
158
|
+
}
|
|
159
|
+
/** Test/reset hook: drop the in-process write queue. Disk untouched. */
|
|
160
|
+
_resetForTests() {
|
|
161
|
+
this.writeQueue = Promise.resolve();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.CandidateStore = CandidateStore;
|
|
@@ -0,0 +1,111 @@
|
|
|
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 (hermes / nous / "portions
|
|
20
|
+
* adapted from" / "original copyright"). The permanent
|
|
21
|
+
* attribution sweep validates this; if a refined output
|
|
22
|
+
* contains any 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 FORBIDDEN_TOKENS_RE = /\b(hermes|nous|portions adapted from|original copyright)\b/i;
|
|
33
|
+
const REFINER_SYSTEM_PROMPT = `
|
|
34
|
+
You polish auto-generated skill markdown for a local-first AI agent.
|
|
35
|
+
|
|
36
|
+
Your job is to improve the WORDING ONLY of an already-valid SKILL.md
|
|
37
|
+
file. The frontmatter (everything between the leading "---" markers)
|
|
38
|
+
must round-trip BYTE-FOR-BYTE unchanged. The "# <name>" heading must
|
|
39
|
+
stay first in the body. The numbered "## Steps" list must remain
|
|
40
|
+
numbered and in the same order; you may rephrase step descriptions
|
|
41
|
+
but must NOT add or remove steps.
|
|
42
|
+
|
|
43
|
+
Hard rules:
|
|
44
|
+
- Output the COMPLETE SKILL.md, not a diff.
|
|
45
|
+
- Do not add boilerplate, citations, or attribution to any other
|
|
46
|
+
agent or codebase. The skill is 100% the user's own.
|
|
47
|
+
- Do not introduce mock/fake values into commands.
|
|
48
|
+
- Keep total length under 6000 characters.
|
|
49
|
+
`.trim();
|
|
50
|
+
/**
|
|
51
|
+
* Refine `skeleton` via the auxiliary client. Always returns a
|
|
52
|
+
* valid SKILL.md string — either the refined output (if it passes
|
|
53
|
+
* round-trip + no-attribution validation) or the original skeleton.
|
|
54
|
+
*/
|
|
55
|
+
async function refine(skeleton, opts = {}) {
|
|
56
|
+
const client = opts.client;
|
|
57
|
+
if (!client || client.isUnavailable()) {
|
|
58
|
+
return skeleton;
|
|
59
|
+
}
|
|
60
|
+
// Parse the skeleton up front to lock the canonical frontmatter
|
|
61
|
+
// shape we'll require the refined output to match.
|
|
62
|
+
let skeletonParsed;
|
|
63
|
+
try {
|
|
64
|
+
skeletonParsed = (0, skillSpec_1.parseSkillContent)(skeleton);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// If the skeleton itself is invalid, refining can only make it
|
|
68
|
+
// worse; bubble the skeleton out and let the caller decide.
|
|
69
|
+
return skeleton;
|
|
70
|
+
}
|
|
71
|
+
const prompt = `${REFINER_SYSTEM_PROMPT}\n\n` +
|
|
72
|
+
`INPUT SKILL.md:\n\`\`\`\n${skeleton}\n\`\`\`\n\n` +
|
|
73
|
+
`Output the refined SKILL.md only — no commentary, no code fences.`;
|
|
74
|
+
let refined;
|
|
75
|
+
try {
|
|
76
|
+
const result = await client.call({
|
|
77
|
+
purpose: 'skill_describe',
|
|
78
|
+
prompt,
|
|
79
|
+
maxTokens: opts.maxTokens ?? 1500,
|
|
80
|
+
timeoutMs: opts.timeoutMs ?? 20000,
|
|
81
|
+
});
|
|
82
|
+
refined = (result.content ?? '').trim();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return skeleton;
|
|
86
|
+
}
|
|
87
|
+
if (refined.length === 0)
|
|
88
|
+
return skeleton;
|
|
89
|
+
// Strip a wrapping ```...``` if the model returned one.
|
|
90
|
+
refined = refined.replace(/^```(?:markdown|md)?\s*\n/, '').replace(/\n```\s*$/, '');
|
|
91
|
+
// Validation 1 — attribution sweep. Refined output must not
|
|
92
|
+
// contain any forbidden token. The permanent sweep would catch
|
|
93
|
+
// this at ship time; we catch it earlier so a single noisy
|
|
94
|
+
// refinement doesn't pollute the candidate queue.
|
|
95
|
+
if (FORBIDDEN_TOKENS_RE.test(refined))
|
|
96
|
+
return skeleton;
|
|
97
|
+
// Validation 2 — round-trip through parseSkillContent and verify
|
|
98
|
+
// the canonical fields match the skeleton.
|
|
99
|
+
let refinedParsed;
|
|
100
|
+
try {
|
|
101
|
+
refinedParsed = (0, skillSpec_1.parseSkillContent)(refined);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return skeleton;
|
|
105
|
+
}
|
|
106
|
+
if (refinedParsed.frontmatter.name !== skeletonParsed.frontmatter.name ||
|
|
107
|
+
refinedParsed.frontmatter.version !== skeletonParsed.frontmatter.version) {
|
|
108
|
+
return skeleton;
|
|
109
|
+
}
|
|
110
|
+
return refined;
|
|
111
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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 hermes/nous/copyright attribution strings — the
|
|
22
|
+
* permanent attribution sweep validates this
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.deriveName = deriveName;
|
|
26
|
+
exports.deriveDescription = deriveDescription;
|
|
27
|
+
exports.draft = draft;
|
|
28
|
+
const STOP_WORDS = new Set([
|
|
29
|
+
'the', 'a', 'an', 'to', 'for', 'of', 'on', 'at', 'in', 'by', 'and', 'or', 'but', 'with',
|
|
30
|
+
'from', 'please', 'can', 'you', 'will', 'do', 'does', 'my', 'our', 'this', 'that', 'these',
|
|
31
|
+
'is', 'are', 'be', 'was', 'were', 'it', 'its', 'as', 'if', 'then', 'else', 'some', 'any',
|
|
32
|
+
'me', 'i', 'your', 'their', 'his', 'her', 'him',
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Derive a kebab-case skill-name from the first user message.
|
|
36
|
+
* Conservative — keeps only [a-z0-9-], collapses runs, max 40 chars.
|
|
37
|
+
* Falls back to `learned-skill-<short-fingerprint>` if extraction
|
|
38
|
+
* yields nothing usable.
|
|
39
|
+
*/
|
|
40
|
+
function deriveName(firstUserPrompt, fingerprint) {
|
|
41
|
+
const lowered = firstUserPrompt.toLowerCase();
|
|
42
|
+
// Pull out non-stopword tokens, max 5 of them.
|
|
43
|
+
const tokens = lowered
|
|
44
|
+
.split(/[^a-z0-9]+/)
|
|
45
|
+
.filter((t) => t.length >= 3 && !STOP_WORDS.has(t))
|
|
46
|
+
.slice(0, 5);
|
|
47
|
+
let stem = tokens.join('-');
|
|
48
|
+
// Strict alphanum-and-dash, collapse multiple dashes, trim leading/trailing.
|
|
49
|
+
stem = stem.replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
50
|
+
if (stem.length === 0) {
|
|
51
|
+
return `learned-skill-${fingerprint.slice(0, 8)}`;
|
|
52
|
+
}
|
|
53
|
+
return stem.slice(0, 40);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the description line — single sentence summarising what the
|
|
57
|
+
* trace did. Keep it under 200 chars (the skill loader caps the
|
|
58
|
+
* displayed description in /skills list).
|
|
59
|
+
*/
|
|
60
|
+
function deriveDescription(trace) {
|
|
61
|
+
if (trace.length === 0)
|
|
62
|
+
return 'Learned workflow.';
|
|
63
|
+
const tools = trace.map((e) => e.name).filter(Boolean);
|
|
64
|
+
const distinct = Array.from(new Set(tools));
|
|
65
|
+
if (distinct.length === 1) {
|
|
66
|
+
return `Learned workflow: ${distinct[0]} (${tools.length}x).`;
|
|
67
|
+
}
|
|
68
|
+
const head = distinct.slice(0, 4).join(' → ');
|
|
69
|
+
const more = distinct.length > 4 ? ` (+${distinct.length - 4} more)` : '';
|
|
70
|
+
return `Learned workflow: ${head}${more}.`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Render the body as numbered tool-call steps. Args are JSON-stringified
|
|
74
|
+
* and clipped at 120 chars per step so a chatty trace doesn't bloat
|
|
75
|
+
* SKILL.md to multi-MB.
|
|
76
|
+
*/
|
|
77
|
+
function renderBody(trace) {
|
|
78
|
+
if (trace.length === 0) {
|
|
79
|
+
return '## Steps\n\n_(empty trace)_\n';
|
|
80
|
+
}
|
|
81
|
+
const out = ['## Steps', ''];
|
|
82
|
+
trace.forEach((entry, i) => {
|
|
83
|
+
const idx = i + 1;
|
|
84
|
+
const argSummary = (() => {
|
|
85
|
+
if (entry.args == null || typeof entry.args !== 'object')
|
|
86
|
+
return '';
|
|
87
|
+
try {
|
|
88
|
+
const json = JSON.stringify(entry.args);
|
|
89
|
+
if (json.length <= 120)
|
|
90
|
+
return ` \\\`${json}\\\``;
|
|
91
|
+
return ` \\\`${json.slice(0, 117)}…\\\``;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
out.push(`${idx}. **${entry.name}**${argSummary}`);
|
|
98
|
+
});
|
|
99
|
+
out.push('');
|
|
100
|
+
out.push('## Notes', '');
|
|
101
|
+
out.push('Mined from a successful turn — review the steps above before relying on it.');
|
|
102
|
+
out.push('');
|
|
103
|
+
return out.join('\n');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a full SKILL.md (frontmatter + body) ready for parseSkillContent.
|
|
107
|
+
* The output is deterministic for a given (trace, context) input —
|
|
108
|
+
* smokes assert this for stable round-trip behaviour.
|
|
109
|
+
*/
|
|
110
|
+
function draft(trace, ctx) {
|
|
111
|
+
const name = ctx.nameOverride?.trim() || deriveName(ctx.firstUserPrompt, ctx.traceFingerprint);
|
|
112
|
+
const description = deriveDescription(trace);
|
|
113
|
+
const version = '0.1.0';
|
|
114
|
+
const createdAt = new Date().toISOString();
|
|
115
|
+
// YAML frontmatter — keep field order stable so smokes can pin
|
|
116
|
+
// shape without depending on YAML library output ordering.
|
|
117
|
+
const frontmatter = [
|
|
118
|
+
'---',
|
|
119
|
+
`name: ${name}`,
|
|
120
|
+
`description: ${JSON.stringify(description)}`,
|
|
121
|
+
`version: ${version}`,
|
|
122
|
+
'category: learned',
|
|
123
|
+
'metadata:',
|
|
124
|
+
' aiden:',
|
|
125
|
+
' learned: true',
|
|
126
|
+
` sourceSessionId: ${JSON.stringify(ctx.sourceSessionId)}`,
|
|
127
|
+
` sourceTurnIdx: ${ctx.sourceTurnIdx}`,
|
|
128
|
+
` createdAt: ${JSON.stringify(createdAt)}`,
|
|
129
|
+
` traceFingerprint: ${JSON.stringify(ctx.traceFingerprint)}`,
|
|
130
|
+
` candidateConfidence: ${ctx.candidateConfidence.toFixed(3)}`,
|
|
131
|
+
'---',
|
|
132
|
+
'',
|
|
133
|
+
`# ${name}`,
|
|
134
|
+
'',
|
|
135
|
+
description,
|
|
136
|
+
'',
|
|
137
|
+
].join('\n');
|
|
138
|
+
return frontmatter + renderBody(trace);
|
|
139
|
+
}
|
|
@@ -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;
|