selftune 0.2.9 → 0.2.10

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 (130) hide show
  1. package/README.md +35 -35
  2. package/apps/local-dashboard/dist/assets/index-BZVLv70T.js +16 -0
  3. package/apps/local-dashboard/dist/assets/{vendor-react-BQH_6WrG.js → vendor-react-BXP54cYo.js} +4 -4
  4. package/apps/local-dashboard/dist/assets/{vendor-table-dK1QMLq9.js → vendor-table-DTF_SXoy.js} +1 -1
  5. package/apps/local-dashboard/dist/assets/{vendor-ui-CO2mrx6e.js → vendor-ui-CWU0d1wd.js} +66 -66
  6. package/apps/local-dashboard/dist/index.html +15 -15
  7. package/bin/selftune.cjs +1 -1
  8. package/cli/selftune/activation-rules.ts +1 -0
  9. package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
  10. package/cli/selftune/alpha-upload/stage-canonical.ts +94 -0
  11. package/cli/selftune/auth/device-code.ts +32 -0
  12. package/cli/selftune/auto-update.ts +12 -0
  13. package/cli/selftune/badge/badge.ts +1 -0
  14. package/cli/selftune/canonical-export.ts +5 -0
  15. package/cli/selftune/claude-agents.ts +154 -0
  16. package/cli/selftune/contribute/bundle.ts +1 -0
  17. package/cli/selftune/contribute/contribute.ts +1 -0
  18. package/cli/selftune/cron/setup.ts +2 -2
  19. package/cli/selftune/dashboard-server.ts +1 -0
  20. package/cli/selftune/eval/hooks-to-evals.ts +1 -0
  21. package/cli/selftune/eval/import-skillsbench.ts +1 -0
  22. package/cli/selftune/eval/synthetic-evals.ts +2 -3
  23. package/cli/selftune/eval/unit-test.ts +1 -0
  24. package/cli/selftune/evolution/deploy-proposal.ts +1 -0
  25. package/cli/selftune/evolution/evolve-body.ts +93 -6
  26. package/cli/selftune/evolution/evolve.ts +0 -1
  27. package/cli/selftune/evolution/propose-body.ts +3 -2
  28. package/cli/selftune/evolution/propose-routing.ts +3 -2
  29. package/cli/selftune/evolution/refine-body.ts +3 -2
  30. package/cli/selftune/export.ts +1 -0
  31. package/cli/selftune/grading/grade-session.ts +8 -0
  32. package/cli/selftune/hooks/auto-activate.ts +1 -0
  33. package/cli/selftune/hooks/evolution-guard.ts +1 -1
  34. package/cli/selftune/hooks/prompt-log.ts +1 -0
  35. package/cli/selftune/hooks/session-stop.ts +34 -40
  36. package/cli/selftune/hooks/skill-change-guard.ts +1 -0
  37. package/cli/selftune/hooks/skill-eval.ts +1 -1
  38. package/cli/selftune/index.ts +23 -14
  39. package/cli/selftune/ingestors/claude-replay.ts +1 -0
  40. package/cli/selftune/ingestors/codex-rollout.ts +1 -0
  41. package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
  42. package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
  43. package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
  44. package/cli/selftune/init.ts +121 -29
  45. package/cli/selftune/localdb/db.ts +1 -0
  46. package/cli/selftune/localdb/direct-write.ts +39 -0
  47. package/cli/selftune/localdb/materialize.ts +2 -0
  48. package/cli/selftune/localdb/queries.ts +53 -0
  49. package/cli/selftune/localdb/schema.ts +28 -0
  50. package/cli/selftune/normalization.ts +1 -0
  51. package/cli/selftune/observability.ts +1 -0
  52. package/cli/selftune/repair/skill-usage.ts +1 -0
  53. package/cli/selftune/routes/orchestrate-runs.ts +1 -0
  54. package/cli/selftune/routes/overview.ts +1 -0
  55. package/cli/selftune/routes/skill-report.ts +1 -0
  56. package/cli/selftune/sync.ts +30 -1
  57. package/cli/selftune/uninstall.ts +412 -0
  58. package/cli/selftune/utils/canonical-log.ts +2 -0
  59. package/cli/selftune/utils/jsonl.ts +1 -0
  60. package/cli/selftune/utils/llm-call.ts +131 -3
  61. package/cli/selftune/utils/skill-log.ts +1 -0
  62. package/cli/selftune/utils/transcript.ts +1 -0
  63. package/cli/selftune/utils/trigger-check.ts +1 -1
  64. package/cli/selftune/workflows/skill-md-writer.ts +5 -5
  65. package/cli/selftune/workflows/workflows.ts +1 -0
  66. package/package.json +37 -33
  67. package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
  68. package/packages/telemetry-contract/package.json +1 -1
  69. package/packages/telemetry-contract/src/schemas.ts +1 -0
  70. package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
  71. package/packages/ui/README.md +35 -34
  72. package/packages/ui/package.json +3 -3
  73. package/packages/ui/src/components/ActivityTimeline.tsx +49 -42
  74. package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
  75. package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
  76. package/packages/ui/src/components/InfoTip.tsx +4 -3
  77. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
  78. package/packages/ui/src/components/section-cards.tsx +19 -24
  79. package/packages/ui/src/components/skill-health-grid.tsx +213 -193
  80. package/packages/ui/src/lib/constants.tsx +1 -0
  81. package/packages/ui/src/primitives/badge.tsx +12 -15
  82. package/packages/ui/src/primitives/button.tsx +7 -7
  83. package/packages/ui/src/primitives/card.tsx +15 -26
  84. package/packages/ui/src/primitives/checkbox.tsx +7 -8
  85. package/packages/ui/src/primitives/collapsible.tsx +5 -5
  86. package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
  87. package/packages/ui/src/primitives/label.tsx +6 -6
  88. package/packages/ui/src/primitives/select.tsx +28 -37
  89. package/packages/ui/src/primitives/table.tsx +17 -44
  90. package/packages/ui/src/primitives/tabs.tsx +14 -21
  91. package/packages/ui/src/primitives/tooltip.tsx +10 -22
  92. package/skill/SKILL.md +70 -57
  93. package/skill/Workflows/AlphaUpload.md +4 -4
  94. package/skill/Workflows/AutoActivation.md +11 -6
  95. package/skill/Workflows/Badge.md +22 -16
  96. package/skill/Workflows/Baseline.md +34 -36
  97. package/skill/Workflows/Composability.md +16 -11
  98. package/skill/Workflows/Contribute.md +26 -21
  99. package/skill/Workflows/Cron.md +23 -22
  100. package/skill/Workflows/Dashboard.md +32 -27
  101. package/skill/Workflows/Doctor.md +33 -27
  102. package/skill/Workflows/Evals.md +48 -47
  103. package/skill/Workflows/EvolutionMemory.md +31 -21
  104. package/skill/Workflows/Evolve.md +84 -82
  105. package/skill/Workflows/EvolveBody.md +58 -47
  106. package/skill/Workflows/Grade.md +16 -13
  107. package/skill/Workflows/ImportSkillsBench.md +9 -6
  108. package/skill/Workflows/Ingest.md +36 -21
  109. package/skill/Workflows/Initialize.md +108 -40
  110. package/skill/Workflows/Orchestrate.md +22 -16
  111. package/skill/Workflows/Replay.md +12 -7
  112. package/skill/Workflows/Rollback.md +13 -6
  113. package/skill/Workflows/Schedule.md +6 -6
  114. package/skill/Workflows/Sync.md +18 -11
  115. package/skill/Workflows/UnitTest.md +28 -17
  116. package/skill/Workflows/Watch.md +28 -21
  117. package/skill/agents/diagnosis-analyst.md +11 -0
  118. package/skill/agents/evolution-reviewer.md +15 -1
  119. package/skill/agents/integration-guide.md +10 -0
  120. package/skill/agents/pattern-analyst.md +12 -1
  121. package/skill/references/grading-methodology.md +23 -24
  122. package/skill/references/interactive-config.md +7 -7
  123. package/skill/references/invocation-taxonomy.md +22 -20
  124. package/skill/references/logs.md +14 -6
  125. package/skill/references/setup-patterns.md +4 -2
  126. package/.claude/agents/diagnosis-analyst.md +0 -156
  127. package/.claude/agents/evolution-reviewer.md +0 -180
  128. package/.claude/agents/integration-guide.md +0 -212
  129. package/.claude/agents/pattern-analyst.md +0 -160
  130. package/apps/local-dashboard/dist/assets/index-C4UYGWKr.js +0 -15
