dsh-continual-evolve 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/fate.js ADDED
@@ -0,0 +1,338 @@
1
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
+ import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals, } from "./wrapup.js";
3
+ /** Turns a declined local-fate proposal stays silent before being offered again. */
4
+ export const FATE_CONSULT_COOLDOWN_TURNS = 10;
5
+ /**
6
+ * Partition an assessed wrap-up classification into concrete fate actions,
7
+ * re-running the deterministic guards against the LIVE global store (state
8
+ * may have changed while the LLM call was in flight). Pure and unit-tested;
9
+ * mirrors the partition step of the wrap-up command.
10
+ */
11
+ export function planLocalFates(items, candidates, globalState) {
12
+ const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
13
+ const { promotable, skipped } = filterPromotable(items, globalState, candidates);
14
+ const promoteItems = promotable.filter((item) => item.verdict === "promote");
15
+ const archiveItems = items.filter((item) => item.verdict === "archive");
16
+ const splits = [];
17
+ const splitSkipped = [];
18
+ for (const item of archiveItems) {
19
+ if (!item.promote)
20
+ continue;
21
+ const candidate = byKey.get(item.key);
22
+ if (!candidate) {
23
+ splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
24
+ continue;
25
+ }
26
+ const blocked = splitPromoteBlocked(item, globalState, candidate.kind);
27
+ if (blocked) {
28
+ splitSkipped.push({ key: item.key, reason: blocked });
29
+ continue;
30
+ }
31
+ splits.push({ item, candidate });
32
+ }
33
+ const plainArchives = archiveItems.filter((item) => !item.promote);
34
+ const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
35
+ return { candidates, promotable: promoteItems, splits, silentArchives, reviewArchives, skipped, splitSkipped };
36
+ }
37
+ /**
38
+ * The cooldown key of a candidate set: the sorted `kind:id` list. The set is
39
+ * the unit of consultation — a declined proposal is not offered again within
40
+ * the cooldown window, and a changed set (new entries appeared) starts a
41
+ * fresh consultation.
42
+ */
43
+ export function fateSetKey(candidates) {
44
+ return candidates
45
+ .map((candidate) => candidateKey(candidate.kind, candidate.id))
46
+ .sort()
47
+ .join("|");
48
+ }
49
+ /**
50
+ * Whether the local-fate dimension is due for this gate run. Turn-interval
51
+ * gates respect the fate cadence (an independent counter — goal-driven
52
+ * sessions run the review EVERY round, the fate assessment must not);
53
+ * compaction is unconditional: experiences about to be summarized away get
54
+ * their fate check regardless.
55
+ */
56
+ export function fateCadenceDue(state, reason, intervalTurns) {
57
+ if (reason === "compact")
58
+ return true;
59
+ return state.turns - state.lastFateAt >= intervalTurns;
60
+ }
61
+ /**
62
+ * Ask the user whether to execute the gate's local-fate proposal. ONE dialog
63
+ * covers every governed action (promotes, split promotions, review-required
64
+ * archives) — the gate never spams questions. Conservative on every edge:
65
+ * no question service → not approved; the question call fails → not approved;
66
+ * the same candidate set was declined within the cooldown → not asked again.
67
+ * A decline records the cooldown (the consultSkillEdits pattern).
68
+ */
69
+ export async function consultLocalFates(ctx, agent, plan, gate) {
70
+ const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
71
+ if (!needsDialog) {
72
+ return { approved: true, asked: false, reason: "nothing-to-ask" };
73
+ }
74
+ const setKey = fateSetKey(plan.candidates);
75
+ const lastReject = gate.fateRejects.get(setKey);
76
+ if (lastReject !== undefined && gate.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
77
+ return { approved: false, asked: false, reason: "cooldown" };
78
+ }
79
+ const userQuestions = ctx.userQuestions;
80
+ if (!userQuestions) {
81
+ return { approved: false, asked: false, reason: "unavailable" };
82
+ }
83
+ try {
84
+ const answer = await userQuestions.ask({
85
+ questions: [
86
+ {
87
+ id: "evolve-fate-consult",
88
+ question: consultQuestion(plan),
89
+ options: [{ label: "执行" }, { label: "不执行" }],
90
+ },
91
+ ],
92
+ agent,
93
+ });
94
+ const approved = answer.answers?.find((entry) => entry.id === "evolve-fate-consult")?.selected?.includes("执行") ?? false;
95
+ if (!approved) {
96
+ gate.fateRejects.set(setKey, gate.turns);
97
+ }
98
+ return { approved, asked: true, reason: approved ? "consented" : "declined" };
99
+ }
100
+ catch {
101
+ return { approved: false, asked: true, reason: "error" };
102
+ }
103
+ }
104
+ /** The user-visible fate proposal: every governed action, with real titles. */
105
+ function consultQuestion(plan) {
106
+ const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
107
+ const lines = [];
108
+ if (plan.promotable.length > 0 || plan.splits.length > 0) {
109
+ lines.push("【提升到跨会话全局 store】");
110
+ for (const item of plan.promotable) {
111
+ lines.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
112
+ }
113
+ for (const { item } of plan.splits) {
114
+ lines.push(`- ${item.key} → 拆出提升「${item.promote?.title}」(原条目随之归档)`);
115
+ }
116
+ }
117
+ if (plan.reviewArchives.length > 0) {
118
+ lines.push("【归档(未被全局覆盖且源自真实对话,需确认)】");
119
+ for (const item of plan.reviewArchives) {
120
+ lines.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
121
+ }
122
+ }
123
+ return [
124
+ "自进化门禁检测到本会话的 local 条目需要归宿处理(提升条目将写入跨会话全局 store,归档条目隐藏但可恢复):",
125
+ ...lines,
126
+ "是否执行?",
127
+ ].join("\n");
128
+ }
129
+ /**
130
+ * Deterministically apply the fate plan. `"full"` applies everything
131
+ * (promotes, splits, silent + review archives); `"silent-only"` applies only
132
+ * the deterministic silent archives (the compaction path — nothing governed).
133
+ * Promotes go through the SAME proposals as the wrap-up command
134
+ * (wholePromoteProposals / splitPromoteProposals), so both paths write
135
+ * identical global entries and local retirement stamps.
136
+ */
137
+ export function applyLocalFates(engine, sessionId, plan, localState, mode) {
138
+ const applied = [];
139
+ const results = [];
140
+ const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
141
+ if (mode === "full") {
142
+ for (const item of plan.promotable) {
143
+ const candidate = byKey.get(item.key);
144
+ if (!candidate)
145
+ continue;
146
+ const proposals = wholePromoteProposals(item, candidate, sessionId);
147
+ const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
148
+ const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
149
+ const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
150
+ scope: "local",
151
+ baselineState: localState,
152
+ });
153
+ results.push(globalResult, localResult);
154
+ applied.push(`提升 ${item.key} → 全局 store(global:${createdId},本地副本已归档)`);
155
+ }
156
+ for (const { item, candidate } of plan.splits) {
157
+ if (!item.promote)
158
+ continue;
159
+ const proposals = splitPromoteProposals(item, candidate, sessionId);
160
+ const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
161
+ const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
162
+ const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
163
+ scope: "local",
164
+ baselineState: localState,
165
+ });
166
+ results.push(globalResult, localResult);
167
+ applied.push(`拆解 ${item.key} → 清洗「${item.promote.title}」入全局(原条目已归档)`);
168
+ }
169
+ }
170
+ const archives = mode === "full" ? [...plan.silentArchives, ...plan.reviewArchives] : plan.silentArchives;
171
+ for (const item of archives) {
172
+ const candidate = byKey.get(item.key);
173
+ if (!candidate)
174
+ continue;
175
+ const proposal = {
176
+ summary: `gate fate: archive local ${item.key} — ${item.reason}`,
177
+ rationale: item.reason,
178
+ expectedOutcome: "The entry stops being injected but stays restorable.",
179
+ edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
180
+ };
181
+ const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
182
+ results.push(result);
183
+ applied.push(`归档 ${item.key}「${candidate.title}」`);
184
+ }
185
+ return { applied, results };
186
+ }
187
+ /**
188
+ * The gate's local-fate phase. Runs after the review phase on every gate
189
+ * trigger (turn_interval / compact), subject to cadence and cooldown. All
190
+ * failures are contained and recorded — a broken fate dimension never
191
+ * disturbs the agent loop.
192
+ */
193
+ export async function runLocalFatePhase(ctx, engine, agent, config, state, reason, record) {
194
+ if (!config.localFate)
195
+ return;
196
+ const logger = ctx.logger("continual-evolve");
197
+ const sessionId = agent.id;
198
+ const localState = engine.load("local", sessionId);
199
+ const globalState = engine.load("global", undefined);
200
+ const candidates = listLocalCandidates(localState, globalState);
201
+ if (candidates.length === 0)
202
+ return;
203
+ if (!fateCadenceDue(state, reason, config.fateIntervalTurns))
204
+ return;
205
+ const setKey = fateSetKey(candidates);
206
+ const lastReject = state.fateRejects.get(setKey);
207
+ if (lastReject !== undefined && state.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
208
+ logger.info(`auto-review local-fate skipped [${sessionId}]: candidate set declined ${state.turns - lastReject} turns ago (cooldown)`);
209
+ return;
210
+ }
211
+ const turnsSinceFate = state.turns - state.lastFateAt;
212
+ state.lastFateAt = state.turns;
213
+ let assessment;
214
+ try {
215
+ assessment = await assessLocalEntries(ctx, agent, candidates);
216
+ }
217
+ catch (cause) {
218
+ const message = cause instanceof Error ? cause.message : String(cause);
219
+ logger.warn(`auto-review local-fate failed for ${sessionId}: ${message}`);
220
+ record({
221
+ sessionId,
222
+ reason,
223
+ turnsSinceLastReview: turnsSinceFate,
224
+ outcome: "failed",
225
+ rationale: `fate assessment error: ${message}`,
226
+ });
227
+ return;
228
+ }
229
+ const plan = planLocalFates(assessment.items, candidates, globalState);
230
+ const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
231
+ let consent = { approved: false, asked: false, reason: "nothing-to-ask" };
232
+ if (reason !== "compact" && needsDialog) {
233
+ consent = await consultLocalFates(ctx, agent, plan, state);
234
+ }
235
+ if (consent.approved) {
236
+ const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "full");
237
+ logger.info(`auto-review local-fate approved (${reason}) [${sessionId}]: ${plan.promotable.length} promoted, ${plan.splits.length} split, ${plan.reviewArchives.length} review-archived, ${plan.silentArchives.length} silent-archived — ${assessment.rationale}`);
238
+ record({
239
+ sessionId,
240
+ reason,
241
+ turnsSinceLastReview: turnsSinceFate,
242
+ outcome: "approved",
243
+ rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
244
+ refinementId: results.map((result) => result.id).join(","),
245
+ });
246
+ if (config.notifyOnAutoReview && reason === "turn_interval" && applied.length > 0) {
247
+ notifyFateApplied(ctx, agent, applied);
248
+ }
249
+ return;
250
+ }
251
+ if (consent.reason === "declined" || consent.reason === "unavailable" || consent.reason === "error") {
252
+ const withheld = consent.reason;
253
+ const outcome = withheld === "declined" ? "declined" : "deferred";
254
+ logger.info(`auto-review local-fate ${outcome} (${reason}) [${sessionId}]: ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld — ${assessment.rationale}`);
255
+ record({
256
+ sessionId,
257
+ reason,
258
+ turnsSinceLastReview: turnsSinceFate,
259
+ outcome,
260
+ rationale: `fate ${withheld}: ${assessment.rationale} (${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld)`,
261
+ });
262
+ return;
263
+ }
264
+ if (reason === "compact" && needsDialog) {
265
+ // Compaction: no dialog. Only deterministic silent archives apply;
266
+ // governed actions are deferred with an audit record.
267
+ const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
268
+ if (applied.length > 0) {
269
+ logger.info(`auto-review local-fate (compact) [${sessionId}]: silent-archived ${applied.length} — ${assessment.rationale}`);
270
+ record({
271
+ sessionId,
272
+ reason,
273
+ turnsSinceLastReview: turnsSinceFate,
274
+ outcome: "approved",
275
+ rationale: `fate (compact): ${assessment.rationale} (${applied.join("; ")})`,
276
+ refinementId: results.map((result) => result.id).join(","),
277
+ });
278
+ }
279
+ record({
280
+ sessionId,
281
+ reason,
282
+ turnsSinceLastReview: turnsSinceFate,
283
+ outcome: "deferred",
284
+ rationale: `fate (compact): ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives deferred — run /evolve wrapup for a full session exit`,
285
+ });
286
+ return;
287
+ }
288
+ // Silent archives on a plain turn-interval gate with nothing to ask.
289
+ if (plan.silentArchives.length > 0) {
290
+ const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
291
+ logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} covered/operational entries — ${assessment.rationale}`);
292
+ record({
293
+ sessionId,
294
+ reason,
295
+ turnsSinceLastReview: turnsSinceFate,
296
+ outcome: "approved",
297
+ rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
298
+ refinementId: results.map((result) => result.id).join(","),
299
+ });
300
+ if (config.notifyOnAutoReview && reason === "turn_interval") {
301
+ notifyFateApplied(ctx, agent, applied);
302
+ }
303
+ return;
304
+ }
305
+ // Assessed, nothing to do.
306
+ logger.info(`auto-review local-fate assessed (${reason}) [${sessionId}]: ${candidates.length} candidates, no action — ${assessment.rationale}`);
307
+ record({
308
+ sessionId,
309
+ reason,
310
+ turnsSinceLastReview: turnsSinceFate,
311
+ outcome: "assessed",
312
+ rationale: `fate: ${assessment.rationale} (${candidates.length} candidates, no action)`,
313
+ });
314
+ }
315
+ /** The user-visible notice after the gate applied local-fate actions. */
316
+ export function buildFateNotice(applied) {
317
+ return [
318
+ "🔎 自动进化门禁:本会话 local 条目归宿处理完成:",
319
+ applied.map((line) => `- ${line}`).join("\n"),
320
+ "查看全部条目:/evolve list;撤销:/evolve rollback <refinement id>",
321
+ "请用一句话简短确认即可,不要调用任何工具。",
322
+ ].join("\n");
323
+ }
324
+ /** Queue the follow-up notice turn (turn_interval only, like the review notice). */
325
+ function notifyFateApplied(ctx, agent, applied) {
326
+ try {
327
+ agent.followup(createUserMessage({
328
+ content: [{ type: "text", text: buildFateNotice(applied) }],
329
+ source: { kind: "plugin", plugin: "dsh-continual-evolve" },
330
+ }));
331
+ }
332
+ catch (cause) {
333
+ ctx
334
+ .logger("continual-evolve")
335
+ .warn(`local-fate notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
336
+ }
337
+ }
338
+ //# sourceMappingURL=fate.js.map
package/lib/index.d.ts CHANGED
@@ -34,6 +34,17 @@ export declare const Config: z<Schemastery.ObjectS<{
34
34
  logMaxBytes: z<number, number>;
35
35
  /** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
36
36
  autoRollbackOnReject: z<boolean, boolean>;
37
+ /**
38
+ * Gate local-fate dimension (#11 P2): the gate audits the session's local
39
+ * entries on its own cadence and proposes promote/archive — consulted
40
+ * first, never written silently. Only meaningful with autoReview on.
41
+ */
42
+ localFate: z<boolean, boolean>;
43
+ /**
44
+ * Minimum turns between local-fate assessments on the turn-interval path
45
+ * (compaction is unconditional). Absent → follows reviewIntervalTurns.
46
+ */
47
+ fateIntervalTurns: z<number, number>;
37
48
  }>, Schemastery.ObjectT<{
38
49
  /** Root for evolution stores; defaults to the resolved DSH home. */
39
50
  baseDir: z<string, string>;
@@ -63,6 +74,17 @@ export declare const Config: z<Schemastery.ObjectS<{
63
74
  logMaxBytes: z<number, number>;
64
75
  /** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
65
76
  autoRollbackOnReject: z<boolean, boolean>;
77
+ /**
78
+ * Gate local-fate dimension (#11 P2): the gate audits the session's local
79
+ * entries on its own cadence and proposes promote/archive — consulted
80
+ * first, never written silently. Only meaningful with autoReview on.
81
+ */
82
+ localFate: z<boolean, boolean>;
83
+ /**
84
+ * Minimum turns between local-fate assessments on the turn-interval path
85
+ * (compaction is unconditional). Absent → follows reviewIntervalTurns.
86
+ */
87
+ fateIntervalTurns: z<number, number>;
66
88
  }>>;
67
89
  /** Structurally typed resolved config (loader passes the validated object). */
68
90
  export interface EvolveConfig {
@@ -84,6 +106,10 @@ export interface EvolveConfig {
84
106
  logMaxBytes?: number;
85
107
  /** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
86
108
  autoRollbackOnReject?: boolean;
109
+ /** Gate local-fate dimension: propose promote/archive of local entries automatically. */
110
+ localFate?: boolean;
111
+ /** Minimum turns between local-fate assessments (compaction is unconditional). */
112
+ fateIntervalTurns?: number;
87
113
  }
88
114
  export interface EvolutionService {
89
115
  readonly engine: EvolutionEngine;
package/lib/index.js CHANGED
@@ -52,6 +52,17 @@ export const Config = z.object({
52
52
  logMaxBytes: z.natural().default(5 * 1024 * 1024),
53
53
  /** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
54
54
  autoRollbackOnReject: z.boolean().default(true),
55
+ /**
56
+ * Gate local-fate dimension (#11 P2): the gate audits the session's local
57
+ * entries on its own cadence and proposes promote/archive — consulted
58
+ * first, never written silently. Only meaningful with autoReview on.
59
+ */
60
+ localFate: z.boolean().default(true),
61
+ /**
62
+ * Minimum turns between local-fate assessments on the turn-interval path
63
+ * (compaction is unconditional). Absent → follows reviewIntervalTurns.
64
+ */
65
+ fateIntervalTurns: z.natural(),
55
66
  });
56
67
  export function apply(ctx, config) {
57
68
  const baseDir = resolveDshHome(config.baseDir);
@@ -59,7 +70,10 @@ export function apply(ctx, config) {
59
70
  const engine = createEvolutionEngine(baseDir, {
60
71
  onApplied: (result) => {
61
72
  try {
62
- syncSkillsFromResult(skillsRoot, result);
73
+ const warnings = syncSkillsFromResult(skillsRoot, result);
74
+ for (const warning of warnings) {
75
+ ctx.logger("continual-evolve").warn(warning);
76
+ }
63
77
  }
64
78
  catch (cause) {
65
79
  ctx
@@ -108,8 +122,10 @@ export function apply(ctx, config) {
108
122
  maxInputChars: config.maxReviewInputChars ?? 40000,
109
123
  budgetTokens: config.reviewBudgetTokens ?? 4096,
110
124
  notifyOnAutoReview: config.notifyOnAutoReview ?? true,
125
+ localFate: config.localFate ?? true,
126
+ fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
111
127
  });
112
- ctx.logger("continual-evolve").info(`continual-evolve auto-review enabled (every ${config.reviewIntervalTurns ?? 6} turns)`);
128
+ ctx.logger("continual-evolve").info(`continual-evolve auto-review enabled (every ${config.reviewIntervalTurns ?? 6} turns; local-fate ${config.localFate ?? true ? "on" : "off"} every ${config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6} turns)`);
113
129
  }
114
130
  ctx.logger("continual-evolve").info(`continual-evolve mounted (baseDir=${baseDir})`);
115
131
  }
package/lib/mount.js CHANGED
@@ -124,6 +124,11 @@ export function renderParameters(entry) {
124
124
  * the ledger records the entry for the next boot.
125
125
  */
126
126
  export async function mountSkill(ctx, baseDir, entry) {
127
+ // Guidance skills are SKILL.md documents with no python reference — there
128
+ // is no function to mount as a tool; only executable skills can hot-mount.
129
+ if (entry.skill_kind === "guidance" || Object.keys(entry.reference ?? {}).length === 0) {
130
+ throw new Error(`skill ${entry.id} has no python reference (guidance skills cannot be mounted — load them with the skill tool instead)`);
131
+ }
127
132
  const dir = renderMountPackage(baseDir, entry);
128
133
  const entryId = `evolve-mount-${skillNameOf(entry.id)}`;
129
134
  const loader = ctx.get("loader");
package/lib/planner.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import type { Context } from "@deepseek-ai/cordis";
10
10
  import type { Agent } from "@deepseek-ai/dsh-agent";
11
11
  import type { HarnessState, RefinementProposal, RefinementResult } from "./types.js";
12
- export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;\n skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Local edits are session-scoped; global edits persist across sessions.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"metadata\": {},\n \"reason\": \"why this edit is useful\"\n }\n ]\n}";
12
+ export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;\n skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Skill entries are authored to the DSH skill quality standard\n (skill-creator, distilled from the official deepseek-harness 11 skills;\n the full facts are provided in the <skill_quality_standard> block below):\n only for a REAL trigger scenario grounded in the trajectory\n (who, in what real task, what signal) \u2014 never invent one to pad the store;\n never duplicate the official 11 skills or existing entries; content is a\n SKILL.md document (frontmatter routing with \"use when / do not use when\"\n description, boundary declaration, prerequisites and exclusions, layered\n information, verifiable completion criteria). Self-check every proposed\n skill against the 7 structural features and state the result in its\n reason field.\n- Repeated multi-step workflows (session start/end routines, recurring\n wrap-up or handoff procedures) may be proposed as guidance skills:\n kind=skill, skill_kind=\"guidance\", content = a SKILL.md document (no\n python reference \u2014 executable skills keep requiring reference +\n arguments). Only propose with repeated evidence in the trajectory, never\n for one-off flows. Guidance skills materialize as discoverable SKILL.md\n files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to\n the user for a decision before they land.\n- Local edits are session-scoped; global edits persist across sessions.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"skill_kind\": \"executable|guidance (optional; skill kind only \u2014 guidance = SKILL.md document without reference)\",\n \"metadata\": {},\n \"reason\": \"why this edit is useful\"\n }\n ]\n}";
13
13
  export interface PlanOptions {
14
14
  agent: Agent;
15
15
  state: HarnessState;
@@ -22,6 +22,13 @@ export interface PlanOptions {
22
22
  * every planning call is grounded in what the user actually said.
23
23
  */
24
24
  trajectory?: string;
25
+ /**
26
+ * Skills root to read the skill-creator template facts from
27
+ * (`<root>/skill-creator/references/template.md`). When omitted or the
28
+ * skills are not installed, the builtin distilled quality guide is
29
+ * injected instead — the skill standard is always present.
30
+ */
31
+ skillsRoot?: string;
25
32
  global?: boolean;
26
33
  signal?: AbortSignal;
27
34
  maxOutputTokens?: number;
package/lib/planner.js CHANGED
@@ -2,6 +2,7 @@ import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-
2
2
  import { parseProposal } from "./plan.js";
3
3
  import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
4
4
  import { recentUserText } from "./inject.js";
5
+ import { skillQualityGuide } from "./skillquality.js";
5
6
  export const PLANNER_SYSTEM_PROMPT = `You are the /evolve continual harness subsystem.
6
7
 
7
8
  Your job is to improve the editable continual harness state. Instead of
@@ -14,6 +15,25 @@ Rules:
14
15
  - prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;
15
16
  skill = repeatable procedures (must carry a python reference {type:"python", import, callable}
16
17
  and an arguments object); subagent = reusable delegation roles.
18
+ - Skill entries are authored to the DSH skill quality standard
19
+ (skill-creator, distilled from the official deepseek-harness 11 skills;
20
+ the full facts are provided in the <skill_quality_standard> block below):
21
+ only for a REAL trigger scenario grounded in the trajectory
22
+ (who, in what real task, what signal) — never invent one to pad the store;
23
+ never duplicate the official 11 skills or existing entries; content is a
24
+ SKILL.md document (frontmatter routing with "use when / do not use when"
25
+ description, boundary declaration, prerequisites and exclusions, layered
26
+ information, verifiable completion criteria). Self-check every proposed
27
+ skill against the 7 structural features and state the result in its
28
+ reason field.
29
+ - Repeated multi-step workflows (session start/end routines, recurring
30
+ wrap-up or handoff procedures) may be proposed as guidance skills:
31
+ kind=skill, skill_kind="guidance", content = a SKILL.md document (no
32
+ python reference — executable skills keep requiring reference +
33
+ arguments). Only propose with repeated evidence in the trajectory, never
34
+ for one-off flows. Guidance skills materialize as discoverable SKILL.md
35
+ files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to
36
+ the user for a decision before they land.
17
37
  - Local edits are session-scoped; global edits persist across sessions.
18
38
  - Ground every edit in evidence: the session trajectory (recent direct user
19
39
  messages) is provided when available; prefer edits backed by it over
@@ -37,6 +57,7 @@ Rules:
37
57
  "path": "optional grouping path",
38
58
  "reference": {"type":"python","import":"pkg.mod","callable":"fn"} ,
39
59
  "arguments": {"name": {"type":"string","required":true,"description":"..."}},
60
+ "skill_kind": "executable|guidance (optional; skill kind only — guidance = SKILL.md document without reference)",
40
61
  "metadata": {},
41
62
  "reason": "why this edit is useful"
42
63
  }
@@ -54,11 +75,17 @@ export async function planWithLlm(ctx, options) {
54
75
  // most recent direct user messages ("" when none qualify — the block is
55
76
  // then omitted entirely, keeping an empty trajectory zero-cost).
56
77
  const trajectory = options.trajectory ?? recentUserText(agent);
78
+ // The skill quality standard is always present: the skill-creator
79
+ // template facts when installed, the builtin distilled guide otherwise
80
+ // (~1KB — planning is low-frequency, and the standard keeps skill
81
+ // proposals from drifting off the quality bar).
82
+ const qualityGuide = skillQualityGuide(options.skillsRoot);
57
83
  const userPrompt = [
58
84
  `<current_harness_state>\n${formatHarnessStateForPrompt(state)}\n</current_harness_state>`,
59
85
  `<refinement_history>\n${historyForPrompt(history)}\n</refinement_history>`,
60
86
  `<scope_policy>\n${scopeInstruction}\n</scope_policy>`,
61
87
  trajectory ? `<session_trajectory>\n${trajectory}\n</session_trajectory>` : "",
88
+ `<skill_quality_standard>\n${qualityGuide.text}\n</skill_quality_standard>`,
62
89
  options.instructions ? `<user_instructions>\n${options.instructions}\n</user_instructions>` : "",
63
90
  "Return only JSON edits. If no useful edit is justified, return an empty edits array with a rationale.",
64
91
  ]
package/lib/render.js CHANGED
@@ -18,7 +18,8 @@ export function entryLine(entry, maxContentLength) {
18
18
  : "";
19
19
  const citationText = citationSuffix(entry);
20
20
  const archivedText = isArchived(entry) ? " [archived]" : "";
21
- return `- [${entry.scope}:${entry.id}] ${entry.title} (${entry.path}, v${entry.version})${archivedText}${referenceText}${argumentsText}${citationText}: ${compactText(entry.content, maxContentLength)}`;
21
+ const formText = entry.kind === "skill" && entry.skill_kind === "guidance" ? " [guidance]" : "";
22
+ return `- [${entry.scope}:${entry.id}] ${entry.title} (${entry.path}, v${entry.version})${archivedText}${formText}${referenceText}${argumentsText}${citationText}: ${compactText(entry.content, maxContentLength)}`;
22
23
  }
23
24
  /** Trajectory citation suffix (` src=sessionId:1,2`), empty when uncited. */
24
25
  function citationSuffix(entry) {
package/lib/review.d.ts CHANGED
@@ -28,7 +28,7 @@ export interface ReviewOptions {
28
28
  signal?: AbortSignal;
29
29
  budgetTokens?: number;
30
30
  }
31
- export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should run /evolve. Auto /evolve writes local\nharness state by default, so approve when the trajectory contains evidence\nuseful to this session's future turns: a repeated failure, a reusable tactic,\na repeated delegation role, a durable fact or preference, a user correction\nthat should persist, or a narrow behavioral policy.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
31
+ export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should run /evolve. Auto /evolve writes local\nharness state by default, so approve when the trajectory contains evidence\nuseful to this session's future turns: a repeated failure, a reusable tactic,\na repeated delegation role, a durable fact or preference, a user correction\nthat should persist, or a narrow behavioral policy.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
32
32
  /** Parse the gate's JSON reply. */
33
33
  export declare function parseAutoRefineReview(text: string): AutoRefineReview;
34
34
  /** Serialize surface events to bounded role-prefixed text. */
package/lib/review.js CHANGED
@@ -22,6 +22,23 @@ refine target: approve with instructions naming the entry ids, and tell the
22
22
  planner to archive them (archive hides from injection, data stays restorable)
23
23
  rather than delete.
24
24
 
25
+ Skill-related trajectories (the evidence concerns creating or improving a
26
+ skill entry) are judged against the DSH skill quality standard (skill-audit
27
+ dimensions: frontmatter routing, the 7 structural features, paragraph
28
+ skeleton, no duplication of the official 11 skills or covered skills).
29
+ Approve only when the trajectory shows a REAL trigger scenario and the
30
+ resulting skill would meet the standard; otherwise decline and say in the
31
+ rationale what must improve — drafting follows skill-creator, and the
32
+ planner receives the standard as its <skill_quality_standard> block.
33
+
34
+ Repeated multi-step workflows (session start/end routines, recurring
35
+ wrap-up or handoff procedures) are a valid refine target: approve with
36
+ instructions telling the planner to propose a guidance skill (kind=skill,
37
+ skill_kind=guidance — a SKILL.md document, no python reference). Only
38
+ propose when the same workflow recurs in the trajectory — never for
39
+ one-off flows. Auto-created skills are always offered to the user for a
40
+ decision before they land; the gate never writes a skill silently.
41
+
25
42
  Return JSON only:
26
43
  {
27
44
  "shouldRefine": true|false,
package/lib/score.d.ts CHANGED
@@ -9,12 +9,28 @@ export interface AggregateOptions {
9
9
  passThreshold: number;
10
10
  /** Per-case regression tolerance: candidate may drop below reference by at most this much. */
11
11
  regressionTolerance: number;
12
+ /**
13
+ * Failure-cell protocol (gap A2): a round with more failed cells than
14
+ * this is rejected outright — failed cells are NEVER averaged in as
15
+ * zeros. Default 0 (any failure rejects the round).
16
+ */
17
+ maxFailedCells: number;
12
18
  }
13
19
  export declare const DEFAULT_AGGREGATE: AggregateOptions;
14
- /** Aggregate raw cells into code-owned per-case means + overall mean. */
15
- export declare function aggregate(cells: readonly CellScore[]): Record<string, number | null> & {
20
+ export interface AggregateResult extends Record<string, number | null> {
16
21
  overall: number | null;
17
- };
22
+ /** Count of failed cells (excluded from every mean). */
23
+ failed: number;
24
+ /** Total cells considered (ok + failed). */
25
+ total: number;
26
+ }
27
+ /**
28
+ * Aggregate raw cells into code-owned per-case means + overall mean.
29
+ * Failure-cell protocol (gap A2): failed cells are EXCLUDED from means and
30
+ * counted separately — a crashed unit can never silently drag the mean down
31
+ * like a zero. A case whose cells all failed reports null (no mean).
32
+ */
33
+ export declare function aggregate(cells: readonly CellScore[]): AggregateResult;
18
34
  export declare function entryFromCells(label: string, cells: readonly CellScore[], refinementId?: string): EvaluationEntry;
19
35
  export interface Decision {
20
36
  accepted: boolean;
@@ -25,7 +41,9 @@ export declare function decisionReport(reference: EvaluationEntry, candidate: Ev
25
41
  /**
26
42
  * Non-regressive acceptance rule (Self-Harness style):
27
43
  * the candidate is accepted iff its overall mean is STRICTLY higher than the
28
- * reference AND no case regresses by more than `regressionTolerance` points.
44
+ * reference, no case regresses by more than `regressionTolerance` points,
45
+ * and neither side has more failed cells than `maxFailedCells` (failure-cell
46
+ * protocol, gap A2 — a partial/invalid round is never accepted).
29
47
  */
30
48
  export declare function decide(reference: EvaluationEntry, candidate: EvaluationEntry, opts: AggregateOptions): Decision;
31
49
  //# sourceMappingURL=score.d.ts.map