dsh-continual-evolve 0.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/README.zh.md +240 -0
  4. package/cordis.patch.yml +9 -0
  5. package/lib/apply.d.ts +24 -0
  6. package/lib/apply.js +131 -0
  7. package/lib/approval.d.ts +14 -0
  8. package/lib/approval.js +27 -0
  9. package/lib/auto.d.ts +34 -0
  10. package/lib/auto.js +217 -0
  11. package/lib/benchmark.d.ts +72 -0
  12. package/lib/benchmark.js +167 -0
  13. package/lib/command.d.ts +36 -0
  14. package/lib/command.js +549 -0
  15. package/lib/evaluate.d.ts +38 -0
  16. package/lib/evaluate.js +142 -0
  17. package/lib/goal.d.ts +72 -0
  18. package/lib/goal.js +72 -0
  19. package/lib/index.d.ts +93 -0
  20. package/lib/index.js +116 -0
  21. package/lib/inject.d.ts +124 -0
  22. package/lib/inject.js +231 -0
  23. package/lib/logfile.d.ts +71 -0
  24. package/lib/logfile.js +159 -0
  25. package/lib/mount.d.ts +42 -0
  26. package/lib/mount.js +198 -0
  27. package/lib/notify.d.ts +31 -0
  28. package/lib/notify.js +42 -0
  29. package/lib/plan.d.ts +16 -0
  30. package/lib/plan.js +121 -0
  31. package/lib/planner.d.ts +30 -0
  32. package/lib/planner.js +110 -0
  33. package/lib/pool.d.ts +7 -0
  34. package/lib/pool.js +25 -0
  35. package/lib/render.d.ts +15 -0
  36. package/lib/render.js +83 -0
  37. package/lib/review.d.ts +37 -0
  38. package/lib/review.js +127 -0
  39. package/lib/rollback.d.ts +11 -0
  40. package/lib/rollback.js +69 -0
  41. package/lib/rubric.d.ts +29 -0
  42. package/lib/rubric.js +119 -0
  43. package/lib/score.d.ts +31 -0
  44. package/lib/score.js +81 -0
  45. package/lib/service.d.ts +30 -0
  46. package/lib/service.js +42 -0
  47. package/lib/skill.d.ts +10 -0
  48. package/lib/skill.js +75 -0
  49. package/lib/source.d.ts +29 -0
  50. package/lib/source.js +42 -0
  51. package/lib/state.d.ts +34 -0
  52. package/lib/state.js +154 -0
  53. package/lib/store.d.ts +20 -0
  54. package/lib/store.js +74 -0
  55. package/lib/tool.d.ts +15 -0
  56. package/lib/tool.js +163 -0
  57. package/lib/types.d.ts +137 -0
  58. package/lib/types.js +62 -0
  59. package/lib/validate.d.ts +11 -0
  60. package/lib/validate.js +55 -0
  61. package/package.json +67 -0
