selftune 0.2.30 → 0.2.32
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 +83 -56
- package/apps/local-dashboard/dist/assets/index-B-ut4w0B.js +15 -0
- package/apps/local-dashboard/dist/assets/index-BFGfCVrL.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-DfowE3Hu.js +1 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/command-surface.ts +613 -2
- package/cli/selftune/create/baseline.ts +429 -0
- package/cli/selftune/create/check.ts +35 -0
- package/cli/selftune/create/init.ts +115 -0
- package/cli/selftune/create/package-candidate-state.ts +771 -0
- package/cli/selftune/create/package-evaluator.ts +710 -0
- package/cli/selftune/create/package-fingerprint.ts +142 -0
- package/cli/selftune/create/package-search.ts +377 -0
- package/cli/selftune/create/publish.ts +431 -0
- package/cli/selftune/create/readiness.ts +495 -0
- package/cli/selftune/create/replay.ts +330 -0
- package/cli/selftune/create/report.ts +74 -0
- package/cli/selftune/create/scaffold.ts +121 -0
- package/cli/selftune/create/skills-ref-adapter.ts +177 -0
- package/cli/selftune/create/status.ts +33 -0
- package/cli/selftune/create/templates.ts +249 -0
- package/cli/selftune/cron/setup.ts +1 -1
- package/cli/selftune/dashboard-action-events.ts +4 -1
- package/cli/selftune/dashboard-action-result.ts +789 -24
- package/cli/selftune/dashboard-action-stream.ts +80 -0
- package/cli/selftune/dashboard-contract.ts +146 -3
- package/cli/selftune/dashboard-server.ts +5 -4
- package/cli/selftune/eval/hooks-to-evals.ts +58 -35
- package/cli/selftune/eval/synthetic-evals.ts +145 -17
- package/cli/selftune/evolution/bounded-mutations.ts +1045 -0
- package/cli/selftune/evolution/evolve-body.ts +9 -36
- package/cli/selftune/evolution/evolve.ts +8 -72
- package/cli/selftune/evolution/stopping-criteria.ts +5 -13
- package/cli/selftune/evolution/unblock-suggestions.ts +0 -16
- package/cli/selftune/evolution/validate-host-replay.ts +115 -15
- package/cli/selftune/improve.ts +206 -0
- package/cli/selftune/index.ts +123 -6
- package/cli/selftune/init.ts +1 -1
- package/cli/selftune/localdb/queries/dashboard.ts +30 -0
- package/cli/selftune/localdb/schema.ts +52 -0
- package/cli/selftune/monitoring/watch.ts +257 -23
- package/cli/selftune/orchestrate/execute.ts +300 -1
- package/cli/selftune/orchestrate/finalize.ts +14 -0
- package/cli/selftune/orchestrate/plan.ts +22 -5
- package/cli/selftune/orchestrate/prepare.ts +59 -4
- package/cli/selftune/orchestrate/report.ts +1 -1
- package/cli/selftune/orchestrate.ts +34 -1
- package/cli/selftune/publish.ts +35 -0
- package/cli/selftune/registry/github-install.ts +256 -0
- package/cli/selftune/registry/index.ts +1 -1
- package/cli/selftune/registry/install.ts +58 -7
- package/cli/selftune/routes/actions.ts +81 -15
- package/cli/selftune/routes/overview.ts +1 -1
- package/cli/selftune/routes/skill-report.ts +147 -2
- package/cli/selftune/run.ts +18 -0
- package/cli/selftune/schedule.ts +3 -3
- package/cli/selftune/search-run.ts +703 -0
- package/cli/selftune/status.ts +35 -11
- package/cli/selftune/testing-readiness.ts +431 -40
- package/cli/selftune/types.ts +316 -0
- package/cli/selftune/utils/eval-readiness.ts +1 -0
- package/cli/selftune/utils/json-output.ts +11 -0
- package/cli/selftune/utils/lifecycle-surface.ts +48 -0
- package/cli/selftune/utils/query-filter.ts +82 -1
- package/cli/selftune/utils/tui.ts +85 -2
- package/cli/selftune/verify.ts +205 -0
- package/cli/selftune/workflows/proposals.ts +1 -1
- package/cli/selftune/workflows/skill-scaffold.ts +141 -63
- package/cli/selftune/workflows/workflows.ts +4 -4
- package/package.json +1 -1
- package/packages/dashboard-core/src/routes/manifest.ts +2 -2
- package/packages/ui/src/components/SkillReportPanels.tsx +7 -7
- package/packages/ui/src/primitives/button.tsx +5 -0
- package/skill/SKILL.md +148 -85
- package/skill/references/cli-quick-reference.md +16 -1
- package/skill/references/creator-playbook.md +31 -10
- package/skill/workflows/Baseline.md +8 -9
- package/skill/workflows/Contributions.md +4 -4
- package/skill/workflows/Create.md +173 -0
- package/skill/workflows/CreateTestDeploy.md +34 -30
- package/skill/workflows/Cron.md +2 -2
- package/skill/workflows/Dashboard.md +3 -3
- package/skill/workflows/Evals.md +13 -7
- package/skill/workflows/Evolve.md +75 -32
- package/skill/workflows/EvolveBody.md +22 -15
- package/skill/workflows/Hook.md +1 -1
- package/skill/workflows/Improve.md +168 -0
- package/skill/workflows/Initialize.md +3 -3
- package/skill/workflows/Orchestrate.md +49 -12
- package/skill/workflows/Publish.md +100 -0
- package/skill/workflows/Registry.md +19 -13
- package/skill/workflows/Run.md +72 -0
- package/skill/workflows/Schedule.md +2 -2
- package/skill/workflows/SearchRun.md +89 -0
- package/skill/workflows/SignalsDashboard.md +2 -2
- package/skill/workflows/UnitTest.md +13 -4
- package/skill/workflows/Verify.md +136 -0
- package/skill/workflows/Watch.md +114 -47
- package/skill/workflows/Workflows.md +13 -8
- package/apps/local-dashboard/dist/assets/index-BcXquWFB.css +0 -1
- package/apps/local-dashboard/dist/assets/index-Coq42hE4.js +0 -15
- package/apps/local-dashboard/dist/assets/vendor-ui-B0H8s1mP.js +0 -1
|
@@ -16,6 +16,8 @@ interface OrchestrateFinalTotals {
|
|
|
16
16
|
watched: number;
|
|
17
17
|
skipped: number;
|
|
18
18
|
autoGraded: number;
|
|
19
|
+
packageSearched: number;
|
|
20
|
+
packageImproved: number;
|
|
19
21
|
freshlyWatchedSkills: string[];
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -27,6 +29,8 @@ export interface FinalizeOrchestrateRunInput {
|
|
|
27
29
|
dryRun: boolean;
|
|
28
30
|
approvalMode: "auto" | "review";
|
|
29
31
|
autoGradedCount: number;
|
|
32
|
+
packageSearched: number;
|
|
33
|
+
packageImproved: number;
|
|
30
34
|
freshlyWatchedSkills: string[];
|
|
31
35
|
pendingSignals: ImprovementSignalRecord[];
|
|
32
36
|
elapsedMs: number;
|
|
@@ -36,6 +40,8 @@ function buildFinalTotals(
|
|
|
36
40
|
skills: SkillStatus[],
|
|
37
41
|
candidates: SkillAction[],
|
|
38
42
|
autoGradedCount: number,
|
|
43
|
+
packageSearched: number,
|
|
44
|
+
packageImproved: number,
|
|
39
45
|
freshlyWatchedSkills: string[],
|
|
40
46
|
): OrchestrateFinalTotals {
|
|
41
47
|
return {
|
|
@@ -50,6 +56,8 @@ function buildFinalTotals(
|
|
|
50
56
|
freshlyWatchedSkills.length,
|
|
51
57
|
skipped: candidates.filter((candidate) => candidate.action === "skip").length,
|
|
52
58
|
autoGraded: autoGradedCount,
|
|
59
|
+
packageSearched,
|
|
60
|
+
packageImproved,
|
|
53
61
|
freshlyWatchedSkills,
|
|
54
62
|
};
|
|
55
63
|
}
|
|
@@ -63,6 +71,8 @@ export function finalizeOrchestrateRun(input: FinalizeOrchestrateRunInput): Orch
|
|
|
63
71
|
dryRun,
|
|
64
72
|
approvalMode,
|
|
65
73
|
autoGradedCount,
|
|
74
|
+
packageSearched,
|
|
75
|
+
packageImproved,
|
|
66
76
|
freshlyWatchedSkills,
|
|
67
77
|
pendingSignals,
|
|
68
78
|
elapsedMs,
|
|
@@ -72,6 +82,8 @@ export function finalizeOrchestrateRun(input: FinalizeOrchestrateRunInput): Orch
|
|
|
72
82
|
statusResult.skills,
|
|
73
83
|
candidates,
|
|
74
84
|
autoGradedCount,
|
|
85
|
+
packageSearched,
|
|
86
|
+
packageImproved,
|
|
75
87
|
freshlyWatchedSkills,
|
|
76
88
|
);
|
|
77
89
|
|
|
@@ -106,6 +118,8 @@ export function finalizeOrchestrateRun(input: FinalizeOrchestrateRunInput): Orch
|
|
|
106
118
|
watched: finalTotals.watched,
|
|
107
119
|
skipped: finalTotals.skipped,
|
|
108
120
|
auto_graded: finalTotals.autoGraded,
|
|
121
|
+
package_searched: finalTotals.packageSearched,
|
|
122
|
+
package_improved: finalTotals.packageImproved,
|
|
109
123
|
skill_actions: candidates.map(
|
|
110
124
|
(candidate): OrchestrateRunSkillAction => ({
|
|
111
125
|
skill: candidate.skill,
|
|
@@ -2,6 +2,22 @@ import type { CandidateContext, SkillAction } from "../orchestrate.js";
|
|
|
2
2
|
import type { SkillStatus } from "../status.js";
|
|
3
3
|
import type { EvolutionAuditEntry } from "../types.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Determines whether a skill should use package search instead of standard
|
|
7
|
+
* evolution. Returns true when the skill has package-level evidence:
|
|
8
|
+
* a frontier candidate exists or a canonical evaluation record is present.
|
|
9
|
+
*
|
|
10
|
+
* This gates the package-search path so only skills with sufficient
|
|
11
|
+
* package-level signal enter the bounded search flow.
|
|
12
|
+
*/
|
|
13
|
+
export function shouldSelectPackageSearch(skill: SkillStatus, context: CandidateContext): boolean {
|
|
14
|
+
// Package search requires that the candidate context carries a
|
|
15
|
+
// packageFrontierSkills set (populated from package-candidate-state).
|
|
16
|
+
// When present and the skill is listed, we route through package search.
|
|
17
|
+
if (!context.packageFrontierSkills) return false;
|
|
18
|
+
return context.packageFrontierSkills.has(skill.name);
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
/** Candidate selection criteria. */
|
|
6
22
|
const CANDIDATE_STATUSES = new Set(["CRITICAL", "WARNING", "UNGRADED"]);
|
|
7
23
|
|
|
@@ -109,18 +125,19 @@ export function selectCandidates(skills: SkillStatus[], options: CandidateContex
|
|
|
109
125
|
continue;
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
const action = shouldSelectPackageSearch(skill, options) ? "package-search" : "evolve";
|
|
112
129
|
actions.push({
|
|
113
130
|
skill: skill.name,
|
|
114
|
-
action
|
|
131
|
+
action,
|
|
115
132
|
reason: `status=${skill.status}, passRate=${skill.passRate !== null ? `${(skill.passRate * 100).toFixed(0)}%` : "—"}, missed=${skill.missedQueries}, trend=${skill.trend}`,
|
|
116
133
|
});
|
|
117
134
|
}
|
|
118
135
|
|
|
119
|
-
let
|
|
136
|
+
let activeCount = 0;
|
|
120
137
|
for (const action of actions) {
|
|
121
|
-
if (action.action === "evolve") {
|
|
122
|
-
|
|
123
|
-
if (
|
|
138
|
+
if (action.action === "evolve" || action.action === "package-search") {
|
|
139
|
+
activeCount++;
|
|
140
|
+
if (activeCount > options.maxSkills) {
|
|
124
141
|
action.action = "skip";
|
|
125
142
|
action.reason = `capped by --max-skills ${options.maxSkills}`;
|
|
126
143
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
buildDefaultGradingOutputPath,
|
|
@@ -7,8 +7,11 @@ import {
|
|
|
7
7
|
gradeSession,
|
|
8
8
|
resolveLatestSessionForSkill,
|
|
9
9
|
} from "../grading/grade-session.js";
|
|
10
|
+
import { selectAcceptedPackageFrontierCandidate } from "../create/package-candidate-state.js";
|
|
10
11
|
import { writeGradingResultToDb } from "../localdb/direct-write.js";
|
|
11
12
|
import { createDefaultSyncOptions } from "../sync.js";
|
|
13
|
+
import { getDb } from "../localdb/db.js";
|
|
14
|
+
import { readCanonicalPackageEvaluationArtifact } from "../testing-readiness.js";
|
|
12
15
|
import type {
|
|
13
16
|
ImprovementSignalRecord,
|
|
14
17
|
QueryLogRecord,
|
|
@@ -17,7 +20,7 @@ import type {
|
|
|
17
20
|
} from "../types.js";
|
|
18
21
|
import { readExcerpt } from "../utils/transcript.js";
|
|
19
22
|
import type { OrchestrateOptions, SkillAction } from "../orchestrate.js";
|
|
20
|
-
import { selectCandidates } from "./plan.js";
|
|
23
|
+
import { MIN_CANDIDATE_EVIDENCE, selectCandidates } from "./plan.js";
|
|
21
24
|
import { groupSignalsBySkill, readPendingSignals } from "./signals.js";
|
|
22
25
|
import type { ResolvedOrchestrateRuntime } from "./runtime.js";
|
|
23
26
|
|
|
@@ -33,6 +36,50 @@ export interface PreparedOrchestrateRun {
|
|
|
33
36
|
autoGradedCount: number;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
export function collectPackageSearchEligibleSkills(
|
|
40
|
+
skillNames: string[],
|
|
41
|
+
options?: {
|
|
42
|
+
db?: import("bun:sqlite").Database;
|
|
43
|
+
resolveSkillPath?: (skillName: string) => string | undefined;
|
|
44
|
+
},
|
|
45
|
+
): Set<string> {
|
|
46
|
+
const eligible = new Set<string>();
|
|
47
|
+
|
|
48
|
+
for (const skillName of skillNames) {
|
|
49
|
+
if (
|
|
50
|
+
selectAcceptedPackageFrontierCandidate(skillName) != null ||
|
|
51
|
+
readCanonicalPackageEvaluationArtifact(skillName) != null
|
|
52
|
+
) {
|
|
53
|
+
eligible.add(skillName);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Second tier: skills with a draft package and sufficient grading evidence
|
|
58
|
+
if (!options?.db || !options?.resolveSkillPath) continue;
|
|
59
|
+
|
|
60
|
+
const skillPath = options.resolveSkillPath(skillName);
|
|
61
|
+
if (!skillPath) continue;
|
|
62
|
+
|
|
63
|
+
const hasDraft = existsSync(join(dirname(skillPath), "selftune.create.json"));
|
|
64
|
+
if (!hasDraft) continue;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const row = options.db
|
|
68
|
+
.query<{ count: number }, [string]>(
|
|
69
|
+
"SELECT COUNT(*) as count FROM grading_results WHERE skill_name = ?",
|
|
70
|
+
)
|
|
71
|
+
.get(skillName);
|
|
72
|
+
if (row && row.count >= MIN_CANDIDATE_EVIDENCE) {
|
|
73
|
+
eligible.add(skillName);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Fail-open: table may not exist yet
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return eligible;
|
|
81
|
+
}
|
|
82
|
+
|
|
36
83
|
/**
|
|
37
84
|
* Detects significant overlap between the positive eval sets of evolution
|
|
38
85
|
* candidates. When two skills share >30% of their positive queries, it
|
|
@@ -270,6 +317,10 @@ export async function prepareOrchestrateRun(
|
|
|
270
317
|
|
|
271
318
|
const pendingSignals = readPendingSignals(runtime.readSignals);
|
|
272
319
|
const signaledSkills = groupSignalsBySkill(pendingSignals);
|
|
320
|
+
const packageFrontierSkills = collectPackageSearchEligibleSkills(
|
|
321
|
+
statusResult.skills.map((skill) => skill.name),
|
|
322
|
+
{ db: getDb(), resolveSkillPath: runtime.resolveSkillPath },
|
|
323
|
+
);
|
|
273
324
|
if (signaledSkills.size > 0) {
|
|
274
325
|
console.error(
|
|
275
326
|
`[orchestrate] Improvement signals: ${pendingSignals.length} pending for ${signaledSkills.size} skill(s)`,
|
|
@@ -281,12 +332,16 @@ export async function prepareOrchestrateRun(
|
|
|
281
332
|
maxSkills: options.maxSkills,
|
|
282
333
|
auditEntries,
|
|
283
334
|
signaledSkills,
|
|
335
|
+
packageFrontierSkills,
|
|
284
336
|
});
|
|
285
337
|
|
|
286
338
|
const evolveCandidates = candidates.filter((candidate) => candidate.action === "evolve");
|
|
339
|
+
const packageSearchCount = candidates.filter(
|
|
340
|
+
(candidate) => candidate.action === "package-search",
|
|
341
|
+
).length;
|
|
287
342
|
const skipCount = candidates.filter((candidate) => candidate.action === "skip").length;
|
|
288
343
|
console.error(
|
|
289
|
-
`[orchestrate] Candidates: ${evolveCandidates.length} to evolve, ${skipCount} skipped`,
|
|
344
|
+
`[orchestrate] Candidates: ${evolveCandidates.length} to evolve, ${packageSearchCount} to package-search, ${skipCount} skipped`,
|
|
290
345
|
);
|
|
291
346
|
for (const candidate of candidates) {
|
|
292
347
|
console.error(
|
|
@@ -123,7 +123,7 @@ export function formatOrchestrateReport(result: OrchestrateResult): string {
|
|
|
123
123
|
const lines: string[] = [];
|
|
124
124
|
|
|
125
125
|
lines.push(separator);
|
|
126
|
-
lines.push("selftune
|
|
126
|
+
lines.push("selftune run — decision report");
|
|
127
127
|
lines.push(separator);
|
|
128
128
|
lines.push("");
|
|
129
129
|
|
|
@@ -24,8 +24,11 @@ import {
|
|
|
24
24
|
autoGradeFreshDeploys,
|
|
25
25
|
buildReplayValidationOptions,
|
|
26
26
|
runEvolutionPhase,
|
|
27
|
+
runPackageSearchPhase,
|
|
27
28
|
watchRecentDeploys,
|
|
28
29
|
} from "./orchestrate/execute.js";
|
|
30
|
+
export { runPackageSearchPhase } from "./orchestrate/execute.js";
|
|
31
|
+
export type { RunPackageSearchPhaseInput } from "./orchestrate/execute.js";
|
|
29
32
|
import { finalizeOrchestrateRun } from "./orchestrate/finalize.js";
|
|
30
33
|
import { acquireLock, releaseLock } from "./orchestrate/locks.js";
|
|
31
34
|
import { runPostOrchestrateSideEffects } from "./orchestrate/post-run.js";
|
|
@@ -67,6 +70,7 @@ export {
|
|
|
67
70
|
DEFAULT_COOLDOWN_HOURS,
|
|
68
71
|
MIN_CANDIDATE_EVIDENCE,
|
|
69
72
|
selectCandidates,
|
|
73
|
+
shouldSelectPackageSearch,
|
|
70
74
|
} from "./orchestrate/plan.js";
|
|
71
75
|
export { autoGradeTopUngraded, detectCrossSkillOverlap } from "./orchestrate/prepare.js";
|
|
72
76
|
export { formatOrchestrateReport } from "./orchestrate/report.js";
|
|
@@ -93,12 +97,20 @@ export interface OrchestrateOptions {
|
|
|
93
97
|
maxAutoGrade: number;
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
export interface PackageSearchResult {
|
|
101
|
+
searched: boolean;
|
|
102
|
+
winnerApplied: boolean;
|
|
103
|
+
candidateCount: number;
|
|
104
|
+
winnerCandidateId?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
export interface SkillAction {
|
|
97
108
|
skill: string;
|
|
98
|
-
action: "evolve" | "watch" | "skip";
|
|
109
|
+
action: "evolve" | "package-search" | "watch" | "skip";
|
|
99
110
|
reason: string;
|
|
100
111
|
evolveResult?: EvolveResult;
|
|
101
112
|
watchResult?: WatchResult;
|
|
113
|
+
packageSearchResult?: PackageSearchResult;
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
/** Context for candidate selection beyond simple status checks. */
|
|
@@ -110,6 +122,8 @@ export interface CandidateContext {
|
|
|
110
122
|
cooldownHours?: number;
|
|
111
123
|
/** Skill name (lowercase) to improvement signal count. */
|
|
112
124
|
signaledSkills?: Map<string, number>;
|
|
125
|
+
/** Skills with an accepted package frontier candidate (eligible for package search). */
|
|
126
|
+
packageFrontierSkills?: Set<string>;
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
export interface OrchestrateResult {
|
|
@@ -127,6 +141,8 @@ export interface OrchestrateResult {
|
|
|
127
141
|
watched: number;
|
|
128
142
|
skipped: number;
|
|
129
143
|
autoGraded: number;
|
|
144
|
+
packageSearched: number;
|
|
145
|
+
packageImproved: number;
|
|
130
146
|
freshlyWatchedSkills: string[];
|
|
131
147
|
dryRun: boolean;
|
|
132
148
|
approvalMode: "auto" | "review";
|
|
@@ -240,6 +256,8 @@ export async function orchestrate(
|
|
|
240
256
|
watched: 0,
|
|
241
257
|
skipped: 0,
|
|
242
258
|
autoGraded: 0,
|
|
259
|
+
packageSearched: 0,
|
|
260
|
+
packageImproved: 0,
|
|
243
261
|
freshlyWatchedSkills: [],
|
|
244
262
|
dryRun: options.dryRun,
|
|
245
263
|
approvalMode: options.approvalMode,
|
|
@@ -288,6 +306,19 @@ export async function orchestrate(
|
|
|
288
306
|
readSkillRecords: runtime.readSkillRecords,
|
|
289
307
|
});
|
|
290
308
|
|
|
309
|
+
// -------------------------------------------------------------------------
|
|
310
|
+
// Step 5c: Package search for candidates tagged with action "package-search"
|
|
311
|
+
// -------------------------------------------------------------------------
|
|
312
|
+
const packageSearchCandidates = candidates.filter(
|
|
313
|
+
(candidate) => candidate.action === "package-search",
|
|
314
|
+
);
|
|
315
|
+
const packageSearchImproved = await runPackageSearchPhase({
|
|
316
|
+
packageSearchCandidates,
|
|
317
|
+
dryRun: options.dryRun,
|
|
318
|
+
agent,
|
|
319
|
+
resolveSkillPath: runtime.resolveSkillPath,
|
|
320
|
+
});
|
|
321
|
+
|
|
291
322
|
// -------------------------------------------------------------------------
|
|
292
323
|
// Step 6: Watch recently evolved skills (including freshly deployed in this run)
|
|
293
324
|
// -------------------------------------------------------------------------
|
|
@@ -336,6 +367,8 @@ export async function orchestrate(
|
|
|
336
367
|
dryRun: options.dryRun,
|
|
337
368
|
approvalMode: options.approvalMode,
|
|
338
369
|
autoGradedCount,
|
|
370
|
+
packageSearched: packageSearchCandidates.length,
|
|
371
|
+
packageImproved: packageSearchImproved.length,
|
|
339
372
|
freshlyWatchedSkills,
|
|
340
373
|
pendingSignals,
|
|
341
374
|
elapsedMs: Date.now() - startTime,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PUBLIC_COMMAND_SURFACES, renderCommandHelp } from "./command-surface.js";
|
|
2
|
+
import { cliMain as createPublishCliMain } from "./create/publish.js";
|
|
3
|
+
import { CLIError, handleCLIError } from "./utils/cli-error.js";
|
|
4
|
+
|
|
5
|
+
export async function cliMain(): Promise<void> {
|
|
6
|
+
const rawArgs = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
9
|
+
console.log(renderCommandHelp(PUBLIC_COMMAND_SURFACES.publish));
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const hasWatch = rawArgs.includes("--watch") || rawArgs.some((arg) => arg.startsWith("--watch="));
|
|
14
|
+
const hasNoWatch = rawArgs.includes("--no-watch");
|
|
15
|
+
|
|
16
|
+
if (hasWatch && hasNoWatch) {
|
|
17
|
+
throw new CLIError(
|
|
18
|
+
"Use either --watch or --no-watch, not both.",
|
|
19
|
+
"INVALID_FLAG",
|
|
20
|
+
"selftune publish --skill-path <path> [--no-watch]",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const delegatedArgs = rawArgs.filter((arg) => arg !== "--no-watch");
|
|
25
|
+
if (!hasWatch && !hasNoWatch) {
|
|
26
|
+
delegatedArgs.push("--watch");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.argv = [process.argv[0], process.argv[1], ...delegatedArgs];
|
|
30
|
+
await createPublishCliMain();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (import.meta.main) {
|
|
34
|
+
cliMain().catch(handleCLIError);
|
|
35
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { cp, mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export interface GithubRegistryInstallTarget {
|
|
11
|
+
owner: string;
|
|
12
|
+
repo: string;
|
|
13
|
+
repoFullName: string;
|
|
14
|
+
ref: string | null;
|
|
15
|
+
skillPath: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeGithubSkillPath(skillPath: string): string {
|
|
19
|
+
const trimmed = skillPath.trim().replace(/\\/g, "/");
|
|
20
|
+
if (!trimmed || trimmed === ".") {
|
|
21
|
+
return ".";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const segments = trimmed.split("/").filter(Boolean);
|
|
25
|
+
if (segments.includes("..")) {
|
|
26
|
+
throw new Error("GitHub skill path must stay within the repository");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalized = path.posix.normalize(trimmed).replace(/^\/+|\/+$/g, "");
|
|
30
|
+
return normalized || ".";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseGithubRegistryInstallTarget(
|
|
34
|
+
rawTarget: string,
|
|
35
|
+
): GithubRegistryInstallTarget | null {
|
|
36
|
+
if (!rawTarget.startsWith("github:")) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const spec = rawTarget.slice("github:".length).trim();
|
|
41
|
+
if (!spec) {
|
|
42
|
+
throw new Error("GitHub install target must be github:owner/repo[@ref][//path]");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pathSeparatorIndex = spec.indexOf("//");
|
|
46
|
+
const repoWithMaybeRef = pathSeparatorIndex === -1 ? spec : spec.slice(0, pathSeparatorIndex);
|
|
47
|
+
const pathWithMaybeRef = pathSeparatorIndex === -1 ? null : spec.slice(pathSeparatorIndex + 2);
|
|
48
|
+
|
|
49
|
+
let ref: string | null = null;
|
|
50
|
+
let repoSpec = repoWithMaybeRef;
|
|
51
|
+
|
|
52
|
+
const repoRefIndex = repoWithMaybeRef.lastIndexOf("@");
|
|
53
|
+
if (repoRefIndex !== -1) {
|
|
54
|
+
repoSpec = repoWithMaybeRef.slice(0, repoRefIndex);
|
|
55
|
+
ref = repoWithMaybeRef.slice(repoRefIndex + 1) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let skillPath: string | null = null;
|
|
59
|
+
if (pathWithMaybeRef != null) {
|
|
60
|
+
const pathRefIndex = pathWithMaybeRef.lastIndexOf("@");
|
|
61
|
+
if (pathRefIndex !== -1) {
|
|
62
|
+
skillPath = pathWithMaybeRef.slice(0, pathRefIndex) || ".";
|
|
63
|
+
ref = pathWithMaybeRef.slice(pathRefIndex + 1) || ref;
|
|
64
|
+
} else {
|
|
65
|
+
skillPath = pathWithMaybeRef || ".";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const match = repoSpec.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
70
|
+
if (!match) {
|
|
71
|
+
throw new Error("GitHub install target must look like github:owner/repo[@ref][//path]");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
owner: match[1],
|
|
76
|
+
repo: match[2],
|
|
77
|
+
repoFullName: `${match[1]}/${match[2]}`,
|
|
78
|
+
ref,
|
|
79
|
+
skillPath: skillPath ? normalizeGithubSkillPath(skillPath) : null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isExcludedEntry(name: string): boolean {
|
|
84
|
+
return name === ".git" || name === "node_modules" || name === ".env" || name.startsWith(".env.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function discoverLocalSkillPaths(rootDir: string): Promise<string[]> {
|
|
88
|
+
async function walk(currentDir: string, basePath: string): Promise<string[]> {
|
|
89
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
90
|
+
const discovered: string[] = [];
|
|
91
|
+
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (isExcludedEntry(entry.name)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
98
|
+
const relativePath = basePath ? path.join(basePath, entry.name) : entry.name;
|
|
99
|
+
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
discovered.push(...(await walk(fullPath, relativePath)));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
106
|
+
discovered.push(basePath ? basePath.split(path.sep).join("/") : ".");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return discovered;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const discovered = await walk(rootDir, "");
|
|
114
|
+
return [...new Set(discovered)].sort((a, b) => a.localeCompare(b));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function resolveGithubSkillPath(
|
|
118
|
+
repoDir: string,
|
|
119
|
+
requestedSkillPath: string | null,
|
|
120
|
+
): Promise<{ skillPath: string; availablePaths: string[] }> {
|
|
121
|
+
const availablePaths = await discoverLocalSkillPaths(repoDir);
|
|
122
|
+
|
|
123
|
+
if (requestedSkillPath) {
|
|
124
|
+
const normalized = normalizeGithubSkillPath(requestedSkillPath);
|
|
125
|
+
const skillMdPath =
|
|
126
|
+
normalized === "."
|
|
127
|
+
? path.join(repoDir, "SKILL.md")
|
|
128
|
+
: path.join(repoDir, ...normalized.split("/"), "SKILL.md");
|
|
129
|
+
await stat(skillMdPath);
|
|
130
|
+
return { skillPath: normalized, availablePaths };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (availablePaths.length === 1) {
|
|
134
|
+
return { skillPath: availablePaths[0] ?? ".", availablePaths };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (availablePaths.length === 0) {
|
|
138
|
+
throw new Error("No SKILL.md found in the GitHub repository");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Multiple skills found in the GitHub repository. Choose one with github:owner/repo//path (available: ${availablePaths.join(", ")})`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function deriveGithubInstallSkillName(
|
|
147
|
+
frontmatterName: string,
|
|
148
|
+
skillPath: string,
|
|
149
|
+
skillDir: string,
|
|
150
|
+
repoName: string,
|
|
151
|
+
): string {
|
|
152
|
+
const trimmedName = frontmatterName.trim();
|
|
153
|
+
if (trimmedName) {
|
|
154
|
+
return trimmedName;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return skillPath === "." ? repoName : path.basename(skillDir);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function cloneGithubRepository(
|
|
161
|
+
target: GithubRegistryInstallTarget,
|
|
162
|
+
cloneDir: string,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const repoUrl = `https://github.com/${target.repoFullName}.git`;
|
|
165
|
+
const args = ["clone", "--depth=1"];
|
|
166
|
+
|
|
167
|
+
if (target.ref) {
|
|
168
|
+
args.push("--branch", target.ref);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
args.push(repoUrl, cloneDir);
|
|
172
|
+
|
|
173
|
+
await execFileAsync("git", args);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function copySkillDirectory(sourceDir: string, targetDir: string): Promise<void> {
|
|
177
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
178
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
179
|
+
|
|
180
|
+
await cp(sourceDir, targetDir, {
|
|
181
|
+
recursive: true,
|
|
182
|
+
filter: (entryPath) => {
|
|
183
|
+
const basename = path.basename(entryPath);
|
|
184
|
+
return !isExcludedEntry(basename);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function installFromGithubTarget(
|
|
190
|
+
rawTarget: string,
|
|
191
|
+
globalFlag: boolean,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const target = parseGithubRegistryInstallTarget(rawTarget);
|
|
194
|
+
if (!target) {
|
|
195
|
+
throw new Error("GitHub install target must start with github:");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), "selftune-github-install-"));
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const cloneDir = path.join(tempRoot, "repo");
|
|
202
|
+
await cloneGithubRepository(target, cloneDir);
|
|
203
|
+
|
|
204
|
+
const { skillPath, availablePaths } = await resolveGithubSkillPath(cloneDir, target.skillPath);
|
|
205
|
+
const skillDir = skillPath === "." ? cloneDir : path.join(cloneDir, ...skillPath.split("/"));
|
|
206
|
+
const skillContent = await readFile(path.join(skillDir, "SKILL.md"), "utf-8");
|
|
207
|
+
const frontmatter = parseFrontmatter(skillContent);
|
|
208
|
+
const skillName = deriveGithubInstallSkillName(
|
|
209
|
+
frontmatter.name,
|
|
210
|
+
skillPath,
|
|
211
|
+
skillDir,
|
|
212
|
+
target.repo,
|
|
213
|
+
);
|
|
214
|
+
const resolvedCommit = (
|
|
215
|
+
await execFileAsync("git", ["-C", cloneDir, "rev-parse", "HEAD"])
|
|
216
|
+
).stdout.trim();
|
|
217
|
+
|
|
218
|
+
const targetBase = globalFlag
|
|
219
|
+
? path.join(process.env.HOME || "~", ".claude", "skills")
|
|
220
|
+
: path.join(process.cwd(), ".claude", "skills");
|
|
221
|
+
const targetDir = path.join(targetBase, skillName);
|
|
222
|
+
|
|
223
|
+
await copySkillDirectory(skillDir, targetDir);
|
|
224
|
+
await writeFile(
|
|
225
|
+
path.join(targetDir, ".selftune-source.json"),
|
|
226
|
+
JSON.stringify(
|
|
227
|
+
{
|
|
228
|
+
source: "github-direct",
|
|
229
|
+
repo: target.repoFullName,
|
|
230
|
+
ref: target.ref ?? "HEAD",
|
|
231
|
+
commit: resolvedCommit,
|
|
232
|
+
skill_path: skillPath,
|
|
233
|
+
available_paths: availablePaths,
|
|
234
|
+
},
|
|
235
|
+
null,
|
|
236
|
+
2,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
JSON.stringify({
|
|
242
|
+
success: true,
|
|
243
|
+
source: "github-direct",
|
|
244
|
+
name: skillName,
|
|
245
|
+
repo: target.repoFullName,
|
|
246
|
+
ref: target.ref ?? "HEAD",
|
|
247
|
+
commit: resolvedCommit,
|
|
248
|
+
skill_path: skillPath,
|
|
249
|
+
path: targetDir,
|
|
250
|
+
global: globalFlag,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
} finally {
|
|
254
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -24,7 +24,7 @@ Usage:
|
|
|
24
24
|
|
|
25
25
|
Subcommands:
|
|
26
26
|
push [name] Push current skill folder as a new version
|
|
27
|
-
install <name> Download
|
|
27
|
+
install <name> Download from the registry or install github:owner/repo[@ref][//path]
|
|
28
28
|
sync Check for updates and pull latest versions
|
|
29
29
|
status Show installed entries and version drift
|
|
30
30
|
rollback <name> Rollback to a previous version
|