dsh-continual-evolve 0.11.0 → 0.12.1

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 (161) hide show
  1. package/README.md +42 -115
  2. package/README.zh.md +33 -93
  3. package/client/client.js +253 -0
  4. package/cordis.patch.yml +2 -2
  5. package/lib/card-routes.d.ts +32 -0
  6. package/lib/card-routes.js +92 -0
  7. package/lib/index.d.ts +65 -217
  8. package/lib/index.js +74 -234
  9. package/lib/known-workspaces.d.ts +22 -0
  10. package/lib/known-workspaces.js +42 -0
  11. package/lib/memory-guide.d.ts +13 -14
  12. package/lib/memory-guide.js +34 -21
  13. package/lib/memory-section.d.ts +77 -0
  14. package/lib/memory-section.js +189 -0
  15. package/lib/memory-snapshot.d.ts +54 -0
  16. package/lib/memory-snapshot.js +125 -0
  17. package/package.json +8 -17
  18. package/lib/apply.d.ts +0 -22
  19. package/lib/apply.js +0 -157
  20. package/lib/approval.d.ts +0 -73
  21. package/lib/approval.js +0 -127
  22. package/lib/audit-log.d.ts +0 -19
  23. package/lib/audit-log.js +0 -52
  24. package/lib/auto.d.ts +0 -236
  25. package/lib/auto.js +0 -773
  26. package/lib/autocase.d.ts +0 -38
  27. package/lib/autocase.js +0 -76
  28. package/lib/benchmark-command.d.ts +0 -9
  29. package/lib/benchmark-command.js +0 -357
  30. package/lib/benchmark.d.ts +0 -151
  31. package/lib/benchmark.js +0 -250
  32. package/lib/command-util.d.ts +0 -70
  33. package/lib/command-util.js +0 -123
  34. package/lib/command.d.ts +0 -13
  35. package/lib/command.js +0 -134
  36. package/lib/consolidate.d.ts +0 -90
  37. package/lib/consolidate.js +0 -180
  38. package/lib/copy.d.ts +0 -37
  39. package/lib/copy.js +0 -58
  40. package/lib/declines.d.ts +0 -103
  41. package/lib/declines.js +0 -179
  42. package/lib/evaluate.d.ts +0 -74
  43. package/lib/evaluate.js +0 -271
  44. package/lib/evolve-event.d.ts +0 -40
  45. package/lib/evolve-event.js +0 -60
  46. package/lib/extraction-phase.d.ts +0 -132
  47. package/lib/extraction-phase.js +0 -236
  48. package/lib/failures.d.ts +0 -42
  49. package/lib/failures.js +0 -171
  50. package/lib/fate.d.ts +0 -130
  51. package/lib/fate.js +0 -350
  52. package/lib/goal-command.d.ts +0 -7
  53. package/lib/goal-command.js +0 -37
  54. package/lib/goal.d.ts +0 -72
  55. package/lib/goal.js +0 -72
  56. package/lib/inject.d.ts +0 -90
  57. package/lib/inject.js +0 -164
  58. package/lib/injection-caps.d.ts +0 -4
  59. package/lib/injection-caps.js +0 -4
  60. package/lib/injection-directory.d.ts +0 -29
  61. package/lib/injection-directory.js +0 -78
  62. package/lib/injection-rank.d.ts +0 -85
  63. package/lib/injection-rank.js +0 -161
  64. package/lib/listener.d.ts +0 -45
  65. package/lib/listener.js +0 -199
  66. package/lib/llm-text.d.ts +0 -104
  67. package/lib/llm-text.js +0 -140
  68. package/lib/logfile.d.ts +0 -71
  69. package/lib/logfile.js +0 -159
  70. package/lib/maintenance-commands.d.ts +0 -23
  71. package/lib/maintenance-commands.js +0 -136
  72. package/lib/memory-agent.d.ts +0 -93
  73. package/lib/memory-agent.js +0 -300
  74. package/lib/memory-apply.d.ts +0 -44
  75. package/lib/memory-apply.js +0 -202
  76. package/lib/memory-benchmark.d.ts +0 -76
  77. package/lib/memory-benchmark.js +0 -170
  78. package/lib/memory-index.d.ts +0 -114
  79. package/lib/memory-index.js +0 -197
  80. package/lib/memory-manifest.d.ts +0 -16
  81. package/lib/memory-manifest.js +0 -70
  82. package/lib/message-source.d.ts +0 -23
  83. package/lib/message-source.js +0 -12
  84. package/lib/mount-command.d.ts +0 -10
  85. package/lib/mount-command.js +0 -48
  86. package/lib/mount.d.ts +0 -42
  87. package/lib/mount.js +0 -212
  88. package/lib/notify.d.ts +0 -48
  89. package/lib/notify.js +0 -57
  90. package/lib/plan-command.d.ts +0 -12
  91. package/lib/plan-command.js +0 -38
  92. package/lib/plan.d.ts +0 -16
  93. package/lib/plan.js +0 -126
  94. package/lib/planner.d.ts +0 -61
  95. package/lib/planner.js +0 -144
  96. package/lib/pool.d.ts +0 -7
  97. package/lib/pool.js +0 -25
  98. package/lib/prefix-cache.d.ts +0 -79
  99. package/lib/prefix-cache.js +0 -168
  100. package/lib/project.d.ts +0 -28
  101. package/lib/project.js +0 -66
  102. package/lib/projection.d.ts +0 -22
  103. package/lib/projection.js +0 -112
  104. package/lib/promotion.d.ts +0 -94
  105. package/lib/promotion.js +0 -171
  106. package/lib/recall.d.ts +0 -85
  107. package/lib/recall.js +0 -142
  108. package/lib/record-language.d.ts +0 -85
  109. package/lib/record-language.js +0 -152
  110. package/lib/render.d.ts +0 -13
  111. package/lib/render.js +0 -80
  112. package/lib/review-scheduler.d.ts +0 -38
  113. package/lib/review-scheduler.js +0 -183
  114. package/lib/review.d.ts +0 -60
  115. package/lib/review.js +0 -145
  116. package/lib/rollback.d.ts +0 -9
  117. package/lib/rollback.js +0 -68
  118. package/lib/rubric.d.ts +0 -29
  119. package/lib/rubric.js +0 -119
  120. package/lib/runtime-commands.d.ts +0 -14
  121. package/lib/runtime-commands.js +0 -115
  122. package/lib/runtime.d.ts +0 -24
  123. package/lib/runtime.js +0 -87
  124. package/lib/score.d.ts +0 -64
  125. package/lib/score.js +0 -191
  126. package/lib/search.d.ts +0 -83
  127. package/lib/search.js +0 -136
  128. package/lib/service.d.ts +0 -63
  129. package/lib/service.js +0 -253
  130. package/lib/skill-render.d.ts +0 -23
  131. package/lib/skill-render.js +0 -69
  132. package/lib/skill.d.ts +0 -15
  133. package/lib/skill.js +0 -80
  134. package/lib/skillquality.d.ts +0 -85
  135. package/lib/skillquality.js +0 -320
  136. package/lib/source.d.ts +0 -30
  137. package/lib/source.js +0 -43
  138. package/lib/state.d.ts +0 -26
  139. package/lib/state.js +0 -151
  140. package/lib/store.d.ts +0 -61
  141. package/lib/store.js +0 -164
  142. package/lib/token-usage.d.ts +0 -68
  143. package/lib/token-usage.js +0 -211
  144. package/lib/tool.d.ts +0 -34
  145. package/lib/tool.js +0 -320
  146. package/lib/turn-snapshot.d.ts +0 -78
  147. package/lib/turn-snapshot.js +0 -252
  148. package/lib/types.d.ts +0 -218
  149. package/lib/types.js +0 -133
  150. package/lib/usage.d.ts +0 -36
  151. package/lib/usage.js +0 -98
  152. package/lib/validate.d.ts +0 -28
  153. package/lib/validate.js +0 -181
  154. package/lib/wrapup-candidates.d.ts +0 -127
  155. package/lib/wrapup-candidates.js +0 -161
  156. package/lib/wrapup-command.d.ts +0 -10
  157. package/lib/wrapup-command.js +0 -243
  158. package/lib/wrapup-proposals.d.ts +0 -101
  159. package/lib/wrapup-proposals.js +0 -281
  160. package/lib/wrapup.d.ts +0 -56
  161. package/lib/wrapup.js +0 -105