@@ -25,7 +25,8 @@ import type {
25
25
  QueryLogRecord,
26
26
  SkillUsageRecord,
27
27
  } from "../types.js";
28
-
28
+ import type { EffortLevel, SubagentCallOptions } from "../utils/llm-call.js";
29
+ import { callViaSubagent } from "../utils/llm-call.js";
29
30
  import { appendAuditEntry } from "./audit.js";
30
31
  import { checkConstitutionSizeOnly } from "./constitutional.js";
31
32
  import { parseSkillSections, replaceBody, replaceSection } from "./deploy-proposal.js";
@@ -57,6 +58,9 @@ export interface EvolveBodyOptions {
57
58
  fewShotExamples?: string[];
58
59
  gradingResults?: GradingResult[];
59
60
  validationModel?: string;
61
+ teacherEffort?: EffortLevel;
62
+ /** Run evolution-reviewer subagent as Gate 4 before deployment. */
63
+ useReviewer?: boolean;
60
64
  }
61
65
 
62
66
  export interface EvolveBodyResult {
@@ -89,6 +93,7 @@ export interface EvolveBodyDeps {
89
93
  readEffectiveSkillUsageRecords?: () => SkillUsageRecord[];
90
94
  readFileSync?: typeof readFileSync;
91
95
  writeFileSync?: (path: string, data: string, encoding: string) => void;
96
+ callViaSubagent?: (options: SubagentCallOptions) => Promise<string>;
92
97
  }
93
98
 
94
99
  // ---------------------------------------------------------------------------
@@ -110,6 +115,19 @@ function createAuditEntry(
110
115
  };
111
116
  }
