dsh-continual-evolve 0.10.6 → 0.12.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/README.md +35 -122
- package/README.zh.md +26 -97
- package/cordis.patch.yml +2 -2
- package/lib/index.d.ts +44 -241
- package/lib/index.js +30 -253
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +32 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/package.json +2 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -72
- package/lib/approval.js +0 -127
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command.d.ts +0 -49
- package/lib/command.js +0 -568
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -74
- package/lib/copy.js +0 -160
- package/lib/declines.d.ts +0 -101
- package/lib/declines.js +0 -175
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -59
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -196
- package/lib/inject.js +0 -412
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/memory-agent.d.ts +0 -129
- package/lib/memory-agent.js +0 -568
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -59
- package/lib/notify.js +0 -97
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -78
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -191
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -49
- package/lib/service.js +0 -180
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -62
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -72
- package/lib/turn-snapshot.js +0 -238
- package/lib/types.d.ts +0 -208
- package/lib/types.js +0 -127
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup.d.ts +0 -260
- package/lib/wrapup.js +0 -539
package/lib/usage.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entry usage tracking (gap B1): records how many times each entry has been
|
|
3
|
-
* injected into system prompts. The counts are durable (persisted to disk)
|
|
4
|
-
* and exposed in `evolve_list` and the gate's archive-candidate reporting,
|
|
5
|
-
* so "zero-usage stale entries" can be surfaced for cleanup.
|
|
6
|
-
*
|
|
7
|
-
* Storage: `<baseDir>/evolve/usage.json` — a flat JSON object mapping
|
|
8
|
-
* `kind:id` to an integer count. Reads are tolerant of missing/corrupt files;
|
|
9
|
-
* writes are atomic (tmp + rename).
|
|
10
|
-
*/
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
const USAGE_FILE = "usage.json";
|
|
14
|
-
function usagePath(baseDir) {
|
|
15
|
-
return join(baseDir, "evolve", USAGE_FILE);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Load the usage store from disk; returns an empty store when absent or
|
|
19
|
-
* corrupt. Accepts BOTH on-disk shapes:
|
|
20
|
-
* - legacy (≤0.3.x): a flat `{ "kind:id": count }` map,
|
|
21
|
-
* - v2: `{ version: 2, counts, lastSession }` with per-session dedup.
|
|
22
|
-
*/
|
|
23
|
-
export function loadUsage(baseDir) {
|
|
24
|
-
const path = usagePath(baseDir);
|
|
25
|
-
try {
|
|
26
|
-
if (!existsSync(path))
|
|
27
|
-
return { counts: {}, lastSession: {} };
|
|
28
|
-
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
-
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
30
|
-
const record = raw;
|
|
31
|
-
if (record["version"] === 2 && typeof record["counts"] === "object" && record["counts"] !== null) {
|
|
32
|
-
return {
|
|
33
|
-
counts: record["counts"],
|
|
34
|
-
lastSession: typeof record["lastSession"] === "object" && record["lastSession"] !== null
|
|
35
|
-
? record["lastSession"]
|
|
36
|
-
: {},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
// Legacy flat map: every key is a count.
|
|
40
|
-
return { counts: raw, lastSession: {} };
|
|
41
|
-
}
|
|
42
|
-
return { counts: {}, lastSession: {} };
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return { counts: {}, lastSession: {} };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/** Persist the usage store atomically (always the v2 shape). */
|
|
49
|
-
export function saveUsage(baseDir, store) {
|
|
50
|
-
const dir = join(baseDir, "evolve");
|
|
51
|
-
mkdirSync(dir, { recursive: true });
|
|
52
|
-
const path = usagePath(baseDir);
|
|
53
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
54
|
-
writeFileSync(tmp, `${JSON.stringify({ version: 2, counts: store.counts, lastSession: store.lastSession ?? {} }, null, 2)}\n`, "utf8");
|
|
55
|
-
renameSync(tmp, path);
|
|
56
|
-
}
|
|
57
|
-
/** Build the usage key for an entry. */
|
|
58
|
-
export function usageKey(kind, id) {
|
|
59
|
-
return `${kind}:${id}`;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Increment injection counts for the entries that were actually injected.
|
|
63
|
-
* Called after `entriesSectionText` renders the injected block. Keys not
|
|
64
|
-
* present in the store are initialized to 1.
|
|
65
|
-
*
|
|
66
|
-
* Session dedup (2026-08-22): with a sessionId, each key counts AT MOST
|
|
67
|
-
* ONCE per session — the old per-build counting produced meaningless
|
|
68
|
-
* numbers (one entry hit 2311× in a week) and hid the real "how many
|
|
69
|
-
* sessions found this useful" signal that staleness decay needs. Without
|
|
70
|
-
* a sessionId the call degrades to legacy always-increment behavior.
|
|
71
|
-
*/
|
|
72
|
-
export function recordInjection(baseDir, injectedKeys, sessionId) {
|
|
73
|
-
if (injectedKeys.length === 0)
|
|
74
|
-
return;
|
|
75
|
-
const store = loadUsage(baseDir);
|
|
76
|
-
let dirty = false;
|
|
77
|
-
for (const key of injectedKeys) {
|
|
78
|
-
if (sessionId !== undefined && store.lastSession?.[key] === sessionId) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
store.counts[key] = (store.counts[key] ?? 0) + 1;
|
|
82
|
-
if (store.lastSession && sessionId !== undefined) {
|
|
83
|
-
store.lastSession[key] = sessionId;
|
|
84
|
-
}
|
|
85
|
-
dirty = true;
|
|
86
|
-
}
|
|
87
|
-
if (dirty) {
|
|
88
|
-
saveUsage(baseDir, store);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Get the injection count for a specific entry. Returns 0 when the entry
|
|
93
|
-
* has never been injected (absent from the store).
|
|
94
|
-
*/
|
|
95
|
-
export function getUsageCount(store, kind, id) {
|
|
96
|
-
return store.counts[usageKey(kind, id)] ?? 0;
|
|
97
|
-
}
|
|
98
|
-
//# sourceMappingURL=usage.js.map
|
package/lib/validate.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code-enforced validation of model-proposed edits. Every rule here exists
|
|
3
|
-
* because the proposal is untrusted input: enum membership, immutability of
|
|
4
|
-
* the base system prompt, required fields per action, and the executable
|
|
5
|
-
* contract skill entries must carry.
|
|
6
|
-
*/
|
|
7
|
-
import type { HarnessEntry, HarnessScope, RefinementEdit } from "./types.js";
|
|
8
|
-
export declare const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
|
|
9
|
-
/**
|
|
10
|
-
* Gap C2: mechanical check that an edit's declared blast radius is coherent
|
|
11
|
-
* with the scope it targets. A session-scoped edit claiming "general" would
|
|
12
|
-
* silently read like a cross-project tactical rule; a global edit claiming
|
|
13
|
-
* "session" would contradict its persistence. Absent blastRadius is NOT
|
|
14
|
-
* rejected (pre-C2 data and manual edits stay compatible) — the planner is
|
|
15
|
-
* instructed to always declare it, and this rule catches what it declares
|
|
16
|
-
* incoherently.
|
|
17
|
-
*/
|
|
18
|
-
export declare function validateBlastRadiusScope(scope: HarnessScope, blastRadius: "general" | "project" | "session"): string | undefined;
|
|
19
|
-
/**
|
|
20
|
-
* Returns a human-readable failure reason, or undefined when the edit passes.
|
|
21
|
-
*
|
|
22
|
-
* `before` (the entry the edit targets, absent for creates/unknown ids) lets
|
|
23
|
-
* update rules distinguish "carrying the persisted value" from "changing it"
|
|
24
|
-
* — e.g. a rollback inverse re-carries the stored skill_kind, which must
|
|
25
|
-
* pass, while a genuine executable↔guidance switch must not.
|
|
26
|
-
*/
|
|
27
|
-
export declare function validateEdit(edit: RefinementEdit, computedId: string | undefined, scope?: HarnessScope, before?: HarnessEntry): string | undefined;
|
|
28
|
-
//# sourceMappingURL=validate.d.ts.map
|
package/lib/validate.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
|
|
2
|
-
import { validateSkillEntryContent } from "./skillquality.js";
|
|
3
|
-
const ACTIONS = new Set(["create", "update", "delete", "archive"]);
|
|
4
|
-
const KINDS = new Set(["prompt", "memory", "skill", "subagent"]);
|
|
5
|
-
export const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
|
|
6
|
-
/**
|
|
7
|
-
* Gap C2: mechanical check that an edit's declared blast radius is coherent
|
|
8
|
-
* with the scope it targets. A session-scoped edit claiming "general" would
|
|
9
|
-
* silently read like a cross-project tactical rule; a global edit claiming
|
|
10
|
-
* "session" would contradict its persistence. Absent blastRadius is NOT
|
|
11
|
-
* rejected (pre-C2 data and manual edits stay compatible) — the planner is
|
|
12
|
-
* instructed to always declare it, and this rule catches what it declares
|
|
13
|
-
* incoherently.
|
|
14
|
-
*/
|
|
15
|
-
export function validateBlastRadiusScope(scope, blastRadius) {
|
|
16
|
-
if (scope === "local" && blastRadius === "general") {
|
|
17
|
-
return "local-scope edit must declare blastRadius \"session\" or \"project\" (\"general\" would claim a cross-project rule)";
|
|
18
|
-
}
|
|
19
|
-
if (scope === "global" && blastRadius === "session") {
|
|
20
|
-
return "global-scope edit must declare blastRadius \"general\" or \"project\" (\"session\" contradicts cross-session persistence)";
|
|
21
|
-
}
|
|
22
|
-
if (scope === "project" && blastRadius === "session") {
|
|
23
|
-
return "project-scope edit must declare blastRadius \"project\" or \"general\" (\"session\" contradicts cross-session persistence)";
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Returns a human-readable failure reason, or undefined when the edit passes.
|
|
29
|
-
*
|
|
30
|
-
* `before` (the entry the edit targets, absent for creates/unknown ids) lets
|
|
31
|
-
* update rules distinguish "carrying the persisted value" from "changing it"
|
|
32
|
-
* — e.g. a rollback inverse re-carries the stored skill_kind, which must
|
|
33
|
-
* pass, while a genuine executable↔guidance switch must not.
|
|
34
|
-
*/
|
|
35
|
-
export function validateEdit(edit, computedId, scope, before) {
|
|
36
|
-
if (!ACTIONS.has(edit.action)) {
|
|
37
|
-
return `unsupported action ${String(edit.action)}`;
|
|
38
|
-
}
|
|
39
|
-
if (!KINDS.has(edit.kind)) {
|
|
40
|
-
return `unsupported kind ${String(edit.kind)}`;
|
|
41
|
-
}
|
|
42
|
-
if (edit.kind === "prompt" && (edit.id === BASE_SYSTEM_PROMPT_ID || computedId === BASE_SYSTEM_PROMPT_ID)) {
|
|
43
|
-
return "base system prompt is not editable";
|
|
44
|
-
}
|
|
45
|
-
if (edit.action !== "create" && !edit.id) {
|
|
46
|
-
return `${edit.action} requires id`;
|
|
47
|
-
}
|
|
48
|
-
// Gap C2: blast-radius/scope coherence is a mechanical property of the
|
|
49
|
-
// edit payload itself — checked for every action, not only create/update.
|
|
50
|
-
if (scope && edit.blastRadius !== undefined) {
|
|
51
|
-
const blastError = validateBlastRadiusScope(scope, edit.blastRadius);
|
|
52
|
-
if (blastError)
|
|
53
|
-
return blastError;
|
|
54
|
-
}
|
|
55
|
-
// Archive only names an existing entry: no title/content payload, and the
|
|
56
|
-
// base system prompt stays immutable under every action.
|
|
57
|
-
if (edit.action === "archive") {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
// Create needs the full payload (nothing to fall back to). Update may
|
|
61
|
-
// carry any subset — apply merges with `?? before` — but must carry at
|
|
62
|
-
// least one change (review audit 2026-08-28 B3/B5: the old blanket
|
|
63
|
-
// "update requires title and content" broke every partial-update path:
|
|
64
|
-
// skill archive/demote, consolidate, and the evolve_update tool contract
|
|
65
|
-
// all construct payload-subset updates).
|
|
66
|
-
if (edit.action === "create" && (!edit.title || !edit.content)) {
|
|
67
|
-
return "create requires title and content";
|
|
68
|
-
}
|
|
69
|
-
if (edit.action === "update" &&
|
|
70
|
-
edit.title === undefined &&
|
|
71
|
-
edit.content === undefined &&
|
|
72
|
-
edit.path === undefined &&
|
|
73
|
-
edit.metadata === undefined &&
|
|
74
|
-
edit.reference === undefined &&
|
|
75
|
-
edit.arguments === undefined) {
|
|
76
|
-
return "update carries no changes";
|
|
77
|
-
}
|
|
78
|
-
if (edit.kind === "memory" && (edit.action === "create" || edit.action === "update")) {
|
|
79
|
-
// One-fact typed memories: every memory create declares its recall
|
|
80
|
-
// type; feedback/project (pitfalls, conventions) additionally carry
|
|
81
|
-
// the Why + How-to-apply structure so the fact is reusable, not just
|
|
82
|
-
// recorded. Updates are partial — only carried fields are checked,
|
|
83
|
-
// resolved against the persisted entry.
|
|
84
|
-
const carriedType = edit.metadata?.[MEMORY_TYPE_KEY];
|
|
85
|
-
if (edit.action === "create" && !isMemoryType(carriedType)) {
|
|
86
|
-
return `create memory requires metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference (one fact per entry; update the existing entry instead of adding a duplicate)`;
|
|
87
|
-
}
|
|
88
|
-
if (carriedType !== undefined && !isMemoryType(carriedType)) {
|
|
89
|
-
return `invalid metadata.${MEMORY_TYPE_KEY} ${JSON.stringify(carriedType)} (one of user|feedback|project|reference)`;
|
|
90
|
-
}
|
|
91
|
-
const effectiveType = isMemoryType(carriedType)
|
|
92
|
-
? carriedType
|
|
93
|
-
: edit.action === "update"
|
|
94
|
-
? before?.metadata?.[MEMORY_TYPE_KEY]
|
|
95
|
-
: undefined;
|
|
96
|
-
if ((effectiveType === "feedback" || effectiveType === "project") &&
|
|
97
|
-
(edit.action === "create" || edit.content !== undefined)) {
|
|
98
|
-
const content = edit.content ?? "";
|
|
99
|
-
if (!/why/i.test(content) || !/how/i.test(content)) {
|
|
100
|
-
return `${effectiveType} memory content must carry a Why and a How to apply section (the fact plus why it matters and how to apply it next time)`;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (edit.action === "create" && edit.kind === "skill") {
|
|
105
|
-
// Guidance skills are SKILL.md documents: no python reference (a
|
|
106
|
-
// reference on a guidance skill would be an invented contract) and
|
|
107
|
-
// no arguments contract. Executable skills keep the full contract.
|
|
108
|
-
if (edit.skill_kind === "guidance") {
|
|
109
|
-
if (edit.reference !== undefined && Object.keys(edit.reference).length > 0) {
|
|
110
|
-
return "guidance skill must not carry a python reference (it is a SKILL.md document, not an executable)";
|
|
111
|
-
}
|
|
112
|
-
if (edit.arguments !== undefined && Object.keys(edit.arguments).length > 0) {
|
|
113
|
-
return "guidance skill must not carry an arguments contract (only executable skills declare inputs)";
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
const contractError = validateSkillContract(edit);
|
|
118
|
-
if (contractError)
|
|
119
|
-
return contractError;
|
|
120
|
-
}
|
|
121
|
-
// The entry body materializes as a SKILL.md under generated
|
|
122
|
-
// frontmatter; content-level mechanics (no shadowing `---`, no
|
|
123
|
-
// escaping resource refs) are code-enforced so a bad body never
|
|
124
|
-
// reaches the store (mirrors skill-creator's validate-frontmatter).
|
|
125
|
-
const contentProblems = validateSkillEntryContent(edit.content ?? "");
|
|
126
|
-
if (contentProblems.length > 0) {
|
|
127
|
-
return contentProblems.join("; ");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (edit.action === "update" && edit.kind === "skill") {
|
|
131
|
-
// skill_kind is immutable on update: a switch without the matching
|
|
132
|
-
// contract pair would leave an invalid entry (guidance carrying a
|
|
133
|
-
// python reference, or an executable with none). Re-carrying the
|
|
134
|
-
// persisted value (rollback inverses do) passes. Recreate the entry
|
|
135
|
-
// to change the kind.
|
|
136
|
-
if (edit.skill_kind !== undefined && before !== undefined && before.skill_kind !== edit.skill_kind) {
|
|
137
|
-
return "update cannot change skill_kind — delete and recreate the entry instead";
|
|
138
|
-
}
|
|
139
|
-
// A carried non-empty reference replaces the contract wholesale, so it
|
|
140
|
-
// must be a complete executable contract on its own. Empty references
|
|
141
|
-
// (guidance skills) and absent payloads pass through — apply keeps
|
|
142
|
-
// the persisted values.
|
|
143
|
-
if (edit.reference !== undefined && Object.keys(edit.reference).length > 0) {
|
|
144
|
-
const contractError = validateSkillContract(edit);
|
|
145
|
-
if (contractError)
|
|
146
|
-
return contractError;
|
|
147
|
-
}
|
|
148
|
-
if (edit.content !== undefined) {
|
|
149
|
-
const contentProblems = validateSkillEntryContent(edit.content);
|
|
150
|
-
if (contentProblems.length > 0) {
|
|
151
|
-
return contentProblems.join("; ");
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
function validateSkillContract(edit) {
|
|
158
|
-
if (edit.arguments === undefined) {
|
|
159
|
-
return "create/update skill requires arguments";
|
|
160
|
-
}
|
|
161
|
-
const reference = edit.reference;
|
|
162
|
-
if (!reference || typeof reference !== "object") {
|
|
163
|
-
return "create/update skill requires python reference";
|
|
164
|
-
}
|
|
165
|
-
const ref = reference;
|
|
166
|
-
if (ref.type !== "python") {
|
|
167
|
-
return "create/update skill reference.type must be python";
|
|
168
|
-
}
|
|
169
|
-
const hasImport = (typeof ref.import === "string" && ref.import.length > 0) ||
|
|
170
|
-
(typeof ref.python_import === "string" && ref.python_import.length > 0);
|
|
171
|
-
const hasCallable = (typeof ref.callable === "string" && ref.callable.length > 0) ||
|
|
172
|
-
(typeof ref.call_pattern === "string" && ref.call_pattern.length > 0);
|
|
173
|
-
if (!hasImport) {
|
|
174
|
-
return "create/update skill requires python import";
|
|
175
|
-
}
|
|
176
|
-
if (!hasCallable) {
|
|
177
|
-
return "create/update skill requires callable or call_pattern";
|
|
178
|
-
}
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
//# sourceMappingURL=validate.js.map
|
package/lib/wrapup-command.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `/evolve wrapup` subcommand handler. Extracted from command.ts (P2-2).
|
|
3
|
-
*/
|
|
4
|
-
import type { Context } from "@deepseek-ai/cordis";
|
|
5
|
-
import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
|
|
6
|
-
import type { EvolutionEngine } from "./service.js";
|
|
7
|
-
import { type PromotionPolicy } from "./promotion.js";
|
|
8
|
-
import { type RecordLanguagePreference } from "./record-language.js";
|
|
9
|
-
export declare function executeWrapupCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, policy?: PromotionPolicy, recordLanguage?: RecordLanguagePreference): Promise<CommandResult>;
|
|
10
|
-
//# sourceMappingURL=wrapup-command.d.ts.map
|
package/lib/wrapup-command.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import { questionServiceOf, requireGlobalApproval } from "./approval.js";
|
|
2
|
-
import { wrapupArchiveCopy } from "./copy.js";
|
|
3
|
-
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, valenceStampProposal, wholePromoteProposals } from "./wrapup.js";
|
|
4
|
-
import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
|
|
5
|
-
import { normalizeRecordLanguage, resolveRecordLanguage } from "./record-language.js";
|
|
6
|
-
import { recentUserText } from "./inject.js";
|
|
7
|
-
function success(text) {
|
|
8
|
-
return { kind: "success", text };
|
|
9
|
-
}
|
|
10
|
-
export async function executeWrapupCommand(ctx, engine, invocation, policy = DEFAULT_PROMOTION_POLICY, recordLanguage) {
|
|
11
|
-
const sessionId = invocation.agent.id;
|
|
12
|
-
const localState = engine.load("local", sessionId);
|
|
13
|
-
const globalState = engine.load("global", undefined);
|
|
14
|
-
const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
|
|
15
|
-
if (candidates.length === 0) {
|
|
16
|
-
return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
|
|
17
|
-
}
|
|
18
|
-
// 1. Classify: the model judges each audited candidate's fate.
|
|
19
|
-
const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
|
|
20
|
-
signal: invocation.signal,
|
|
21
|
-
tokenUsage: {
|
|
22
|
-
baseDir: engine.baseDir,
|
|
23
|
-
sessionId,
|
|
24
|
-
retain: engine.retention.tokenUsage,
|
|
25
|
-
onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
26
|
-
},
|
|
27
|
-
tokenUsagePhase: "wrapup",
|
|
28
|
-
});
|
|
29
|
-
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
30
|
-
// 2. Partition by action. Deterministic guards re-check the LIVE global
|
|
31
|
-
// store right before anything lands (state may have changed mid-call).
|
|
32
|
-
const { promotable, skipped } = filterPromotable(assessment.items, globalState, candidates, policy);
|
|
33
|
-
const promoteItems = promotable.filter((item) => item.verdict === "promote");
|
|
34
|
-
const archiveItems = assessment.items.filter((item) => item.verdict === "archive");
|
|
35
|
-
// Split promotion (A-form): archive a mixed entry but promote ONLY the
|
|
36
|
-
// cleaned durable part the model extracted. Guarded the same way as whole
|
|
37
|
-
// promotes — a split that would duplicate a globally covered topic is
|
|
38
|
-
// dropped and the entry archives plain.
|
|
39
|
-
const splitItems = [];
|
|
40
|
-
const splitSkipped = [];
|
|
41
|
-
for (const item of archiveItems) {
|
|
42
|
-
if (!item.promote)
|
|
43
|
-
continue;
|
|
44
|
-
const candidate = byKey.get(item.key);
|
|
45
|
-
if (!candidate) {
|
|
46
|
-
splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
|
|
50
|
-
if (blocked) {
|
|
51
|
-
splitSkipped.push({ key: item.key, reason: blocked });
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
splitItems.push({ item, candidate });
|
|
55
|
-
}
|
|
56
|
-
// Plain archives (no split payload): the symmetric guard — an archive that
|
|
57
|
-
// is NOT globally covered AND was distilled from real user messages must
|
|
58
|
-
// not proceed silently.
|
|
59
|
-
const plainArchives = archiveItems.filter((item) => !item.promote);
|
|
60
|
-
const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
|
|
61
|
-
const keepItems = assessment.items.filter((item) => item.verdict === "keep");
|
|
62
|
-
// 3. Report the assessment before touching anything.
|
|
63
|
-
const lines = [
|
|
64
|
-
`wrapup assessment (${sessionId}): ${candidates.length} candidates${candidates.some((c) => c.coveredGlobally) ? `, ${candidates.filter((c) => c.coveredGlobally).length} covered globally` : ""}`,
|
|
65
|
-
`${assessment.rationale}`,
|
|
66
|
-
];
|
|
67
|
-
for (const [heading, items] of [
|
|
68
|
-
["PROMOTE (to global)", promoteItems],
|
|
69
|
-
["SPLIT (archive + promote durable part)", splitItems.map((split) => split.item)],
|
|
70
|
-
["ARCHIVE", silentArchives],
|
|
71
|
-
["ARCHIVE (needs review)", reviewArchives],
|
|
72
|
-
["KEEP", keepItems],
|
|
73
|
-
]) {
|
|
74
|
-
lines.push(`${heading}: ${items.length}`);
|
|
75
|
-
for (const item of items) {
|
|
76
|
-
const candidate = byKey.get(item.key);
|
|
77
|
-
const title = candidate ? candidate.title : item.key;
|
|
78
|
-
const splitNote = item.promote ? ` → 拆出提升「${item.promote.title}」` : "";
|
|
79
|
-
lines.push(`- ${item.key} "${title}"${splitNote} — ${item.reason}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
for (const skip of skipped) {
|
|
83
|
-
lines.push(`- promote skipped: ${skip.key} — ${skip.reason}`);
|
|
84
|
-
}
|
|
85
|
-
for (const skip of splitSkipped) {
|
|
86
|
-
lines.push(`- split skipped: ${skip.key} — ${skip.reason}`);
|
|
87
|
-
}
|
|
88
|
-
lines.push("");
|
|
89
|
-
const applied = [];
|
|
90
|
-
// 4. Global writes: governed resource — ONE human approval gate covers
|
|
91
|
-
// every create (whole promotes AND split promotions). On approval:
|
|
92
|
-
// - whole promote → create global copy + stamp local promotedTo+archivedAt;
|
|
93
|
-
// - split → create the cleaned durable part + archive the original with
|
|
94
|
-
// promotedTo. On rejection: whole promotes are not written, and each
|
|
95
|
-
// split's original STILL archives plain (its snapshot half deserves
|
|
96
|
-
// the archive; the durable half is reported for manual handling).
|
|
97
|
-
const wholeCreates = promoteItems.map((item) => ({ item, candidate: byKey.get(item.key) }));
|
|
98
|
-
const splitCreates = splitItems;
|
|
99
|
-
const allCreates = new Set([...wholeCreates.map((c) => c.item.key), ...splitCreates.map((c) => c.item.key)]);
|
|
100
|
-
if (allCreates.size > 0) {
|
|
101
|
-
const what = `wrapup 将写入跨会话 global store(共 ${allCreates.size} 条:${promoteItems.length} 条整条提升 + ${splitItems.length} 条拆解提升):\n${[
|
|
102
|
-
...promoteItems.map((item) => `- 整条提升 ${item.key} "${byKey.get(item.key)?.title ?? item.key}"`),
|
|
103
|
-
...splitItems.map((split) => `- 拆解提升 ${split.item.key} → 清洗「${split.item.promote?.title}」(原条目随之归档)`),
|
|
104
|
-
].join("\n")}`;
|
|
105
|
-
let promoteAllowed = true;
|
|
106
|
-
try {
|
|
107
|
-
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what, recordLanguage);
|
|
108
|
-
}
|
|
109
|
-
catch (cause) {
|
|
110
|
-
promoteAllowed = false;
|
|
111
|
-
const message = `global 写入未批准 — 整条提升与拆解提升均未写入 (${cause instanceof Error ? cause.message : String(cause)})`;
|
|
112
|
-
applied.push(message);
|
|
113
|
-
lines.push(message);
|
|
114
|
-
}
|
|
115
|
-
if (promoteAllowed) {
|
|
116
|
-
// Whole promotes: create global entry, retire the local copy.
|
|
117
|
-
// Shared proposal builders keep the wrap-up command and the gate's
|
|
118
|
-
// local-fate dimension writing IDENTICAL edits.
|
|
119
|
-
for (const { item, candidate } of wholeCreates) {
|
|
120
|
-
if (!candidate)
|
|
121
|
-
continue;
|
|
122
|
-
const proposals = wholePromoteProposals(item, candidate, sessionId);
|
|
123
|
-
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
124
|
-
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
125
|
-
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
126
|
-
scope: "local",
|
|
127
|
-
baselineState: localState,
|
|
128
|
-
});
|
|
129
|
-
applied.push(`promoted ${item.key} → global:${createdId} (${globalResult.id}; local stamped ${localResult.id})`);
|
|
130
|
-
}
|
|
131
|
-
// Split promotions: create the cleaned durable part, retire the
|
|
132
|
-
// original local entry (its snapshot half is archived along).
|
|
133
|
-
for (const { item, candidate } of splitCreates) {
|
|
134
|
-
if (!item.promote)
|
|
135
|
-
continue;
|
|
136
|
-
const proposals = splitPromoteProposals(item, candidate, sessionId);
|
|
137
|
-
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
138
|
-
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
139
|
-
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
140
|
-
scope: "local",
|
|
141
|
-
baselineState: localState,
|
|
142
|
-
});
|
|
143
|
-
applied.push(`split ${item.key}: promoted cleaned part → global:${createdId} (${globalResult.id}); original archived (${localResult.id})`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
// Rejected: whole promotes stay un-written; each split's original
|
|
148
|
-
// still archives plain (reported, data restorable).
|
|
149
|
-
for (const { item, candidate } of splitCreates) {
|
|
150
|
-
if (!candidate)
|
|
151
|
-
continue;
|
|
152
|
-
const result = engine.apply("local", sessionId, {
|
|
153
|
-
summary: `wrapup: split promotion not approved — archive original ${item.key} plain`,
|
|
154
|
-
rationale: item.reason,
|
|
155
|
-
expectedOutcome: `The original leaves injection; the cleaned part was NOT written (reported for manual handling).`,
|
|
156
|
-
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
157
|
-
}, { scope: "local", baselineState: localState });
|
|
158
|
-
applied.push(`split ${item.key}: promotion not approved — original archived plain (${result.id})`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// 5. Silent archives: deterministic local action (hidden from injection,
|
|
163
|
-
// data kept restorable) — covered topics and operational entries need no
|
|
164
|
-
// confirmation, matching the original behavior.
|
|
165
|
-
for (const item of silentArchives) {
|
|
166
|
-
const candidate = byKey.get(item.key);
|
|
167
|
-
if (!candidate)
|
|
168
|
-
continue;
|
|
169
|
-
const result = engine.apply("local", sessionId, {
|
|
170
|
-
summary: `wrapup: archive local ${item.key} — ${item.reason}`,
|
|
171
|
-
rationale: item.reason,
|
|
172
|
-
expectedOutcome: `The entry stops being injected but stays restorable.`,
|
|
173
|
-
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
174
|
-
}, { scope: "local", baselineState: localState });
|
|
175
|
-
applied.push(`archived ${item.key} (${result.id})`);
|
|
176
|
-
}
|
|
177
|
-
// 6. Review archives (symmetric guard): not covered globally + distilled
|
|
178
|
-
// from real user messages — the user decides before this content is
|
|
179
|
-
// hidden from future sessions. No question service → conservative keep.
|
|
180
|
-
const userQuestions = questionServiceOf(ctx);
|
|
181
|
-
for (const item of reviewArchives) {
|
|
182
|
-
const candidate = byKey.get(item.key);
|
|
183
|
-
if (!candidate)
|
|
184
|
-
continue;
|
|
185
|
-
if (!userQuestions) {
|
|
186
|
-
applied.push(`kept ${item.key} — archive pending user confirmation (no question service)`);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const questionId = "evolve-wrapup-archive-review";
|
|
190
|
-
let archiveConfirmed = false;
|
|
191
|
-
try {
|
|
192
|
-
const copy = wrapupArchiveCopy(candidate.title, normalizeRecordLanguage(recordLanguage) ??
|
|
193
|
-
resolveRecordLanguage({ ctx, trajectoryText: recentUserText(invocation.agent) }));
|
|
194
|
-
const answer = await userQuestions.ask({
|
|
195
|
-
questions: [
|
|
196
|
-
{
|
|
197
|
-
id: questionId,
|
|
198
|
-
question: copy.question,
|
|
199
|
-
options: copy.options,
|
|
200
|
-
},
|
|
201
|
-
],
|
|
202
|
-
agent: invocation.agent,
|
|
203
|
-
signal: invocation.signal,
|
|
204
|
-
});
|
|
205
|
-
const selected = answer.answers?.find((entry) => entry.id === questionId)?.selected ?? [];
|
|
206
|
-
archiveConfirmed = selected.includes("归档") || selected.includes("Archive");
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
archiveConfirmed = false;
|
|
210
|
-
}
|
|
211
|
-
if (archiveConfirmed) {
|
|
212
|
-
const result = engine.apply("local", sessionId, {
|
|
213
|
-
summary: `wrapup: archive local ${item.key} (user-confirmed) — ${item.reason}`,
|
|
214
|
-
rationale: item.reason,
|
|
215
|
-
expectedOutcome: `The entry stops being injected but stays restorable.`,
|
|
216
|
-
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
217
|
-
}, { scope: "local", baselineState: localState });
|
|
218
|
-
applied.push(`archived ${item.key} (user-confirmed, ${result.id})`);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
applied.push(`kept ${item.key} — user declined the archive`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// 7. Valence stamps (P1 效价反馈): keep-verdict entries the assessor marked
|
|
225
|
-
// contradicted get their negative counter bumped — the entry stays live
|
|
226
|
-
// but sinks in injection ranking and the next assessment prefers
|
|
227
|
-
// archiving it. Deterministic local action, no approval needed.
|
|
228
|
-
for (const item of keepItems) {
|
|
229
|
-
if (!item.contradicted)
|
|
230
|
-
continue;
|
|
231
|
-
const candidate = byKey.get(item.key);
|
|
232
|
-
if (!candidate)
|
|
233
|
-
continue;
|
|
234
|
-
const proposal = valenceStampProposal(item, candidate);
|
|
235
|
-
if (!proposal)
|
|
236
|
-
continue;
|
|
237
|
-
const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
|
|
238
|
-
applied.push(`contradicted stamp ${item.key} → valenceNegative=${candidate.negativeCount + 1} (${result.id})`);
|
|
239
|
-
}
|
|
240
|
-
lines.push(...(applied.length > 0 ? applied : ["(no changes applied — all entries kept)"]));
|
|
241
|
-
return success(lines.join("\n"));
|
|
242
|
-
}
|
|
243
|
-
//# sourceMappingURL=wrapup-command.js.map
|