package/lib/inject.js ADDED
@@ -0,0 +1,231 @@
1
+ import { isArchived } from "./types.js";
2
+ import { mergeHarnessStates } from "./state.js";
3
+ import { entryLine } from "./render.js";
4
+ /** Prompt sections render at most this many entries per kind. */
5
+ export const MAX_INJECTED_ENTRIES_PER_KIND = 6;
6
+ /** Per-entry content budget inside the injected block (matches render.ts). */
7
+ export const MAX_INJECTED_CONTENT_LENGTH = 180;
8
+ /** How many `parentSession` hops a child walks to inherit entries. */
9
+ export const MAX_PARENT_CHAIN_DEPTH = 8;
10
+ /** Recency half-life for the injection ranking: an entry this old scores 0. */
11
+ export const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
12
+ /** At most this many recent user messages feed the relevance query. */
13
+ export const MAX_QUERY_MESSAGES = 3;
14
+ /** Query text handed to the relevance scorer is capped at this many chars. */
15
+ export const MAX_QUERY_CHARS = 400;
16
+ /** Stable dictionary-order tiebreak used when two entries score equally. */
17
+ function stableCompare(a, b) {
18
+ return [a.path, a.title, a.id].join("\0").localeCompare([b.path, b.title, b.id].join("\0"));
19
+ }
20
+ /**
21
+ * Lowercase tokenization for the keyword relevance scorer: runs of ASCII
22
+ * alphanumerics and CJK characters become tokens (CJK is not split so whole
23
+ * Chinese words/characters stay comparable), everything else is a separator.
24
+ */
25
+ export function tokenize(text) {
26
+ return text
27
+ .toLowerCase()
28
+ .split(/[^a-z0-9\u4e00-\u9fff]+/)
29
+ .filter((token) => token.length > 0);
30
+ }
31
+ /**
32
+ * Keyword hit count of `query` tokens inside an entry: title hits weigh 2×,
33
+ * content/path hits 1×. BM25-level relevance without any external service.
34
+ */
35
+ export function relevanceHits(entry, query) {
36
+ const titleTokens = tokenize(entry.title);
37
+ const bodyTokens = tokenize(`${entry.content} ${entry.path}`);
38
+ let hits = 0;
39
+ for (const token of tokenize(query)) {
40
+ hits += titleTokens.filter((t) => t === token).length * 2;
41
+ hits += bodyTokens.filter((t) => t === token).length;
42
+ }
43
+ return hits;
44
+ }
45
+ /**
46
+ * Normalized recency in [0, 1]: 1 when the entry was just updated, decaying
47
+ * linearly to 0 after {@link RECENCY_HALF_LIFE_MS}. Unparseable timestamps
48
+ * score 0 (never preferred over a timestamped entry).
49
+ */
50
+ export function recencyScore(entry, now) {
51
+ const updatedAt = Date.parse(entry.updated_at);
52
+ if (Number.isNaN(updatedAt)) {
53
+ return 0;
54
+ }
55
+ const age = now - updatedAt;
56
+ if (age <= 0) {
57
+ return 1;
58
+ }
59
+ return Math.max(0, 1 - age / RECENCY_HALF_LIFE_MS);
60
+ }
61
+ /**
62
+ * Rank entries for injection, best first. With no query the ranking is pure
63
+ * recency (newest first). With a query, any entry with at least one keyword
64
+ * hit outranks every hit-less entry (`hits * 2 + recency <= 1` for the
65
+ * latter), and hits decide the order among relevant entries; recency then
66
+ * breaks remaining ties, and the stable dictionary order is the final
67
+ * tiebreak, so the result is deterministic.
68
+ */
69
+ export function rankEntries(entries, query, now = Date.now()) {
70
+ const q = (query ?? "").trim();
71
+ return [...entries].sort((a, b) => {
72
+ if (q.length > 0) {
73
+ const relevanceDelta = relevanceHits(b, q) * 2 - relevanceHits(a, q) * 2;
74
+ if (relevanceDelta !== 0) {
75
+ return relevanceDelta;
76
+ }
77
+ }
78
+ const recencyDelta = recencyScore(b, now) - recencyScore(a, now);
79
+ if (recencyDelta !== 0) {
80
+ return recencyDelta;
81
+ }
82
+ return stableCompare(a, b);
83
+ });
84
+ }
85
+ function sortedEntries(entries, query) {
86
+ return rankEntries(entries, query);
87
+ }
88
+ /**
89
+ * Extract the text of a message's content blocks without depending on the
90
+ * dsh-llm ContentBlock type: string blocks pass through, object blocks
91
+ * contribute their `text` field when present.
92
+ */
93
+ function extractBlockText(content) {
94
+ if (typeof content === "string") {
95
+ return content;
96
+ }
97
+ if (!Array.isArray(content)) {
98
+ return "";
99
+ }
100
+ return content
101
+ .map((block) => {
102
+ if (typeof block === "string") {
103
+ return block;
104
+ }
105
+ if (block !== null && typeof block === "object" && "text" in block && typeof block.text === "string") {
106
+ return block.text;
107
+ }
108
+ return "";
109
+ })
110
+ .filter((text) => text.length > 0)
111
+ .join(" ");
112
+ }
113
+ /**
114
+ * Compose the relevance query from the assembling agent's most recent direct
115
+ * user messages (event rows whose `type` is `user/message` and whose source
116
+ * is a human `user`, so injected plugin context and tool results never leak
117
+ * into the query). Returns "" when nothing qualifies — the ranking then
118
+ * falls back to pure recency.
119
+ */
120
+ export function recentUserText(agent, opts) {
121
+ const events = agent?.session?.events;
122
+ if (!events || events.length === 0) {
123
+ return "";
124
+ }
125
+ const maxMessages = opts?.maxMessages ?? MAX_QUERY_MESSAGES;
126
+ const maxChars = opts?.maxChars ?? MAX_QUERY_CHARS;
127
+ const parts = [];
128
+ for (let i = events.length - 1; i >= 0 && parts.length < maxMessages; i -= 1) {
129
+ const event = events[i];
130
+ if (event?.type !== "user/message") {
131
+ continue;
132
+ }
133
+ const source = event.data?.source;
134
+ if (source && source.kind !== "user") {
135
+ continue;
136
+ }
137
+ const text = extractBlockText(event.data?.content).trim();
138
+ if (text.length > 0) {
139
+ parts.unshift(text);
140
+ }
141
+ }
142
+ return parts.join(" ").slice(0, maxChars);
143
+ }
144
+ /** True when the state carries at least one entry of any kind. */
145
+ export function hasAnyEntries(state) {
146
+ return Object.values(state.entries).some((byKind) => Object.keys(byKind).length > 0);
147
+ }
148
+ /** The additive prompt-notes block (empty when there are no visible prompt entries). */
149
+ export function formatPromptEntriesSection(entries, query) {
150
+ const visible = entries.filter((entry) => !isArchived(entry));
151
+ if (visible.length === 0) {
152
+ return "";
153
+ }
154
+ const lines = [
155
+ "# Continual Harness — Prompt Notes",
156
+ "Supplemental prompt notes (the base system prompt is immutable). Use evolve_list for the full text of any note.",
157
+ ];
158
+ for (const entry of sortedEntries(visible, query).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
159
+ lines.push(entryLine(entry, MAX_INJECTED_CONTENT_LENGTH));
160
+ }
161
+ const overflow = visible.length - Math.min(visible.length, MAX_INJECTED_ENTRIES_PER_KIND);
162
+ if (overflow > 0) {
163
+ lines.push(`- +${overflow} more prompt notes (evolve_list)`);
164
+ }
165
+ return lines.join("\n");
166
+ }
167
+ /** The reusable delegation-specs block (empty when there are no visible subagent entries). */
168
+ export function formatSubagentSpecsSection(entries, query) {
169
+ const visible = entries.filter((entry) => !isArchived(entry));
170
+ if (visible.length === 0) {
171
+ return "";
172
+ }
173
+ const lines = [
174
+ "# Continual Harness — Delegation Specs",
175
+ "Reusable subagent specs: when you delegate work that matches a spec, assemble the child prompt from its content. Children inherit these specs through their parent chain.",
176
+ ];
177
+ for (const entry of sortedEntries(visible, query).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
178
+ lines.push(entryLine(entry, MAX_INJECTED_CONTENT_LENGTH));
179
+ }
180
+ const overflow = visible.length - Math.min(visible.length, MAX_INJECTED_ENTRIES_PER_KIND);
181
+ if (overflow > 0) {
182
+ lines.push(`- +${overflow} more delegation specs (evolve_list)`);
183
+ }
184
+ return lines.join("\n");
185
+ }
186
+ /**
187
+ * Walk the parent-session chain from `agent` upward and return the nearest
188
+ * session whose local store is non-empty, if any. Children inherit their
189
+ * ancestor's prompt notes and delegation specs; the chain walk stops at the
190
+ * first store that has entries (deep descendants do not re-inject ancestors
191
+ * beyond the nearest carrying store).
192
+ */
193
+ export function nearestLocalStateWithEntries(engine, agent) {
194
+ let cursor = agent;
195
+ for (let depth = 0; cursor !== undefined && depth < MAX_PARENT_CHAIN_DEPTH; depth += 1) {
196
+ const state = engine.load("local", cursor.id);
197
+ if (hasAnyEntries(state)) {
198
+ return state;
199
+ }
200
+ cursor = cursor.session?.header?.parentSession
201
+ ? { id: cursor.session.header.parentSession }
202
+ : undefined;
203
+ }
204
+ return undefined;
205
+ }
206
+ /**
207
+ * Compose the full injected block for one assembling agent: global entries
208
+ * merged with the nearest carrying local store (local wins on id collision).
209
+ * The optional `query` — when absent, derived from the agent's most recent
210
+ * direct user messages — ranks which entries fill the per-kind cap
211
+ * (relevance first, then recency; see {@link rankEntries}). Returns "" when
212
+ * nothing is injectable — the prompt renderer then drops the section, so an
213
+ * empty store adds zero tokens to every assembly.
214
+ */
215
+ export function entriesSectionText(engine, agent, query) {
216
+ if (!agent) {
217
+ return "";
218
+ }
219
+ const globalState = engine.load("global", undefined);
220
+ const localState = nearestLocalStateWithEntries(engine, agent);
221
+ const merged = localState ? mergeHarnessStates(globalState, localState) : globalState;
222
+ const promptEntries = Object.values(merged.entries.prompt);
223
+ const subagentEntries = Object.values(merged.entries.subagent);
224
+ const relevanceQuery = (query ?? recentUserText(agent)).trim();
225
+ const parts = [
226
+ formatPromptEntriesSection(promptEntries, relevanceQuery),
227
+ formatSubagentSpecsSection(subagentEntries, relevanceQuery),
228
+ ].filter((part) => part.length > 0);
229
+ return parts.join("\n\n");
230
+ }
231
+ //# sourceMappingURL=inject.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Plugin-owned file logging: a cordis logger exporter that appends every
3
+ * log message — from this plugin or any other — to a JSONL file under the
4
+ * evolve store. Logging becomes a property of the plugin itself: no extra
5
+ * component to install, and it works no matter how `dsh web` is launched
6
+ * (foreground terminal, nohup, restart scripts, ...). The official
7
+ * `cordis-plugin-logger-console` remains an optional add-on for live
8
+ * terminal output; this file exporter is the baseline that always exists.
9
+ *
10
+ * - file: `<baseDir>/evolve/plugin.log` (0600, JSONL)
11
+ * - rotation: when the file exceeds `logMaxBytes` it is renamed to
12
+ * `plugin.log.1` (replacing the previous `.1`)
13
+ * - level: `levels.default` = logLevel (0=error, 1=info, 2=warn, 3=debug)
14
+ * - failure containment: a write error is swallowed — logging never
15
+ * disturbs the agent loop
16
+ */
17
+ import type { Context } from "@deepseek-ai/cordis";
18
+ /** Name of the plugin log file under `<baseDir>/evolve/`. */
19
+ export declare const PLUGIN_LOG_FILE_NAME = "plugin.log";
20
+ /** Default rotation threshold (5 MiB). */
21
+ export declare const DEFAULT_LOG_MAX_BYTES: number;
22
+ export interface FileLoggerConfig {
23
+ /** 0=error, 1=info, 2=warn, 3=debug. */
24
+ logLevel?: number;
25
+ /** Rotate the log when it exceeds this many bytes. */
26
+ logMaxBytes?: number;
27
+ }
28
+ /** Full path of the plugin log file. */
29
+ export declare function pluginLogFilePath(baseDir: string): string;
30
+ /** JSON-safe rendering of a log message's arguments. */
31
+ export declare function renderArgs(args: readonly unknown[]): unknown[];
32
+ /** One JSONL record as written to the log file. */
33
+ export declare function logRecord(message: {
34
+ ts: number;
35
+ type: string;
36
+ name: string;
37
+ args: readonly unknown[];
38
+ }): string;
39
+ /** Human-readable rendering of one stored JSONL line (unparseable lines pass through). */
40
+ export declare function formatLogLine(line: string): string;
41
+ /**
42
+ * The distinct session ids mentioned in one stored log line, drawn from the
43
+ * rendered message and the raw args (unparseable lines fall back to a raw
44
+ * text scan). Exact-token matching, so `session-abc` never matches
45
+ * `session-abcd`.
46
+ */
47
+ export declare function sessionIdsInLine(line: string): string[];
48
+ /**
49
+ * Keep only the lines mentioning the given session id (exact token match).
50
+ * An empty/whitespace session id filters everything out — the caller should
51
+ * validate the argument before calling.
52
+ */
53
+ export declare function filterLogBySession(lines: readonly string[], sessionId: string): string[];
54
+ /** Append one line, rotating the file first when it exceeds maxBytes. */
55
+ export declare function appendOrRotate(path: string, maxBytes: number, line: string): void;
56
+ /**
57
+ * Register the file exporter on the context. Returns the exporter so tests
58
+ * can drive it directly. The exporter is disposed with the plugin's scope.
59
+ */
60
+ export declare function registerFileLogger(ctx: Context, baseDir: string, config?: FileLoggerConfig): {
61
+ levels: {
62
+ default: number;
63
+ };
64
+ export(message: {
65
+ ts: number;
66
+ type: string;
67
+ name: string;
68
+ args: readonly unknown[];
69
+ }): void;
70
+ };
71
+ //# sourceMappingURL=logfile.d.ts.map
package/lib/logfile.js ADDED
@@ -0,0 +1,159 @@
1
+ import { Logger } from "@deepseek-ai/cordis";
2
+ import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ /** Name of the plugin log file under `<baseDir>/evolve/`. */
5
+ export const PLUGIN_LOG_FILE_NAME = "plugin.log";
6
+ /** Default rotation threshold (5 MiB). */
7
+ export const DEFAULT_LOG_MAX_BYTES = 5 * 1024 * 1024;
8
+ /** Full path of the plugin log file. */
9
+ export function pluginLogFilePath(baseDir) {
10
+ return join(baseDir, "evolve", PLUGIN_LOG_FILE_NAME);
11
+ }
12
+ /** JSON-safe rendering of a log message's arguments. */
13
+ export function renderArgs(args) {
14
+ return args.map((arg) => {
15
+ if (arg instanceof Error) {
16
+ return { name: arg.name, message: arg.message, stack: arg.stack };
17
+ }
18
+ if (typeof arg === "object" && arg !== null) {
19
+ try {
20
+ return JSON.parse(JSON.stringify(arg));
21
+ }
22
+ catch {
23
+ return String(arg);
24
+ }
25
+ }
26
+ return arg;
27
+ });
28
+ }
29
+ /** One JSONL record as written to the log file. */
30
+ export function logRecord(message) {
31
+ // `message` is the printf-rendered text (cordis handles %s/%o/...), so
32
+ // humans and machines both get a usable line; `args` keeps the raw
33
+ // payload for tooling. Errors render as their stack (JSON.stringify of
34
+ // an Error is just {}).
35
+ const rendered = Logger.format({
36
+ formatters: {
37
+ o: (value) => (value instanceof Error ? value.stack ?? value.message : JSON.stringify(value)),
38
+ },
39
+ maxLength: 10240,
40
+ export: () => { },
41
+ }, {
42
+ sn: 0,
43
+ ts: message.ts,
44
+ name: message.name,
45
+ type: message.type,
46
+ level: 0,
47
+ args: message.args,
48
+ });
49
+ return JSON.stringify({
50
+ ts: new Date(message.ts).toISOString(),
51
+ type: message.type,
52
+ name: message.name,
53
+ args: renderArgs(message.args),
54
+ message: rendered,
55
+ });
56
+ }
57
+ /** Human-readable rendering of one stored JSONL line (unparseable lines pass through). */
58
+ export function formatLogLine(line) {
59
+ try {
60
+ const record = JSON.parse(line);
61
+ const ts = record.ts ?? "";
62
+ const type = record.type ? `[${record.type[0]?.toUpperCase() ?? "?"}]` : "[?]";
63
+ const name = record.name ?? "";
64
+ const body = typeof record.message === "string" && record.message.length > 0 ? record.message : "";
65
+ const args = body
66
+ ? ""
67
+ : Array.isArray(record.args)
68
+ ? record.args.map((arg) => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg))).join(" ")
69
+ : "";
70
+ return `${ts} ${type} ${name} ${body || args}`.trimEnd();
71
+ }
72
+ catch {
73
+ return line;
74
+ }
75
+ }
76
+ /** A session id as it appears in log text (dsh `session-<hex-uuid>` ids). */
77
+ const SESSION_TOKEN_RE = /\bsession-[0-9a-fA-F-]+\b/g;
78
+ /**
79
+ * The distinct session ids mentioned in one stored log line, drawn from the
80
+ * rendered message and the raw args (unparseable lines fall back to a raw
81
+ * text scan). Exact-token matching, so `session-abc` never matches
82
+ * `session-abcd`.
83
+ */
84
+ export function sessionIdsInLine(line) {
85
+ const ids = [];
86
+ const collect = (text) => {
87
+ for (const match of text.matchAll(SESSION_TOKEN_RE)) {
88
+ ids.push(match[0]);
89
+ }
90
+ };
91
+ try {
92
+ const record = JSON.parse(line);
93
+ if (typeof record.message === "string")
94
+ collect(record.message);
95
+ if (Array.isArray(record.args)) {
96
+ for (const arg of record.args) {
97
+ collect(typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg));
98
+ }
99
+ }
100
+ }
101
+ catch {
102
+ collect(line);
103
+ }
104
+ return [...new Set(ids)];
105
+ }
106
+ /**
107
+ * Keep only the lines mentioning the given session id (exact token match).
108
+ * An empty/whitespace session id filters everything out — the caller should
109
+ * validate the argument before calling.
110
+ */
111
+ export function filterLogBySession(lines, sessionId) {
112
+ const needle = sessionId.trim();
113
+ if (!needle) {
114
+ return [];
115
+ }
116
+ return lines.filter((line) => sessionIdsInLine(line).includes(needle));
117
+ }
118
+ /** Append one line, rotating the file first when it exceeds maxBytes. */
119
+ export function appendOrRotate(path, maxBytes, line) {
120
+ const dir = dirname(path);
121
+ if (!existsSync(path)) {
122
+ mkdirSync(dir, { recursive: true });
123
+ // 0600: the log can carry session context and must not be world-readable
124
+ writeFileSync(path, "", { encoding: "utf8", mode: 0o600 });
125
+ }
126
+ if (maxBytes > 0) {
127
+ try {
128
+ if (statSync(path).size > maxBytes) {
129
+ renameSync(path, `${path}.1`);
130
+ }
131
+ }
132
+ catch {
133
+ // stat/rename failures fall through to the append attempt
134
+ }
135
+ }
136
+ appendFileSync(path, `${line}\n`, "utf8");
137
+ }
138
+ /**
139
+ * Register the file exporter on the context. Returns the exporter so tests
140
+ * can drive it directly. The exporter is disposed with the plugin's scope.
141
+ */
142
+ export function registerFileLogger(ctx, baseDir, config = {}) {
143
+ const path = pluginLogFilePath(baseDir);
144
+ const maxBytes = config.logMaxBytes ?? DEFAULT_LOG_MAX_BYTES;
145
+ const exporter = {
146
+ levels: { default: config.logLevel ?? 1 },
147
+ export(message) {
148
+ try {
149
+ appendOrRotate(path, maxBytes, logRecord(message));
150
+ }
151
+ catch {
152
+ // logging must never disturb the agent loop
153
+ }
154
+ },
155
+ };
156
+ ctx.logger.exporter(exporter);
157
+ return exporter;
158
+ }
159
+ //# sourceMappingURL=logfile.js.map
package/lib/mount.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import type { HarnessEntry } from "./types.js";
3
+ export interface MountRecord {
4
+ id: string;
5
+ /** Loader entry id (the `evolve:<name>` key). */
6
+ entryId: string;
7
+ /** Absolute path of the generated plugin package. */
8
+ path: string;
9
+ /** Entry revision the mount was generated from. */
10
+ version: number;
11
+ mountedAt: string;
12
+ }
13
+ export interface MountLedger {
14
+ mounted: MountRecord[];
15
+ }
16
+ export declare function mountedDir(baseDir: string): string;
17
+ export declare function ledgerPath(baseDir: string): string;
18
+ export declare function loadLedger(baseDir: string): MountLedger;
19
+ export declare function saveLedger(baseDir: string, ledger: MountLedger): void;
20
+ /** Generate the plugin package files for one skill entry; returns the package dir. */
21
+ export declare function renderMountPackage(baseDir: string, entry: HarnessEntry): string;
22
+ /** The generated plugin source: one tool registration, no external imports. */
23
+ export declare function renderPluginSource(toolName: string, entry: HarnessEntry): string;
24
+ /**
25
+ * Map a skill entry's arguments contract to the tool-parameter schema.
26
+ * Requiredness goes in a root-level `required` array (valid JSON Schema):
27
+ * the mounted plugin registers via raw `ctx.tools.register`, and dsh-llm
28
+ * sends `parameters` verbatim to the API, which rejects per-property
29
+ * `required: true` ("true is not of type array").
30
+ */
31
+ export declare function renderParameters(entry: HarnessEntry): Record<string, unknown>;
32
+ /**
33
+ * Mount one skill entry into the live loader. Requires the `loader` service
34
+ * (resolved lazily, see FAQ #1); without it the package is still written and
35
+ * the ledger records the entry for the next boot.
36
+ */
37
+ export declare function mountSkill(ctx: Context, baseDir: string, entry: HarnessEntry): Promise<MountRecord>;
38
+ /** Unmount a skill entry: remove the loader entry and the generated package. */
39
+ export declare function unmountSkill(ctx: Context, baseDir: string, id: string): Promise<MountRecord | undefined>;
40
+ /** Re-mount every ledger entry at plugin boot (restart persistence). */
41
+ export declare function restoreMounted(ctx: Context, baseDir: string): Promise<void>;
42
+ //# sourceMappingURL=mount.d.ts.map