112
117
 
118
+ // ---------------------------------------------------------------------------
119
+ // Pipeline defaults — enforced even when the calling agent omits flags
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /** Default teacher model: Opus 4.6 for highest-quality proposals. */
123
+ const DEFAULT_TEACHER_MODEL = "opus";
124
+
125
+ /** Default student model: Haiku for cheap, fast validation gates. */
126
+ const DEFAULT_STUDENT_MODEL = "haiku";
127
+
128
+ /** Default teacher effort: extended thinking for multi-constraint reasoning. */
129
+ const DEFAULT_TEACHER_EFFORT: EffortLevel = "high";
130
+
113
131
  // ---------------------------------------------------------------------------
114
132
  // Main orchestrator
115
133
  // ---------------------------------------------------------------------------
@@ -124,8 +142,6 @@ export async function evolveBody(
124
142
  target,
125
143
  teacherAgent,
126
144
  studentAgent,
127
- teacherModel,
128
- studentModel,
129
145
  evalSetPath,
130
146
  dryRun,
131
147
  maxIterations,
@@ -133,6 +149,11 @@ export async function evolveBody(
133
149
  fewShotExamples,
134
150
  } = options;
135
151
 
152
+ // Apply pipeline defaults for models/effort when not explicitly provided
153
+ const teacherModel = options.teacherModel ?? DEFAULT_TEACHER_MODEL;
154
+ const studentModel = options.studentModel ?? DEFAULT_STUDENT_MODEL;
155
+ const teacherEffort = options.teacherEffort ?? DEFAULT_TEACHER_EFFORT;
156
+
136
157
  // Resolve injectable dependencies
137
158
  const _extractFailurePatterns = _deps.extractFailurePatterns ?? extractFailurePatterns;
138
159
  const _generateBodyProposal = _deps.generateBodyProposal ?? generateBodyProposal;
@@ -151,6 +172,7 @@ export async function evolveBody(
151
172
  });
152
173
  const _readFileSync = _deps.readFileSync ?? readFileSync;
153
174
  const _writeFileSync = _deps.writeFileSync ?? (await import("node:fs")).writeFileSync;
175
+ const _callViaSubagent = _deps.callViaSubagent ?? callViaSubagent;
154
176
 
155
177
  const auditEntries: EvolutionAuditEntry[] = [];
156
178
 
@@ -306,6 +328,7 @@ export async function evolveBody(
306
328
  skillPath,
307
329
  teacherAgent,
308
330
  teacherModel,
331
+ teacherEffort,
309
332
  );
