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.
- package/README.md +42 -115
- package/README.zh.md +33 -93
- package/client/client.js +253 -0
- package/cordis.patch.yml +2 -2
- package/lib/card-routes.d.ts +32 -0
- package/lib/card-routes.js +92 -0
- package/lib/index.d.ts +65 -217
- package/lib/index.js +74 -234
- package/lib/known-workspaces.d.ts +22 -0
- package/lib/known-workspaces.js +42 -0
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +34 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/lib/memory-snapshot.d.ts +54 -0
- package/lib/memory-snapshot.js +125 -0
- package/package.json +8 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -73
- package/lib/approval.js +0 -127
- package/lib/audit-log.d.ts +0 -19
- package/lib/audit-log.js +0 -52
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command-util.d.ts +0 -70
- package/lib/command-util.js +0 -123
- package/lib/command.d.ts +0 -13
- package/lib/command.js +0 -134
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -37
- package/lib/copy.js +0 -58
- package/lib/declines.d.ts +0 -103
- package/lib/declines.js +0 -179
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -60
- package/lib/extraction-phase.d.ts +0 -132
- package/lib/extraction-phase.js +0 -236
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -90
- package/lib/inject.js +0 -164
- package/lib/injection-caps.d.ts +0 -4
- package/lib/injection-caps.js +0 -4
- package/lib/injection-directory.d.ts +0 -29
- package/lib/injection-directory.js +0 -78
- package/lib/injection-rank.d.ts +0 -85
- package/lib/injection-rank.js +0 -161
- package/lib/listener.d.ts +0 -45
- package/lib/listener.js +0 -199
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/maintenance-commands.d.ts +0 -23
- package/lib/maintenance-commands.js +0 -136
- package/lib/memory-agent.d.ts +0 -93
- package/lib/memory-agent.js +0 -300
- package/lib/memory-apply.d.ts +0 -44
- package/lib/memory-apply.js +0 -202
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/memory-manifest.d.ts +0 -16
- package/lib/memory-manifest.js +0 -70
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -48
- package/lib/notify.js +0 -57
- package/lib/plan-command.d.ts +0 -12
- package/lib/plan-command.js +0 -38
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -79
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -183
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime-commands.d.ts +0 -14
- package/lib/runtime-commands.js +0 -115
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -63
- package/lib/service.js +0 -253
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -61
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -78
- package/lib/turn-snapshot.js +0 -252
- package/lib/types.d.ts +0 -218
- package/lib/types.js +0 -133
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-candidates.d.ts +0 -127
- package/lib/wrapup-candidates.js +0 -161
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup-proposals.d.ts +0 -101
- package/lib/wrapup-proposals.js +0 -281
- package/lib/wrapup.d.ts +0 -56
- 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
|
package/lib/skill-render.d.ts
DELETED
|
@@ -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
|
package/lib/skill-render.js
DELETED
|
@@ -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
|