selftune 0.2.10 → 0.2.12

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.
@@ -5,11 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>selftune — Dashboard</title>
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
- <script type="module" crossorigin src="/assets/index-BZVLv70T.js"></script>
9
- <link rel="modulepreload" crossorigin href="/assets/vendor-react-BXP54cYo.js">
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-ui-CWU0d1wd.js">
11
- <link rel="modulepreload" crossorigin href="/assets/vendor-table-DTF_SXoy.js">
12
- <link rel="stylesheet" crossorigin href="/assets/index-Bs3Y4ixf.css">
8
+ <script type="module" crossorigin src="/assets/index-4_dAY17K.js"></script>
9
+ <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Dw2cE7zH.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-react-CKkiCskZ.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/vendor-ui-7xD7fNEU.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/vendor-table-pHbDxq36.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-BxV5WZHc.css">
13
14
  </head>
14
15
  <body>
15
16
  <div id="root"></div>
@@ -1,98 +1,18 @@
1
1
  /**
2
2
  * deploy-proposal.ts
3
3
  *
4
- * Deploys a validated evolution proposal by updating SKILL.md, creating a
5
- * backup, building a commit message with metrics, and optionally creating
6
- * a git branch and PR via `gh pr create`.
7
- */
8
-
9
- import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
10
-
11
- import type { EvolutionProposal, SkillSections } from "../types.js";
12
- import type { ValidationResult } from "./validate-proposal.js";
13
-
14
- // ---------------------------------------------------------------------------
15
- // Types
16
- // ---------------------------------------------------------------------------
17
-
18
- export interface DeployOptions {
19
- proposal: EvolutionProposal;
20
- validation: ValidationResult;
21
- skillPath: string;
22
- createPr: boolean;
23
- branchPrefix?: string; // default "selftune/evolve"
24
- }
25
-
26
- export interface DeployResult {
27
- skillMdUpdated: boolean;
28
- backupPath: string | null;
29
- branchName: string | null;
30
- commitMessage: string;
31
- }
32
-
33
- // ---------------------------------------------------------------------------
34
- // SKILL.md reading
35
- // ---------------------------------------------------------------------------
36
-
37
- /** Read the contents of a SKILL.md file. Throws if the file does not exist. */
38
- export function readSkillMd(skillPath: string): string {
39
- if (!existsSync(skillPath)) {
40
- throw new Error(`SKILL.md not found at ${skillPath}`);
41
- }
42
- return readFileSync(skillPath, "utf-8");
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Description replacement
47
- // ---------------------------------------------------------------------------
48
-
49
- /**
50
- * Replace the description section of a SKILL.md file.
4
+ * SKILL.md manipulation utilities for the evolution pipeline: description
5
+ * replacement, structured section parsing, section replacement, and full
6
+ * body replacement.
51
7
  *
52
- * The description is defined as the content between the first `#` heading
53
- * and the first `##` heading. If no `##` heading exists, the entire body
54
- * after the first heading is replaced.
8
+ * Evolution is a local personalization the evolved description reflects how
9
+ * *this user* works, not a change the skill creator should adopt. A future
10
+ * upstream feedback channel (anonymized patterns, not raw descriptions) may
11
+ * let end-users send useful signal back to skill creators, but that's a
12
+ * separate concern from deploy. See TD-019 in tech-debt-tracker.md.
55
13
  */
56
- export function replaceDescription(currentContent: string, newDescription: string): string {
57
- const lines = currentContent.split("\n");
58
14
 
59
- // Find the first # heading line
60
- let headingIndex = -1;
61
- for (let i = 0; i < lines.length; i++) {
62
- if (lines[i].startsWith("# ") && !lines[i].startsWith("## ")) {
63
- headingIndex = i;
64
- break;
65
- }
66
- }
67
-
68
- // If no heading found, just prepend the description
69
- if (headingIndex === -1) {
70
- return `${newDescription}\n${currentContent}`;
71
- }
72
-
73
- // Find the first ## heading after the main heading
74
- let subHeadingIndex = -1;
75
- for (let i = headingIndex + 1; i < lines.length; i++) {
76
- if (lines[i].startsWith("## ")) {
77
- subHeadingIndex = i;
78
- break;
79
- }
80
- }
81
-
82
- // Build the new content, preserving any preamble before the first heading
83
- const preamble = headingIndex > 0 ? `${lines.slice(0, headingIndex).join("\n")}\n` : "";
84
- const headingLine = lines[headingIndex];
85
- const descriptionBlock = newDescription.length > 0 ? `\n${newDescription}\n` : "\n";
86
-
87
- if (subHeadingIndex === -1) {
88
- // No sub-heading: preamble + heading + new description + trailing newline
89
- return `${preamble}${headingLine}\n${descriptionBlock}\n`;
90
- }
91
-
92
- // Preamble + heading + description + everything from the first ## onward
93
- const afterSubHeading = lines.slice(subHeadingIndex).join("\n");
94
- return `${preamble}${headingLine}\n${descriptionBlock}\n${afterSubHeading}`;
95
- }
15
+ import type { SkillSections } from "../types.js";
96
16
 
97
17
  // ---------------------------------------------------------------------------
98
18
  // Structured SKILL.md parsing
@@ -234,153 +154,3 @@ export function replaceBody(currentContent: string, proposedBody: string): strin
234
154
 
235
155
  return `${parts.join("\n").trimEnd()}\n`;
236
156
  }
