dsh-continual-evolve 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -17
- package/README.zh.md +87 -10
- package/lib/apply.js +3 -1
- package/lib/approval.d.ts +25 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +99 -5
- package/lib/auto.js +165 -6
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +331 -0
- package/lib/benchmark.d.ts +84 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.js +33 -221
- package/lib/evaluate.d.ts +43 -7
- package/lib/evaluate.js +172 -43
- package/lib/evolve-event.d.ts +38 -0
- package/lib/evolve-event.js +49 -0
- package/lib/failures.d.ts +39 -0
- package/lib/failures.js +170 -0
- package/lib/fate.d.ts +128 -0
- package/lib/fate.js +342 -0
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +51 -21
- package/lib/index.js +32 -2
- package/lib/inject.d.ts +8 -0
- package/lib/inject.js +51 -4
- package/lib/llm-text.d.ts +30 -0
- package/lib/llm-text.js +49 -0
- package/lib/mount-command.d.ts +10 -0
- package/lib/mount-command.js +48 -0
- package/lib/mount.js +5 -0
- package/lib/plan.js +5 -0
- package/lib/planner.d.ts +8 -1
- package/lib/planner.js +40 -39
- package/lib/render.d.ts +1 -3
- package/lib/render.js +2 -5
- package/lib/review.d.ts +5 -2
- package/lib/review.js +27 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +37 -4
- package/lib/score.js +120 -10
- package/lib/service.d.ts +2 -2
- package/lib/service.js +5 -2
- package/lib/skill-render.d.ts +15 -0
- package/lib/skill-render.js +30 -0
- package/lib/skill.d.ts +12 -7
- package/lib/skill.js +36 -31
- package/lib/skillquality.d.ts +80 -0
- package/lib/skillquality.js +311 -0
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +28 -4
- package/lib/types.d.ts +39 -0
- package/lib/types.js +19 -0
- package/lib/usage.d.ts +32 -0
- package/lib/usage.js +84 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +51 -2
- package/lib/wrapup-command.d.ts +8 -0
- package/lib/wrapup-command.js +211 -0
- package/lib/wrapup.d.ts +215 -0
- package/lib/wrapup.js +427 -0
- package/package.json +8 -8
package/lib/score.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
export const DEFAULT_AGGREGATE = {
|
|
2
|
-
|
|
1
|
+
export const DEFAULT_AGGREGATE = {
|
|
2
|
+
passThreshold: 60,
|
|
3
|
+
regressionTolerance: 0,
|
|
4
|
+
maxFailedCells: 0,
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Aggregate raw cells into code-owned per-case means + overall mean.
|
|
8
|
+
* Failure-cell protocol (gap A2): failed cells are EXCLUDED from means and
|
|
9
|
+
* counted separately — a crashed unit can never silently drag the mean down
|
|
10
|
+
* like a zero. A case whose cells all failed reports null (no mean).
|
|
11
|
+
*/
|
|
3
12
|
export function aggregate(cells) {
|
|
4
13
|
const byCase = new Map();
|
|
14
|
+
let failed = 0;
|
|
5
15
|
for (const cell of cells) {
|
|
16
|
+
if (cell.status === "failed") {
|
|
17
|
+
failed += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
6
20
|
const list = byCase.get(cell.caseId) ?? [];
|
|
7
21
|
list.push(clampScore(cell.score));
|
|
8
22
|
byCase.set(cell.caseId, list);
|
|
@@ -12,7 +26,19 @@ export function aggregate(cells) {
|
|
|
12
26
|
perCase[caseId] = mean(scores);
|
|
13
27
|
}
|
|
14
28
|
const all = [...byCase.values()].flat();
|
|
15
|
-
|
|
29
|
+
let totalDurationMs = 0;
|
|
30
|
+
for (const cell of cells) {
|
|
31
|
+
if (cell.durationMs !== undefined && cell.durationMs >= 0) {
|
|
32
|
+
totalDurationMs += cell.durationMs;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
...perCase,
|
|
37
|
+
overall: all.length > 0 ? mean(all) : null,
|
|
38
|
+
failed,
|
|
39
|
+
total: cells.length,
|
|
40
|
+
totalDurationMs,
|
|
41
|
+
};
|
|
16
42
|
}
|
|
17
43
|
export function entryFromCells(label, cells, refinementId) {
|
|
18
44
|
const aggr = aggregate(cells);
|
|
@@ -28,33 +54,111 @@ export function entryFromCells(label, cells, refinementId) {
|
|
|
28
54
|
/** Human-readable decision report with per-case before → after deltas. */
|
|
29
55
|
export function decisionReport(reference, candidate, decision) {
|
|
30
56
|
const lines = [`overall: ${reference.overall ?? "?"} → ${candidate.overall ?? "?"}`];
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
// Per-case deltas over the actual evaluated cases only (aggregate() mixes
|
|
58
|
+
// case means with metadata keys like totalDurationMs — never render those
|
|
59
|
+
// as cases).
|
|
60
|
+
for (const caseId of new Set(reference.cells.map((cell) => cell.caseId))) {
|
|
61
|
+
const refScore = reference.aggregate[caseId];
|
|
62
|
+
if (refScore === null || refScore === undefined)
|
|
33
63
|
continue;
|
|
34
64
|
const candScore = candidate.aggregate[caseId];
|
|
35
|
-
|
|
65
|
+
const failedMark = isCaseFailed(reference, caseId) || isCaseFailed(candidate, caseId) ? " (failed)" : "";
|
|
66
|
+
lines.push(` ${caseId}: ${refScore} → ${candScore ?? "?"}${failedMark}`);
|
|
67
|
+
}
|
|
68
|
+
const refFailed = reference.aggregate.failed ?? 0;
|
|
69
|
+
const candFailed = candidate.aggregate.failed ?? 0;
|
|
70
|
+
if (refFailed > 0 || candFailed > 0) {
|
|
71
|
+
lines.push(`failed cells: reference ${refFailed}/${reference.aggregate.total ?? 0} · candidate ${candFailed}/${candidate.aggregate.total ?? 0}`);
|
|
72
|
+
}
|
|
73
|
+
// Gap C3: show duration summary when available.
|
|
74
|
+
const refDuration = reference.aggregate.totalDurationMs ?? 0;
|
|
75
|
+
const candDuration = candidate.aggregate.totalDurationMs ?? 0;
|
|
76
|
+
if (refDuration > 0 || candDuration > 0) {
|
|
77
|
+
lines.push(`duration: ${formatDuration(refDuration)} → ${formatDuration(candDuration)}`);
|
|
36
78
|
}
|
|
37
79
|
lines.push(decision.accepted
|
|
38
80
|
? "DECISION: ACCEPTED — overall improved, no regression"
|
|
39
81
|
: `DECISION: REJECTED — ${decision.reasons.join("; ")}`);
|
|
40
82
|
return lines;
|
|
41
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Gap A3 (version_changed semantics): detect cells whose case material
|
|
86
|
+
* changed between the reference and candidate evaluation runs. A candidate
|
|
87
|
+
* cell whose `caseHash` differs from the reference cell of the SAME case
|
|
88
|
+
* means the statement/rubric was edited between the two runs — its score is
|
|
89
|
+
* not comparable to the baseline and must not count toward the decision.
|
|
90
|
+
*
|
|
91
|
+
* The check is conservative: cells without a hash on either side (pre-A3
|
|
92
|
+
* data) and cells already failed are left untouched. Mismatched cells are
|
|
93
|
+
* returned re-marked as `failed` with a reason in notes, so aggregation
|
|
94
|
+
* excludes them and the acceptance rule can reject the round.
|
|
95
|
+
*/
|
|
96
|
+
export function flagMaterialDrift(reference, candidateCells) {
|
|
97
|
+
const referenceHashes = new Map();
|
|
98
|
+
for (const cell of reference.cells) {
|
|
99
|
+
if (cell.status !== "failed" && cell.caseHash !== undefined && !referenceHashes.has(cell.caseId)) {
|
|
100
|
+
referenceHashes.set(cell.caseId, cell.caseHash);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (referenceHashes.size === 0) {
|
|
104
|
+
return [...candidateCells];
|
|
105
|
+
}
|
|
106
|
+
return candidateCells.map((cell) => {
|
|
107
|
+
if (cell.status === "failed" || cell.caseHash === undefined) {
|
|
108
|
+
return cell;
|
|
109
|
+
}
|
|
110
|
+
const refHash = referenceHashes.get(cell.caseId);
|
|
111
|
+
if (refHash !== undefined && refHash !== cell.caseHash) {
|
|
112
|
+
return {
|
|
113
|
+
...cell,
|
|
114
|
+
status: "failed",
|
|
115
|
+
passed: false,
|
|
116
|
+
notes: `materials changed: case ${cell.caseId} hash ${cell.caseHash} ≠ reference ${refHash} (re-run the reference or fix the material)`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return cell;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function isCaseFailed(entry, caseId) {
|
|
123
|
+
return entry.cells.some((cell) => cell.caseId === caseId && cell.status === "failed");
|
|
124
|
+
}
|
|
42
125
|
/**
|
|
43
126
|
* Non-regressive acceptance rule (Self-Harness style):
|
|
44
127
|
* the candidate is accepted iff its overall mean is STRICTLY higher than the
|
|
45
|
-
* reference
|
|
128
|
+
* reference, no case regresses by more than `regressionTolerance` points,
|
|
129
|
+
* and neither side has more failed cells than `maxFailedCells` (failure-cell
|
|
130
|
+
* protocol, gap A2 — a partial/invalid round is never accepted).
|
|
46
131
|
*/
|
|
47
132
|
export function decide(reference, candidate, opts) {
|
|
48
133
|
const reasons = [];
|
|
49
134
|
if (reference.overall === null || candidate.overall === null) {
|
|
50
135
|
return { accepted: false, reasons: ["reference or candidate evaluation is incomplete"] };
|
|
51
136
|
}
|
|
137
|
+
const refFailed = reference.aggregate.failed ?? 0;
|
|
138
|
+
const candFailed = candidate.aggregate.failed ?? 0;
|
|
139
|
+
if (refFailed > opts.maxFailedCells) {
|
|
140
|
+
reasons.push(`reference has ${refFailed} failed cells (max ${opts.maxFailedCells})`);
|
|
141
|
+
}
|
|
142
|
+
if (candFailed > opts.maxFailedCells) {
|
|
143
|
+
reasons.push(`candidate has ${candFailed} failed cells (max ${opts.maxFailedCells})`);
|
|
144
|
+
}
|
|
145
|
+
if (reasons.length > 0) {
|
|
146
|
+
return { accepted: false, reasons };
|
|
147
|
+
}
|
|
52
148
|
if (candidate.overall <= reference.overall) {
|
|
53
149
|
reasons.push(`overall not improved: ${candidate.overall} <= ${reference.overall}`);
|
|
54
150
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
151
|
+
// Per-case regression is judged ONLY over the cases that actually exist in
|
|
152
|
+
// the evaluation — aggregate() mixes per-case means with metadata keys
|
|
153
|
+
// (failed/total/totalDurationMs), and iterating raw keys would (and did)
|
|
154
|
+
// treat totalDurationMs as a case score, rejecting a candidate whose run
|
|
155
|
+
// merely took longer. Derived from reference.cells, never from the
|
|
156
|
+
// aggregate key set.
|
|
157
|
+
for (const caseId of new Set(reference.cells.map((cell) => cell.caseId))) {
|
|
158
|
+
const refScore = reference.aggregate[caseId];
|
|
159
|
+
if (refScore === null || refScore === undefined) {
|
|
160
|
+
continue; // case had no comparable mean (all cells failed) — nothing to regress
|
|
161
|
+
}
|
|
58
162
|
const candScore = candidate.aggregate[caseId];
|
|
59
163
|
if (candScore === null || candScore === undefined) {
|
|
60
164
|
reasons.push(`candidate missing case ${caseId}`);
|
|
@@ -78,4 +182,10 @@ function clampScore(score) {
|
|
|
78
182
|
function round2(value) {
|
|
79
183
|
return Math.round(value * 100) / 100;
|
|
80
184
|
}
|
|
185
|
+
/** Gap C3: human-readable duration (ms → "1.2s" or "340ms"). */
|
|
186
|
+
function formatDuration(ms) {
|
|
187
|
+
if (ms < 1000)
|
|
188
|
+
return `${Math.round(ms)}ms`;
|
|
189
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
190
|
+
}
|
|
81
191
|
//# sourceMappingURL=score.js.map
|
package/lib/service.d.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { EntrySource, HarnessScope, RefinementProposal, RefinementResult } from "./types.js";
|
|
7
7
|
import { applyRefinementProposal } from "./apply.js";
|
|
8
|
-
import { storePaths } from "./store.js";
|
|
9
8
|
export interface ApplyContext {
|
|
10
9
|
scope: HarnessScope;
|
|
11
10
|
sessionId?: string;
|
|
@@ -13,6 +12,8 @@ export interface ApplyContext {
|
|
|
13
12
|
baselineState?: Parameters<typeof applyRefinementProposal>[0];
|
|
14
13
|
/** Trajectory citation stamped into newly created entries (see apply.ts). */
|
|
15
14
|
source?: EntrySource | undefined;
|
|
15
|
+
/** Marks the resulting refinement as the deterministic rollback of another (audit chain). */
|
|
16
|
+
rollbackOf?: string | undefined;
|
|
16
17
|
}
|
|
17
18
|
export interface EvolutionHooks {
|
|
18
19
|
/** Called after every applied refinement (side-effect boundary: skills sync, etc.). */
|
|
@@ -26,5 +27,4 @@ export declare function createEvolutionEngine(baseDir: string, hooks?: Evolution
|
|
|
26
27
|
baseDir: string;
|
|
27
28
|
};
|
|
28
29
|
export type EvolutionEngine = ReturnType<typeof createEvolutionEngine>;
|
|
29
|
-
export { storePaths };
|
|
30
30
|
//# sourceMappingURL=service.d.ts.map
|
package/lib/service.js
CHANGED
|
@@ -17,6 +17,7 @@ export function createEvolutionEngine(baseDir, hooks = {}) {
|
|
|
17
17
|
scope,
|
|
18
18
|
...(context?.source ? { source: context.source } : {}),
|
|
19
19
|
...(context?.baselineState ? { baselineState: context.baselineState } : {}),
|
|
20
|
+
...(context?.rollbackOf ? { rollbackOf: context.rollbackOf } : {}),
|
|
20
21
|
});
|
|
21
22
|
saveHarnessState(paths.stateDir, state);
|
|
22
23
|
appendResult(paths, result);
|
|
@@ -31,12 +32,14 @@ export function createEvolutionEngine(baseDir, hooks = {}) {
|
|
|
31
32
|
throw new Error(`Refinement ${refinementId} not found in ${scope} history`);
|
|
32
33
|
}
|
|
33
34
|
const proposal = rollbackProposal(target);
|
|
34
|
-
|
|
35
|
+
// The rollback refinement carries rollbackOf so the audit chain links
|
|
36
|
+
// the inverse operation back to its origin (previously the rollback
|
|
37
|
+
// record only echoed "Rollback refinement <id>" in its summary text).
|
|
38
|
+
return apply(scope, sessionId, proposal, { scope, rollbackOf: refinementId });
|
|
35
39
|
}
|
|
36
40
|
function history(scope, sessionId) {
|
|
37
41
|
return loadResults(storePaths(baseDir, scope, sessionId));
|
|
38
42
|
}
|
|
39
43
|
return { load, apply, rollback, history, baseDir };
|
|
40
44
|
}
|
|
41
|
-
export { storePaths };
|
|
42
45
|
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill rendering: pure functions that convert harness entries into
|
|
3
|
+
* SKILL.md documents. Extracted from skill.ts to break the circular
|
|
4
|
+
* dependency between skill.ts ↔ skillquality.ts.
|
|
5
|
+
*
|
|
6
|
+
* Both skill.ts (materializer) and skillquality.ts (validator) need
|
|
7
|
+
* these rendering functions; importing from this shared leaf module
|
|
8
|
+
* keeps the dependency graph acyclic.
|
|
9
|
+
*/
|
|
10
|
+
import type { HarnessEntry } from "./types.js";
|
|
11
|
+
/** Convert a harness entry id (underscore slug) to a kebab-case skill name. */
|
|
12
|
+
export declare function skillNameOf(id: string): string;
|
|
13
|
+
/** Render a harness skill entry as a discoverable SKILL.md document. */
|
|
14
|
+
export declare function renderSkillMarkdown(entry: HarnessEntry): string;
|
|
15
|
+
//# sourceMappingURL=skill-render.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Convert a harness entry id (underscore slug) to a kebab-case skill name. */
|
|
2
|
+
export function skillNameOf(id) {
|
|
3
|
+
return id.toLowerCase().replace(/_/g, "-");
|
|
4
|
+
}
|
|
5
|
+
/** Render a harness skill entry as a discoverable SKILL.md document. */
|
|
6
|
+
export function renderSkillMarkdown(entry) {
|
|
7
|
+
const lines = [
|
|
8
|
+
"---",
|
|
9
|
+
`name: ${skillNameOf(entry.id)}`,
|
|
10
|
+
`description: ${oneLine(entry.title)}`,
|
|
11
|
+
"---",
|
|
12
|
+
"",
|
|
13
|
+
entry.content.trim(),
|
|
14
|
+
];
|
|
15
|
+
const reference = entry.reference;
|
|
16
|
+
if (reference && typeof reference === "object" && Object.keys(reference).length > 0) {
|
|
17
|
+
lines.push("", "## Invocation");
|
|
18
|
+
for (const [key, value] of Object.entries(reference)) {
|
|
19
|
+
lines.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (Object.keys(entry.arguments).length > 0) {
|
|
23
|
+
lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
|
|
24
|
+
}
|
|
25
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
26
|
+
}
|
|
27
|
+
function oneLine(text) {
|
|
28
|
+
return text.replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=skill-render.js.map
|
package/lib/skill.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export declare function skillNameOf(id: string): string;
|
|
1
|
+
import type { RefinementResult } from "./types.js";
|
|
2
|
+
export { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
|
|
4
3
|
/** Resolve and defend the skill directory for an entry id. */
|
|
5
4
|
export declare function skillDir(skillsRoot: string, id: string): string;
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Apply the skill-kind edits of an applied refinement to the skills root.
|
|
7
|
+
* Returns materialization warnings (rendered-SKILL.md mechanical problems
|
|
8
|
+
* and dangling resource references) — the file is still written, but the
|
|
9
|
+
* caller should surface them: a rendered file that fails the platform's
|
|
10
|
+
* frontmatter rules would be IGNORED by the skill loader, and a body
|
|
11
|
+
* referencing resources the entry does not ship would load with broken
|
|
12
|
+
* links.
|
|
13
|
+
*/
|
|
14
|
+
export declare function syncSkillsFromResult(skillsRoot: string, result: RefinementResult): string[];
|
|
10
15
|
//# sourceMappingURL=skill.d.ts.map
|
package/lib/skill.js
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join, resolve, sep } from "node:path";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
12
|
+
import { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
|
|
13
|
+
import { skillResourceRefs, validateRenderedSkill } from "./skillquality.js";
|
|
14
|
+
export { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
|
|
16
15
|
/** Resolve and defend the skill directory for an entry id. */
|
|
17
16
|
export function skillDir(skillsRoot, id) {
|
|
18
17
|
const root = resolve(skillsRoot);
|
|
@@ -22,30 +21,17 @@ export function skillDir(skillsRoot, id) {
|
|
|
22
21
|
}
|
|
23
22
|
return dir;
|
|
24
23
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
];
|
|
35
|
-
const reference = entry.reference;
|
|
36
|
-
if (reference && typeof reference === "object" && Object.keys(reference).length > 0) {
|
|
37
|
-
lines.push("", "## Invocation");
|
|
38
|
-
for (const [key, value] of Object.entries(reference)) {
|
|
39
|
-
lines.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (Object.keys(entry.arguments).length > 0) {
|
|
43
|
-
lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
|
|
44
|
-
}
|
|
45
|
-
return `${lines.join("\n").trimEnd()}\n`;
|
|
46
|
-
}
|
|
47
|
-
/** Apply the skill-kind edits of an applied refinement to the skills root. */
|
|
24
|
+
/**
|
|
25
|
+
* Apply the skill-kind edits of an applied refinement to the skills root.
|
|
26
|
+
* Returns materialization warnings (rendered-SKILL.md mechanical problems
|
|
27
|
+
* and dangling resource references) — the file is still written, but the
|
|
28
|
+
* caller should surface them: a rendered file that fails the platform's
|
|
29
|
+
* frontmatter rules would be IGNORED by the skill loader, and a body
|
|
30
|
+
* referencing resources the entry does not ship would load with broken
|
|
31
|
+
* links.
|
|
32
|
+
*/
|
|
48
33
|
export function syncSkillsFromResult(skillsRoot, result) {
|
|
34
|
+
const warnings = [];
|
|
49
35
|
for (const edit of result.appliedEdits) {
|
|
50
36
|
if (edit.kind !== "skill" || !edit.applied)
|
|
51
37
|
continue;
|
|
@@ -53,15 +39,37 @@ export function syncSkillsFromResult(skillsRoot, result) {
|
|
|
53
39
|
removeSkill(skillsRoot, edit.id);
|
|
54
40
|
continue;
|
|
55
41
|
}
|
|
56
|
-
writeSkill(skillsRoot, edit.after);
|
|
42
|
+
warnings.push(...writeSkill(skillsRoot, edit.after));
|
|
57
43
|
}
|
|
44
|
+
return warnings;
|
|
58
45
|
}
|
|
46
|
+
/** Write one skill entry as a SKILL.md; returns materialization warnings. */
|
|
59
47
|
function writeSkill(skillsRoot, entry) {
|
|
60
48
|
const dir = skillDir(skillsRoot, entry.id);
|
|
61
49
|
mkdirSync(dir, { recursive: true });
|
|
62
50
|
const temp = join(dir, `SKILL.md.${process.pid}.tmp`);
|
|
63
51
|
writeFileSync(temp, renderSkillMarkdown(entry), "utf8");
|
|
64
52
|
renameSync(temp, join(dir, "SKILL.md"));
|
|
53
|
+
return materializationWarnings(dir, entry);
|
|
54
|
+
}
|
|
55
|
+
/** Post-write checks on the exact file that landed on disk. */
|
|
56
|
+
function materializationWarnings(dir, entry) {
|
|
57
|
+
const warnings = [];
|
|
58
|
+
for (const problem of validateRenderedSkill(entry)) {
|
|
59
|
+
warnings.push(`skill ${entry.id}: rendered SKILL.md would be ignored by the platform: ${problem}`);
|
|
60
|
+
}
|
|
61
|
+
const root = resolve(dir);
|
|
62
|
+
for (const ref of skillResourceRefs(entry.content)) {
|
|
63
|
+
const target = resolve(root, ref);
|
|
64
|
+
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
65
|
+
warnings.push(`skill ${entry.id}: body resource reference escapes the skill directory: ${ref}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!existsSync(target)) {
|
|
69
|
+
warnings.push(`skill ${entry.id}: body references missing resource ${ref} (expected at ${target})`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return warnings;
|
|
65
73
|
}
|
|
66
74
|
function removeSkill(skillsRoot, id) {
|
|
67
75
|
const dir = skillDir(skillsRoot, id);
|
|
@@ -69,7 +77,4 @@ function removeSkill(skillsRoot, id) {
|
|
|
69
77
|
rmSync(dir, { recursive: true, force: true });
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
|
-
function oneLine(text) {
|
|
73
|
-
return text.replace(/\s+/g, " ").trim();
|
|
74
|
-
}
|
|
75
80
|
//# sourceMappingURL=skill.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { HarnessEntry } from "./types.js";
|
|
2
|
+
/** Relative location of the skill-creator template facts. */
|
|
3
|
+
export declare const SKILL_CREATOR_TEMPLATE_REL: string;
|
|
4
|
+
/**
|
|
5
|
+
* Read the skill-creator template facts
|
|
6
|
+
* (`<skillsRoot>/skill-creator/references/template.md`; facts distilled
|
|
7
|
+
* from the official deepseek-harness skills). Returns null when the skills
|
|
8
|
+
* are not installed — callers fall back to the builtin distilled guide.
|
|
9
|
+
* Reading is a runtime reference, never a copy: template updates in the
|
|
10
|
+
* skill are picked up automatically.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readSkillCreatorTemplate(skillsRoot: string): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* Builtin distilled skill-quality guide (fallback when the skill-creator
|
|
15
|
+
* template is not installed). Condenses the template facts — frontmatter
|
|
16
|
+
* schema, the 7 structural features, paragraph skeleton, and the
|
|
17
|
+
* no-duplication / real-trigger rules — so a planner still authors skills
|
|
18
|
+
* to the standard on installs without the skill-creator / skill-audit
|
|
19
|
+
* skills.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BUILTIN_SKILL_QUALITY_GUIDE = "DSH skill quality standard (distilled by the author from the official deepseek-harness 11 skills; the full facts live in <skillsRoot>/skill-creator/references/template.md when installed):\n\nFrontmatter schema (platform-enforced; violations make the platform IGNORE the whole file):\n- name: required, kebab-case only (^[a-z0-9]+(?:-[a-z0-9]+)*$)\n- description: required, non-empty; write \"use when / do not use when\" routing so the model can select it correctly\n- invocation booleans accept true/false/yes/no/on/off/1/0; legacy camelCase keys (disableModelInvocation / modelInvocable / userInvocable) are rejected\n- whenToUse (optional): non-empty string; metadata (optional): object\n\nThe 7 structural features of the official deepseek-harness skills:\n1. Frontmatter is routing metadata, not a summary (description = when to use / when not to use)\n2. Opens with a boundary declaration (guidance, not a script; mechanical flow skills may omit the disclaimer)\n3. Prerequisites + exclusions: explicit required input, stop when missing (report the required input and stop), excluded scenarios\n4. Layered information: Sources of truth (link only, do not re-summarize) -> numbered blocking requirements -> manual checks -> verification commands -> report format; all executable, no slogans\n5. Skill interlinks: reference a single source of truth instead of duplicating it\n6. Verifiable completion criteria: explicit verification commands and report format\n7. Real use + iteration: a real trigger scenario must exist; calibration conclusions distill into references/\n\nParagraph skeleton (writing order): frontmatter -> H1 + boundary declaration -> Sources of truth -> numbered requirements / workflow (full commands) -> exclusions / stop conditions -> verification and report.\n\nCreation rules: only create a skill for a REAL trigger scenario (who, in what real task, what signal) grounded in the trajectory \u2014 never invent one to pad the store; do not duplicate the official 11 skills or existing entries; skill bodies should be a SKILL.md document (this is what materializes under <skillsRoot>/<kebab-name>/SKILL.md).";
|
|
22
|
+
export interface SkillQualityGuide {
|
|
23
|
+
/** Where the guide text came from: the on-disk template or the builtin guide. */
|
|
24
|
+
source: "template" | "builtin";
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The quality guide handed to the planner: the skill-creator template facts
|
|
29
|
+
* when the skills are installed, otherwise the builtin distilled guide.
|
|
30
|
+
* Never throws — a missing/unreadable template degrades to the builtin.
|
|
31
|
+
*/
|
|
32
|
+
export declare function skillQualityGuide(skillsRoot: string | undefined): SkillQualityGuide;
|
|
33
|
+
/** Split frontmatter out of a raw SKILL.md. Returns { yaml, body } or null when delimiters are missing. */
|
|
34
|
+
export declare function splitFrontmatter(raw: string): {
|
|
35
|
+
yaml: string;
|
|
36
|
+
body: string;
|
|
37
|
+
} | null;
|
|
38
|
+
/**
|
|
39
|
+
* Mechanical frontmatter validation of a rendered SKILL.md, mirroring
|
|
40
|
+
* `skill-creator/scripts/validate-frontmatter.mjs` (and the platform's
|
|
41
|
+
* skill-filesystem rules): delimiter structure, name kebab-case, non-empty
|
|
42
|
+
* description, invocation-boolean spellings, legacy camelCase key rejection,
|
|
43
|
+
* whenToUse/metadata types. Returns human-readable problems; an empty array
|
|
44
|
+
* means the file would load.
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateRenderedSkillMarkdown(markdown: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Mechanical validation of a skill entry's raw `content` (the SKILL.md body
|
|
49
|
+
* that materializes under the generated frontmatter). Code-enforced at
|
|
50
|
+
* apply time so a bad entry never reaches the store:
|
|
51
|
+
* - empty content is rejected;
|
|
52
|
+
* - content must not open with a `---` block: the materializer generates
|
|
53
|
+
* its own frontmatter, and a second frontmatter in the body would be
|
|
54
|
+
* parsed instead of the generated one (the platform reads the FIRST
|
|
55
|
+
* closing `---`), so the file could be ignored or routed wrongly;
|
|
56
|
+
* - resource references (`references/…`, `scripts/…`) must be skill-local
|
|
57
|
+
* relative paths — parent-relative (`../`) or absolute targets escape the
|
|
58
|
+
* skill directory and are rejected.
|
|
59
|
+
* Returns human-readable problems; an empty array means the content is
|
|
60
|
+
* mechanically acceptable.
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateSkillEntryContent(content: string): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Validate the FULL rendered SKILL.md of an entry (generated frontmatter +
|
|
65
|
+
* body) — the exact bytes that materialize on disk. Used as the final
|
|
66
|
+
* code-enforced line after materialization; problems here mean the platform
|
|
67
|
+
* would refuse to load the file.
|
|
68
|
+
*/
|
|
69
|
+
export declare function validateRenderedSkill(entry: HarnessEntry): string[];
|
|
70
|
+
/**
|
|
71
|
+
* Resource references (`references/…`, `scripts/…`) found in a skill body —
|
|
72
|
+
* the same scanning policy as validate-frontmatter.mjs: markdown link
|
|
73
|
+
* targets starting with the category, plus backticked/prose paths carrying
|
|
74
|
+
* a filename extension. Used after materialization to warn about dangling
|
|
75
|
+
* references (a body referencing a resource the entry never ships).
|
|
76
|
+
*/
|
|
77
|
+
export declare function skillResourceRefs(content: string): string[];
|
|
78
|
+
/** Kebab-case name under which the entry materializes (exported for diagnostics). */
|
|
79
|
+
export { skillNameOf } from "./skill-render.js";
|
|
80
|
+
//# sourceMappingURL=skillquality.d.ts.map
|