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.
Files changed (66) hide show
  1. package/README.md +83 -371
  2. package/README.zh.md +84 -235
  3. package/lib/apply.js +8 -2
  4. package/lib/approval.d.ts +6 -0
  5. package/lib/approval.js +9 -1
  6. package/lib/auto.d.ts +55 -4
  7. package/lib/auto.js +61 -5
  8. package/lib/benchmark-command.d.ts +9 -0
  9. package/lib/benchmark-command.js +333 -0
  10. package/lib/benchmark.d.ts +70 -0
  11. package/lib/benchmark.js +107 -1
  12. package/lib/command.d.ts +3 -0
  13. package/lib/command.js +62 -441
  14. package/lib/evaluate.d.ts +7 -0
  15. package/lib/evaluate.js +22 -7
  16. package/lib/evolve-event.d.ts +38 -0
  17. package/lib/evolve-event.js +49 -0
  18. package/lib/failures.d.ts +39 -0
  19. package/lib/failures.js +170 -0
  20. package/lib/fate.d.ts +5 -2
  21. package/lib/fate.js +13 -8
  22. package/lib/goal-command.d.ts +7 -0
  23. package/lib/goal-command.js +37 -0
  24. package/lib/index.d.ts +51 -25
  25. package/lib/index.js +33 -1
  26. package/lib/inject.d.ts +24 -1
  27. package/lib/inject.js +93 -5
  28. package/lib/llm-text.d.ts +30 -0
  29. package/lib/llm-text.js +49 -0
  30. package/lib/mount-command.d.ts +10 -0
  31. package/lib/mount-command.js +48 -0
  32. package/lib/plan.js +5 -0
  33. package/lib/planner.d.ts +1 -1
  34. package/lib/planner.js +13 -39
  35. package/lib/promotion.d.ts +62 -0
  36. package/lib/promotion.js +102 -0
  37. package/lib/render.d.ts +1 -3
  38. package/lib/render.js +0 -4
  39. package/lib/review.d.ts +4 -1
  40. package/lib/review.js +10 -38
  41. package/lib/rollback.d.ts +1 -3
  42. package/lib/rollback.js +0 -8
  43. package/lib/score.d.ts +15 -0
  44. package/lib/score.js +74 -5
  45. package/lib/service.d.ts +2 -2
  46. package/lib/service.js +7 -3
  47. package/lib/skill-render.d.ts +23 -0
  48. package/lib/skill-render.js +68 -0
  49. package/lib/skill.d.ts +2 -5
  50. package/lib/skill.js +2 -29
  51. package/lib/skillquality.d.ts +1 -2
  52. package/lib/skillquality.js +2 -2
  53. package/lib/state.js +6 -1
  54. package/lib/store.d.ts +1 -3
  55. package/lib/store.js +0 -7
  56. package/lib/tool.js +22 -1
  57. package/lib/types.d.ts +8 -0
  58. package/lib/usage.d.ts +45 -0
  59. package/lib/usage.js +115 -0
  60. package/lib/validate.d.ts +12 -2
  61. package/lib/validate.js +26 -1
  62. package/lib/wrapup-command.d.ts +9 -0
  63. package/lib/wrapup-command.js +212 -0
  64. package/lib/wrapup.d.ts +29 -15
  65. package/lib/wrapup.js +69 -42
  66. 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
- if (!agent.options.provider || !agent.options.model) {
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 assembler = new BlockAssembler();
105
- for await (const chunk of ctx.llm.stream({
106
- provider: agent.options.provider,
107
- model: agent.options.model,
106
+ const text = await streamText(ctx, {
107
+ provider,
108
+ model,
108
109
  system: AUTO_REVIEW_SYSTEM_PROMPT,
109
- messages: [
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
- ...(options.signal ? { signal: options.signal } : {}),
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 { HarnessEntry, RefinementProposal, RefinementResult } from "./types.js";
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
- for (const [caseId, refScore] of Object.entries(reference.aggregate)) {
51
- if (caseId === "overall" || caseId === "failed" || caseId === "total" || refScore === null)
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
- for (const [caseId, refScore] of Object.entries(reference.aggregate)) {
97
- if (caseId === "overall" || caseId === "failed" || caseId === "total" || refScore === null)
98
- continue;
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)}_${Math.random().toString(36).slice(2, 8)}`;
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
- return apply(scope, sessionId, proposal);
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 { HarnessEntry, RefinementResult } from "./types.js";
2
- /** Convert a harness entry id (underscore slug) to a kebab-case skill name. */
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
- /** Convert a harness entry id (underscore slug) to a kebab-case skill name. */
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
@@ -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
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import { existsSync, readFileSync } from "node:fs";
23
23
  import { join } from "node:path";
24
- import { renderSkillMarkdown, skillNameOf } from "./skill.js";
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: entry["source"] === "evolve" ? "evolve" : "evolve",
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, HarnessState, RefinementResult } from "./types.js";
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
- return textResult(formatHarnessStateForPrompt(state));
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