237
-
238
- // ---------------------------------------------------------------------------
239
- // Commit message builder
240
- // ---------------------------------------------------------------------------
241
-
242
- /** Build a commit message that includes the skill name and pass rate change. */
243
- export function buildCommitMessage(
244
- proposal: EvolutionProposal,
245
- validation: ValidationResult,
246
- ): string {
247
- const changePercent = Math.round(validation.net_change * 100);
248
- const sign = changePercent >= 0 ? "+" : "";
249
- const passRateStr = `${sign}${changePercent}% pass rate`;
250
-
251
- return `evolve(${proposal.skill_name}): ${passRateStr}`;
252
- }
253
-
254
- // ---------------------------------------------------------------------------
255
- // Git/GH operations (PR creation)
256
- // ---------------------------------------------------------------------------
257
-
258
- /** Sanitize a string for use in a git branch name. */
259
- function sanitizeForGitRef(name: string): string {
260
- return name
261
- .replace(/[^a-zA-Z0-9._-]/g, "-")
262
- .replace(/\.{2,}/g, ".")
263
- .replace(/^[.-]|[.-]$/g, "")
264
- .replace(/-{2,}/g, "-");
265
- }
266
-
267
- /** Generate a branch name from the prefix and skill name. */
268
- function makeBranchName(prefix: string, skillName: string): string {
269
- const timestamp = Date.now();
270
- const safeName = sanitizeForGitRef(skillName) || "untitled";
271
- return `${prefix}/${safeName}-${timestamp}`;
272
- }
273
-
274
- /**
275
- * Run a git/gh command via Bun.spawn. Returns stdout on success.
276
- * Throws on non-zero exit code or if the command exceeds timeoutMs.
277
- */
278
- async function runCommand(args: string[], cwd?: string, timeoutMs = 30_000): Promise<string> {
279
- const proc = Bun.spawn(args, {
280
- cwd,
281
- stdout: "pipe",
282
- stderr: "pipe",
283
- });
284
-
285
- let timedOut = false;
286
- const timer = setTimeout(() => {
287
- timedOut = true;
288
- proc.kill();
289
- }, timeoutMs);
290
-
291
- try {
292
- // Read stdout and stderr concurrently to avoid deadlock when both pipes fill.
293
- const [stdout, stderr] = await Promise.all([
294
- new Response(proc.stdout).text(),
295
- new Response(proc.stderr).text(),
296
- ]);
297
- const exitCode = await proc.exited;
298
-
299
- if (timedOut) {
300
- throw new Error(`Command timed out after ${timeoutMs}ms: ${args.join(" ")}`);
301
- }
302
-
303
- if (exitCode !== 0) {
304
- throw new Error(`Command failed (exit ${exitCode}): ${args.join(" ")}\n${stderr}`);
305
- }
306
-
307
- return stdout.trim();
308
- } finally {
309
- clearTimeout(timer);
310
- }
311
- }
312
-
313
- // ---------------------------------------------------------------------------
314
- // Main deploy function
315
- // ---------------------------------------------------------------------------
316
-
317
- /** Deploy a validated evolution proposal to SKILL.md and optionally create a PR. */
318
- export async function deployProposal(options: DeployOptions): Promise<DeployResult> {
319
- const { proposal, validation, skillPath, createPr, branchPrefix = "selftune/evolve" } = options;
320
-
321
- // Step 1: Read current SKILL.md
322
- const currentContent = readSkillMd(skillPath);
323
-
324
- // Step 2: Create backup (unique per deploy to avoid overwriting previous backups)
325
- const backupTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
326
- const backupPath = `${skillPath}.${backupTimestamp}.bak`;
327
- copyFileSync(skillPath, backupPath);
328
-
329
- // Step 3: Replace description and write
330
- const updatedContent = replaceDescription(currentContent, proposal.proposed_description);
331
- writeFileSync(skillPath, updatedContent, "utf-8");
332
-
333
- // Step 4: Build commit message
334
- const commitMessage = buildCommitMessage(proposal, validation);
335
-
336
- // Step 5: Optionally create branch and PR
337
- let branchName: string | null = null;
338
-
339
- if (createPr) {
340
- branchName = makeBranchName(branchPrefix, proposal.skill_name);
341
-
342
- try {
343
- // Create and checkout branch
344
- await runCommand(["git", "checkout", "-b", branchName]);
345
-
346
- // Stage the SKILL.md
347
- await runCommand(["git", "add", skillPath]);
348
-
349
- // Commit
350
- await runCommand(["git", "commit", "-m", commitMessage]);
351
-
352
- // Push
353
- await runCommand(["git", "push", "-u", "origin", branchName]);
354
-
355
- // Create PR
356
- await runCommand([
357
- "gh",
358
- "pr",
359
- "create",
360
- "--title",
361
- commitMessage,
362
- "--body",
363
- `Proposal: ${proposal.proposal_id}\nRationale: ${proposal.rationale}\nNet change: ${validation.net_change > 0 ? "+" : ""}${Math.round(validation.net_change * 100)}%`,
364
- ]);
365
- } catch (err) {
366
- // Git/GH operations are best-effort in test environments.
367
- // The branch name is still returned for tracking.
368
- console.error(`[WARN] Git/GH operation failed: ${err instanceof Error ? err.message : err}`);
369
- }
370
- }
371
-
372
- return {
373
- skillMdUpdated: true,
374
- backupPath,
375
- branchName,
376
- commitMessage,
377
- };
378
- }
379
-
380
- // ---------------------------------------------------------------------------
381
- // CLI entry guard
382
- // ---------------------------------------------------------------------------
383
-
384
- if (import.meta.main) {
385
- console.log("deploy-proposal: use deployProposal() programmatically or via evolve CLI");
386
- }
@@ -36,7 +36,7 @@ import type {
36
36
  SessionTelemetryRecord,
37
37
  SkillUsageRecord,
38
38
  } from "../types.js";