310
333
  } else {
311
334
  proposal = await _generateBodyProposal(
@@ -318,6 +341,7 @@ export async function evolveBody(
318
341
  teacherModel,
319
342
  fewShotExamples,
320
343
  executionContext,
344
+ teacherEffort,
321
345
  );
322
346
  }
323
347
  } else if (lastProposal && lastValidation) {
@@ -327,6 +351,7 @@ export async function evolveBody(
327
351
  lastValidation,
328
352
  teacherAgent,
329
353
  teacherModel,
354
+ options.teacherEffort,
330
355
  );
331
356
  } else {
332
357
  break;
@@ -496,7 +521,63 @@ export async function evolveBody(
496
521
  }
497
522
  }
498
523
 
499
- // Step 5: Deploy or dry-run
524
+ // Step 5: Optional evolution-reviewer gate (Gate 4)
525
+ if (options.useReviewer && lastProposal && lastValidation?.improved) {
526
+ try {
527
+ const reviewPrompt = [
528
+ `Review this ${target} evolution proposal for the "${skillName}" skill.`,
529
+ ``,
530
+ `Proposal ID: ${lastProposal.proposal_id}`,
531
+ `Skill path: ${skillPath}`,
532
+ `Target: ${target}`,
533
+ `Confidence: ${lastProposal.confidence}`,
534
+ `Validation: ${lastValidation.gates_passed}/${lastValidation.gates_total} gates passed`,
535
+ `Regressions: ${lastValidation.regressions.length > 0 ? lastValidation.regressions.join(", ") : "none"}`,
536
+ ``,
537
+ `Original content:`,
538
+ lastProposal.original_body,
539
+ ``,
540
+ `Proposed content:`,
541
+ lastProposal.proposed_body,
542
+ ``,
543
+ `Rationale: ${lastProposal.rationale}`,
544
+ ].join("\n");
545
+
546
+ const reviewOutput = await _callViaSubagent({
547
+ agentName: "evolution-reviewer",
548
+ prompt: reviewPrompt,
549
+ maxTurns: 8,
550
+ allowedTools: ["Read", "Grep", "Glob", "Bash"],
551
+ });
552
+
553
+ const isRejected = /\bREJECT\b/.test(reviewOutput) && !/\bAPPROVE\b/.test(reviewOutput);
554
+ recordAudit(
555
+ lastProposal.proposal_id,
556
+ isRejected ? "rejected" : "validated",
557
+ `Evolution reviewer: ${isRejected ? "REJECTED" : "APPROVED"}`,
558
+ );
559
+
560
+ if (isRejected) {
561
+ return {
562
+ proposal: lastProposal,
563
+ validation: lastValidation,
564
+ deployed: false,
565
+ auditEntries,
566
+ reason: `Evolution reviewer rejected proposal: ${reviewOutput.slice(0, 500)}`,
567
+ };
568
+ }
569
+ } catch (reviewError) {
570
+ // Fail-open: if reviewer crashes, log it and continue to deploy
571
+ const msg = reviewError instanceof Error ? reviewError.message : String(reviewError);
572
+ recordAudit(
573
+ lastProposal.proposal_id,
574
+ "validated",
575
+ `Evolution reviewer failed (fail-open): ${msg}`,
576
+ );
577
+ }
578
+ }
579
+
580
+ // Step 6: Deploy or dry-run
500
581
  if (dryRun) {
501
582
  return {
502
583
  proposal: lastProposal,
@@ -594,6 +675,8 @@ export async function cliMain(): Promise<void> {
594
675
  "task-description": { type: "string" },
595
676
  "few-shot": { type: "string" },
596
677
  "validation-model": { type: "string" },
678
+ "teacher-effort": { type: "string", default: "high" },
679
+ review: { type: "boolean", default: false },
597
680
  help: { type: "boolean", default: false },
598
681
  },
599
682
  strict: true,
@@ -611,8 +694,8 @@ Options:
611
694
  --target Evolution target: body, routing (default: body)
612
695
  --teacher-agent Teacher agent CLI (claude, codex, etc.)
613
696
  --student-agent Student agent CLI for validation
614
- --teacher-model Model flag for teacher agent
615
- --student-model Model flag for student agent
697
+ --teacher-model Model flag for teacher agent (default: opus)
698
+ --student-model Model flag for student agent (default: haiku)
616
699
  --eval-set Path to eval set JSON
617
700
  --dry-run Validate without deploying
618
701
  --max-iterations Max refinement iterations (default: 3)
@@ -620,6 +703,8 @@ Options:
620
703
  --task-description Optional task description context
621
704
  --few-shot Comma-separated paths to example skill files
622
705
  --validation-model Model for trigger-check validation calls (overrides --student-model for validation)
706
+ --teacher-effort Effort level for teacher LLM: low, medium, high, max (default: high)
707
+ --review Run evolution-reviewer subagent before deployment (Gate 4)
623
708
  --help Show this help message`);
624
709
  process.exit(0);
625
710
  }
@@ -669,6 +754,8 @@ Options:
669
754
  fewShotExamples,
670
755
  gradingResults,
671
756
  validationModel: values["validation-model"],
757
+ teacherEffort: (values["teacher-effort"] as EffortLevel) ?? "high",
758
+ useReviewer: values.review ?? false,
672
759
  });
673
760
 
674
761
  console.log(JSON.stringify(result, null, 2));
@@ -37,7 +37,6 @@ import type {
37
37
  SkillUsageRecord,
38
38
  } from "../types.js";
39
39
  import { parseFrontmatter, replaceFrontmatterDescription } from "../utils/frontmatter.js";
40
-
41
40
  import { createEvolveTUI } from "../utils/tui.js";
42
41
  import { appendAuditEntry } from "./audit.js";
43
42
  import { checkConstitution } from "./constitutional.js";
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { BodyEvolutionProposal, EvolutionTarget, FailurePattern } from "../types.js";
10
- import { callLlm, stripMarkdownFences } from "../utils/llm-call.js";
10
+ import { type EffortLevel, callLlm, stripMarkdownFences } from "../utils/llm-call.js";
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // System prompt
@@ -160,6 +160,7 @@ export async function generateBodyProposal(
160
160
  modelFlag?: string,
161
161
  fewShotExamples?: string[],
162
162
  executionContext?: ExecutionContext,
163
+ effort?: EffortLevel,
163
164
  ): Promise<BodyEvolutionProposal> {
164
165
  const prompt = buildBodyGenerationPrompt(
165
166
  currentContent,
@@ -169,7 +170,7 @@ export async function generateBodyProposal(
169
170
  fewShotExamples,
170
171
  executionContext,
171
172
  );
172
- const rawResponse = await callLlm(BODY_GENERATOR_SYSTEM, prompt, agent, modelFlag);
173
+ const rawResponse = await callLlm(BODY_GENERATOR_SYSTEM, prompt, agent, modelFlag, effort);
173
174
  const { proposed_body, rationale, confidence } = parseBodyProposalResponse(rawResponse);
174
175
 
175
176
  return {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { BodyEvolutionProposal, EvolutionTarget, FailurePattern } from "../types.js";
9
- import { callLlm, stripMarkdownFences } from "../utils/llm-call.js";
9
+ import { type EffortLevel, callLlm, stripMarkdownFences } from "../utils/llm-call.js";
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
12
  // System prompt
@@ -139,6 +139,7 @@ export async function generateRoutingProposal(
139
139
  skillPath: string,
140
140
  agent: string,
141
141
  modelFlag?: string,
142
+ effort?: EffortLevel,
142
143
  ): Promise<BodyEvolutionProposal> {
143
144
  const prompt = buildRoutingProposalPrompt(
144
145
  currentRouting,
@@ -147,7 +148,7 @@ export async function generateRoutingProposal(
147
148
  missedQueries,
148
149
  skillName,
149
150
  );
150
- const rawResponse = await callLlm(ROUTING_PROPOSER_SYSTEM, prompt, agent, modelFlag);
151
+ const rawResponse = await callLlm(ROUTING_PROPOSER_SYSTEM, prompt, agent, modelFlag, effort);
151
152
  const { proposed_routing, rationale, confidence } = parseRoutingProposalResponse(rawResponse);
152
153
 
153
154
  return {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { BodyEvolutionProposal, BodyValidationResult } from "../types.js";
9
- import { callLlm, stripMarkdownFences } from "../utils/llm-call.js";
9
+ import { type EffortLevel, callLlm, stripMarkdownFences } from "../utils/llm-call.js";
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
12
  // System prompt
@@ -118,6 +118,7 @@ export async function refineBodyProposal(
118
118
  validationResult: BodyValidationResult,
119
119
  agent: string,
120
120
  modelFlag?: string,
121
+ effort?: EffortLevel,
121
122
  ): Promise<BodyEvolutionProposal> {
122
123
  const prompt = buildRefinementPrompt(
123
124
  proposal.proposed_body,
@@ -126,7 +127,7 @@ export async function refineBodyProposal(
126
127
  validationResult.regressions,
127
128
  );
128
129
 
129
- const rawResponse = await callLlm(BODY_REFINER_SYSTEM, prompt, agent, modelFlag);
130
+ const rawResponse = await callLlm(BODY_REFINER_SYSTEM, prompt, agent, modelFlag, effort);
130
131
  const { refined_body, changes_made, confidence } = parseRefinementResponse(rawResponse);
131
132
 
132
133
  return {
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { mkdirSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
+
8
9
  import { getDb } from "./localdb/db.js";
9
10
  import {
10
11
  getOrchestrateRuns,
@@ -884,6 +884,14 @@ Options:
884
884
  }
885
885
  writeFileSync(outputPath, JSON.stringify(result, null, 2), "utf-8");
886
886
 
887
+ // Persist to SQLite for upload staging (fail-open)
888
+ try {
889
+ const { writeGradingResultToDb } = await import("../localdb/direct-write.js");
890
+ writeGradingResultToDb(result);
891
+ } catch {
892
+ // fail-open: grading file is already written above
893
+ }
894
+
887
895
  printSummary(result);
888
896
  console.log(`\nWrote ${outputPath}`);
889
897
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { dirname } from "node:path";
14
+
14
15
  import {
15
16
  CLAUDE_SETTINGS_PATH,
16
17
  EVOLUTION_AUDIT_LOG,
@@ -16,8 +16,8 @@
16
16
 
17
17
  import { existsSync, readFileSync } from "node:fs";
18
18
  import { basename, dirname, join } from "node:path";
19
- import { EVOLUTION_AUDIT_LOG, SELFTUNE_CONFIG_DIR } from "../constants.js";
20
19
 
20
+ import { EVOLUTION_AUDIT_LOG, SELFTUNE_CONFIG_DIR } from "../constants.js";
21
21
  import type { PreToolUsePayload } from "../types.js";
22
22
  import { readJsonl } from "../utils/jsonl.js";
23
23
 
@@ -11,6 +11,7 @@
11
11
  import { readdirSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
+
14
15
  import { CANONICAL_LOG, QUERY_LOG, SKIP_PREFIXES } from "../constants.js";
15
16
  import {
16
17
  appendCanonicalRecord,
@@ -9,9 +9,9 @@
9
9
  */
10
10
 
11
11
  import { execSync } from "node:child_process";
12
- import { closeSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
13
- import { CANONICAL_LOG, ORCHESTRATE_LOCK, TELEMETRY_LOG } from "../constants.js";
12
+ import { readFileSync } from "node:fs";
14
13
 
14
+ import { CANONICAL_LOG, ORCHESTRATE_LOCK, TELEMETRY_LOG } from "../constants.js";
15
15
  import {
16
16
  appendCanonicalRecords,
17
17
  buildCanonicalExecutionFact,
@@ -25,6 +25,22 @@ import { parseTranscript } from "../utils/transcript.js";
25
25
 
26
26
  const LOCK_STALE_MS = 30 * 60 * 1000;
27
27
 
28
+ interface ReactiveSpawnDeps {
29
+ spawnOrchestrate?: () => boolean;
30
+ }
31
+
32
+ function hasFreshOrchestrateLock(lockPath: string): boolean {
33
+ try {
34
+ const lockContent = readFileSync(lockPath, "utf8");
35
+ const lock = JSON.parse(lockContent) as { timestamp?: string };
36
+ if (typeof lock.timestamp !== "string") return false;
37
+ const lockAge = Date.now() - new Date(lock.timestamp).getTime();
38
+ return Number.isFinite(lockAge) && lockAge < LOCK_STALE_MS;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
28
44
  /**
29
45
  * Check for pending improvement signals and spawn a focused orchestrate run
30
46
  * in the background if warranted. Fire-and-forget — the hook exits immediately.
@@ -33,6 +49,7 @@ const LOCK_STALE_MS = 30 * 60 * 1000;
33
49
  */
34
50
  export async function maybeSpawnReactiveOrchestrate(
35
51
  lockPath: string = ORCHESTRATE_LOCK,
52
+ deps: ReactiveSpawnDeps = {},
36
53
  ): Promise<boolean> {
37
54
  try {
38
55
  // Read pending signals from SQLite (dynamic import to reduce hook startup cost)
@@ -42,48 +59,25 @@ export async function maybeSpawnReactiveOrchestrate(
42
59
  const pending = queryImprovementSignals(db, false);
43
60
  if (pending.length === 0) return false;
44
61
 
45
- // Atomically claim the lock openSync with "wx" fails if file exists
46
- let fd: number;
47
- try {
48
- fd = openSync(lockPath, "wx");
49
- writeFileSync(fd, JSON.stringify({ timestamp: new Date().toISOString(), pid: process.pid }));
50
- closeSync(fd);
51
- } catch (lockErr: unknown) {
52
- // Lock exists — check if stale
53
- if ((lockErr as NodeJS.ErrnoException).code === "EEXIST") {
54
- try {
55
- const lockContent = readFileSync(lockPath, "utf8");
56
- const lock = JSON.parse(lockContent);
57
- const lockAge = Date.now() - new Date(lock.timestamp).getTime();
58
- if (lockAge < LOCK_STALE_MS) return false; // Active lock, skip
59
- // Stale lock — override
60
- writeFileSync(
61
- lockPath,
62
- JSON.stringify({ timestamp: new Date().toISOString(), pid: process.pid }),
63
- );
64
- } catch {
65
- return false; // Can't read lock, skip
66
- }
67
- } else {
68
- return false;
69
- }
70
- }
62
+ // Do not pre-claim the orchestrate lock here. The spawned process must
63
+ // acquire its own lock or it will immediately self-block on startup.
64
+ if (hasFreshOrchestrateLock(lockPath)) return false;
71
65
 
72
66
  // Spawn orchestrate in background (fire-and-forget)
73
67
  try {
74
- const proc = Bun.spawn(["selftune", "orchestrate", "--max-skills", "2"], {
75
- stdout: "ignore",
76
- stderr: "ignore",
77
- stdin: "ignore",
78
- });
79
- proc.unref();
68
+ const spawnOrchestrate =
69
+ deps.spawnOrchestrate ??
70
+ (() => {
71
+ const proc = Bun.spawn(["selftune", "orchestrate", "--max-skills", "2"], {
72
+ stdout: "ignore",
73
+ stderr: "ignore",
74
+ stdin: "ignore",
75
+ });
76
+ proc.unref();
77
+ return true;
78
+ });
79
+ if (!spawnOrchestrate()) return false;
80
80
  } catch {
81
- // Spawn failed — release our lock
82
- try {
83
- unlinkSync(lockPath);
84
- } catch {
85
- /* ignore */
86
- }
87
81
  return false;
88
82
  }
89
83
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { basename, dirname } from "node:path";
15
+
15
16
  import { SESSION_STATE_DIR } from "../constants.js";
16
17
  import type { PreToolUsePayload } from "../types.js";
17
18
 
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { basename, dirname } from "node:path";
16
+
16
17
  import { CANONICAL_LOG, SKILL_LOG } from "../constants.js";
17
18
  import {
18
19
  appendCanonicalRecord,
@@ -24,7 +25,6 @@ import {
24
25
  getLatestPromptIdentity,
25
26
  } from "../normalization.js";
26
27
  import type { PostToolUsePayload, SkillUsageRecord } from "../types.js";
27
-
28
28
  import { classifySkillPath } from "../utils/skill-discovery.js";
29
29
  import { getLastUserMessage } from "../utils/transcript.js";
30
30
 
@@ -10,6 +10,7 @@
10
10
  * selftune sync — Sync source-truth telemetry across supported agents
11
11
  * selftune orchestrate — Run autonomous core loop (sync → status → evolve → watch)
12
12
  * selftune init — Initialize agent identity and config
13
+ * selftune uninstall — Clean removal of all selftune data and config
13
14
  * selftune status — Show skill health summary
14
15
  * selftune watch — Monitor post-deploy skill health
15
16
  * selftune doctor — Run health checks
@@ -44,6 +45,7 @@ Commands:
44
45
  sync Sync source-truth telemetry across supported agents
45
46
  orchestrate Run autonomous core loop (sync → status → evolve → watch)
46
47
  init Initialize agent identity and config
48
+ uninstall Clean removal of all selftune data and config
47
49
  status Show skill health summary
48
50
  watch Monitor post-deploy skill health
49
51
  doctor Run health checks
@@ -338,6 +340,11 @@ Run 'selftune eval <action> --help' for action-specific options.`);
338
340
  await cliMain();
339
341
  break;
340
342
  }
