dsh-mnemon-source-skills 0.5.9 → 0.5.10

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 CHANGED
@@ -61,3 +61,5 @@ The optional `./client` entry is an ordinary DSH Client plugin. This package own
61
61
  From a source checkout, install the declared dependencies and run `pnpm verify` to check the Source's behavior, the market's parsers and archive reader, and Source-backed page interactions.
62
62
 
63
63
  [Plugin development](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/en/development/extensions.md) · [中文指南](https://github.com/omdsh-dev/dsh-mnemon/blob/main/docs/zh-CN/development/extensions.md)
64
+
65
+ The optional Host `skills.autoAdopt` setting is off by default. It queues only new idle-review suggestions, requires complete paired text backtests without per-case or per-check regression, retries at most two rewrites, and uses Source-owned adoption under the storage lock. Source checks the exact draft/cases/results and current file again; cancellation is checked inside queued writes. Automatic activity and manual-review reasons are visible on each suggestion. See the [Skills guide](../../docs/en/guides/skills.md#optional-automatic-adoption) for limits and the distinction from real external-action validation.
package/lib/client.js CHANGED
@@ -35,7 +35,7 @@ window.__ModuleLoader__.load({
35
35
  function json(value) {
36
36
  return JSON.parse(JSON.stringify(value ?? null));
37
37
  }
38
- const CONFLICT = /revision conflict|changed since it was read; reload/u;
38
+ const CONFLICT = /revision conflict|Skills Source changed since it was read; reload/u;
39
39
  /**
40
40
  * The Source's revision moves with every background review and run, so each
41
41
  * write fetches it first and tries once more if a background write slipped in
@@ -127,6 +127,7 @@ window.__ModuleLoader__.load({
127
127
  id,
128
128
  content
129
129
  }),
130
+ cancelRun: (id) => mutate("run-cancel", { id }),
130
131
  adoptDraft: (id, root) => mutate("draft-adopt", root === void 0 ? { id } : {
131
132
  id,
132
133
  root
@@ -535,7 +536,34 @@ window.__ModuleLoader__.load({
535
536
  "evolution.loadFailedBody": "请重试,或在左侧选择其他建议。",
536
537
  "tests.model": "全局模型",
537
538
  "tests.modelCard": "所选回测模型",
538
- "tests.modelAuto": "跟随默认"
539
+ "tests.modelAuto": "跟随默认",
540
+ "draft.baseChanged": "原技能已变更,此草案已过期。请基于当前文件重新生成并回测。",
541
+ "draft.targetExists": "同名技能已存在。请重新生成为改进草案并回测,再确认采纳。",
542
+ "draft.refreshGuidance": "基于当前技能文件重新生成,保留最近的修改,并重新运行回测。",
543
+ "draft.regenerate": "重新生成并回测",
544
+ "draft.cancelBacktest": "取消回测",
545
+ "draft.interrupted": "回测已中断。可以重新回测,或在检查草案后手动采纳。",
546
+ "evolution.auto.title": "自动采纳",
547
+ "evolution.auto.history": "自动采纳活动记录",
548
+ "evolution.auto.attempt": "第 {count} 轮",
549
+ "evolution.auto.scores": "基线 {baseline} / 草案 {draft}",
550
+ "evolution.auto.stage.queued": "等待处理",
551
+ "evolution.auto.stage.drafting": "正在起草",
552
+ "evolution.auto.stage.backtesting": "正在对比回测",
553
+ "evolution.auto.stage.rewriting": "依据退步结果重写",
554
+ "evolution.auto.stage.adopted": "已自动采纳",
555
+ "evolution.auto.stage.paused": "已停止自动处理",
556
+ "evolution.auto.stage.review": "需要人工审查",
557
+ "evolution.auto.reason.disabled": "自动采纳或所需权限已关闭",
558
+ "evolution.auto.reason.no-cases": "没有启用的回测用例",
559
+ "evolution.auto.reason.case-limit": "启用的用例超过每轮上限;调整用例或上限后手动处理本建议",
560
+ "evolution.auto.reason.unmeasured": "回测未完整测出所有检查项,或存在图片依赖、错误、中断",
561
+ "evolution.auto.reason.regressed": "至少一个用例或原先通过的检查项出现退步",
562
+ "evolution.auto.reason.changed": "建议、草案或用例已变动",
563
+ "evolution.auto.reason.failed": "自动处理失败,保留人工处理入口",
564
+ "evolution.auto.reason.queue-full": "自动处理队列已满",
565
+ "evolution.auto.reason.model-unavailable": "无法确定起草或回测模型",
566
+ "evolution.auto.reason.interrupted": "Host 已停止,不自动重启此建议"
539
567
  };
540
568
  var en = {
541
569
  "page.title": "Skills",
@@ -891,7 +919,34 @@ window.__ModuleLoader__.load({
891
919
  "evolution.loadFailedBody": "Retry, or select another suggestion on the left.",
892
920
  "tests.model": "Global model",
893
921
  "tests.modelCard": "Selected model",
894
- "tests.modelAuto": "Follow default"
922
+ "tests.modelAuto": "Follow default",
923
+ "draft.baseChanged": "The original skill changed. This draft is out of date; regenerate it against the current file and backtest it.",
924
+ "draft.targetExists": "A skill with this name now exists. Regenerate an update and backtest it before adopting.",
925
+ "draft.refreshGuidance": "Regenerate against the current skill file, preserving its latest changes, then run backtests.",
926
+ "draft.regenerate": "Regenerate and backtest",
927
+ "draft.cancelBacktest": "Cancel backtests",
928
+ "draft.interrupted": "Backtests were interrupted. Run them again, or review the draft before adopting manually.",
929
+ "evolution.auto.title": "Automatic adoption",
930
+ "evolution.auto.history": "Automatic adoption activity",
931
+ "evolution.auto.attempt": "Attempt {count}",
932
+ "evolution.auto.scores": "Baseline {baseline} / draft {draft}",
933
+ "evolution.auto.stage.queued": "Queued",
934
+ "evolution.auto.stage.drafting": "Drafting",
935
+ "evolution.auto.stage.backtesting": "Backtesting a pair",
936
+ "evolution.auto.stage.rewriting": "Rewriting after regression",
937
+ "evolution.auto.stage.adopted": "Automatically adopted",
938
+ "evolution.auto.stage.paused": "Automatic work stopped",
939
+ "evolution.auto.stage.review": "Manual review required",
940
+ "evolution.auto.reason.disabled": "Automatic adoption or its required permissions were disabled",
941
+ "evolution.auto.reason.no-cases": "No enabled evaluation cases",
942
+ "evolution.auto.reason.case-limit": "Enabled cases exceed the per-run limit; adjust cases or the limit and review this suggestion manually",
943
+ "evolution.auto.reason.unmeasured": "Some checks were not fully measured, or a case needs images, failed or was interrupted",
944
+ "evolution.auto.reason.regressed": "At least one case or previously passing check regressed",
945
+ "evolution.auto.reason.changed": "The suggestion, draft or evaluation cases changed",
946
+ "evolution.auto.reason.failed": "Automatic work failed; manual review remains available",
947
+ "evolution.auto.reason.queue-full": "Automatic queue is full",
948
+ "evolution.auto.reason.model-unavailable": "No resolved draft or backtest model",
949
+ "evolution.auto.reason.interrupted": "Host stopped; this suggestion will not restart automatically"
895
950
  };
896
951
  //#endregion
897
952
  //#region \0source-css:/home/runner/work/dsh-mnemon/dsh-mnemon/plugins/dsh-mnemon-source-skills/presentation/page.module.css.mjs
@@ -3730,6 +3785,7 @@ window.__ModuleLoader__.load({
3730
3785
  context.refresh();
3731
3786
  } catch (error) {
3732
3787
  context.fail(error);
3788
+ context.refresh();
3733
3789
  } finally {
3734
3790
  setBusy(false);
3735
3791
  }
@@ -3821,6 +3877,7 @@ window.__ModuleLoader__.load({
3821
3877
  className: css.skWarn,
3822
3878
  children: t("evolution.skillChanged")
3823
3879
  }),
3880
+ suggestion.autoAdoption !== void 0 && suggestion.autoAdoption.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoAdoptionHistory, { events: suggestion.autoAdoption }),
3824
3881
  suggestion.status === "drafting" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: t("evolution.drafting") }),
3825
3882
  suggestion.lastError !== void 0 && suggestion.status === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3826
3883
  className: css.inlineError,
@@ -3979,6 +4036,7 @@ window.__ModuleLoader__.load({
3979
4036
  context.refresh();
3980
4037
  } catch (error) {
3981
4038
  context.fail(error);
4039
+ context.refresh();
3982
4040
  } finally {
3983
4041
  setBusy(false);
3984
4042
  }
@@ -4004,7 +4062,8 @@ window.__ModuleLoader__.load({
4004
4062
  });
4005
4063
  await context.client.linkRun(draft.id, started.runId);
4006
4064
  });
4007
- const redraft = (note) => act(() => context.client.generateDraft({
4065
+ const redraft = (note, backtest = false) => act(() => context.client.generateDraft({
4066
+ ...backtest ? { backtest: true } : {},
4008
4067
  draftId: draft.id,
4009
4068
  ...draft.suggestionId === void 0 ? {} : { suggestionId: draft.suggestionId },
4010
4069
  guidance: note,
@@ -4045,7 +4104,7 @@ window.__ModuleLoader__.load({
4045
4104
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4046
4105
  variant: "primary",
4047
4106
  size: "sm",
4048
- disabled: !context.writable || busy || editing !== null || running || !originalAvailable,
4107
+ disabled: !context.writable || busy || editing !== null || running || view.conflict !== void 0 || !originalAvailable,
4049
4108
  onClick: () => regressed ? setConfirmAdopt(true) : void adopt(),
4050
4109
  children: t("draft.adopt")
4051
4110
  }),
@@ -4074,6 +4133,16 @@ window.__ModuleLoader__.load({
4074
4133
  className: css.skWarn,
4075
4134
  children: t(view.resolution === "origin-unavailable" ? "evolution.originUnavailable" : "evolution.skillGone")
4076
4135
  }),
4136
+ view.conflict !== void 0 && ready && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4137
+ className: css.skWarn,
4138
+ role: "status",
4139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t(view.conflict === "target-exists" ? "draft.targetExists" : "draft.baseChanged") }), originalAvailable && context.client.canAssist("skill-draft") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4140
+ size: "sm",
4141
+ disabled: !context.writable || busy || running,
4142
+ onClick: () => void redraft(t("draft.refreshGuidance"), true),
4143
+ children: t("draft.regenerate")
4144
+ })]
4145
+ }),
4077
4146
  draft.notes !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Clamp, { text: draft.notes }),
4078
4147
  draft.changes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MoreList, {
4079
4148
  items: draft.changes,
@@ -4082,37 +4151,53 @@ window.__ModuleLoader__.load({
4082
4151
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4083
4152
  className: css.skBacktest,
4084
4153
  "data-tone": regressed ? "bad" : candidateScore !== void 0 && baselineScore !== void 0 && candidateScore > baselineScore ? "good" : void 0,
4085
- children: [running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: t("draft.backtesting", {
4086
- done: progress(candidate) + progress(baseline),
4087
- total
4088
- }) }) : candidateScore !== void 0 && baselineScore !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4089
- type: "button",
4090
- className: css.skBacktestLine,
4091
- "aria-expanded": details,
4092
- onClick: () => setDetails((value) => !value),
4093
- children: [
4094
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("draft.backtest") }),
4095
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [
4096
- baseline?.target.kind === "none" ? t("draft.withoutSkill") : t("draft.current"),
4097
- " ",
4098
- points(baselineScore),
4099
- " → ",
4100
- t("draft.candidate"),
4101
- " ",
4102
- points(candidateScore)
4103
- ] }),
4104
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: candidateScore === baselineScore ? t("draft.same") : `${candidateScore > baselineScore ? "+" : "−"}${Math.round(Math.abs(candidateScore - baselineScore) * 100)}` })
4105
- ]
4106
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4107
- className: css.skMuted,
4108
- children: cases === 0 ? t("draft.noTests") : view.runs.length === 0 ? t("draft.notTested") : t("draft.notComparable")
4109
- }), !running && (cases ?? 0) > 0 && context.client.canAssist("skill-backtest") && ready && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4110
- variant: "ghost",
4111
- size: "sm",
4112
- disabled: !context.writable || busy || !originalAvailable,
4113
- onClick: () => void backtest(),
4114
- children: view.runs.length === 0 ? t("draft.runBacktest") : t("draft.rerun")
4115
- })]
4154
+ children: [
4155
+ running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: t("draft.backtesting", {
4156
+ done: progress(candidate) + progress(baseline),
4157
+ total
4158
+ }) }) : candidateScore !== void 0 && baselineScore !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4159
+ type: "button",
4160
+ className: css.skBacktestLine,
4161
+ "aria-expanded": details,
4162
+ onClick: () => setDetails((value) => !value),
4163
+ children: [
4164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("draft.backtest") }),
4165
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [
4166
+ baseline?.target.kind === "none" ? t("draft.withoutSkill") : t("draft.current"),
4167
+ " ",
4168
+ points(baselineScore),
4169
+ " → ",
4170
+ t("draft.candidate"),
4171
+ " ",
4172
+ points(candidateScore)
4173
+ ] }),
4174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: candidateScore === baselineScore ? t("draft.same") : `${candidateScore > baselineScore ? "+" : "−"}${Math.round(Math.abs(candidateScore - baselineScore) * 100)}` })
4175
+ ]
4176
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4177
+ className: css.skMuted,
4178
+ children: cases === 0 ? t("draft.noTests") : view.runs.length === 0 ? t("draft.notTested") : t("draft.notComparable")
4179
+ }),
4180
+ running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4181
+ variant: "ghost",
4182
+ size: "sm",
4183
+ disabled: !context.writable || busy,
4184
+ onClick: () => void act(async () => {
4185
+ for (const run of view.runs.filter((item) => item.status === "running")) await context.client.cancelRun(run.id);
4186
+ }),
4187
+ children: t("draft.cancelBacktest")
4188
+ }),
4189
+ !running && (candidate?.status === "interrupted" || baseline?.status === "interrupted") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4190
+ role: "status",
4191
+ children: t("draft.interrupted")
4192
+ }),
4193
+ !running && (cases ?? 0) > 0 && context.client.canAssist("skill-backtest") && ready && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4194
+ variant: "ghost",
4195
+ size: "sm",
4196
+ disabled: !context.writable || busy || !originalAvailable,
4197
+ onClick: () => void backtest(),
4198
+ children: view.runs.length === 0 ? t("draft.runBacktest") : t("draft.rerun")
4199
+ })
4200
+ ]
4116
4201
  }),
4117
4202
  excluded > 0 && !running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4118
4203
  className: css.skMuted,
@@ -4208,6 +4293,35 @@ window.__ModuleLoader__.load({
4208
4293
  ]
4209
4294
  });
4210
4295
  }
4296
+ function AutoAdoptionHistory(props) {
4297
+ const t = useT();
4298
+ const latest = props.events[props.events.length - 1];
4299
+ const describe = (event) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4300
+ t(`evolution.auto.stage.${event.stage}`),
4301
+ event.attempt > 0 ? ` · ${t("evolution.auto.attempt", { count: event.attempt })}` : "",
4302
+ event.reason === void 0 ? "" : ` · ${t(`evolution.auto.reason.${event.reason}`)}`,
4303
+ event.baselineScore === void 0 || event.draftScore === void 0 ? "" : ` · ${t("evolution.auto.scores", {
4304
+ baseline: points(event.baselineScore),
4305
+ draft: points(event.draftScore)
4306
+ })}`,
4307
+ event.detail === void 0 ? "" : ` · ${event.detail}`
4308
+ ] });
4309
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4310
+ "aria-label": t("evolution.auto.title"),
4311
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
4312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("evolution.auto.title") }),
4313
+ " · ",
4314
+ describe(latest)
4315
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Fold, {
4316
+ title: t("evolution.auto.history"),
4317
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", { children: props.events.map((event, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
4318
+ stamp(event.at),
4319
+ " · ",
4320
+ describe(event)
4321
+ ] }, index)) })
4322
+ })]
4323
+ });
4324
+ }
4211
4325
  //#endregion
4212
4326
  //#region src/contracts.ts
4213
4327
  const SKILL_ROOT_KINDS = [
@@ -0,0 +1,119 @@
1
+ //#region src/auto-adoption.ts
2
+ /** Stable evaluation inputs, excluding presentation timestamps and provenance. Hash at the server boundary. */
3
+ function skillCaseSignature(cases) {
4
+ return JSON.stringify([...cases].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((entry) => [
5
+ entry.id,
6
+ entry.skillName,
7
+ entry.title,
8
+ entry.prompt,
9
+ entry.context ?? "",
10
+ entry.enabled,
11
+ entry.requiresVision === true,
12
+ entry.checks.map((check) => [
13
+ check.id,
14
+ check.text,
15
+ check.must
16
+ ])
17
+ ]));
18
+ }
19
+ /** Automatic decisions require full matching coverage, not the UI's scored intersection. */
20
+ function compareAutomaticSkillRuns(cases, baseline, draft, casesHash) {
21
+ const unmeasured = { outcome: "unmeasured" };
22
+ if (cases.length === 0 || cases.some((entry) => !entry.enabled || entry.requiresVision === true || entry.checks.length === 0)) return unmeasured;
23
+ const ids = new Set(cases.map((entry) => entry.id));
24
+ if (ids.size !== cases.length || baseline === void 0 || draft === void 0 || baseline.model === void 0 || draft.model === void 0 || baseline.model.provider !== draft.model.provider || baseline.model.model !== draft.model.model || baseline.model.reasoningEffort !== draft.model.reasoningEffort) return unmeasured;
25
+ const complete = (run) => {
26
+ if (run.status !== "completed" || run.casesHash !== casesHash || run.errors || run.caseIds.length !== ids.size || new Set(run.caseIds).size !== ids.size || run.caseIds.some((id) => !ids.has(id)) || run.results.length !== ids.size || run.progress.done !== ids.size || run.progress.total !== ids.size) return;
27
+ const results = new Map(run.results.map((result) => [result.caseId, result]));
28
+ if (results.size !== ids.size) return;
29
+ for (const entry of cases) {
30
+ const result = results.get(entry.id);
31
+ if (result === void 0 || result.fullyGraded !== true || result.verdict === "error" || result.unmeasuredReason !== void 0 || result.error !== void 0 || !Number.isFinite(result.score)) return;
32
+ const checks = new Map(result.checks.map((check) => [check.checkId, check.pass]));
33
+ if (checks.size !== entry.checks.length || result.checks.length !== checks.size || entry.checks.some((check) => typeof checks.get(check.id) !== "boolean")) return;
34
+ const weight = entry.checks.reduce((sum, check) => sum + (check.must ? 2 : 1), 0);
35
+ const score = entry.checks.reduce((sum, check) => sum + (checks.get(check.id) ? check.must ? 2 : 1 : 0), 0) / weight;
36
+ const verdict = entry.checks.every((check) => !check.must || checks.get(check.id)) ? "pass" : "fail";
37
+ if (Math.abs(result.score - score) > 1e-8 || result.verdict !== verdict) return;
38
+ }
39
+ return results;
40
+ };
41
+ const before = complete(baseline);
42
+ const after = complete(draft);
43
+ if (before === void 0 || after === void 0) return unmeasured;
44
+ const baselineScore = cases.reduce((sum, entry) => sum + before.get(entry.id).score, 0) / cases.length;
45
+ const draftScore = cases.reduce((sum, entry) => sum + after.get(entry.id).score, 0) / cases.length;
46
+ return {
47
+ outcome: cases.some((entry) => {
48
+ const left = before.get(entry.id);
49
+ const right = after.get(entry.id);
50
+ const checks = new Map(right.checks.map((check) => [check.checkId, check.pass]));
51
+ return right.score + 1e-8 < left.score || left.checks.some((check) => check.pass && !checks.get(check.checkId));
52
+ }) ? "regressed" : "not-worse",
53
+ baselineScore,
54
+ draftScore
55
+ };
56
+ }
57
+ //#endregion
58
+ //#region src/contracts.ts
59
+ const SKILL_ROOT_KINDS = [
60
+ "project-dsh",
61
+ "project-agents",
62
+ "user-dsh",
63
+ "user-agents"
64
+ ];
65
+ const SKILL_ROOT_RANK = {
66
+ "project-dsh": 100,
67
+ "project-agents": 200,
68
+ "user-dsh": 400,
69
+ "user-agents": 500
70
+ };
71
+ const EVOLUTION_SIGNALS = [
72
+ "correction",
73
+ "directive",
74
+ "tool-failure",
75
+ "approach-change",
76
+ "procedure",
77
+ "skill-gap",
78
+ "skill-misleading",
79
+ "success"
80
+ ];
81
+ const SUGGESTION_STATUSES = [
82
+ "pending",
83
+ "drafting",
84
+ "review",
85
+ "adopted",
86
+ "dismissed",
87
+ "expired",
88
+ "invalidated"
89
+ ];
90
+ const DEFAULT_SKILLS_POLICY = Object.freeze({
91
+ allowCreate: true,
92
+ allowUpdate: true,
93
+ minConfidence: .6,
94
+ expiryDays: 7,
95
+ maxPendingPerSession: 5
96
+ });
97
+ const AUTO_ADOPTION_STAGES = [
98
+ "queued",
99
+ "drafting",
100
+ "backtesting",
101
+ "rewriting",
102
+ "adopted",
103
+ "paused",
104
+ "review"
105
+ ];
106
+ const AUTO_ADOPTION_REASONS = [
107
+ "disabled",
108
+ "no-cases",
109
+ "case-limit",
110
+ "unmeasured",
111
+ "regressed",
112
+ "changed",
113
+ "failed",
114
+ "queue-full",
115
+ "model-unavailable",
116
+ "interrupted"
117
+ ];
118
+ //#endregion
119
+ export { SKILL_ROOT_KINDS as a, compareAutomaticSkillRuns as c, EVOLUTION_SIGNALS as i, skillCaseSignature as l, AUTO_ADOPTION_STAGES as n, SKILL_ROOT_RANK as o, DEFAULT_SKILLS_POLICY as r, SUGGESTION_STATUSES as s, AUTO_ADOPTION_REASONS as t };
@@ -20,6 +20,16 @@ interface DetectedSource {
20
20
  label: string;
21
21
  }
22
22
  //#endregion
23
+ //#region src/auto-adoption.d.ts
24
+ /** Stable evaluation inputs, excluding presentation timestamps and provenance. Hash at the server boundary. */
25
+ declare function skillCaseSignature(cases: readonly EvalCase[]): string;
26
+ /** Automatic decisions require full matching coverage, not the UI's scored intersection. */
27
+ declare function compareAutomaticSkillRuns(cases: readonly EvalCase[], baseline: BacktestRun | undefined, draft: BacktestRun | undefined, casesHash: string): {
28
+ outcome: 'unmeasured' | 'regressed' | 'not-worse';
29
+ baselineScore?: number;
30
+ draftScore?: number;
31
+ };
32
+ //#endregion
23
33
  //#region src/contracts.d.ts
24
34
  /** The DSH skill roots this Source reads and writes, in DSH's precedence order. */
25
35
  type SkillRootKind = 'project-dsh' | 'project-agents' | 'user-dsh' | 'user-agents';
@@ -192,6 +202,7 @@ interface SkillSuggestion {
192
202
  status: SuggestionStatus;
193
203
  draftId?: string;
194
204
  lastError?: string;
205
+ autoAdoption?: AutoAdoptionEvent[];
195
206
  createdAt: string;
196
207
  updatedAt: string;
197
208
  expiresAt: string;
@@ -292,6 +303,8 @@ interface CaseResult {
292
303
  error?: string;
293
304
  /** Kept under verdict=error so older readers also exclude it from scores. */
294
305
  unmeasuredReason?: 'requires-vision';
306
+ /** Every expected judge check was explicitly returned; absent on older runs. */
307
+ fullyGraded?: boolean;
295
308
  }
296
309
  type RunStatus = 'running' | 'completed' | 'failed' | 'interrupted';
297
310
  interface BacktestRun {
@@ -304,6 +317,11 @@ interface BacktestRun {
304
317
  total: number;
305
318
  };
306
319
  caseIds: string[];
320
+ /** Source-verified case inputs at start; absent on older runs. */
321
+ casesHash?: string;
322
+ model?: ModelRoute & {
323
+ reasoningEffort?: string;
324
+ };
307
325
  results: CaseResult[];
308
326
  /** Over the graded cases; absent when none could be graded. */
309
327
  passRate?: number;
@@ -330,6 +348,10 @@ interface ReviewLogEntry {
330
348
  error?: string;
331
349
  /** The conversation turn the reviewed window ended with. */
332
350
  turn?: number;
351
+ autoAdoptions?: Array<AutoAdoptionEvent & {
352
+ suggestionId: string;
353
+ skillName: string;
354
+ }>;
333
355
  /** The suggestions this review created or strengthened. */
334
356
  suggestionIds?: string[];
335
357
  }
@@ -433,6 +455,8 @@ interface ReviewRecordResult {
433
455
  reviewId: string;
434
456
  trajectoryId?: string;
435
457
  suggestionIds: string[];
458
+ /** Newly inserted ids only; older Sources omit this capability. */
459
+ createdSuggestionIds?: string[];
436
460
  inserted: number;
437
461
  updated: number;
438
462
  casesAdded: number;
@@ -487,6 +511,8 @@ interface SuggestionView {
487
511
  }
488
512
  interface DraftView {
489
513
  draft: SkillDraft;
514
+ /** File changes need a new draft, never a retry with a newer revision. */
515
+ conflict?: 'base-changed' | 'target-exists';
490
516
  /** The file the draft replaces, as it is now. */
491
517
  base?: SkillDocument;
492
518
  resolution?: SkillResolution;
@@ -637,5 +663,27 @@ interface MarketInstall {
637
663
  files: number;
638
664
  installedAt: string;
639
665
  }
666
+ declare const AUTO_ADOPTION_STAGES: readonly ["queued", "drafting", "backtesting", "rewriting", "adopted", "paused", "review"];
667
+ declare const AUTO_ADOPTION_REASONS: readonly ["disabled", "no-cases", "case-limit", "unmeasured", "regressed", "changed", "failed", "queue-full", "model-unavailable", "interrupted"];
668
+ interface AutoAdoptionEvent {
669
+ at: string;
670
+ stage: typeof AUTO_ADOPTION_STAGES[number];
671
+ attempt: number;
672
+ reason?: typeof AUTO_ADOPTION_REASONS[number];
673
+ detail?: string;
674
+ baselineScore?: number;
675
+ draftScore?: number;
676
+ }
677
+ type DraftExpectation = {
678
+ id: string;
679
+ hash: string;
680
+ } | null;
681
+ interface AutomaticAdoption {
682
+ attempt: number;
683
+ draftHash: string;
684
+ baselineRunId: string;
685
+ draftRunId: string;
686
+ casesHash: string;
687
+ }
640
688
  //#endregion
641
- export { BacktestRun, CaseResult, CatalogView, DEFAULT_SKILLS_POLICY, type DetectedSource, DraftPreferences, DraftPreferencesView, DraftStatus, DraftView, EVOLUTION_SIGNALS, EvalCase, EvalCheck, EvidencePack, EvidenceReference, EvolutionSignal, LocalizedText, MarketAudit, MarketEntry, MarketInstall, MarketInstallState, MarketItem, MarketPreview, MarketRegistry, MarketRegistryFile, MarketSearchResult, MarketSource, type MarketSourceKind, MarketView, ModelRoute, ReviewLogEntry, ReviewRecordInput, ReviewRecordResult, ReviewScore, RunStatus, RunTarget, RunTargetKind, SKILL_ROOT_KINDS, SKILL_ROOT_RANK, SUGGESTION_STATUSES, SkillCatalog, SkillDocument, SkillDraft, SkillInvocation, SkillOrigin, SkillProblem, SkillResolution, SkillRoot, SkillRootKind, SkillSuggestion, SkillSummary, SkillVersion, SkillVersionReason, SkillsOverview, SkillsPolicy, SuggestionStatus, SuggestionView, Trajectory, TrajectoryAnnotation, TrajectoryStats, TrajectoryStep, TrajectoryStepKind, TrajectorySummary, TrashEntry, TrashRestoreInput, TrashRestorePreview };
689
+ export { AUTO_ADOPTION_REASONS, AUTO_ADOPTION_STAGES, AutoAdoptionEvent, AutomaticAdoption, BacktestRun, CaseResult, CatalogView, DEFAULT_SKILLS_POLICY, type DetectedSource, DraftExpectation, DraftPreferences, DraftPreferencesView, DraftStatus, DraftView, EVOLUTION_SIGNALS, EvalCase, EvalCheck, EvidencePack, EvidenceReference, EvolutionSignal, LocalizedText, MarketAudit, MarketEntry, MarketInstall, MarketInstallState, MarketItem, MarketPreview, MarketRegistry, MarketRegistryFile, MarketSearchResult, MarketSource, type MarketSourceKind, MarketView, ModelRoute, ReviewLogEntry, ReviewRecordInput, ReviewRecordResult, ReviewScore, RunStatus, RunTarget, RunTargetKind, SKILL_ROOT_KINDS, SKILL_ROOT_RANK, SUGGESTION_STATUSES, SkillCatalog, SkillDocument, SkillDraft, SkillInvocation, SkillOrigin, SkillProblem, SkillResolution, SkillRoot, SkillRootKind, SkillSuggestion, SkillSummary, SkillVersion, SkillVersionReason, SkillsOverview, SkillsPolicy, SuggestionStatus, SuggestionView, Trajectory, TrajectoryAnnotation, TrajectoryStats, TrajectoryStep, TrajectoryStepKind, TrajectorySummary, TrashEntry, TrashRestoreInput, TrashRestorePreview, compareAutomaticSkillRuns, skillCaseSignature };
package/lib/contracts.js CHANGED
@@ -1,41 +1,2 @@
1
- //#region src/contracts.ts
2
- const SKILL_ROOT_KINDS = [
3
- "project-dsh",
4
- "project-agents",
5
- "user-dsh",
6
- "user-agents"
7
- ];
8
- const SKILL_ROOT_RANK = {
9
- "project-dsh": 100,
10
- "project-agents": 200,
11
- "user-dsh": 400,
12
- "user-agents": 500
13
- };
14
- const EVOLUTION_SIGNALS = [
15
- "correction",
16
- "directive",
17
- "tool-failure",
18
- "approach-change",
19
- "procedure",
20
- "skill-gap",
21
- "skill-misleading",
22
- "success"
23
- ];
24
- const SUGGESTION_STATUSES = [
25
- "pending",
26
- "drafting",
27
- "review",
28
- "adopted",
29
- "dismissed",
30
- "expired",
31
- "invalidated"
32
- ];
33
- const DEFAULT_SKILLS_POLICY = Object.freeze({
34
- allowCreate: true,
35
- allowUpdate: true,
36
- minConfidence: .6,
37
- expiryDays: 7,
38
- maxPendingPerSession: 5
39
- });
40
- //#endregion
41
- export { DEFAULT_SKILLS_POLICY, EVOLUTION_SIGNALS, SKILL_ROOT_KINDS, SKILL_ROOT_RANK, SUGGESTION_STATUSES };
1
+ import { a as SKILL_ROOT_KINDS, c as compareAutomaticSkillRuns, i as EVOLUTION_SIGNALS, l as skillCaseSignature, n as AUTO_ADOPTION_STAGES, o as SKILL_ROOT_RANK, r as DEFAULT_SKILLS_POLICY, s as SUGGESTION_STATUSES, t as AUTO_ADOPTION_REASONS } from "./contracts-CwhbQi-u.js";
2
+ export { AUTO_ADOPTION_REASONS, AUTO_ADOPTION_STAGES, DEFAULT_SKILLS_POLICY, EVOLUTION_SIGNALS, SKILL_ROOT_KINDS, SKILL_ROOT_RANK, SUGGESTION_STATUSES, compareAutomaticSkillRuns, skillCaseSignature };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_SKILLS_POLICY, EVOLUTION_SIGNALS, SKILL_ROOT_KINDS, SKILL_ROOT_RANK } from "./contracts.js";
1
+ import { a as SKILL_ROOT_KINDS, c as compareAutomaticSkillRuns, i as EVOLUTION_SIGNALS, l as skillCaseSignature, n as AUTO_ADOPTION_STAGES, o as SKILL_ROOT_RANK, r as DEFAULT_SKILLS_POLICY, t as AUTO_ADOPTION_REASONS } from "./contracts-CwhbQi-u.js";
2
2
  import { defineMemoryPlugin, defineMemorySource, installMemory, memoryConfigurationDigest, memoryInputRecord, memoryInputStringArray, memoryInputText, withMemoryStorageLock } from "dsh-mnemon/extension-sdk";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -837,15 +837,39 @@ function orphanedDraft(suggestion) {
837
837
  */
838
838
  function expireSuggestions(ledger, now) {
839
839
  let changed = false;
840
- for (const suggestion of ledger.suggestions) if (suggestion.status === "pending" && Date.parse(suggestion.expiresAt) < now.getTime()) {
841
- suggestion.status = "expired";
842
- suggestion.updatedAt = now.toISOString();
843
- changed = true;
844
- } else if (suggestion.status === "drafting" && orphanedDraft(suggestion)) {
845
- suggestion.status = "pending";
846
- suggestion.updatedAt = now.toISOString();
847
- suggestion.lastError = ORPHAN_DRAFT_ERROR;
848
- changed = true;
840
+ for (const suggestion of ledger.suggestions) {
841
+ const lastAuto = suggestion.autoAdoption?.at(-1);
842
+ if (lastAuto !== void 0 && [
843
+ "queued",
844
+ "drafting",
845
+ "backtesting",
846
+ "rewriting"
847
+ ].includes(lastAuto.stage) && (!Number.isFinite(Date.parse(lastAuto.at)) || Date.parse(lastAuto.at) < PROCESS_STARTED_AT)) {
848
+ const event = {
849
+ at: now.toISOString(),
850
+ stage: "paused",
851
+ attempt: lastAuto.attempt,
852
+ reason: "interrupted"
853
+ };
854
+ suggestion.autoAdoption = [...suggestion.autoAdoption, event].slice(-16);
855
+ const review = ledger.reviews.find((entry) => entry.id === suggestion.reviewId);
856
+ if (review !== void 0) review.autoAdoptions = [...review.autoAdoptions ?? [], {
857
+ ...event,
858
+ suggestionId: suggestion.id,
859
+ skillName: suggestion.skillName
860
+ }].slice(-160);
861
+ changed = true;
862
+ }
863
+ if (suggestion.status === "pending" && Date.parse(suggestion.expiresAt) < now.getTime()) {
864
+ suggestion.status = "expired";
865
+ suggestion.updatedAt = now.toISOString();
866
+ changed = true;
867
+ } else if (suggestion.status === "drafting" && orphanedDraft(suggestion)) {
868
+ suggestion.status = "pending";
869
+ suggestion.updatedAt = now.toISOString();
870
+ suggestion.lastError = ORPHAN_DRAFT_ERROR;
871
+ changed = true;
872
+ }
849
873
  }
850
874
  return changed;
851
875
  }
@@ -900,6 +924,7 @@ function admitReview(context, input) {
900
924
  const steps = (values) => [...new Set(values.filter((value) => Number.isInteger(value) && stepIndexes.has(value)))].sort((a, b) => a - b);
901
925
  const skipped = [];
902
926
  const allowed = (action) => (action === "create" ? policy.allowCreate : policy.allowUpdate) !== false;
927
+ const createdSuggestionIds = [];
903
928
  let inserted = 0;
904
929
  let updated = 0;
905
930
  let casesAdded = 0;
@@ -1038,6 +1063,7 @@ function admitReview(context, input) {
1038
1063
  expiresAt: new Date(now.getTime() + policy.expiryDays * 24 * 60 * 60 * 1e3).toISOString()
1039
1064
  };
1040
1065
  ledger.suggestions.unshift(suggestion);
1066
+ createdSuggestionIds.push(suggestion.id);
1041
1067
  touched.push(suggestion);
1042
1068
  inserted++;
1043
1069
  }
@@ -1151,6 +1177,7 @@ function admitReview(context, input) {
1151
1177
  reviewId,
1152
1178
  trajectoryId,
1153
1179
  suggestionIds: touched.map((suggestion) => suggestion.id),
1180
+ createdSuggestionIds,
1154
1181
  inserted,
1155
1182
  updated,
1156
1183
  casesAdded,
@@ -3266,15 +3293,17 @@ var SkillsService = class {
3266
3293
  const ledger = this.store.ledger();
3267
3294
  return ledger.drafts.filter((draft) => (filter.suggestionId === void 0 || draft.suggestionId === filter.suggestionId) && (filter.skill === void 0 || draft.skillName === filter.skill) && (filter.path === void 0 || this.draftOriginal(ledger, draft).skill?.path === filter.path));
3268
3295
  }
3269
- draft(_workspace, id) {
3296
+ draft(workspace, id) {
3270
3297
  const ledger = this.store.ledger();
3271
3298
  const draft = ledger.drafts.find((candidate) => candidate.id === id);
3272
3299
  if (draft === void 0) throw new Error("draft not found: " + id);
3273
3300
  const original = draft.action === "update" ? this.draftOriginal(ledger, draft) : void 0;
3274
3301
  const base = original?.skill;
3302
+ const conflict = draft.action === "update" ? base !== void 0 && base.hash !== draft.baseHash ? "base-changed" : void 0 : activeSkill(this.catalog(workspace), draft.skillName) !== void 0 ? "target-exists" : void 0;
3275
3303
  const now = this.clock().getTime();
3276
3304
  return {
3277
3305
  draft,
3306
+ ...conflict === void 0 ? {} : { conflict },
3278
3307
  ...original === void 0 ? {} : { resolution: original.resolution },
3279
3308
  ...base === void 0 ? {} : { base: readSkillDocument(base) },
3280
3309
  runs: draft.runIds.flatMap((id) => {
@@ -3648,7 +3677,9 @@ var SkillsService = class {
3648
3677
  }
3649
3678
  /** The Host marks a suggestion `drafting` before a task Agent writes, and back to `pending` if it fails. */
3650
3679
  suggestionStatus(input) {
3651
- return this.withSuggestion(input.id, (suggestion) => {
3680
+ return this.withSuggestion(input.id, (suggestion, ledger) => {
3681
+ input.signal?.throwIfAborted();
3682
+ this.requireExpectedDraft(ledger, suggestion, input.expectedDraft);
3652
3683
  if (input.status === "drafting") {
3653
3684
  if (suggestion.status !== "pending" && suggestion.status !== "review") throw new Error("this suggestion is " + suggestion.status + "; only an open suggestion can be drafted");
3654
3685
  suggestion.status = "drafting";
@@ -3661,6 +3692,28 @@ var SkillsService = class {
3661
3692
  return suggestion;
3662
3693
  });
3663
3694
  }
3695
+ requireExpectedDraft(ledger, suggestion, expected) {
3696
+ if (expected === void 0) return;
3697
+ const current = ledger.drafts.find((draft) => draft.id === suggestion?.draftId);
3698
+ if (!(expected === null ? suggestion !== void 0 && suggestion.draftId === void 0 : current?.id === expected.id && current.status === "ready" && sha256(current.content) === expected.hash)) throw new Error("The suggestion or draft changed while this draft was being generated; leave it for manual review");
3699
+ }
3700
+ recordAutoAdoption(id, input) {
3701
+ return this.withSuggestion(id, (suggestion, ledger) => {
3702
+ const event = {
3703
+ ...input,
3704
+ ...input.detail === void 0 ? {} : { detail: clip(input.detail, 500) },
3705
+ at: this.clock().toISOString()
3706
+ };
3707
+ suggestion.autoAdoption = [...suggestion.autoAdoption ?? [], event].slice(-16);
3708
+ const review = ledger.reviews.find((entry) => entry.id === suggestion.reviewId);
3709
+ if (review !== void 0) review.autoAdoptions = [...review.autoAdoptions ?? [], {
3710
+ ...event,
3711
+ suggestionId: id,
3712
+ skillName: suggestion.skillName
3713
+ }].slice(-160);
3714
+ return suggestion;
3715
+ });
3716
+ }
3664
3717
  pinTrajectory(id, pinned) {
3665
3718
  return this.write(() => {
3666
3719
  const ledger = this.store.ledger();
@@ -3783,6 +3836,7 @@ var SkillsService = class {
3783
3836
  }
3784
3837
  recordDraft(workspace, input) {
3785
3838
  return this.write(() => {
3839
+ input.signal?.throwIfAborted();
3786
3840
  validateSkillContent(input.content, input.skillName);
3787
3841
  const ledger = this.store.ledger();
3788
3842
  const catalog = this.catalog(workspace);
@@ -3793,8 +3847,10 @@ var SkillsService = class {
3793
3847
  const suggestion = input.suggestionId === void 0 ? void 0 : ledger.suggestions.find((candidate) => candidate.id === input.suggestionId);
3794
3848
  if (input.suggestionId !== void 0 && suggestion === void 0) throw new Error("suggestion not found: " + input.suggestionId);
3795
3849
  if (suggestion !== void 0 && suggestion.skillName !== input.skillName) throw new Error("suggestion is for another skill");
3850
+ this.requireExpectedDraft(ledger, suggestion, input.expectedDraft);
3796
3851
  const original = prior?.action === "update" ? this.draftOriginal(ledger, prior) : suggestion?.action === "update" ? this.suggestionOriginal(ledger, suggestion) : void 0;
3797
3852
  const loaded = original === void 0 ? activeSkill(catalog, input.skillName) : this.requireOriginal(original);
3853
+ if (input.expectedBaseHash !== void 0 && (loaded?.hash ?? null) !== input.expectedBaseHash) throw new Error("The skill changed while this draft was being generated; regenerate it from the current file");
3798
3854
  const origin = loaded === void 0 ? void 0 : original?.origin ?? skillOrigin(catalog, loaded);
3799
3855
  for (const draft of ledger.drafts) if (draft.status === "ready" && draft.skillName === input.skillName && draft.suggestionId === input.suggestionId && (loaded === void 0 ? draft.action === "create" : this.draftOriginal(ledger, draft).skill?.path === loaded.path)) {
3800
3856
  draft.status = "discarded";
@@ -3848,6 +3904,19 @@ var SkillsService = class {
3848
3904
  const ledger = this.store.ledger();
3849
3905
  const draft = ledger.drafts.find((candidate) => candidate.id === id);
3850
3906
  if (draft === void 0 || draft.status !== "ready") throw new Error("only a draft waiting for review can be adopted");
3907
+ let automaticResult;
3908
+ if (input.automatic !== void 0) {
3909
+ const proof = input.automatic;
3910
+ const suggestion = ledger.suggestions.find((entry) => entry.id === draft.suggestionId);
3911
+ const evals = this.store.evals();
3912
+ const cases = evals.cases.filter((entry) => entry.skillName === draft.skillName && entry.enabled);
3913
+ const casesHash = sha256(skillCaseSignature(cases));
3914
+ const before = evals.runs.find((run) => run.id === proof.baselineRunId);
3915
+ const after = evals.runs.find((run) => run.id === proof.draftRunId);
3916
+ automaticResult = compareAutomaticSkillRuns(cases, before, after, casesHash);
3917
+ if (suggestion?.status !== "review" || suggestion.draftId !== draft.id || Date.parse(suggestion.expiresAt) <= this.clock().getTime() || proof.draftHash !== sha256(draft.content) || casesHash !== proof.casesHash || before?.skillName !== draft.skillName || after?.skillName !== draft.skillName || !(Date.parse(before.startedAt) >= Date.parse(draft.createdAt)) || !(Date.parse(after.startedAt) >= Date.parse(draft.createdAt)) || after.target.kind !== "draft" || after.target.draftId !== draft.id || after.target.hash !== proof.draftHash || (draft.action === "update" ? before.target.kind !== "current" || before.target.hash !== draft.baseHash : before.target.kind !== "none") || automaticResult.outcome !== "not-worse") throw new Error("The draft, cases, or complete matching backtest results changed; automatic adoption requires manual review");
3918
+ }
3919
+ input.signal?.throwIfAborted();
3851
3920
  validateSkillContent(draft.content, draft.skillName);
3852
3921
  const catalog = this.catalog(workspace);
3853
3922
  let key;
@@ -3885,6 +3954,22 @@ var SkillsService = class {
3885
3954
  if (suggestion !== void 0) {
3886
3955
  suggestion.status = "adopted";
3887
3956
  suggestion.updatedAt = now;
3957
+ if (input.automatic !== void 0 && automaticResult !== void 0) {
3958
+ const event = {
3959
+ at: now,
3960
+ stage: "adopted",
3961
+ attempt: input.automatic.attempt,
3962
+ ...automaticResult.baselineScore === void 0 ? {} : { baselineScore: automaticResult.baselineScore },
3963
+ ...automaticResult.draftScore === void 0 ? {} : { draftScore: automaticResult.draftScore }
3964
+ };
3965
+ suggestion.autoAdoption = [...suggestion.autoAdoption ?? [], event].slice(-16);
3966
+ const review = ledger.reviews.find((entry) => entry.id === suggestion.reviewId);
3967
+ if (review !== void 0) review.autoAdoptions = [...review.autoAdoptions ?? [], {
3968
+ ...event,
3969
+ suggestionId: suggestion.id,
3970
+ skillName: suggestion.skillName
3971
+ }].slice(-160);
3972
+ }
3888
3973
  }
3889
3974
  this.store.saveLedger(ledger);
3890
3975
  return {
@@ -3945,6 +4030,26 @@ var SkillsService = class {
3945
4030
  return { reclaimed: Math.max(0, before - after) };
3946
4031
  });
3947
4032
  }
4033
+ /** Persist terminal state so expired tasks cannot later revive themselves. */
4034
+ reclaimStaleRuns() {
4035
+ return this.write(() => {
4036
+ const evals = this.store.evals();
4037
+ const now = this.clock();
4038
+ let reclaimed = 0;
4039
+ for (const run of evals.runs) {
4040
+ if (run.status !== "running" || Number.isFinite(Date.parse(run.startedAt)) && now.getTime() - Date.parse(run.startedAt) <= RUN_STALE_MS) continue;
4041
+ run.status = "interrupted";
4042
+ run.finishedAt = now.toISOString();
4043
+ run.error = "Backtest exceeded its 45 minute deadline; run it again.";
4044
+ reclaimed++;
4045
+ }
4046
+ if (reclaimed > 0) {
4047
+ this.store.saveEvals(evals);
4048
+ this.touch();
4049
+ }
4050
+ return { reclaimed };
4051
+ });
4052
+ }
3948
4053
  /** The content a run tests, resolved from its target. */
3949
4054
  targetContent(workspace, skillName, target) {
3950
4055
  if (target.kind === "none") return { target: { kind: "none" } };
@@ -3990,9 +4095,12 @@ var SkillsService = class {
3990
4095
  }
3991
4096
  startRun(input) {
3992
4097
  return this.write(() => {
4098
+ input.signal?.throwIfAborted();
3993
4099
  const evals = this.store.evals();
3994
4100
  const cases = input.caseIds.map((id) => evals.cases.find((entry) => entry.id === id && entry.skillName === input.skillName));
3995
4101
  if (cases.length === 0 || cases.some((entry) => entry === void 0)) throw new Error("a run needs at least one case of this skill");
4102
+ const casesHash = sha256(skillCaseSignature(cases));
4103
+ if (input.expectedCasesHash !== void 0 && input.expectedCasesHash !== casesHash) throw new Error("Eval cases changed before this backtest started");
3996
4104
  const run = {
3997
4105
  id: randomUUID(),
3998
4106
  skillName: input.skillName,
@@ -4003,6 +4111,8 @@ var SkillsService = class {
4003
4111
  total: cases.length
4004
4112
  },
4005
4113
  caseIds: input.caseIds,
4114
+ casesHash,
4115
+ ...input.model === void 0 ? {} : { model: input.model },
4006
4116
  results: [],
4007
4117
  startedAt: this.clock().toISOString()
4008
4118
  };
@@ -4029,6 +4139,7 @@ var SkillsService = class {
4029
4139
  }
4030
4140
  progressRun(input) {
4031
4141
  return this.write(() => {
4142
+ input.signal?.throwIfAborted();
4032
4143
  const evals = this.store.evals();
4033
4144
  const run = evals.runs.find((candidate) => candidate.id === input.id);
4034
4145
  if (run === void 0 || run.status !== "running") throw new Error("run is not running: " + input.id);
@@ -4055,9 +4166,11 @@ var SkillsService = class {
4055
4166
  }
4056
4167
  finishRun(input) {
4057
4168
  return this.write(() => {
4169
+ input.signal?.throwIfAborted();
4058
4170
  const evals = this.store.evals();
4059
4171
  const run = evals.runs.find((candidate) => candidate.id === input.id);
4060
4172
  if (run === void 0) throw new Error("run not found: " + input.id);
4173
+ if (run.status !== "running") return run;
4061
4174
  run.status = input.status;
4062
4175
  run.finishedAt = this.clock().toISOString();
4063
4176
  if (input.error !== void 0) run.error = clip(input.error, 500);
@@ -4551,6 +4664,55 @@ function draftModelInput(value) {
4551
4664
  model: memoryInputText(picked.model, "model.model", 200)
4552
4665
  };
4553
4666
  }
4667
+ function backtestModel(value) {
4668
+ if (value === void 0) return {};
4669
+ const model = draftModelInput(value);
4670
+ if (model === null) throw new Error("a backtest model must name its provider and model");
4671
+ const input = memoryInputRecord(value, "model");
4672
+ return { model: {
4673
+ ...model,
4674
+ ...typeof input.reasoningEffort === "string" ? { reasoningEffort: memoryInputText(input.reasoningEffort, "reasoningEffort", 100) } : {}
4675
+ } };
4676
+ }
4677
+ function draftExpectation(value) {
4678
+ if (value === void 0) return {};
4679
+ if (value === null) return { expectedDraft: null };
4680
+ const expected = memoryInputRecord(value, "expectedDraft");
4681
+ return { expectedDraft: {
4682
+ id: memoryInputText(expected.id, "expectedDraft.id", 100),
4683
+ hash: memoryInputText(expected.hash, "expectedDraft.hash", 100)
4684
+ } };
4685
+ }
4686
+ function automaticAdoption(value) {
4687
+ if (value === void 0) return {};
4688
+ const input = memoryInputRecord(value, "automatic");
4689
+ if (!Number.isInteger(input.attempt) || Number(input.attempt) < 1 || Number(input.attempt) > 3) throw new Error("automatic adoption attempt must be 1..3");
4690
+ return { automatic: {
4691
+ attempt: Number(input.attempt),
4692
+ draftHash: memoryInputText(input.draftHash, "draftHash", 100),
4693
+ baselineRunId: memoryInputText(input.baselineRunId, "baselineRunId", 100),
4694
+ draftRunId: memoryInputText(input.draftRunId, "draftRunId", 100),
4695
+ casesHash: memoryInputText(input.casesHash, "casesHash", 100)
4696
+ } };
4697
+ }
4698
+ function autoAdoptionEvent(value) {
4699
+ if (!AUTO_ADOPTION_STAGES.includes(value.stage)) throw new Error("unknown automatic adoption stage");
4700
+ if (value.reason !== void 0 && !AUTO_ADOPTION_REASONS.includes(value.reason)) throw new Error("unknown automatic adoption reason");
4701
+ if (!Number.isInteger(value.attempt) || Number(value.attempt) < 0 || Number(value.attempt) > 3) throw new Error("automatic adoption attempt must be 0..3");
4702
+ const score = (key) => {
4703
+ if (value[key] === void 0) return {};
4704
+ if (typeof value[key] !== "number" || !Number.isFinite(value[key]) || value[key] < 0 || value[key] > 1) throw new Error("invalid automatic adoption score");
4705
+ return { [key]: value[key] };
4706
+ };
4707
+ return {
4708
+ stage: value.stage,
4709
+ attempt: Number(value.attempt),
4710
+ ...value.reason === void 0 ? {} : { reason: value.reason },
4711
+ ...typeof value.detail === "string" ? { detail: value.detail } : {},
4712
+ ...score("baselineScore"),
4713
+ ...score("draftScore")
4714
+ };
4715
+ }
4554
4716
  function caseInput(value, skillName) {
4555
4717
  const input = memoryInputRecord(value, "case");
4556
4718
  const checks = input.checks;
@@ -4610,7 +4772,8 @@ function caseResult(value) {
4610
4772
  answer: typeof input.answer === "string" ? input.answer : "",
4611
4773
  ...typeof input.summary === "string" ? { summary: input.summary } : {},
4612
4774
  ...typeof input.error === "string" ? { error: input.error } : {},
4613
- ...unmeasuredReason === void 0 ? {} : { unmeasuredReason }
4775
+ ...unmeasuredReason === void 0 ? {} : { unmeasuredReason },
4776
+ ...typeof input.fullyGraded === "boolean" ? { fullyGraded: input.fullyGraded } : {}
4614
4777
  };
4615
4778
  }
4616
4779
  function signal(value) {
@@ -4777,6 +4940,11 @@ function createSkillsMemorySource(config = {}) {
4777
4940
  ...effective.agentsHome === void 0 ? {} : { agentsHome: effective.agentsHome }
4778
4941
  });
4779
4942
  service.reclaimOrphanDrafts().catch(() => void 0);
4943
+ const recovery = service.reclaimStaleRuns();
4944
+ const reaper = setInterval(() => {
4945
+ service.reclaimStaleRuns().catch(() => void 0);
4946
+ }, 6e4);
4947
+ reaper.unref?.();
4780
4948
  const facts = (scope, management) => management ? {
4781
4949
  sourceInstanceKey: context.sourceInstanceKey,
4782
4950
  sourceTypeId: SKILLS_SOURCE_TYPE_ID,
@@ -4798,6 +4966,9 @@ function createSkillsMemorySource(config = {}) {
4798
4966
  hints: { reason: "management-only" }
4799
4967
  };
4800
4968
  return {
4969
+ dispose() {
4970
+ clearInterval(reaper);
4971
+ },
4801
4972
  facts(request) {
4802
4973
  return facts(request.scope, request.scenario.startsWith("management."));
4803
4974
  },
@@ -4805,6 +4976,7 @@ function createSkillsMemorySource(config = {}) {
4805
4976
  return { fragments: [] };
4806
4977
  },
4807
4978
  async manage(request) {
4979
+ await recovery;
4808
4980
  const root = workspace(request.scope);
4809
4981
  const input = request.input ?? {};
4810
4982
  const fields = typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
@@ -4931,12 +5103,17 @@ function createSkillsMemorySource(config = {}) {
4931
5103
  const status = fields.status;
4932
5104
  if (status !== "drafting" && status !== "pending") throw new Error("status must be drafting or pending");
4933
5105
  value = await service.suggestionStatus({
5106
+ ...request.signal === void 0 ? {} : { signal: request.signal },
4934
5107
  id: memoryInputText(fields.id, "id", 100),
4935
5108
  status,
5109
+ ...draftExpectation(fields.expectedDraft),
4936
5110
  ...typeof fields.error === "string" ? { error: fields.error } : {}
4937
5111
  });
4938
5112
  break;
4939
5113
  }
5114
+ case "auto-adoption-record":
5115
+ value = await service.recordAutoAdoption(memoryInputText(fields.id, "id", 100), autoAdoptionEvent(fields));
5116
+ break;
4940
5117
  case "trajectory-pin":
4941
5118
  value = await service.pinTrajectory(memoryInputText(fields.id, "id", 100), bool(fields.pinned, "pinned"));
4942
5119
  break;
@@ -4962,6 +5139,9 @@ function createSkillsMemorySource(config = {}) {
4962
5139
  }
4963
5140
  case "draft-record":
4964
5141
  value = await service.recordDraft(root, {
5142
+ ...request.signal === void 0 ? {} : { signal: request.signal },
5143
+ ...draftExpectation(fields.expectedDraft),
5144
+ ...fields.expectedBaseHash === void 0 ? {} : { expectedBaseHash: fields.expectedBaseHash === null ? null : memoryInputText(fields.expectedBaseHash, "expectedBaseHash", 100) },
4965
5145
  ...optionalText(fields.suggestionId, "suggestionId", 100) === void 0 ? {} : { suggestionId: optionalText(fields.suggestionId, "suggestionId", 100) },
4966
5146
  ...optionalText(fields.draftId, "draftId", 100) === void 0 ? {} : { draftId: optionalText(fields.draftId, "draftId", 100) },
4967
5147
  skillName: memoryInputText(fields.skillName, "skillName", 64),
@@ -4979,7 +5159,11 @@ function createSkillsMemorySource(config = {}) {
4979
5159
  });
4980
5160
  break;
4981
5161
  case "draft-adopt":
4982
- value = await service.adoptDraft(root, memoryInputText(fields.id, "id", 100), { ...rootKind(fields.root) === void 0 ? {} : { root: rootKind(fields.root) } });
5162
+ value = await service.adoptDraft(root, memoryInputText(fields.id, "id", 100), {
5163
+ ...request.signal === void 0 ? {} : { signal: request.signal },
5164
+ ...automaticAdoption(fields.automatic),
5165
+ ...rootKind(fields.root) === void 0 ? {} : { root: rootKind(fields.root) }
5166
+ });
4983
5167
  break;
4984
5168
  case "draft-discard":
4985
5169
  value = await service.discardDraft(memoryInputText(fields.id, "id", 100));
@@ -5002,9 +5186,12 @@ function createSkillsMemorySource(config = {}) {
5002
5186
  }
5003
5187
  case "run-start":
5004
5188
  value = await service.startRun({
5189
+ ...request.signal === void 0 ? {} : { signal: request.signal },
5005
5190
  skillName: memoryInputText(fields.skillName, "skillName", 64),
5006
5191
  target: runTarget(fields.target),
5007
- caseIds: memoryInputStringArray(fields.caseIds, "caseIds", 20) ?? []
5192
+ caseIds: memoryInputStringArray(fields.caseIds, "caseIds", 40) ?? [],
5193
+ ...fields.expectedCasesHash === void 0 ? {} : { expectedCasesHash: memoryInputText(fields.expectedCasesHash, "expectedCasesHash", 100) },
5194
+ ...backtestModel(fields.model)
5008
5195
  });
5009
5196
  break;
5010
5197
  case "run-link":
@@ -5012,13 +5199,23 @@ function createSkillsMemorySource(config = {}) {
5012
5199
  break;
5013
5200
  case "run-progress":
5014
5201
  value = await service.progressRun({
5202
+ ...request.signal === void 0 ? {} : { signal: request.signal },
5015
5203
  id: memoryInputText(fields.id, "id", 100),
5016
5204
  result: caseResult(fields.result)
5017
5205
  });
5018
5206
  break;
5207
+ case "run-cancel":
5208
+ value = await service.finishRun({
5209
+ ...request.signal === void 0 ? {} : { signal: request.signal },
5210
+ id: memoryInputText(fields.id, "id", 100),
5211
+ status: "interrupted",
5212
+ error: "Backtest cancelled by the user."
5213
+ });
5214
+ break;
5019
5215
  case "run-finish": {
5020
- const status = fields.status === "completed" ? "completed" : "failed";
5216
+ const status = fields.status === "completed" ? "completed" : fields.status === "interrupted" ? "interrupted" : "failed";
5021
5217
  value = await service.finishRun({
5218
+ ...request.signal === void 0 ? {} : { signal: request.signal },
5022
5219
  id: memoryInputText(fields.id, "id", 100),
5023
5220
  status,
5024
5221
  ...typeof fields.error === "string" ? { error: fields.error } : {}
@@ -0,0 +1,9 @@
1
+ import type { BacktestRun, EvalCase } from './contracts.ts';
2
+ /** Stable evaluation inputs, excluding presentation timestamps and provenance. Hash at the server boundary. */
3
+ export declare function skillCaseSignature(cases: readonly EvalCase[]): string;
4
+ /** Automatic decisions require full matching coverage, not the UI's scored intersection. */
5
+ export declare function compareAutomaticSkillRuns(cases: readonly EvalCase[], baseline: BacktestRun | undefined, draft: BacktestRun | undefined, casesHash: string): {
6
+ outcome: 'unmeasured' | 'regressed' | 'not-worse';
7
+ baselineScore?: number;
8
+ draftScore?: number;
9
+ };
@@ -101,6 +101,7 @@ export interface SkillsPageClient {
101
101
  discardDraft(id: string): Promise<SkillDraft>;
102
102
  /** Attach a baseline run to the draft it is compared with. */
103
103
  linkRun(draftId: string, runId: string): Promise<void>;
104
+ cancelRun(id: string): Promise<BacktestRun>;
104
105
  /** The model each piece of model work follows: the two global picks and their per-item overrides. */
105
106
  draftPreferences(): Promise<DraftPreferencesView>;
106
107
  /** Pick the model for drafts, for backtests, or for one suggestion or skill; `null` follows the task Agent again. */
@@ -140,6 +141,7 @@ export interface SkillsPageClient {
140
141
  skill?: string;
141
142
  guidance?: string;
142
143
  targetRoot?: SkillRootKind;
144
+ backtest?: boolean;
143
145
  provider?: string;
144
146
  model?: string;
145
147
  }): Promise<{
@@ -174,6 +174,7 @@ export interface SkillSuggestion {
174
174
  status: SuggestionStatus;
175
175
  draftId?: string;
176
176
  lastError?: string;
177
+ autoAdoption?: AutoAdoptionEvent[];
177
178
  createdAt: string;
178
179
  updatedAt: string;
179
180
  expiresAt: string;
@@ -274,6 +275,8 @@ export interface CaseResult {
274
275
  error?: string;
275
276
  /** Kept under verdict=error so older readers also exclude it from scores. */
276
277
  unmeasuredReason?: 'requires-vision';
278
+ /** Every expected judge check was explicitly returned; absent on older runs. */
279
+ fullyGraded?: boolean;
277
280
  }
278
281
  export type RunStatus = 'running' | 'completed' | 'failed' | 'interrupted';
279
282
  export interface BacktestRun {
@@ -286,6 +289,11 @@ export interface BacktestRun {
286
289
  total: number;
287
290
  };
288
291
  caseIds: string[];
292
+ /** Source-verified case inputs at start; absent on older runs. */
293
+ casesHash?: string;
294
+ model?: ModelRoute & {
295
+ reasoningEffort?: string;
296
+ };
289
297
  results: CaseResult[];
290
298
  /** Over the graded cases; absent when none could be graded. */
291
299
  passRate?: number;
@@ -312,6 +320,10 @@ export interface ReviewLogEntry {
312
320
  error?: string;
313
321
  /** The conversation turn the reviewed window ended with. */
314
322
  turn?: number;
323
+ autoAdoptions?: Array<AutoAdoptionEvent & {
324
+ suggestionId: string;
325
+ skillName: string;
326
+ }>;
315
327
  /** The suggestions this review created or strengthened. */
316
328
  suggestionIds?: string[];
317
329
  }
@@ -415,6 +427,8 @@ export interface ReviewRecordResult {
415
427
  reviewId: string;
416
428
  trajectoryId?: string;
417
429
  suggestionIds: string[];
430
+ /** Newly inserted ids only; older Sources omit this capability. */
431
+ createdSuggestionIds?: string[];
418
432
  inserted: number;
419
433
  updated: number;
420
434
  casesAdded: number;
@@ -469,6 +483,8 @@ export interface SuggestionView {
469
483
  }
470
484
  export interface DraftView {
471
485
  draft: SkillDraft;
486
+ /** File changes need a new draft, never a retry with a newer revision. */
487
+ conflict?: 'base-changed' | 'target-exists';
472
488
  /** The file the draft replaces, as it is now. */
473
489
  base?: SkillDocument;
474
490
  resolution?: SkillResolution;
@@ -620,3 +636,26 @@ export interface MarketInstall {
620
636
  files: number;
621
637
  installedAt: string;
622
638
  }
639
+ export declare const AUTO_ADOPTION_STAGES: readonly ["queued", "drafting", "backtesting", "rewriting", "adopted", "paused", "review"];
640
+ export declare const AUTO_ADOPTION_REASONS: readonly ["disabled", "no-cases", "case-limit", "unmeasured", "regressed", "changed", "failed", "queue-full", "model-unavailable", "interrupted"];
641
+ export interface AutoAdoptionEvent {
642
+ at: string;
643
+ stage: typeof AUTO_ADOPTION_STAGES[number];
644
+ attempt: number;
645
+ reason?: typeof AUTO_ADOPTION_REASONS[number];
646
+ detail?: string;
647
+ baselineScore?: number;
648
+ draftScore?: number;
649
+ }
650
+ export type DraftExpectation = {
651
+ id: string;
652
+ hash: string;
653
+ } | null;
654
+ export interface AutomaticAdoption {
655
+ attempt: number;
656
+ draftHash: string;
657
+ baselineRunId: string;
658
+ draftRunId: string;
659
+ casesHash: string;
660
+ }
661
+ export { skillCaseSignature, compareAutomaticSkillRuns } from './auto-adoption.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mnemon-source-skills",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "Skills Source for Composable View Memory: DSH skill management, evolution suggestions, evidence trajectories and eval sets.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -62,7 +62,7 @@
62
62
  "@types/node": "^22.0.0",
63
63
  "@types/react": "^18.3.0",
64
64
  "@types/react-dom": "^18.3.0",
65
- "dsh-mnemon": "^0.5.42",
65
+ "dsh-mnemon": "^0.5.43",
66
66
  "jsdom": "^26.0.0",
67
67
  "react": "^18.2.0",
68
68
  "react-dom": "^18.2.0",
@@ -353,7 +353,34 @@
353
353
  "evolution.loadFailedBody": "请重试,或在左侧选择其他建议。",
354
354
  "tests.model": "全局模型",
355
355
  "tests.modelCard": "所选回测模型",
356
- "tests.modelAuto": "跟随默认"
356
+ "tests.modelAuto": "跟随默认",
357
+ "draft.baseChanged": "原技能已变更,此草案已过期。请基于当前文件重新生成并回测。",
358
+ "draft.targetExists": "同名技能已存在。请重新生成为改进草案并回测,再确认采纳。",
359
+ "draft.refreshGuidance": "基于当前技能文件重新生成,保留最近的修改,并重新运行回测。",
360
+ "draft.regenerate": "重新生成并回测",
361
+ "draft.cancelBacktest": "取消回测",
362
+ "draft.interrupted": "回测已中断。可以重新回测,或在检查草案后手动采纳。",
363
+ "evolution.auto.title": "自动采纳",
364
+ "evolution.auto.history": "自动采纳活动记录",
365
+ "evolution.auto.attempt": "第 {count} 轮",
366
+ "evolution.auto.scores": "基线 {baseline} / 草案 {draft}",
367
+ "evolution.auto.stage.queued": "等待处理",
368
+ "evolution.auto.stage.drafting": "正在起草",
369
+ "evolution.auto.stage.backtesting": "正在对比回测",
370
+ "evolution.auto.stage.rewriting": "依据退步结果重写",
371
+ "evolution.auto.stage.adopted": "已自动采纳",
372
+ "evolution.auto.stage.paused": "已停止自动处理",
373
+ "evolution.auto.stage.review": "需要人工审查",
374
+ "evolution.auto.reason.disabled": "自动采纳或所需权限已关闭",
375
+ "evolution.auto.reason.no-cases": "没有启用的回测用例",
376
+ "evolution.auto.reason.case-limit": "启用的用例超过每轮上限;调整用例或上限后手动处理本建议",
377
+ "evolution.auto.reason.unmeasured": "回测未完整测出所有检查项,或存在图片依赖、错误、中断",
378
+ "evolution.auto.reason.regressed": "至少一个用例或原先通过的检查项出现退步",
379
+ "evolution.auto.reason.changed": "建议、草案或用例已变动",
380
+ "evolution.auto.reason.failed": "自动处理失败,保留人工处理入口",
381
+ "evolution.auto.reason.queue-full": "自动处理队列已满",
382
+ "evolution.auto.reason.model-unavailable": "无法确定起草或回测模型",
383
+ "evolution.auto.reason.interrupted": "Host 已停止,不自动重启此建议"
357
384
  },
358
385
  "en": {
359
386
  "page.title": "Skills",
@@ -709,6 +736,33 @@
709
736
  "evolution.loadFailedBody": "Retry, or select another suggestion on the left.",
710
737
  "tests.model": "Global model",
711
738
  "tests.modelCard": "Selected model",
712
- "tests.modelAuto": "Follow default"
739
+ "tests.modelAuto": "Follow default",
740
+ "draft.baseChanged": "The original skill changed. This draft is out of date; regenerate it against the current file and backtest it.",
741
+ "draft.targetExists": "A skill with this name now exists. Regenerate an update and backtest it before adopting.",
742
+ "draft.refreshGuidance": "Regenerate against the current skill file, preserving its latest changes, then run backtests.",
743
+ "draft.regenerate": "Regenerate and backtest",
744
+ "draft.cancelBacktest": "Cancel backtests",
745
+ "draft.interrupted": "Backtests were interrupted. Run them again, or review the draft before adopting manually.",
746
+ "evolution.auto.title": "Automatic adoption",
747
+ "evolution.auto.history": "Automatic adoption activity",
748
+ "evolution.auto.attempt": "Attempt {count}",
749
+ "evolution.auto.scores": "Baseline {baseline} / draft {draft}",
750
+ "evolution.auto.stage.queued": "Queued",
751
+ "evolution.auto.stage.drafting": "Drafting",
752
+ "evolution.auto.stage.backtesting": "Backtesting a pair",
753
+ "evolution.auto.stage.rewriting": "Rewriting after regression",
754
+ "evolution.auto.stage.adopted": "Automatically adopted",
755
+ "evolution.auto.stage.paused": "Automatic work stopped",
756
+ "evolution.auto.stage.review": "Manual review required",
757
+ "evolution.auto.reason.disabled": "Automatic adoption or its required permissions were disabled",
758
+ "evolution.auto.reason.no-cases": "No enabled evaluation cases",
759
+ "evolution.auto.reason.case-limit": "Enabled cases exceed the per-run limit; adjust cases or the limit and review this suggestion manually",
760
+ "evolution.auto.reason.unmeasured": "Some checks were not fully measured, or a case needs images, failed or was interrupted",
761
+ "evolution.auto.reason.regressed": "At least one case or previously passing check regressed",
762
+ "evolution.auto.reason.changed": "The suggestion, draft or evaluation cases changed",
763
+ "evolution.auto.reason.failed": "Automatic work failed; manual review remains available",
764
+ "evolution.auto.reason.queue-full": "Automatic queue is full",
765
+ "evolution.auto.reason.model-unavailable": "No resolved draft or backtest model",
766
+ "evolution.auto.reason.interrupted": "Host stopped; this suggestion will not restart automatically"
713
767
  }
714
768
  }