39
- import { parseFrontmatter, replaceFrontmatterDescription } from "../utils/frontmatter.js";
39
+ import { parseFrontmatter, replaceDescription } from "../utils/frontmatter.js";
40
40
  import { createEvolveTUI } from "../utils/tui.js";
41
41
  import { appendAuditEntry } from "./audit.js";
42
42
  import { checkConstitution } from "./constitutional.js";
@@ -958,11 +958,8 @@ export async function evolve(
958
958
  copyFileSync(skillPath, backupPath);
959
959
  tui.done(`Backup created at ${backupPath}`);
960
960
 
961
- // Replace the frontmatter description
962
- const updatedContent = replaceFrontmatterDescription(
963
- rawContent,
964
- lastProposal.proposed_description,
965
- );
961
+ // Replace the description (handles both frontmatter and plain markdown)
962
+ const updatedContent = replaceDescription(rawContent, lastProposal.proposed_description);
966
963
  writeFileSync(skillPath, updatedContent, "utf-8");
967
964
  tui.done(`Deployed updated description to ${skillPath}`);
968
965
 
@@ -13,8 +13,8 @@ import { parseArgs } from "node:util";
13
13
 
14
14
  import { updateContextAfterRollback } from "../memory/writer.js";
15
15
  import type { EvolutionAuditEntry } from "../types.js";
16
+ import { replaceDescription } from "../utils/frontmatter.js";
16
17
  import { appendAuditEntry, getLastDeployedProposal, readAuditTrail } from "./audit.js";
17
- import { replaceDescription } from "./deploy-proposal.js";
18
18
 
19
19
  // ---------------------------------------------------------------------------
20
20
  // Types
@@ -180,7 +180,7 @@ function buildReportHTML(
180
180
  <tr><th>Metric</th><th>Value</th></tr>
181
181
  <tr><td>Total Skills</td><td>${statusResult.skills.length}</td></tr>
182
182
  <tr><td>Unmatched Queries</td><td>${statusResult.unmatchedQueries}</td></tr>
183
- <tr><td>Pending Proposals</td><td>${statusResult.pendingProposals}</td></tr>
183
+ <tr><td>Undeployed Proposals</td><td>${statusResult.pendingProposals}</td></tr>
184
184
  <tr><td>Last Session</td><td>${escapeHtml(statusResult.lastSession ?? "\u2014")}</td></tr>
185
185
  </table>
186
186
  </div>
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Route handler: GET /api/v2/skills/:name
3
3
  *
4
- * Returns SQLite-backed per-skill report with evolution audit, pending proposals,
4
+ * Returns SQLite-backed per-skill report with evolution audit, undeployed proposals,
5
5
  * invocation details, duration stats, selftune resource usage, prompt samples,
6
6
  * and session metadata.
7
7
  */
@@ -324,7 +324,7 @@ export function formatStatus(result: StatusResult): string {
324
324
 
325
325
  // Summary stats
326
326
  lines.push(`Unmatched queries: ${result.unmatchedQueries}`);
327
- lines.push(`Pending proposals: ${result.pendingProposals}`);
327
+ lines.push(`Undeployed proposals: ${result.pendingProposals}`);
328
328
 
329
329
  // Last session
330
330
  if (result.lastSession) {
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Line-based YAML frontmatter parser for SKILL.md files.
5
5
  * Extracts name, description, and version without a YAML library.
6
+ * Also provides `replaceDescription` — the single public API for replacing
7
+ * a skill's description in SKILL.md (handles both frontmatter and plain markdown).
6
8
  */
7
9
 
8
10
  // ---------------------------------------------------------------------------
@@ -137,16 +139,57 @@ export function parseFrontmatter(content: string): SkillFrontmatter {
137
139
  // ---------------------------------------------------------------------------
138
140
 
139
141
  /**
140
- * Replace the `description:` field in YAML frontmatter, preserving all other
141
- * content. If the new description contains special YAML characters, it is
142
- * written as a folded scalar (`description: >`).
142
+ * Replace the description between the first `#` heading and the first `##`
143
+ * heading. If no `##` heading exists, the entire body after the heading is
144
+ * replaced. Used as fallback when no YAML frontmatter is present.
145
+ */
146
+ function replaceMarkdownDescription(currentContent: string, newDescription: string): string {
147
+ const lines = currentContent.split("\n");
148
+
149
+ let headingIndex = -1;
150
+ for (let i = 0; i < lines.length; i++) {
151
+ if (lines[i].startsWith("# ") && !lines[i].startsWith("## ")) {
152
+ headingIndex = i;
153
+ break;
154
+ }
155
+ }
156
+
157
+ if (headingIndex === -1) {
158
+ return `${newDescription}\n${currentContent}`;
159
+ }
160
+
161
+ let subHeadingIndex = -1;
162
+ for (let i = headingIndex + 1; i < lines.length; i++) {
163
+ if (lines[i].startsWith("## ")) {
164
+ subHeadingIndex = i;
165
+ break;
166
+ }
167
+ }
168
+
169
+ const preamble = headingIndex > 0 ? `${lines.slice(0, headingIndex).join("\n")}\n` : "";
170
+ const headingLine = lines[headingIndex];
171
+ const descriptionBlock = newDescription.length > 0 ? `\n${newDescription}\n` : "\n";
172
+
173
+ if (subHeadingIndex === -1) {
174
+ return `${preamble}${headingLine}\n${descriptionBlock}\n`;
175
+ }
176
+
177
+ const afterSubHeading = lines.slice(subHeadingIndex).join("\n");
178
+ return `${preamble}${headingLine}\n${descriptionBlock}\n${afterSubHeading}`;
179
+ }
180
+
181
+ /**
182
+ * Replace a skill's description in SKILL.md.
143
183
  *
144
- * Returns the original content unchanged if no frontmatter is found.
184
+ * If the file has YAML frontmatter, replaces the `description:` field
185
+ * (using folded scalar for long/special-char descriptions).
186
+ * Otherwise, falls back to markdown heading-based replacement.
145
187
  */
146
- export function replaceFrontmatterDescription(content: string, newDescription: string): string {
188
+ export function replaceDescription(content: string, newDescription: string): string {
147
189
  const lines = content.split("\n");
148
190
 
149
- if (lines[0]?.trim() !== "---") return content;
191
+ // No frontmatter fall back to markdown heading-based replacement
192
+ if (lines[0]?.trim() !== "---") return replaceMarkdownDescription(content, newDescription);
150
193
 
151
194
  let endIdx = -1;
152
195
  for (let i = 1; i < lines.length; i++) {
@@ -155,7 +198,7 @@ export function replaceFrontmatterDescription(content: string, newDescription: s
155
198
  break;
156
199
  }
157
200
  }
158
- if (endIdx < 0) return content;
201
+ if (endIdx < 0) return replaceMarkdownDescription(content, newDescription);
159
202
 
160
203
  // Find and replace the description within frontmatter lines
161
204
  const yamlLines = lines.slice(1, endIdx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selftune",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Self-improving skills CLI for AI agents",
5
5
  "keywords": [
6
6
  "agent",
@@ -65,9 +65,9 @@
65
65
  "test:fast": "bun test $(find tests -name '*.test.ts' ! -name 'evolve.test.ts' ! -name 'integration.test.ts' ! -name 'dashboard-server.test.ts' ! -path '*/blog-proof/*')",
66
66
  "test:slow": "bun test tests/evolution/evolve.test.ts tests/evolution/integration.test.ts tests/monitoring/integration.test.ts tests/dashboard/dashboard-server.test.ts",
67
67
  "build:dashboard": "cd apps/local-dashboard && bunx vite build",
68
- "link:claude-workspace": "bash scripts/link-claude-workspace.sh",
69
68
  "prepack": "node scripts/publish-package-json.cjs prepare",
70
69
  "postpack": "node scripts/publish-package-json.cjs restore",
70
+ "link:claude-workspace": "bash scripts/link-claude-workspace.sh",
71
71
  "sync-version": "bun run scripts/sync-skill-version.ts",
72
72
  "validate:subagents": "bun run scripts/validate-subagent-docs.ts",
73
73
  "prepublishOnly": "bun run sync-version && bun run build:dashboard",
@@ -57,7 +57,7 @@ Presentational components for selftune dashboard views. No data fetching, no rou
57
57
  | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
58
58
  | `SkillHealthGrid` | Sortable/filterable data table with drag-and-drop, pagination, and view tabs. Accepts `renderSkillName` prop for custom routing. |
59
59
  | `EvolutionTimeline` | Proposal lifecycle timeline grouped by proposal ID, with pass rate deltas. |
60
- | `ActivityPanel` | Tabbed activity feed (pending proposals, timeline events, unmatched queries). |
60
+ | `ActivityPanel` | Tabbed activity feed (undeployed proposals, timeline events, unmatched queries). |
61
61
  | `EvidenceViewer` | Full evidence trail for a proposal — side-by-side diffs, validation results, iteration rounds. |
62
62
  | `SectionCards` | Dashboard metric stat cards (skills count, pass rate, unmatched, sessions, etc.). |
63
63
  | `OrchestrateRunsPanel` | Collapsible orchestrate run reports with per-skill action details. |
@@ -77,7 +77,7 @@ export function ActivityPanel({
77
77
  {pendingProposals.length}
78
78
  </Badge>
79
79
  </TooltipTrigger>
80
- <TooltipContent>Pending proposals</TooltipContent>
80
+ <TooltipContent>Undeployed proposals</TooltipContent>
81
81
  </Tooltip>
82
82
  )}
83
83
  <Tooltip>
@@ -125,7 +125,7 @@ export function SectionCards({
125
125
  <CardHeader>
126
126
  <CardDescription className="flex items-center gap-1.5">
127
127
  <AlertTriangleIcon className="size-3.5" />
128
- Pending Proposals
128
+ Undeployed Proposals
129
129
  <InfoTip text="Evolution proposals that have been generated but not yet validated or deployed. Requires running selftune evolve." />
130
130
  </CardDescription>
131
131
  <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
@@ -137,7 +137,7 @@ export function SectionCards({
137
137
  no evolution runs yet
138
138
  </Badge>
139
139
  ) : pendingCount > 0 ? (
140
- <Badge variant="secondary">awaiting review</Badge>
140
+ <Badge variant="secondary">not yet deployed</Badge>
141
141
  ) : null}
142
142
  </CardAction>
143
143
  </CardHeader>
package/skill/SKILL.md CHANGED
@@ -12,7 +12,7 @@ description: >
12
12
  even if they don't say "selftune" explicitly.
13
13
  metadata:
14
14
  author: selftune-dev
15
- version: 0.2.10
15
+ version: 0.2.12
16
16
  category: developer-tools
17
17
  ---
18
18