package/lib/search.js DELETED
@@ -1,136 +0,0 @@
1
- /** BM25 term-saturation constant (Okapi k1). */
2
- export const BM25_K1 = 1.5;
3
- /** BM25 document-length normalization constant (Okapi b). */
4
- export const BM25_B = 0.75;
5
- /** Multiplier for title-field matches (title hits outweigh body hits 2:1). */
6
- export const TITLE_FIELD_WEIGHT = 2;
7
- /**
8
- * One pass, two run shapes: ASCII alphanumeric words, or maximal CJK runs.
9
- * Runs never mix scripts because the alternation splits at the boundary, so
10
- * 「深色主题dark主题」 tokenizes as 深色/色主/主题, "dark", 主题.
11
- */
12
- const TOKEN_RUN_PATTERN = /[a-z0-9]+|[\u4e00-\u9fff]+/g;
13
- const ASCII_RUN_PATTERN = /^[a-z0-9]+$/;
14
- /**
15
- * Tokenize text for relevance scoring: lowercase ASCII runs become word
16
- * tokens; CJK runs become overlapping character bigrams (a single-character
17
- * run stays a unigram). Deterministic and allocation-light — safe to call per
18
- * injection build.
19
- *
20
- * @param text Arbitrary entry or query text.
21
- * @returns Tokens in occurrence order (duplicates kept; consumers dedupe when
22
- * order-independent weighting is wanted).
23
- */
24
- export function tokenize(text) {
25
- const tokens = [];
26
- for (const match of text.toLowerCase().matchAll(TOKEN_RUN_PATTERN)) {
27
- const run = match[0];
28
- if (ASCII_RUN_PATTERN.test(run)) {
29
- tokens.push(run);
30
- }
31
- else if (run.length === 1) {
32
- tokens.push(run);
33
- }
34
- else {
35
- for (let i = 0; i < run.length - 1; i += 1) {
36
- tokens.push(run.slice(i, i + 2));
37
- }
38
- }
39
- }
40
- return tokens;
41
- }
42
- function buildFieldStats(fieldTokens) {
43
- const tfs = fieldTokens.map((tokens) => {
44
- const tf = new Map();
45
- for (const token of tokens) {
46
- tf.set(token, (tf.get(token) ?? 0) + 1);
47
- }
48
- return tf;
49
- });
50
- const df = new Map();
51
- let totalLength = 0;
52
- for (let position = 0; position < tfs.length; position += 1) {
53
- const tf = tfs[position];
54
- let length = 0;
55
- for (const count of tf.values()) {
56
- length += count;
57
- }
58
- totalLength += length;
59
- for (const token of tf.keys()) {
60
- df.set(token, (df.get(token) ?? 0) + 1);
61
- }
62
- }
63
- return {
64
- tfs,
65
- lengths: fieldTokens.map((tokens) => tokens.length),
66
- df,
67
- averageLength: tfs.length === 0 ? 0 : totalLength / tfs.length,
68
- };
69
- }
70
- /**
71
- * Build the corpus statistics (per-field tf, df, average length) for one
72
- * ranking call. Pure: the index reads its input snapshot and never mutates
73
- * the entries.
74
- *
75
- * @param entries The candidate entries being ranked this call.
76
- * @returns An index that answers {@link relevanceScore} for exactly these
77
- * entries (identity-keyed).
78
- */
79
- export function buildRelevanceIndex(entries) {
80
- const titles = [];
81
- const bodies = [];
82
- for (const entry of entries) {
83
- titles.push(tokenize(entry.title));
84
- bodies.push(tokenize(`${entry.content} ${entry.path}`));
85
- }
86
- return {
87
- size: entries.length,
88
- positions: new Map(entries.map((entry, position) => [entry, position])),
89
- titles: buildFieldStats(titles),
90
- bodies: buildFieldStats(bodies),
91
- };
92
- }
93
- /** One field's BM25 contribution for a single term (0 when the term is absent). */
94
- function bm25Term(termFrequency, documentFrequency, corpusSize, docLength, averageLength) {
95
- if (termFrequency <= 0 || documentFrequency <= 0) {
96
- return 0;
97
- }
98
- const inverseDocumentFrequency = Math.log(1 + (corpusSize - documentFrequency + 0.5) / (documentFrequency + 0.5));
99
- const normalization = averageLength > 0 ? 1 - BM25_B + BM25_B * (docLength / averageLength) : 1;
100
- return inverseDocumentFrequency * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + BM25_K1 * normalization));
101
- }
102
- /**
103
- * Field-weighted BM25 score of `entry` against `query` within `index`:
104
- * `TITLE_FIELD_WEIGHT × bm25(title) + bm25(body)`. Query tokens are deduped,
105
- * so repeating a word adds no weight.
106
- *
107
- * @returns A score > 0 when at least one query token occurs in the entry
108
- * (either field), and exactly 0 otherwise — the property rankEntries'
109
- * relevance-first ordering relies on. Never throws; an entry outside
110
- * the index simply scores 0.
111
- */
112
- export function relevanceScore(index, entry, query) {
113
- const position = index.positions.get(entry);
114
- if (position === undefined || index.size === 0) {
115
- return 0;
116
- }
117
- const seen = new Set();
118
- let titleScore = 0;
119
- let bodyScore = 0;
120
- for (const token of tokenize(query)) {
121
- if (seen.has(token)) {
122
- continue;
123
- }
124
- seen.add(token);
125
- const titleDf = index.titles.df.get(token) ?? 0;
126
- if (titleDf > 0) {
127
- titleScore += bm25Term(index.titles.tfs[position]?.get(token) ?? 0, titleDf, index.size, index.titles.lengths[position] ?? 0, index.titles.averageLength);
128
- }
129
- const bodyDf = index.bodies.df.get(token) ?? 0;
130
- if (bodyDf > 0) {
131
- bodyScore += bm25Term(index.bodies.tfs[position]?.get(token) ?? 0, bodyDf, index.size, index.bodies.lengths[position] ?? 0, index.bodies.averageLength);
132
- }
133
- }
134
- return TITLE_FIELD_WEIGHT * titleScore + bodyScore;
135
- }
136
- //# sourceMappingURL=search.js.map
package/lib/service.d.ts DELETED
@@ -1,63 +0,0 @@
1
- /**
2
- * The evolution engine: the only entry point that mutates harness state.
3
- * Every mutation path goes through here so snapshot-before-write, apply
4
- * accounting, persistence, and result history are enforced in one place.
5
- */
6
- import type { EntrySource, HarnessScope, HarnessState, RefinementProposal, RefinementResult } from "./types.js";
7
- import { applyRefinementProposal } from "./apply.js";
8
- import type { HistoryRetention } from "./store.js";
9
- /** A post-commit apply failure that still carries the durable applied result. */
10
- export declare class EvolutionApplyPostCommitError extends Error {
11
- readonly result: RefinementResult;
12
- readonly scope: HarnessScope;
13
- readonly storeId: string | undefined;
14
- readonly cause: unknown;
15
- constructor(message: string, result: RefinementResult, scope: HarnessScope, storeId: string | undefined, cause: unknown);
16
- }
17
- export interface ApplyContext {
18
- scope: HarnessScope;
19
- sessionId?: string;
20
- /** When set, optimistic-concurrency checks reject edits whose entries changed since this baseline. */
21
- baselineState?: Parameters<typeof applyRefinementProposal>[0];
22
- /** Trajectory citation stamped into newly created entries (see apply.ts). */
23
- source?: EntrySource | undefined;
24
- /** Marks the resulting refinement as the deterministic rollback of another (audit chain). */
25
- rollbackOf?: string | undefined;
26
- }
27
- export interface EvolutionHooks {
28
- /** Called after every applied refinement (side-effect boundary: skills sync, etc.). */
29
- onApplied?: (result: RefinementResult) => void;
30
- }
31
- export interface EvolutionEngineOptions {
32
- /**
33
- * Storage-hygiene retention (#20): how many snapshots / JSONL tail
34
- * lines each apply keeps. Resolved with generous defaults — rollback
35
- * only needs recent snapshots and readers need a working window.
36
- */
37
- historyRetain?: Partial<HistoryRetention>;
38
- }
39
- export declare function createEvolutionEngine(baseDir: string, hooks?: EvolutionHooks, opts?: EvolutionEngineOptions): {
40
- load: (scope: HarnessScope, sessionId: string | undefined) => HarnessState;
41
- apply: (scope: HarnessScope, sessionId: string | undefined, proposal: RefinementProposal, context?: ApplyContext) => RefinementResult;
42
- rollback: (scope: HarnessScope, sessionId: string | undefined, refinementId: string) => RefinementResult;
43
- rollbackResult: (scope: HarnessScope, sessionId: string | undefined, target: RefinementResult) => RefinementResult;
44
- history: (scope: HarnessScope, sessionId: string | undefined) => RefinementResult[];
45
- exportStore: (scope: HarnessScope, sessionId: string | undefined) => ExportedStore;
46
- importStore: (scope: HarnessScope, sessionId: string | undefined, payload: unknown) => {
47
- entries: number;
48
- refinements: number;
49
- };
50
- baseDir: string;
51
- retention: HistoryRetention;
52
- };
53
- /** The on-disk shape `/evolve export` writes and `/evolve import` reads. */
54
- export interface ExportedStore {
55
- version: number;
56
- scope: HarnessScope;
57
- schema: number;
58
- entries: HarnessState["entries"];
59
- refinements: HarnessState["refinements"];
60
- history: RefinementResult[];
61
- }
62
- export type EvolutionEngine = ReturnType<typeof createEvolutionEngine>;
63
- //# sourceMappingURL=service.d.ts.map
package/lib/service.js DELETED
@@ -1,253 +0,0 @@
1
- import { CONFLICT_HINT_KEY } from "./types.js";
2
- import { applyRefinementProposal } from "./apply.js";
3
- import { randomUUID } from "node:crypto";
4
- import { rollbackProposal } from "./rollback.js";
5
- import { loadHarnessState, saveHarnessState } from "./state.js";
6
- import { appendResult, loadResults, pruneJsonlFile, pruneSnapshots, resolveHistoryRetention, snapshotBefore, storePaths } from "./store.js";
7
- import { materializeMemoryProjection } from "./projection.js";
8
- import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
9
- /** A post-commit apply failure that still carries the durable applied result. */
10
- export class EvolutionApplyPostCommitError extends Error {
11
- result;
12
- scope;
13
- storeId;
14
- cause;
15
- constructor(message, result, scope, storeId, cause) {
16
- super(message);
17
- this.result = result;
18
- this.scope = scope;
19
- this.storeId = storeId;
20
- this.cause = cause;
21
- }
22
- }
23
- export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
24
- const retention = resolveHistoryRetention(opts.historyRetain);
25
- /**
26
- * Load a scope's state. For `project` the `sessionId` parameter carries
27
- * the project key (see `resolveProjectKey` in project.ts); callers
28
- * derive it with `projectKeyOf(agent)` and pass it through unchanged.
29
- */
30
- function load(scope, sessionId) {
31
- return loadHarnessState(storePaths(baseDir, scope, sessionId).stateDir, scope);
32
- }
33
- function apply(scope, sessionId, proposal, context) {
34
- const paths = storePaths(baseDir, scope, sessionId);
35
- // Working state is ALWAYS the freshest on-disk snapshot: apply mutates
36
- // and persists the state whole-file, so building on the caller's
37
- // planning-time copy would silently overwrite concurrent writers
38
- // (another gate run, another session writing global). The caller's
39
- // baselineState is ONLY the optimistic-concurrency comparison baseline
40
- // — "reject edits whose target changed since planning" (review audit
41
- // 2026-08-28 B1: the two roles were previously folded into one object,
42
- // which made the advertised guard unreachable).
43
- const state = load(scope, sessionId);
44
- // Write-time conflict guard (R2): persistent-scope creates are
45
- // checked against the existing same-kind entries BEFORE any side
46
- // effect — a near-duplicate is rejected with an actionable error
47
- // (evolve_update instead), a moderate overlap proceeds stamped with
48
- // CONFLICT_HINT_KEY. Rollbacks bypass the guard: re-creating an entry
49
- // that resembles its successor is the point of rollback. Local scope
50
- // is never blocked (scratch space); the wrapup/fate promotion path
51
- // already enforces its own overlap policy there.
52
- //
53
- // Secret-leak guard (P0, same throat): every persistent-scope create
54
- // and update is screened before any side effect. Project and global
55
- // stores both outlive the current session, so approval never turns a
56
- // credential-shaped literal into an allowed write. The screen covers
57
- // every field the edit can plant: title, content, and the JSON forms
58
- // of reference, arguments, and metadata. Fixed patterns, not policy-
59
- // configurable.
60
- const warnHits = new Map();
61
- if ((scope === "project" || scope === "global") && !context?.rollbackOf) {
62
- for (const [index, edit] of proposal.edits.entries()) {
63
- if (edit.action === "create" || edit.action === "update") {
64
- const screenable = [
65
- typeof edit.id === "string" ? edit.id : "",
66
- typeof edit.path === "string" ? edit.path : "",
67
- typeof edit.title === "string" ? edit.title : "",
68
- typeof edit.content === "string" ? edit.content : "",
69
- edit.reference !== undefined ? JSON.stringify(edit.reference) : "",
70
- edit.arguments !== undefined ? JSON.stringify(edit.arguments) : "",
71
- edit.metadata !== undefined ? JSON.stringify(edit.metadata) : "",
72
- ];
73
- const secret = secretLeakReason(screenable.join("\n"));
74
- if (secret) {
75
- throw new Error(`${edit.action} blocked: ${secret}`);
76
- }
77
- }
78
- if (edit.action !== "create")
79
- continue;
80
- // An unknown kind must fail per-edit in validateEdit, never
81
- // crash the whole proposal here (review audit 2026-08-28 S1).
82
- const corpus = state.entries[edit.kind];
83
- if (!corpus)
84
- continue;
85
- const hit = mostSimilarEntry(Object.values(corpus), edit.title ?? "", edit.content ?? "", CONFLICT_WARN_SCORE);
86
- if (!hit)
87
- continue;
88
- if (hit.score >= CONFLICT_BLOCK_SCORE) {
89
- throw new Error(`create blocked: ${buildConflictNotice(hit)} already lives in the global ${edit.kind} store — use evolve_update on it instead of adding a duplicate`);
90
- }
91
- warnHits.set(index, hit);
92
- }
93
- }
94
- const id = `evolve_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
95
- // Code-enforced snapshot: runs before any mutation, cannot be skipped by the model.
96
- snapshotBefore(paths, id);
97
- const result = applyRefinementProposal(state, proposal, {
98
- id,
99
- scope,
100
- ...(context?.source ? { source: context.source } : {}),
101
- ...(context?.baselineState ? { baselineState: context.baselineState } : {}),
102
- ...(context?.rollbackOf ? { rollbackOf: context.rollbackOf } : {}),
103
- });
104
- // Stamp warn-tier conflicts onto the freshly created entries (both the
105
- // live state and the result's after-snapshot stay coherent).
106
- for (const [index, hit] of warnHits) {
107
- const applied = result.appliedEdits[index];
108
- if (!applied?.applied || applied.action !== "create" || !applied.id)
109
- continue;
110
- const hint = `${applied.kind}:${hit.id}:${hit.score.toFixed(2)}`;
111
- const live = state.entries[applied.kind][applied.id];
112
- if (live) {
113
- live.metadata[CONFLICT_HINT_KEY] = hint;
114
- if (applied.after) {
115
- applied.after.metadata[CONFLICT_HINT_KEY] = hint;
116
- }
117
- }
118
- }
119
- saveHarnessState(paths.stateDir, state);
120
- try {
121
- appendResult(paths, result);
122
- }
123
- catch (cause) {
124
- throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but failed to append history: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
125
- }
126
- // Storage hygiene (#20): bound the append-only past at write time —
127
- // snapshots keep the newest N, the per-store history keeps its tail.
128
- // Best-effort (never throws): a prune failure must not fail the apply.
129
- try {
130
- pruneSnapshots(paths.snapshotsDir, retention.snapshots);
131
- }
132
- catch {
133
- // ignored — the next apply retries
134
- }
135
- try {
136
- pruneJsonlFile(paths.resultsPath, retention.refinements);
137
- }
138
- catch {
139
- // ignored — the next apply retries
140
- }
141
- // Readable Markdown projection (P1): rewritten from the post-apply
142
- // state whenever a memory edit landed, so rollback and archive stay
143
- // in sync through this same apply path. The model has no writer for
144
- // these files — only this engine entry point materializes them.
145
- if (result.appliedEdits.some((edit) => edit.applied && edit.kind === "memory")) {
146
- try {
147
- materializeMemoryProjection(paths.stateDir, state);
148
- }
149
- catch {
150
- // ignored — the projection is derived (JSON stays the source
151
- // of truth) and the next memory apply retries
152
- }
153
- }
154
- try {
155
- hooks.onApplied?.(result);
156
- }
157
- catch (cause) {
158
- throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but a post-commit hook failed: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
159
- }
160
- return result;
161
- }
162
- /** Roll back an already-observed result without consulting history. */
163
- function rollbackResult(scope, sessionId, target) {
164
- return apply(scope, sessionId, rollbackProposal(target), { scope, rollbackOf: target.id });
165
- }
166
- function rollback(scope, sessionId, refinementId) {
167
- const paths = storePaths(baseDir, scope, sessionId);
168
- const history = loadResults(paths);
169
- const target = history.find((item) => item.id === refinementId);
170
- if (!target) {
171
- throw new Error(`Refinement ${refinementId} not found in ${scope} history`);
172
- }
173
- return rollbackResult(scope, sessionId, target);
174
- }
175
- function history(scope, sessionId) {
176
- return loadResults(storePaths(baseDir, scope, sessionId));
177
- }
178
- /** Serialize one store (entries + refinement history) for `/evolve export`. */
179
- function exportStore(scope, sessionId) {
180
- const state = load(scope, sessionId);
181
- return {
182
- version: 1,
183
- scope,
184
- schema: state.schema,
185
- entries: state.entries,
186
- refinements: state.refinements,
187
- history: history(scope, sessionId),
188
- };
189
- }
190
- /**
191
- * Replace one store from an export payload (raw state write — the same
192
- * semantics the command always had: no snapshot, no per-edit validation,
193
- * the payload is trusted because a human handed over the file).
194
- */
195
- function importStore(scope, sessionId, payload) {
196
- if (!isValidExport(payload)) {
197
- throw new Error("invalid export file shape: expected {version, entries: {prompt, memory, skill, subagent}, refinements, history}");
198
- }
199
- const entriesBlock = payload["entries"];
200
- const state = {
201
- schema: typeof payload["schema"] === "number" ? payload["schema"] : 1,
202
- entries: {
203
- prompt: toEntryRecord(entriesBlock["prompt"]),
204
- memory: toEntryRecord(entriesBlock["memory"]),
205
- skill: toEntryRecord(entriesBlock["skill"]),
206
- subagent: toEntryRecord(entriesBlock["subagent"]),
207
- },
208
- refinements: Array.isArray(payload["refinements"]) ? payload["refinements"] : [],
209
- };
210
- const paths = storePaths(baseDir, scope, sessionId);
211
- saveHarnessState(paths.stateDir, state);
212
- let refinements = 0;
213
- if (Array.isArray(payload["history"])) {
214
- for (const result of payload["history"]) {
215
- if (isResultRecord(result)) {
216
- appendResult(paths, result);
217
- refinements += 1;
218
- }
219
- }
220
- // Storage hygiene (#20): an imported history obeys the same tail
221
- // budget as a live one.
222
- try {
223
- pruneJsonlFile(paths.resultsPath, retention.refinements);
224
- }
225
- catch {
226
- // ignored — the next apply retries
227
- }
228
- }
229
- const entries = Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
230
- return { entries, refinements };
231
- }
232
- return { load, apply, rollback, rollbackResult, history, exportStore, importStore, baseDir, retention };
233
- }
234
- function toEntryRecord(value) {
235
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
236
- return {};
237
- }
238
- return value;
239
- }
240
- function isValidExport(payload) {
241
- if (typeof payload !== "object" || payload === null || Array.isArray(payload))
242
- return false;
243
- const record = payload;
244
- const entries = record["entries"];
245
- if (typeof entries !== "object" || entries === null || Array.isArray(entries))
246
- return false;
247
- const kinds = ["prompt", "memory", "skill", "subagent"];
248
- return kinds.every((kind) => Object.prototype.hasOwnProperty.call(entries, kind));
249
- }
250
- function isResultRecord(value) {
251
- return typeof value === "object" && value !== null && "id" in value && "appliedEdits" in value;
252
- }
253
- //# sourceMappingURL=service.js.map
@@ -1,23 +0,0 @@
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
@@ -1,69 +0,0 @@
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
- const args = entry.arguments;
61
- if (args && typeof args === "object" && Object.keys(args).length > 0) {
62
- lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
63
- }
64
- return `${lines.join("\n").trimEnd()}\n`;
65
- }
66
- function oneLine(text) {
67
- return text.replace(/\s+/g, " ").trim();
68
- }
69
- //# sourceMappingURL=skill-render.js.map
package/lib/skill.d.ts DELETED
@@ -1,15 +0,0 @@
1
- import type { RefinementResult } from "./types.js";
2
- export { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
3
- /** Resolve and defend the skill directory for an entry id. */
4
- export declare function skillDir(skillsRoot: string, id: string): string;
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[];
15
- //# sourceMappingURL=skill.d.ts.map
package/lib/skill.js DELETED
@@ -1,80 +0,0 @@
1
- /**
2
- * Skill materializer: syncs skill-kind harness entries to the DSH skills
3
- * filesystem (`$DSH_HOME/skills/<kebab-name>/SKILL.md`) so the `skill` tool
4
- * and catalog can discover and load them. Writes are atomic (tmp + rename)
5
- * so the filesystem watcher never sees a partial file.
6
- *
7
- * Skill names must be kebab-case (the harness store ids are underscore slugs;
8
- * the materialized name converts `_` → `-`).
9
- */
10
- import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
11
- import { join, resolve, sep } from "node:path";
12
- import { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
13
- import { skillResourceRefs, validateRenderedSkill } from "./skillquality.js";
14
- export { renderSkillMarkdown, skillNameOf } from "./skill-render.js";
15
- /** Resolve and defend the skill directory for an entry id. */
16
- export function skillDir(skillsRoot, id) {
17
- const root = resolve(skillsRoot);
18
- const dir = resolve(join(root, skillNameOf(id)));
19
- if (dir !== root && !dir.startsWith(`${root}/`) && !dir.startsWith(`${root}${sep}`)) {
20
- throw new Error(`skill path escapes skills root: ${dir}`);
21
- }
22
- return dir;
23
- }
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
- */
33
- export function syncSkillsFromResult(skillsRoot, result) {
34
- const warnings = [];
35
- for (const edit of result.appliedEdits) {
36
- if (edit.kind !== "skill" || !edit.applied)
37
- continue;
38
- if (edit.action === "delete" || !edit.after) {
39
- removeSkill(skillsRoot, edit.id);
40
- continue;
41
- }
42
- warnings.push(...writeSkill(skillsRoot, edit.after));
43
- }
44
- return warnings;
45
- }
46
- /** Write one skill entry as a SKILL.md; returns materialization warnings. */
47
- function writeSkill(skillsRoot, entry) {
48
- const dir = skillDir(skillsRoot, entry.id);
49
- mkdirSync(dir, { recursive: true });
50
- const temp = join(dir, `SKILL.md.${process.pid}.tmp`);
51
- writeFileSync(temp, renderSkillMarkdown(entry), "utf8");
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;
73
- }
74
- function removeSkill(skillsRoot, id) {
75
- const dir = skillDir(skillsRoot, id);
76
- if (existsSync(dir)) {
77
- rmSync(dir, { recursive: true, force: true });
78
- }
79
- }
80
- //# sourceMappingURL=skill.js.map