343
+ case "uninstall": {
344
+ const { cliMain } = await import("./uninstall.js");
345
+ await cliMain();
346
+ break;
347
+ }
341
348
  case "contribute": {
342
349
  const { cliMain } = await import("./contribute/contribute.js");
343
350
  await cliMain();
@@ -464,7 +471,7 @@ Run 'selftune cron <subcommand> --help' for subcommand-specific options.`);
464
471
  }
465
472
  case "sync": {
466
473
  const { cliMain } = await import("./sync.js");
467
- cliMain();
474
+ await cliMain();
468
475
  break;
469
476
  }
470
477
  case "workflows": {
@@ -606,9 +613,8 @@ Output:
606
613
  const { readAlphaIdentity } = await import("./alpha-identity.js");
607
614
  const { getDb } = await import("./localdb/db.js");
608
615
  const { runUploadCycle } = await import("./alpha-upload/index.js");
609
- const { getSelftuneVersion, readConfiguredAgentType } = await import(
610
- "./utils/selftune-meta.js"
611
- );
616
+ const { getSelftuneVersion, readConfiguredAgentType } =
617
+ await import("./utils/selftune-meta.js");
612
618
 
613
619
  const identity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
614
620
  if (!identity?.enrolled) {
@@ -670,36 +676,39 @@ Output:
670
676
  }
671
677
  case "relink": {
672
678
  const { SELFTUNE_CONFIG_PATH } = await import("./constants.js");
673
- const { readAlphaIdentity, writeAlphaIdentity, generateUserId } = await import(
674
- "./alpha-identity.js"
675
- );
676
- const { requestDeviceCode, pollDeviceCode } = await import("./auth/device-code.js");
679
+ const { readAlphaIdentity, writeAlphaIdentity, generateUserId } =
680
+ await import("./alpha-identity.js");
681
+ const { buildVerificationUrl, pollDeviceCode, requestDeviceCode, tryOpenUrl } =
682
+ await import("./auth/device-code.js");
677
683
  const { chmodSync } = await import("node:fs");
678
684
 
679
685
  const existingIdentity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
680
686
  process.stderr.write("[alpha relink] Starting device-code authentication flow...\n");
681
687
 
682
688
  const grant = await requestDeviceCode();
689
+ const verificationUrlWithCode = buildVerificationUrl(
690
+ grant.verification_url,
691
+ grant.user_code,
692
+ );
683
693
 
684
694
  console.log(
685
695
  JSON.stringify({
686
696
  level: "info",
687
697
  code: "device_code_issued",
688
698
  verification_url: grant.verification_url,
699
+ verification_url_with_code: verificationUrlWithCode,
689
700
  user_code: grant.user_code,
690
701
  expires_in: grant.expires_in,
691
- message: `Open ${grant.verification_url} and enter code: ${grant.user_code}`,
702
+ message: `Open ${verificationUrlWithCode} to approve.`,
692
703
  }),
693
704
  );
694
705
 
695
706
  // Try to open browser
696
- try {
697
- const url = `${grant.verification_url}?code=${grant.user_code}`;
698
- Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
707
+ if (tryOpenUrl(verificationUrlWithCode)) {
699
708
  process.stderr.write("[alpha relink] Browser opened. Waiting for approval...\n");
700
- } catch {
709
+ } else {
701
710
  process.stderr.write(
702
- "[alpha relink] Could not open browser. Visit the URL above manually.\n",
711
+ `[alpha relink] Could not open browser. Visit ${verificationUrlWithCode} manually.\n`,
703
712
  );
704
713
  }
705
714
 
@@ -24,6 +24,7 @@
24
24
  import { statSync } from "node:fs";
25
25
  import { basename } from "node:path";
26
26
  import { parseArgs } from "node:util";
27
+
27
28
  import {
28
29
  CANONICAL_LOG,
29
30
  CLAUDE_CODE_MARKER,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
30
  import {
30
31
  appendCanonicalRecords,
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { homedir } from "node:os";
21
21
  import { join } from "node:path";
22
+
22
23
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
23
24
  import {
24
25
  appendCanonicalRecords,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import {
29
30
  CANONICAL_LOG,
30
31
  OPENCLAW_AGENTS_DIR,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
30
  import {
30
31
  appendCanonicalRecords,