dsh-continual-evolve 0.2.0 → 0.4.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 +83 -371
- package/README.zh.md +84 -235
- package/lib/apply.js +8 -2
- package/lib/approval.d.ts +6 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +55 -4
- package/lib/auto.js +61 -5
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +333 -0
- package/lib/benchmark.d.ts +70 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.d.ts +3 -0
- package/lib/command.js +62 -441
- package/lib/evaluate.d.ts +7 -0
- package/lib/evaluate.js +22 -7
- 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 +5 -2
- package/lib/fate.js +13 -8
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +51 -25
- package/lib/index.js +33 -1
- package/lib/inject.d.ts +24 -1
- package/lib/inject.js +93 -5
- 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/plan.js +5 -0
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +13 -39
- package/lib/promotion.d.ts +62 -0
- package/lib/promotion.js +102 -0
- package/lib/render.d.ts +1 -3
- package/lib/render.js +0 -4
- package/lib/review.d.ts +4 -1
- package/lib/review.js +10 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +15 -0
- package/lib/score.js +74 -5
- package/lib/service.d.ts +2 -2
- package/lib/service.js +7 -3
- package/lib/skill-render.d.ts +23 -0
- package/lib/skill-render.js +68 -0
- package/lib/skill.d.ts +2 -5
- package/lib/skill.js +2 -29
- package/lib/skillquality.d.ts +1 -2
- package/lib/skillquality.js +2 -2
- package/lib/state.js +6 -1
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +22 -1
- package/lib/types.d.ts +8 -0
- package/lib/usage.d.ts +45 -0
- package/lib/usage.js +115 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +26 -1
- package/lib/wrapup-command.d.ts +9 -0
- package/lib/wrapup-command.js +212 -0
- package/lib/wrapup.d.ts +29 -15
- package/lib/wrapup.js +69 -42
- package/package.json +10 -8
package/lib/review.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
1
|
import { extractJsonObject } from "./plan.js";
|
|
3
2
|
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
3
|
+
import { streamText } from "./llm-text.js";
|
|
4
4
|
export const AUTO_REVIEW_SYSTEM_PROMPT = `You are the automatic /evolve review gate.
|
|
5
5
|
|
|
6
6
|
Decide whether this checkpoint should run /evolve. Auto /evolve writes local
|
|
@@ -88,7 +88,9 @@ export function serializeSurface(events, maxChars) {
|
|
|
88
88
|
}
|
|
89
89
|
export async function reviewAutoRefine(ctx, options) {
|
|
90
90
|
const { agent, state, history } = options;
|
|
91
|
-
|
|
91
|
+
const provider = options.overrideProvider ?? agent.options.provider;
|
|
92
|
+
const model = options.overrideModel ?? agent.options.model;
|
|
93
|
+
if (!provider || !model) {
|
|
92
94
|
throw new Error("evolve: no provider/model route for the review gate");
|
|
93
95
|
}
|
|
94
96
|
if (!options.trajectory || options.trajectory.length === 0) {
|
|
@@ -101,44 +103,14 @@ export async function reviewAutoRefine(ctx, options) {
|
|
|
101
103
|
`<conversation>\n${options.trajectory}\n</conversation>`,
|
|
102
104
|
"Return shouldRefine=true when the trajectory contains evidence useful to this session's future turns. Prefer local edits; do not ask for global refinement here.",
|
|
103
105
|
].join("\n\n");
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
model: agent.options.model,
|
|
106
|
+
const text = await streamText(ctx, {
|
|
107
|
+
provider,
|
|
108
|
+
model,
|
|
108
109
|
system: AUTO_REVIEW_SYSTEM_PROMPT,
|
|
109
|
-
|
|
110
|
-
createUserMessage({
|
|
111
|
-
content: [{ type: "text", text: userPrompt }],
|
|
112
|
-
source: { kind: "plugin", plugin: "dsh-continual-evolve" },
|
|
113
|
-
}),
|
|
114
|
-
],
|
|
115
|
-
// Force non-reasoning output so the model spends its budget on the JSON
|
|
116
|
-
// answer, not on visible thinking (reasoning models otherwise produce
|
|
117
|
-
// zero text blocks — the exact failure recorded in reviews.jsonl).
|
|
118
|
-
reasoningEffort: ReasoningEffortId("off"),
|
|
110
|
+
prompt: userPrompt,
|
|
119
111
|
maxTokens: options.budgetTokens ?? 8000,
|
|
120
|
-
|
|
121
|
-
})
|
|
122
|
-
assembler.push(chunk);
|
|
123
|
-
}
|
|
124
|
-
const finish = assembler.finish;
|
|
125
|
-
if (finish.kind === "error") {
|
|
126
|
-
throw new Error(`evolve: review gate call failed: ${finish.failure?.message ?? "unknown"}`);
|
|
127
|
-
}
|
|
128
|
-
if (finish.kind === "aborted") {
|
|
129
|
-
throw new Error("evolve: review gate call aborted");
|
|
130
|
-
}
|
|
131
|
-
if (finish.kind === "max-tokens") {
|
|
132
|
-
throw new Error("evolve: review gate output budget exhausted (max-tokens)");
|
|
133
|
-
}
|
|
134
|
-
const text = assembler
|
|
135
|
-
.blocks()
|
|
136
|
-
.filter((block) => block.type === "text")
|
|
137
|
-
.map((block) => block.text)
|
|
138
|
-
.join("\n");
|
|
139
|
-
if (text.length === 0) {
|
|
140
|
-
throw new Error("evolve: review gate produced no text");
|
|
141
|
-
}
|
|
112
|
+
signal: options.signal,
|
|
113
|
+
});
|
|
142
114
|
return parseAutoRefineReview(text);
|
|
143
115
|
}
|
|
144
116
|
//# sourceMappingURL=review.js.map
|
package/lib/rollback.d.ts
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
* result, in reverse order. Rollback is pure data transformation — no LLM
|
|
4
4
|
* is asked to "guess" the previous state.
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
6
|
+
import type { RefinementProposal, RefinementResult } from "./types.js";
|
|
7
7
|
/** Build the inverse proposal for an applied refinement. */
|
|
8
8
|
export declare function rollbackProposal(target: RefinementResult): RefinementProposal;
|
|
9
|
-
/** Recreate an entry from a prior snapshot (used when an inverse edit is an update). */
|
|
10
|
-
export declare function restoreEntry(prior: HarnessEntry): HarnessEntry;
|
|
11
9
|
//# sourceMappingURL=rollback.d.ts.map
|
package/lib/rollback.js
CHANGED
|
@@ -58,12 +58,4 @@ function inverseEdit(edit, refinementId) {
|
|
|
58
58
|
}
|
|
59
59
|
return undefined;
|
|
60
60
|
}
|
|
61
|
-
/** Recreate an entry from a prior snapshot (used when an inverse edit is an update). */
|
|
62
|
-
export function restoreEntry(prior) {
|
|
63
|
-
return {
|
|
64
|
-
...prior,
|
|
65
|
-
updated_at: new Date().toISOString(),
|
|
66
|
-
version: prior.version + 1,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
61
|
//# sourceMappingURL=rollback.js.map
|
package/lib/score.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface AggregateResult extends Record<string, number | null> {
|
|
|
23
23
|
failed: number;
|
|
24
24
|
/** Total cells considered (ok + failed). */
|
|
25
25
|
total: number;
|
|
26
|
+
/** Gap C3: total wall-clock duration of all cells in milliseconds. */
|
|
27
|
+
totalDurationMs: number;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Aggregate raw cells into code-owned per-case means + overall mean.
|
|
@@ -38,6 +40,19 @@ export interface Decision {
|
|
|
38
40
|
}
|
|
39
41
|
/** Human-readable decision report with per-case before → after deltas. */
|
|
40
42
|
export declare function decisionReport(reference: EvaluationEntry, candidate: EvaluationEntry, decision: Decision): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Gap A3 (version_changed semantics): detect cells whose case material
|
|
45
|
+
* changed between the reference and candidate evaluation runs. A candidate
|
|
46
|
+
* cell whose `caseHash` differs from the reference cell of the SAME case
|
|
47
|
+
* means the statement/rubric was edited between the two runs — its score is
|
|
48
|
+
* not comparable to the baseline and must not count toward the decision.
|
|
49
|
+
*
|
|
50
|
+
* The check is conservative: cells without a hash on either side (pre-A3
|
|
51
|
+
* data) and cells already failed are left untouched. Mismatched cells are
|
|
52
|
+
* returned re-marked as `failed` with a reason in notes, so aggregation
|
|
53
|
+
* excludes them and the acceptance rule can reject the round.
|
|
54
|
+
*/
|
|
55
|
+
export declare function flagMaterialDrift(reference: EvaluationEntry, candidateCells: readonly CellScore[]): CellScore[];
|
|
41
56
|
/**
|
|
42
57
|
* Non-regressive acceptance rule (Self-Harness style):
|
|
43
58
|
* the candidate is accepted iff its overall mean is STRICTLY higher than the
|
package/lib/score.js
CHANGED
|
@@ -26,11 +26,18 @@ export function aggregate(cells) {
|
|
|
26
26
|
perCase[caseId] = mean(scores);
|
|
27
27
|
}
|
|
28
28
|
const all = [...byCase.values()].flat();
|
|
29
|
+
let totalDurationMs = 0;
|
|
30
|
+
for (const cell of cells) {
|
|
31
|
+
if (cell.durationMs !== undefined && cell.durationMs >= 0) {
|
|
32
|
+
totalDurationMs += cell.durationMs;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
29
35
|
return {
|
|
30
36
|
...perCase,
|
|
31
37
|
overall: all.length > 0 ? mean(all) : null,
|
|
32
38
|
failed,
|
|
33
39
|
total: cells.length,
|
|
40
|
+
totalDurationMs,
|
|
34
41
|
};
|
|
35
42
|
}
|
|
36
43
|
export function entryFromCells(label, cells, refinementId) {
|
|
@@ -47,8 +54,12 @@ export function entryFromCells(label, cells, refinementId) {
|
|
|
47
54
|
/** Human-readable decision report with per-case before → after deltas. */
|
|
48
55
|
export function decisionReport(reference, candidate, decision) {
|
|
49
56
|
const lines = [`overall: ${reference.overall ?? "?"} → ${candidate.overall ?? "?"}`];
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
52
63
|
continue;
|
|
53
64
|
const candScore = candidate.aggregate[caseId];
|
|
54
65
|
const failedMark = isCaseFailed(reference, caseId) || isCaseFailed(candidate, caseId) ? " (failed)" : "";
|
|
@@ -59,11 +70,55 @@ export function decisionReport(reference, candidate, decision) {
|
|
|
59
70
|
if (refFailed > 0 || candFailed > 0) {
|
|
60
71
|
lines.push(`failed cells: reference ${refFailed}/${reference.aggregate.total ?? 0} · candidate ${candFailed}/${candidate.aggregate.total ?? 0}`);
|
|
61
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)}`);
|
|
78
|
+
}
|
|
62
79
|
lines.push(decision.accepted
|
|
63
80
|
? "DECISION: ACCEPTED — overall improved, no regression"
|
|
64
81
|
: `DECISION: REJECTED — ${decision.reasons.join("; ")}`);
|
|
65
82
|
return lines;
|
|
66
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
|
+
}
|
|
67
122
|
function isCaseFailed(entry, caseId) {
|
|
68
123
|
return entry.cells.some((cell) => cell.caseId === caseId && cell.status === "failed");
|
|
69
124
|
}
|
|
@@ -93,9 +148,17 @@ export function decide(reference, candidate, opts) {
|
|
|
93
148
|
if (candidate.overall <= reference.overall) {
|
|
94
149
|
reasons.push(`overall not improved: ${candidate.overall} <= ${reference.overall}`);
|
|
95
150
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
}
|
|
99
162
|
const candScore = candidate.aggregate[caseId];
|
|
100
163
|
if (candScore === null || candScore === undefined) {
|
|
101
164
|
reasons.push(`candidate missing case ${caseId}`);
|
|
@@ -119,4 +182,10 @@ function clampScore(score) {
|
|
|
119
182
|
function round2(value) {
|
|
120
183
|
return Math.round(value * 100) / 100;
|
|
121
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
|
+
}
|
|
122
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyRefinementProposal } from "./apply.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { rollbackProposal } from "./rollback.js";
|
|
3
4
|
import { loadHarnessState, saveHarnessState } from "./state.js";
|
|
4
5
|
import { appendResult, loadResults, snapshotBefore, storePaths } from "./store.js";
|
|
@@ -9,7 +10,7 @@ export function createEvolutionEngine(baseDir, hooks = {}) {
|
|
|
9
10
|
function apply(scope, sessionId, proposal, context) {
|
|
10
11
|
const paths = storePaths(baseDir, scope, sessionId);
|
|
11
12
|
const state = context?.baselineState ?? load(scope, sessionId);
|
|
12
|
-
const id = `evolve_${Date.now().toString(36)}_${
|
|
13
|
+
const id = `evolve_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
13
14
|
// Code-enforced snapshot: runs before any mutation, cannot be skipped by the model.
|
|
14
15
|
snapshotBefore(paths, id);
|
|
15
16
|
const result = applyRefinementProposal(state, proposal, {
|
|
@@ -17,6 +18,7 @@ export function createEvolutionEngine(baseDir, hooks = {}) {
|
|
|
17
18
|
scope,
|
|
18
19
|
...(context?.source ? { source: context.source } : {}),
|
|
19
20
|
...(context?.baselineState ? { baselineState: context.baselineState } : {}),
|
|
21
|
+
...(context?.rollbackOf ? { rollbackOf: context.rollbackOf } : {}),
|
|
20
22
|
});
|
|
21
23
|
saveHarnessState(paths.stateDir, state);
|
|
22
24
|
appendResult(paths, result);
|
|
@@ -31,12 +33,14 @@ export function createEvolutionEngine(baseDir, hooks = {}) {
|
|
|
31
33
|
throw new Error(`Refinement ${refinementId} not found in ${scope} history`);
|
|
32
34
|
}
|
|
33
35
|
const proposal = rollbackProposal(target);
|
|
34
|
-
|
|
36
|
+
// The rollback refinement carries rollbackOf so the audit chain links
|
|
37
|
+
// the inverse operation back to its origin (previously the rollback
|
|
38
|
+
// record only echoed "Rollback refinement <id>" in its summary text).
|
|
39
|
+
return apply(scope, sessionId, proposal, { scope, rollbackOf: refinementId });
|
|
35
40
|
}
|
|
36
41
|
function history(scope, sessionId) {
|
|
37
42
|
return loadResults(storePaths(baseDir, scope, sessionId));
|
|
38
43
|
}
|
|
39
44
|
return { load, apply, rollback, history, baseDir };
|
|
40
45
|
}
|
|
41
|
-
export { storePaths };
|
|
42
46
|
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* Render a harness skill entry as a discoverable SKILL.md document.
|
|
15
|
+
*
|
|
16
|
+
* 2026-08-22: the frontmatter description now carries a ROUTING HINT —
|
|
17
|
+
* title plus the first meaningful content line — instead of the bare title.
|
|
18
|
+
* The skill catalog matches on description; a title-only description gave
|
|
19
|
+
* loaders nothing to route on (observed: materialized skills were 7-line
|
|
20
|
+
* stubs with a one-line description and no use-when signal).
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderSkillMarkdown(entry: HarnessEntry): string;
|
|
23
|
+
//# sourceMappingURL=skill-render.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* First content line usable as a routing hint: non-empty, not a Markdown
|
|
7
|
+
* heading, not a list marker, not frontmatter. Undefined when the body is
|
|
8
|
+
* effectively empty.
|
|
9
|
+
*/
|
|
10
|
+
function routingHint(content) {
|
|
11
|
+
for (const rawLine of content.split("\n")) {
|
|
12
|
+
const line = rawLine.trim();
|
|
13
|
+
if (line.length === 0)
|
|
14
|
+
continue;
|
|
15
|
+
if (line.startsWith("#") || line.startsWith("---") || line.startsWith("-") || line.startsWith("*"))
|
|
16
|
+
continue;
|
|
17
|
+
return oneLine(line);
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Max rendered frontmatter description length (loaders truncate anyway). */
|
|
22
|
+
const MAX_DESCRIPTION_LENGTH = 240;
|
|
23
|
+
/**
|
|
24
|
+
* Render a harness skill entry as a discoverable SKILL.md document.
|
|
25
|
+
*
|
|
26
|
+
* 2026-08-22: the frontmatter description now carries a ROUTING HINT —
|
|
27
|
+
* title plus the first meaningful content line — instead of the bare title.
|
|
28
|
+
* The skill catalog matches on description; a title-only description gave
|
|
29
|
+
* loaders nothing to route on (observed: materialized skills were 7-line
|
|
30
|
+
* stubs with a one-line description and no use-when signal).
|
|
31
|
+
*/
|
|
32
|
+
export function renderSkillMarkdown(entry) {
|
|
33
|
+
const hint = routingHint(entry.content);
|
|
34
|
+
const base = oneLine(entry.title);
|
|
35
|
+
let description;
|
|
36
|
+
if (base.length === 0) {
|
|
37
|
+
description = (hint ?? "").slice(0, MAX_DESCRIPTION_LENGTH);
|
|
38
|
+
}
|
|
39
|
+
else if (hint !== undefined && !base.toLowerCase().includes(hint.toLowerCase())) {
|
|
40
|
+
description = `${base} — use when: ${hint}`.slice(0, MAX_DESCRIPTION_LENGTH);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
description = base.slice(0, MAX_DESCRIPTION_LENGTH);
|
|
44
|
+
}
|
|
45
|
+
const lines = [
|
|
46
|
+
"---",
|
|
47
|
+
`name: ${skillNameOf(entry.id)}`,
|
|
48
|
+
`description: ${description}`,
|
|
49
|
+
"---",
|
|
50
|
+
"",
|
|
51
|
+
entry.content.trim(),
|
|
52
|
+
];
|
|
53
|
+
const reference = entry.reference;
|
|
54
|
+
if (reference && typeof reference === "object" && Object.keys(reference).length > 0) {
|
|
55
|
+
lines.push("", "## Invocation");
|
|
56
|
+
for (const [key, value] of Object.entries(reference)) {
|
|
57
|
+
lines.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(entry.arguments).length > 0) {
|
|
61
|
+
lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
|
|
62
|
+
}
|
|
63
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
64
|
+
}
|
|
65
|
+
function oneLine(text) {
|
|
66
|
+
return text.replace(/\s+/g, " ").trim();
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=skill-render.js.map
|
package/lib/skill.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
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
|
-
/** Render a harness skill entry as a discoverable SKILL.md document. */
|
|
7
|
-
export declare function renderSkillMarkdown(entry: HarnessEntry): string;
|
|
8
5
|
/**
|
|
9
6
|
* Apply the skill-kind edits of an applied refinement to the skills root.
|
|
10
7
|
* Returns materialization warnings (rendered-SKILL.md mechanical problems
|
package/lib/skill.js
CHANGED
|
@@ -9,11 +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
|
+
import { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
|
|
12
13
|
import { skillResourceRefs, validateRenderedSkill } from "./skillquality.js";
|
|
13
|
-
|
|
14
|
-
export function skillNameOf(id) {
|
|
15
|
-
return id.toLowerCase().replace(/_/g, "-");
|
|
16
|
-
}
|
|
14
|
+
export { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
|
|
17
15
|
/** Resolve and defend the skill directory for an entry id. */
|
|
18
16
|
export function skillDir(skillsRoot, id) {
|
|
19
17
|
const root = resolve(skillsRoot);
|
|
@@ -23,28 +21,6 @@ export function skillDir(skillsRoot, id) {
|
|
|
23
21
|
}
|
|
24
22
|
return dir;
|
|
25
23
|
}
|
|
26
|
-
/** Render a harness skill entry as a discoverable SKILL.md document. */
|
|
27
|
-
export function renderSkillMarkdown(entry) {
|
|
28
|
-
const lines = [
|
|
29
|
-
"---",
|
|
30
|
-
`name: ${skillNameOf(entry.id)}`,
|
|
31
|
-
`description: ${oneLine(entry.title)}`,
|
|
32
|
-
"---",
|
|
33
|
-
"",
|
|
34
|
-
entry.content.trim(),
|
|
35
|
-
];
|
|
36
|
-
const reference = entry.reference;
|
|
37
|
-
if (reference && typeof reference === "object" && Object.keys(reference).length > 0) {
|
|
38
|
-
lines.push("", "## Invocation");
|
|
39
|
-
for (const [key, value] of Object.entries(reference)) {
|
|
40
|
-
lines.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (Object.keys(entry.arguments).length > 0) {
|
|
44
|
-
lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
|
|
45
|
-
}
|
|
46
|
-
return `${lines.join("\n").trimEnd()}\n`;
|
|
47
|
-
}
|
|
48
24
|
/**
|
|
49
25
|
* Apply the skill-kind edits of an applied refinement to the skills root.
|
|
50
26
|
* Returns materialization warnings (rendered-SKILL.md mechanical problems
|
|
@@ -101,7 +77,4 @@ function removeSkill(skillsRoot, id) {
|
|
|
101
77
|
rmSync(dir, { recursive: true, force: true });
|
|
102
78
|
}
|
|
103
79
|
}
|
|
104
|
-
function oneLine(text) {
|
|
105
|
-
return text.replace(/\s+/g, " ").trim();
|
|
106
|
-
}
|
|
107
80
|
//# sourceMappingURL=skill.js.map
|
package/lib/skillquality.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { HarnessEntry } from "./types.js";
|
|
2
|
-
import { skillNameOf } from "./skill.js";
|
|
3
2
|
/** Relative location of the skill-creator template facts. */
|
|
4
3
|
export declare const SKILL_CREATOR_TEMPLATE_REL: string;
|
|
5
4
|
/**
|
|
@@ -77,5 +76,5 @@ export declare function validateRenderedSkill(entry: HarnessEntry): string[];
|
|
|
77
76
|
*/
|
|
78
77
|
export declare function skillResourceRefs(content: string): string[];
|
|
79
78
|
/** Kebab-case name under which the entry materializes (exported for diagnostics). */
|
|
80
|
-
export { skillNameOf };
|
|
79
|
+
export { skillNameOf } from "./skill-render.js";
|
|
81
80
|
//# sourceMappingURL=skillquality.d.ts.map
|
package/lib/skillquality.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { existsSync, readFileSync } from "node:fs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
-
import { renderSkillMarkdown
|
|
24
|
+
import { renderSkillMarkdown } from "./skill-render.js";
|
|
25
25
|
/** Skill-name regex the platform enforces (skill-filesystem). */
|
|
26
26
|
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
27
27
|
const TRUE_WORDS = new Set(["true", "yes", "on", "1"]);
|
|
@@ -307,5 +307,5 @@ export function skillResourceRefs(content) {
|
|
|
307
307
|
return [...refs];
|
|
308
308
|
}
|
|
309
309
|
/** Kebab-case name under which the entry materializes (exported for diagnostics). */
|
|
310
|
-
export { skillNameOf };
|
|
310
|
+
export { skillNameOf } from "./skill-render.js";
|
|
311
311
|
//# sourceMappingURL=skillquality.js.map
|
package/lib/state.js
CHANGED
|
@@ -80,8 +80,13 @@ export function loadHarnessState(stateDir, scope = "global") {
|
|
|
80
80
|
scope: normalizeScope(entry["scope"], scope),
|
|
81
81
|
reference: objectRecord(entry["reference"]) ?? {},
|
|
82
82
|
arguments: objectRecord(entry["arguments"]) ?? {},
|
|
83
|
+
// skill_kind must survive persistence: /evolve mount and the
|
|
84
|
+
// listing render branch on it (guidance vs executable).
|
|
85
|
+
...(entry["skill_kind"] === "guidance" || entry["skill_kind"] === "executable"
|
|
86
|
+
? { skill_kind: entry["skill_kind"] }
|
|
87
|
+
: {}),
|
|
83
88
|
metadata: objectRecord(entry["metadata"]) ?? {},
|
|
84
|
-
source:
|
|
89
|
+
source: "evolve",
|
|
85
90
|
created_at: typeof entry["created_at"] === "string" ? entry["created_at"] : new Date(0).toISOString(),
|
|
86
91
|
updated_at: typeof entry["updated_at"] === "string" ? entry["updated_at"] : new Date(0).toISOString(),
|
|
87
92
|
version: typeof entry["version"] === "number" ? entry["version"] : 1,
|
package/lib/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HarnessScope,
|
|
1
|
+
import type { HarnessScope, RefinementResult } from "./types.js";
|
|
2
2
|
export declare const EVOLVE_DIR = "evolve";
|
|
3
3
|
export interface StorePaths {
|
|
4
4
|
/** Directory holding harness_state.json. */
|
|
@@ -15,6 +15,4 @@ export declare function snapshotBefore(paths: StorePaths, refinementId: string):
|
|
|
15
15
|
export declare function appendResult(paths: StorePaths, result: RefinementResult): void;
|
|
16
16
|
/** Read the applied results history; malformed lines are skipped, never fatal. */
|
|
17
17
|
export declare function loadResults(paths: StorePaths): RefinementResult[];
|
|
18
|
-
/** Load a state file into memory, returning empty state when absent. */
|
|
19
|
-
export declare function loadStateFile(paths: StorePaths): HarnessState;
|
|
20
18
|
//# sourceMappingURL=store.d.ts.map
|
package/lib/store.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import { emptyHarnessState } from "./types.js";
|
|
18
17
|
import { stateFilePath } from "./state.js";
|
|
19
18
|
export const EVOLVE_DIR = "evolve";
|
|
20
19
|
export function storePaths(baseDir, scope, sessionId) {
|
|
@@ -65,10 +64,4 @@ export function loadResults(paths) {
|
|
|
65
64
|
function isResult(data) {
|
|
66
65
|
return typeof data === "object" && data !== null && "id" in data && "appliedEdits" in data;
|
|
67
66
|
}
|
|
68
|
-
/** Load a state file into memory, returning empty state when absent. */
|
|
69
|
-
export function loadStateFile(paths) {
|
|
70
|
-
return existsSync(stateFilePath(paths.stateDir))
|
|
71
|
-
? JSON.parse(readFileSync(stateFilePath(paths.stateDir), "utf8"))
|
|
72
|
-
: emptyHarnessState();
|
|
73
|
-
}
|
|
74
67
|
//# sourceMappingURL=store.js.map
|
package/lib/tool.js
CHANGED
|
@@ -2,6 +2,8 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
2
2
|
import { formatHarnessStateForPrompt } from "./render.js";
|
|
3
3
|
import { requireGlobalApproval } from "./approval.js";
|
|
4
4
|
import { entrySourceOf } from "./source.js";
|
|
5
|
+
import { getUsageCount, loadUsage } from "./usage.js";
|
|
6
|
+
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
5
7
|
const SCOPES = ["local", "global"];
|
|
6
8
|
/** Accept both the boolean tool parameter (`global: true`) and the string form. */
|
|
7
9
|
export function scopeOf(value, fallback) {
|
|
@@ -32,7 +34,22 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
32
34
|
execute: async (args, exec) => {
|
|
33
35
|
const scope = scopeOf(args.scope, "local");
|
|
34
36
|
const state = engine.load(scope, sessionIdOf(exec));
|
|
35
|
-
|
|
37
|
+
const text = formatHarnessStateForPrompt(state);
|
|
38
|
+
// Append injection usage counts (gap B1).
|
|
39
|
+
const usage = loadUsage(engine.baseDir);
|
|
40
|
+
const usageLines = [];
|
|
41
|
+
for (const kind of Object.keys(state.entries)) {
|
|
42
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
43
|
+
const count = getUsageCount(usage, kind, entry.id);
|
|
44
|
+
if (count > 0) {
|
|
45
|
+
usageLines.push(`${kind}:${entry.id} — injected ${count}×`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (usageLines.length > 0) {
|
|
50
|
+
return textResult(`${text}\n\n# Injection Usage\n${usageLines.join("\n")}`);
|
|
51
|
+
}
|
|
52
|
+
return textResult(text);
|
|
36
53
|
},
|
|
37
54
|
}));
|
|
38
55
|
ctx.tools.register(defineTool({
|
|
@@ -154,6 +171,10 @@ function applyEditsText(engine, scope, sessionId, edits, agent) {
|
|
|
154
171
|
: { scope });
|
|
155
172
|
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
156
173
|
const failed = result.appliedEdits.filter((e) => !e.applied);
|
|
174
|
+
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
175
|
+
if (applied.length > 0 && sessionId) {
|
|
176
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", sessionId));
|
|
177
|
+
}
|
|
157
178
|
const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
|
|
158
179
|
for (const e of applied) {
|
|
159
180
|
lines.push(`- ${e.action} ${e.kind}:${e.id} (v${(e.after?.version ?? e.before?.version) ?? "?"})`);
|
package/lib/types.d.ts
CHANGED
|
@@ -124,6 +124,14 @@ export interface RefinementEdit {
|
|
|
124
124
|
skill_kind?: SkillKind;
|
|
125
125
|
metadata?: Record<string, unknown>;
|
|
126
126
|
reason?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Gap C2: blast-radius annotation — how broadly this edit applies.
|
|
129
|
+
* Values: "general" (cross-project tactical), "project" (single project),
|
|
130
|
+
* "session" (one-off session-specific). The review gate checks that
|
|
131
|
+
* local-scope edits are "session" or "project" and global-scope edits
|
|
132
|
+
* are "general" or "project".
|
|
133
|
+
*/
|
|
134
|
+
blastRadius?: "general" | "project" | "session";
|
|
127
135
|
}
|
|
128
136
|
/** The structured output of a planning pass. */
|
|
129
137
|
export interface RefinementProposal {
|
package/lib/usage.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { HarnessState, RefinementKind } from "./types.js";
|
|
2
|
+
export interface UsageStore {
|
|
3
|
+
/** Injection count per entry key (`kind:id`). */
|
|
4
|
+
counts: Record<string, number>;
|
|
5
|
+
/** Session dedup marker: the last session id each key was counted in (v2). */
|
|
6
|
+
lastSession?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load the usage store from disk; returns an empty store when absent or
|
|
10
|
+
* corrupt. Accepts BOTH on-disk shapes:
|
|
11
|
+
* - legacy (≤0.3.x): a flat `{ "kind:id": count }` map,
|
|
12
|
+
* - v2: `{ version: 2, counts, lastSession }` with per-session dedup.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadUsage(baseDir: string): UsageStore;
|
|
15
|
+
/** Persist the usage store atomically (always the v2 shape). */
|
|
16
|
+
export declare function saveUsage(baseDir: string, store: UsageStore): void;
|
|
17
|
+
/** Build the usage key for an entry. */
|
|
18
|
+
export declare function usageKey(kind: RefinementKind, id: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Increment injection counts for the entries that were actually injected.
|
|
21
|
+
* Called after `entriesSectionText` renders the injected block. Keys not
|
|
22
|
+
* present in the store are initialized to 1.
|
|
23
|
+
*
|
|
24
|
+
* Session dedup (2026-08-22): with a sessionId, each key counts AT MOST
|
|
25
|
+
* ONCE per session — the old per-build counting produced meaningless
|
|
26
|
+
* numbers (one entry hit 2311× in a week) and hid the real "how many
|
|
27
|
+
* sessions found this useful" signal that staleness decay needs. Without
|
|
28
|
+
* a sessionId the call degrades to legacy always-increment behavior.
|
|
29
|
+
*/
|
|
30
|
+
export declare function recordInjection(baseDir: string, injectedKeys: readonly string[], sessionId?: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get the injection count for a specific entry. Returns 0 when the entry
|
|
33
|
+
* has never been injected (absent from the store).
|
|
34
|
+
*/
|
|
35
|
+
export declare function getUsageCount(store: UsageStore, kind: RefinementKind, id: string): number;
|
|
36
|
+
/**
|
|
37
|
+
* Find entries with zero injection usage. Returns `{kind, id, title}` for
|
|
38
|
+
* each entry that has never been injected — prime candidates for archival.
|
|
39
|
+
*/
|
|
40
|
+
export declare function zeroUsageEntries(state: HarnessState, store: UsageStore): {
|
|
41
|
+
kind: RefinementKind;
|
|
42
|
+
id: string;
|
|
43
|
+
title: string;
|
|
44
|
+
}[];
|
|
45
|
+
//# sourceMappingURL=usage.d.ts.map
|