dsh-engram 0.7.4 → 0.8.6
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 +93 -6
- package/README.zh.md +6 -2
- package/docs/CONTEXT-GC-GUIDE.zh.md +15 -1
- package/docs/G6-quality-ab.zh.md +540 -0
- package/docs/GATES.zh.md +41 -0
- package/docs/PROPOSAL-evolution.zh.md +12 -0
- package/docs/RELEASE-NOTES-v0.7.10.md +49 -0
- package/docs/RELEASE-NOTES-v0.7.11.md +31 -0
- package/docs/RELEASE-NOTES-v0.7.12.md +22 -0
- package/docs/RELEASE-NOTES-v0.7.13.md +41 -0
- package/docs/RELEASE-NOTES-v0.7.14.md +37 -0
- package/docs/RELEASE-NOTES-v0.7.15.md +36 -0
- package/docs/RELEASE-NOTES-v0.7.16.md +42 -0
- package/docs/RELEASE-NOTES-v0.7.5.md +134 -0
- package/docs/RELEASE-NOTES-v0.7.6.md +74 -0
- package/docs/RELEASE-NOTES-v0.7.7.md +60 -0
- package/docs/RELEASE-NOTES-v0.7.8.md +56 -0
- package/docs/RELEASE-NOTES-v0.7.9.md +46 -0
- package/docs/RELEASE-NOTES-v0.8.0.md +59 -0
- package/docs/RELEASE-NOTES-v0.8.1.md +70 -0
- package/docs/RELEASE-NOTES-v0.8.2.md +62 -0
- package/docs/RELEASE-NOTES-v0.8.3.md +65 -0
- package/docs/RELEASE-NOTES-v0.8.4.md +56 -0
- package/docs/RELEASE-NOTES-v0.8.5.md +60 -0
- package/docs/RELEASE-NOTES-v0.8.6.md +71 -0
- package/eval/gate-audit.mjs +538 -0
- package/lib/api.js +70 -0
- package/lib/approvals.js +84 -0
- package/lib/boundary-prune.js +71 -5
- package/lib/capsule.js +134 -0
- package/lib/capture.js +83 -5
- package/lib/client.js +66 -0
- package/lib/compaction.js +4 -11
- package/lib/context-gc.js +63 -2
- package/lib/index-block.js +79 -4
- package/lib/index.js +91 -18
- package/lib/ledger.js +200 -10
- package/lib/pack-telemetry.js +10 -3
- package/lib/prune.js +89 -0
- package/lib/session-events.js +44 -2
- package/lib/skill-export.js +289 -0
- package/lib/status-insights.js +5 -0
- package/lib/status.js +138 -0
- package/lib/store.js +34 -0
- package/lib/usage.js +32 -3
- package/lib/web-provision.js +59 -14
- package/package.json +15 -8
- package/scripts/dsh-engram.mjs +229 -4
package/lib/web-provision.js
CHANGED
|
@@ -50,6 +50,9 @@ import { dirname, join } from "node:path";
|
|
|
50
50
|
/** The engine package subpath a preset row mounts dsh-engram's engine under. */
|
|
51
51
|
export const ENGRAM_ROW_NAME = "dsh-engram/compaction";
|
|
52
52
|
|
|
53
|
+
/** The subpath the realm-side BoundaryPrune row mounts. */
|
|
54
|
+
export const ENGRAM_PRUNE_ROW_NAME = "dsh-engram/prune";
|
|
55
|
+
|
|
53
56
|
/** The stock row name the shipped presets mount inside the `compaction` group. */
|
|
54
57
|
export const STOCK_ROW_NAME = "'@deepseek-ai/dsh-compaction-basic'";
|
|
55
58
|
|
|
@@ -59,6 +62,9 @@ const STOCK_NAME = "@deepseek-ai/dsh-compaction-basic";
|
|
|
59
62
|
/** Row ids the provisioner looks for (list-item `- id:` values). */
|
|
60
63
|
const STOCK_ROW_ID = "compaction-basic";
|
|
61
64
|
const ENGRAM_ROW_ID = "engram-compaction";
|
|
65
|
+
/** BoundaryPrune (L1) row: it must live in the SAME realm as the sessions it
|
|
66
|
+
* prunes, otherwise its listener never sees a `todo/write` boundary. */
|
|
67
|
+
const ENGRAM_PRUNE_ROW_ID = "engram-prune";
|
|
62
68
|
const COMPACTION_GROUP_ID = "compaction";
|
|
63
69
|
|
|
64
70
|
/** Engine config the plugin writes into each wired preset row (settings-driven). */
|
|
@@ -102,6 +108,12 @@ export function engramCompactionRowText(config = {}, indent = 4) {
|
|
|
102
108
|
].join("\n");
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
/** The realm-side BoundaryPrune row (no config: it reads the plugin defaults). */
|
|
112
|
+
export function engramPruneRowText(indent = 4) {
|
|
113
|
+
const pad = " ".repeat(indent);
|
|
114
|
+
return [`${pad}- id: ${ENGRAM_PRUNE_ROW_ID}`, `${pad} name: ${ENGRAM_PRUNE_ROW_NAME}`].join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
/** The replacement row at default settings (back-compat export). */
|
|
106
118
|
export const ENGRAM_COMPACTION_ROW = engramCompactionRowText(DEFAULT_ROW_CONFIG);
|
|
107
119
|
|
|
@@ -193,7 +205,7 @@ function locateRows(text) {
|
|
|
193
205
|
const lines = text.split("\n");
|
|
194
206
|
const groups = compactionGroupSpans(lines);
|
|
195
207
|
// Two compaction groups in one composition: refuse to guess which one runs.
|
|
196
|
-
if (groups.length > 1) return { lines, stock: [], engram: [], ambiguous: true };
|
|
208
|
+
if (groups.length > 1) return { lines, stock: [], engram: [], prune: [], ambiguous: true };
|
|
197
209
|
const span = groups[0] ?? { start: 0, end: lines.length, fallback: true };
|
|
198
210
|
const valid = (rowId, name) => {
|
|
199
211
|
const hits = [];
|
|
@@ -210,6 +222,7 @@ function locateRows(text) {
|
|
|
210
222
|
lines,
|
|
211
223
|
stock: valid(STOCK_ROW_ID, STOCK_NAME),
|
|
212
224
|
engram: valid(ENGRAM_ROW_ID, ENGRAM_ROW_NAME),
|
|
225
|
+
prune: valid(ENGRAM_PRUNE_ROW_ID, ENGRAM_PRUNE_ROW_NAME),
|
|
213
226
|
ambiguous: false,
|
|
214
227
|
};
|
|
215
228
|
}
|
|
@@ -244,6 +257,7 @@ function invariantsOk(text) {
|
|
|
244
257
|
!text.includes(STOCK_NAME) &&
|
|
245
258
|
text.includes("command-compact") &&
|
|
246
259
|
text.includes("tool-result-pruner") &&
|
|
260
|
+
text.includes(ENGRAM_PRUNE_ROW_NAME) &&
|
|
247
261
|
text.includes("cordis:group") &&
|
|
248
262
|
text.includes("isolate:")
|
|
249
263
|
);
|
|
@@ -260,29 +274,50 @@ function invariantsOk(text) {
|
|
|
260
274
|
* @returns {string | null}
|
|
261
275
|
*/
|
|
262
276
|
export function swapCompactionRow(text, rowConfig = {}) {
|
|
263
|
-
const { lines, stock, engram, ambiguous } = locateRows(text);
|
|
277
|
+
const { lines, stock, engram, prune, ambiguous } = locateRows(text);
|
|
264
278
|
if (ambiguous) return null;
|
|
265
279
|
if (engram.length > 0) {
|
|
266
280
|
if (engram.length > 1 || stock.length > 0) return null;
|
|
267
281
|
const index = engram[0];
|
|
268
282
|
const end = blockEnd(lines, index);
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
283
|
+
const indent = indentOf(lines[index]);
|
|
284
|
+
const rowText = engramCompactionRowText(rowConfig, indent);
|
|
285
|
+
let next = lines.slice(index, end).join("\n") === rowText ? text : replaceBlock(lines, index, end, rowText);
|
|
286
|
+
// An older wiring has the engine row but no prune row: add it in the same
|
|
287
|
+
// realm so L1 can finally see a boundary.
|
|
288
|
+
next = ensurePruneRow(next, indent);
|
|
272
289
|
return invariantsOk(next) ? next : null;
|
|
273
290
|
}
|
|
274
291
|
if (stock.length !== 1) return null;
|
|
275
292
|
const index = stock[0];
|
|
276
293
|
const end = blockEnd(lines, index);
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
index,
|
|
280
|
-
|
|
281
|
-
engramCompactionRowText(rowConfig, indentOf(lines[index])),
|
|
294
|
+
const indent = indentOf(lines[index]);
|
|
295
|
+
const next = ensurePruneRow(
|
|
296
|
+
replaceBlock(lines, index, end, engramCompactionRowText(rowConfig, indent)),
|
|
297
|
+
indent,
|
|
282
298
|
);
|
|
283
299
|
return invariantsOk(next) ? next : null;
|
|
284
300
|
}
|
|
285
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Insert the BoundaryPrune row directly after the engine row when the
|
|
304
|
+
* composition does not already carry one. Idempotent.
|
|
305
|
+
* @param {string} text
|
|
306
|
+
* @param {number} indent - indentation of the row being inserted next to.
|
|
307
|
+
* @returns {string}
|
|
308
|
+
*/
|
|
309
|
+
export function ensurePruneRow(text, indent = 4) {
|
|
310
|
+
const { lines, engram, prune } = locateRows(text);
|
|
311
|
+
if (prune !== undefined && prune.length > 0) return text;
|
|
312
|
+
if (engram === undefined || engram.length !== 1) return text;
|
|
313
|
+
const end = blockEnd(lines, engram[0]);
|
|
314
|
+
return [
|
|
315
|
+
...lines.slice(0, end),
|
|
316
|
+
...engramPruneRowText(indent).split("\n"),
|
|
317
|
+
...lines.slice(end),
|
|
318
|
+
].join("\n");
|
|
319
|
+
}
|
|
320
|
+
|
|
286
321
|
/**
|
|
287
322
|
* Reverse {@link swapCompactionRow}: restore the stock `compaction-basic` row
|
|
288
323
|
* from a wired composition (any engram row block — engine config included).
|
|
@@ -291,11 +326,21 @@ export function swapCompactionRow(text, rowConfig = {}) {
|
|
|
291
326
|
* @returns {string | null}
|
|
292
327
|
*/
|
|
293
328
|
export function restoreCompactionRow(text) {
|
|
294
|
-
const { lines, engram, ambiguous } = locateRows(text);
|
|
329
|
+
const { lines, engram, prune, ambiguous } = locateRows(text);
|
|
295
330
|
if (ambiguous || engram.length !== 1) return null;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
331
|
+
// Remove the realm prune row first (it is not stock), then restore the engine row.
|
|
332
|
+
let working = text;
|
|
333
|
+
if (prune !== undefined && prune.length === 1) {
|
|
334
|
+
const pIndex = prune[0];
|
|
335
|
+
// Splice the row out entirely — leaving a blank line behind would break the
|
|
336
|
+
// byte-identical round trip back to the stock text.
|
|
337
|
+
working = [...lines.slice(0, pIndex), ...lines.slice(blockEnd(lines, pIndex))].join("\n");
|
|
338
|
+
}
|
|
339
|
+
const after = locateRows(working);
|
|
340
|
+
if (after.engram.length !== 1) return null;
|
|
341
|
+
const index = after.engram[0];
|
|
342
|
+
const end = blockEnd(after.lines, index);
|
|
343
|
+
const next = replaceBlock(after.lines, index, end, stockCompactionRowText(indentOf(after.lines[index])));
|
|
299
344
|
return compactionWireStatus(next) === "stock" ? next : null;
|
|
300
345
|
}
|
|
301
346
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engram",
|
|
3
3
|
"description": "Minimalist long-term memory for DeepSeek Harness, distilling symbolic-index + pi-esr ideas: zero-LLM auto-capture, a symbolic [ENGRAM] index with progressive disclosure, and an ESR-lite evidence-closure protocol (esr_task / esr_node / esr_close / esr_link). Real agent-behaviour telemetry (usage rollup + /stats) and a deterministic offline recall benchmark (npm run eval). Hot path is model-free; storage on ctx.storageDomain; web memory viewer, observability panel and config card through DSH's native settings slots.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.6",
|
|
5
5
|
"packageManager": "pnpm@11.22.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./lib/index.js",
|
|
22
22
|
"./compaction": "./lib/compaction.js",
|
|
23
|
+
"./prune": "./lib/prune.js",
|
|
23
24
|
"./client": "./lib/client.js",
|
|
24
25
|
"./package.json": "./package.json",
|
|
25
26
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"lib",
|
|
29
30
|
"scripts",
|
|
30
31
|
"docs",
|
|
32
|
+
"eval/gate-audit.mjs",
|
|
31
33
|
"cordis.patch.yml",
|
|
32
34
|
"README.md",
|
|
33
35
|
"README.zh.md",
|
|
@@ -38,19 +40,24 @@
|
|
|
38
40
|
"dsh": ">=0.1.2-alpha.2 <0.3.0-0"
|
|
39
41
|
},
|
|
40
42
|
"scripts": {
|
|
41
|
-
"
|
|
42
|
-
"prepublishOnly": "node scripts/prepublish-check.mjs",
|
|
43
|
+
"bench:ledger": "node eval/ledger-bench.mjs",
|
|
43
44
|
"build:client": "node client/build.mjs && node client/build-model.mjs",
|
|
44
|
-
"
|
|
45
|
+
"dsh-engram": "node scripts/dsh-engram.mjs",
|
|
46
|
+
"dsh-engram:doctor": "node scripts/dsh-engram.mjs doctor",
|
|
45
47
|
"eval": "node eval/recall-bench.mjs",
|
|
48
|
+
"gate:audit": "node eval/gate-audit.mjs",
|
|
49
|
+
"playbook:audit": "node eval/playbook-audit.mjs",
|
|
50
|
+
"prepublishOnly": "node scripts/prepublish-check.mjs",
|
|
51
|
+
"quality:ab": "node eval/quality-ab.mjs",
|
|
52
|
+
"release-notes": "node scripts/gen-release-notes.mjs",
|
|
53
|
+
"setup": "node scripts/setup-links.mjs",
|
|
54
|
+
"skill:check": "node eval/skill-export-check.mjs",
|
|
55
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
46
56
|
"typecheck": "node client/typecheck.mjs",
|
|
47
57
|
"web-compaction": "node scripts/dsh-engram.mjs",
|
|
48
|
-
"web-compaction:status": "node scripts/dsh-engram.mjs status",
|
|
49
58
|
"web-compaction:enable": "node scripts/dsh-engram.mjs enable",
|
|
50
59
|
"web-compaction:revert": "node scripts/dsh-engram.mjs revert",
|
|
51
|
-
"
|
|
52
|
-
"dsh-engram:doctor": "node scripts/dsh-engram.mjs doctor",
|
|
53
|
-
"release-notes": "node scripts/gen-release-notes.mjs"
|
|
60
|
+
"web-compaction:status": "node scripts/dsh-engram.mjs status"
|
|
54
61
|
},
|
|
55
62
|
"publishConfig": {
|
|
56
63
|
"access": "public"
|
package/scripts/dsh-engram.mjs
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* npx dsh-engram enable # re-run the boot-time web auto-wiring (usually a no-op)
|
|
8
8
|
* npx dsh-engram revert # restore the stock compaction rows everywhere —
|
|
9
9
|
* # run BEFORE uninstalling dsh-engram
|
|
10
|
+
* npx dsh-engram skill export [--workspace <dir>] [--dir <skills>] [--dry-run]
|
|
11
|
+
* # export evidence-backed procedures as DSH Skills
|
|
12
|
+
* # (default: <workspace>/.dsh/skills)
|
|
10
13
|
*
|
|
11
14
|
* `status`/`doctor` read the durable snapshot the plugin writes at boot
|
|
12
15
|
* (`$DSH_HOME/engram/context-gc.status.json`) and cross-check the live preset
|
|
@@ -28,7 +31,13 @@ import {
|
|
|
28
31
|
provisionWebCompaction,
|
|
29
32
|
revertWebCompaction,
|
|
30
33
|
} from "../lib/web-provision.js";
|
|
31
|
-
import { contextGcStatusPath, readContextGcStatus } from "../lib/status.js";
|
|
34
|
+
import { contextGcStatusPath, readContextGcStatus, readPruneAttempts } from "../lib/status.js";
|
|
35
|
+
import { readLedger } from "../lib/ledger.js";
|
|
36
|
+
import { defaultSkillDir, exportSkills, skillDescription, skillName, planSkillExport } from "../lib/skill-export.js";
|
|
37
|
+
import { approvedIds, isApproved, loadApprovals, recordApproval, revokeApproval } from "../lib/approvals.js";
|
|
38
|
+
import { isProvenProcedure, invalidatedProcedure } from "../lib/status-insights.js";
|
|
39
|
+
import { workspaceKey } from "../lib/util.js";
|
|
40
|
+
import { ENGRAM_PRUNE_ROW_NAME } from "../lib/web-provision.js";
|
|
32
41
|
|
|
33
42
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
43
|
|
|
@@ -193,6 +202,40 @@ async function printStatus(presetsDirFlag) {
|
|
|
193
202
|
const presets = await discoverPresets(status, presetsDirFlag);
|
|
194
203
|
|
|
195
204
|
log("── dsh-engram Context GC 状态 ──────────────────────────────");
|
|
205
|
+
// The two ledger legs decide whether L1 is worth turning on at all: if the
|
|
206
|
+
// host prunes far more than engram does, enabling it only repeats that work.
|
|
207
|
+
// Printed regardless of the status snapshot (it is ledger data, not plugin
|
|
208
|
+
// startup state).
|
|
209
|
+
try {
|
|
210
|
+
// Honour DSH_HOME like every other read in this CLI: without it the legs
|
|
211
|
+
// line silently reports the real machine's ledger even under a sandbox.
|
|
212
|
+
const ledger = readLedger({ home: join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "engram") });
|
|
213
|
+
const rows = ledger.sessions ?? [];
|
|
214
|
+
if (rows.length > 0) {
|
|
215
|
+
const engram = rows.reduce((sum, row) => sum + (row.prunedChars ?? 0), 0);
|
|
216
|
+
const host = ledger.hostPrunedChars ?? 0;
|
|
217
|
+
const verdict =
|
|
218
|
+
host > 0 && engram === 0
|
|
219
|
+
? "(宿主全干了,engram 一条没剪 → L1 开着只是重复劳动)"
|
|
220
|
+
: host > engram * 5
|
|
221
|
+
? "(宿主腿远大于 engram 腿 → 先确认边界源有没有覆盖到你的用法)"
|
|
222
|
+
: "";
|
|
223
|
+
log(` 账本两条腿:engram 剪枝 ${engram} 字符 · 宿主自剪 ${host} 字符${verdict}`);
|
|
224
|
+
log("");
|
|
225
|
+
const insights = await readPlaybookInsights();
|
|
226
|
+
if (insights !== null) {
|
|
227
|
+
log(` playbook(当前 workspace):${insights.total} 条 procedure · 过门 ${insights.promoted} · 未过门 ${insights.unproven} · 失效 ${insights.invalidated}`);
|
|
228
|
+
if (insights.capsules > 0) {
|
|
229
|
+
log(` subagent 胶囊:${insights.capsules} 条 · ${insights.capsuleSource} → ${insights.capsuleChars} 字符(只做留档,父代理仍收到全文)`);
|
|
230
|
+
}
|
|
231
|
+
if (insights.promoted > 0) {
|
|
232
|
+
log(" 可导出为 DSH Skill:npx dsh-engram skill export(写入 <workspace>/.dsh/skills)");
|
|
233
|
+
}
|
|
234
|
+
log("");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
|
|
196
239
|
if (status === undefined) {
|
|
197
240
|
log(" 尚无状态文件(插件还没写过,或本机没跑过 dsh web)。");
|
|
198
241
|
} else {
|
|
@@ -204,9 +247,28 @@ async function printStatus(presetsDirFlag) {
|
|
|
204
247
|
const bp = status.boundaryPrune;
|
|
205
248
|
log(` L1 边界剪枝:${bp.armed ? "已武装" : "未启用"}${bp.reason ? `(${bp.reason})` : ""}`);
|
|
206
249
|
if (!bp.armed) {
|
|
207
|
-
|
|
250
|
+
const reason = String(bp.reason ?? "");
|
|
251
|
+
if (/boundaryPrune:false|config/i.test(reason)) {
|
|
252
|
+
// Default off since v0.7.11 — not a misconfiguration.
|
|
253
|
+
log(" 说明:L1 自 0.7.11 起默认关闭(宿主会自己剪超阈值结果,边界源只在用 todo/goal 的规划型会话里有收益)。");
|
|
254
|
+
log(" 需要时在设置卡或 profile 里设 boundaryPrune: true;打开前先看上面那行「账本两条腿」。");
|
|
255
|
+
} else {
|
|
256
|
+
log(" ⚠ L1 未武装(服务缺失/装配问题):此时账本只有注入支出,没有剪枝入账。");
|
|
257
|
+
}
|
|
208
258
|
}
|
|
209
259
|
}
|
|
260
|
+
const attempts = await readPruneAttempts(20).catch(() => []);
|
|
261
|
+
if (attempts.length === 0) {
|
|
262
|
+
log(" L1 边界尝试:0 次(还没有任何 todo/goal 边界出现过——注意「已武装」不等于「会开火」)");
|
|
263
|
+
} else {
|
|
264
|
+
const byPlane = new Map();
|
|
265
|
+
const fired = attempts.filter((a) => a.kind === "fired" && a.pruned > 0);
|
|
266
|
+
for (const a of attempts) byPlane.set(a.plane ?? "?", (byPlane.get(a.plane ?? "?") ?? 0) + 1);
|
|
267
|
+
const planeLine = [...byPlane].map(([plane, n]) => `${plane} ${n}`).join(" · ");
|
|
268
|
+
const last = attempts[attempts.length - 1];
|
|
269
|
+
log(` L1 边界尝试:${attempts.length} 次见过的边界(${planeLine})· 真剪到东西的 ${fired.length} 次`);
|
|
270
|
+
log(` 最近一次:${last.at ?? "?"} · ${last.kind}${last.boundary?.kind ? ` · ${last.boundary.kind}` : ""}${last.pruned !== undefined ? ` · 剪了 ${last.pruned} 条 / ${last.charsRemoved ?? 0} 字符` : ""}`);
|
|
271
|
+
}
|
|
210
272
|
if (status.configSource?.autoWebCompaction === false) {
|
|
211
273
|
log(" ⚠ autoWebCompaction=false:web 平面不会自动接管(设置卡「记忆 GC」里打开后重启 dsh web)");
|
|
212
274
|
}
|
|
@@ -233,8 +295,9 @@ async function printStatus(presetsDirFlag) {
|
|
|
233
295
|
continue;
|
|
234
296
|
}
|
|
235
297
|
const cs = compactionWireStatus(text);
|
|
298
|
+
const hasPrune = text.includes(ENGRAM_PRUNE_ROW_NAME);
|
|
236
299
|
const note = cs === "wired"
|
|
237
|
-
?
|
|
300
|
+
? `已接管(dsh-engram/compaction 行${hasPrune ? " + dsh-engram/prune 行" : " · 缺 prune 行:L1 在这个预设里看不见边界,重跑 enable 补齐"})`
|
|
238
301
|
: cs === "stock"
|
|
239
302
|
? "出厂布局,重启 dsh web 后自动接管"
|
|
240
303
|
: "自定义/无 compaction,插件不碰";
|
|
@@ -267,6 +330,20 @@ async function printDoctor(presetsDirFlag) {
|
|
|
267
330
|
}
|
|
268
331
|
}
|
|
269
332
|
|
|
333
|
+
// v0.8-D: an approval prompt nobody sees is a gate that silently blocks. The
|
|
334
|
+
// doctor line is where a user finds out there is something to approve.
|
|
335
|
+
try {
|
|
336
|
+
const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
337
|
+
const { key, rows } = await loadExportable(process.cwd(), home);
|
|
338
|
+
const approved = new Set(approvedIds(loadApprovals(home), key));
|
|
339
|
+
const pending = exportSkills(rows, { config: SKILL_GATE, dir: defaultSkillDir(key), dryRun: true, approved, requireApproval: true }).pending;
|
|
340
|
+
if (pending.length > 0) {
|
|
341
|
+
notes.push(`有 ${pending.length} 条过门的 auto procedure 等批准才能导出成 skill:npx dsh-engram skill pending`);
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
// no store yet — nothing to approve
|
|
345
|
+
}
|
|
346
|
+
|
|
270
347
|
const stockLeft = presets.filter((p) => {
|
|
271
348
|
try {
|
|
272
349
|
return compactionWireStatus(readFileSyncSafe(p.path)) === "stock";
|
|
@@ -387,6 +464,127 @@ async function runEnableRevert(command, fileFlag, presetsDirFlag) {
|
|
|
387
464
|
if (command === "revert" && changed > 0) log(" 已全部还原为出厂 compaction 行——可以安全卸载 dsh-engram。");
|
|
388
465
|
}
|
|
389
466
|
|
|
467
|
+
/** Evidence gate defaults; mirrors lib/index.js (kept here so the CLI stays DSH-free). */
|
|
468
|
+
const SKILL_GATE = { procedureMinEvidence: 3, procedureMaxAgeDays: 14, promoteHits: 3 };
|
|
469
|
+
|
|
470
|
+
/** Rows the exporter would consider (proof gate passed, auto-captured). */
|
|
471
|
+
async function loadExportable(workspace, home) {
|
|
472
|
+
const storage = join(home, "storages", "dsh_engram.json");
|
|
473
|
+
if (!(await exists(storage))) throw new Error(`找不到记忆库:${storage}(DSH 还没在这个机器上写过记忆?)`);
|
|
474
|
+
const unit = JSON.parse(await readFile(storage, "utf8"));
|
|
475
|
+
const table = unit?.tables?.memories ?? unit?.memories ?? {};
|
|
476
|
+
const all = Array.isArray(table) ? table : Object.values(table);
|
|
477
|
+
const key = workspaceKey(workspace);
|
|
478
|
+
return { key, rows: all.filter((m) => m?.workspace === key) };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function runSkillApprove({ workspace, home, id, note, revoke }) {
|
|
482
|
+
const { key, rows } = await loadExportable(workspace, home);
|
|
483
|
+
const row = rows.find((m) => m?.id === id || String(m?.id ?? "").startsWith(id));
|
|
484
|
+
if (row === undefined) throw new Error(`workspace ${key} 里没有 id 以 "${id}" 开头的记忆`);
|
|
485
|
+
if (revoke) {
|
|
486
|
+
revokeApproval(home, key, row.id);
|
|
487
|
+
log(`已撤销批准:${row.id}(${skillName(row)})——下次 export 会删掉它的 SKILL.md`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const state = recordApproval(home, key, row.id, { note: note ?? null });
|
|
491
|
+
const plan = planSkillExport(rows, { config: SKILL_GATE, dir: defaultSkillDir(key), approved: new Set(approvedIds(state, key)), requireApproval: true });
|
|
492
|
+
log(`已批准:${row.id} → ${skillName(row)}${note ? `(${note})` : ""}`);
|
|
493
|
+
log(` 证据:${row.evidenceCount ?? 0} 次验证 · 最近 ${row.lastVerifiedAt ? new Date(row.lastVerifiedAt).toISOString().slice(0, 10) : "未知"}`);
|
|
494
|
+
log(plan.length > 0 ? " 现在可以运行:npx dsh-engram skill export" : " 但它还没通过证据门(≥3 次验证 / 14 天内 / 未失效),批准不替代证据。");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function runSkillPending({ workspace, home }) {
|
|
498
|
+
const { key, rows } = await loadExportable(workspace, home);
|
|
499
|
+
const state = loadApprovals(home);
|
|
500
|
+
const approved = new Set(approvedIds(state, key));
|
|
501
|
+
const report = exportSkills(rows, { config: SKILL_GATE, dir: defaultSkillDir(key), dryRun: true, approved, requireApproval: true });
|
|
502
|
+
const ready = planSkillExport(rows, { config: SKILL_GATE, dir: defaultSkillDir(key), approved, requireApproval: true });
|
|
503
|
+
log(`skill pending · workspace ${key}`);
|
|
504
|
+
if (ready.length > 0) {
|
|
505
|
+
log(` 已批准待导出 ${ready.length}:`);
|
|
506
|
+
for (const skill of ready) log(` ${skill.name} (${skill.memoryId})`);
|
|
507
|
+
}
|
|
508
|
+
if (report.pending.length > 0) {
|
|
509
|
+
log(` 等批准 ${report.pending.length}(批准后才写文件):`);
|
|
510
|
+
for (const item of report.pending) log(` ${item.name} ${item.id.slice(0, 8)} ${item.description}`);
|
|
511
|
+
log(" 批准:npx dsh-engram skill approve <id> [--note \"…\"]");
|
|
512
|
+
}
|
|
513
|
+
if (ready.length === 0 && report.pending.length === 0) log(" 没有过门的 auto procedure —— 无事可批。");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function runSkillExport({ workspace, dir, home, dryRun, force }) {
|
|
517
|
+
const storage = join(home, "storages", "dsh_engram.json");
|
|
518
|
+
if (!(await exists(storage))) throw new Error(`找不到记忆库:${storage}(DSH 还没在这个机器上写过记忆?)`);
|
|
519
|
+
const unit = JSON.parse(await readFile(storage, "utf8"));
|
|
520
|
+
const table = unit?.tables?.memories ?? unit?.memories ?? {};
|
|
521
|
+
const all = Array.isArray(table) ? table : Object.values(table);
|
|
522
|
+
const key = workspaceKey(workspace);
|
|
523
|
+
const rows = all.filter((m) => m?.workspace === key);
|
|
524
|
+
const target = dir ?? defaultSkillDir(key);
|
|
525
|
+
// v0.8-D: evidence gets a procedure into the index; a human yes gets it into
|
|
526
|
+
// a file the host advertises. `--force` is the audited escape hatch.
|
|
527
|
+
const approved = new Set(approvedIds(loadApprovals(home), key));
|
|
528
|
+
const requireApproval = !force;
|
|
529
|
+
const report = exportSkills(rows, { config: SKILL_GATE, dir: target, dryRun, approved, requireApproval });
|
|
530
|
+
log(`skill export · workspace ${key} · ${rows.length} 条记忆 · 目标 ${target}${dryRun ? "(dry-run)" : ""}${force ? " · --force(跳过批准门)" : ""}`);
|
|
531
|
+
if (report.written.length > 0) log(` 写入 ${report.written.length}: ${report.written.join(", ")}`);
|
|
532
|
+
if (report.unchanged.length > 0) log(` 未变 ${report.unchanged.length}: ${report.unchanged.join(", ")}`);
|
|
533
|
+
if (report.removed.length > 0) log(` 移除(证据不再成立)${report.removed.length}: ${report.removed.join(", ")}`);
|
|
534
|
+
for (const skip of report.skipped) log(` 跳过 ${skip}`);
|
|
535
|
+
if (report.pending.length > 0) {
|
|
536
|
+
log(` 等批准 ${report.pending.length}(证据已够,但写文件需要你点头):`);
|
|
537
|
+
for (const item of report.pending) log(` ${item.name} ${item.id.slice(0, 8)}`);
|
|
538
|
+
log(" 批准:npx dsh-engram skill approve <id>;或本次强制:--force");
|
|
539
|
+
}
|
|
540
|
+
for (const error of report.errors) log(` 失败 ${error}`);
|
|
541
|
+
if (report.written.length + report.unchanged.length + report.removed.length === 0 && report.skipped.length === 0 && report.pending.length === 0) {
|
|
542
|
+
log(" 没有 procedure 通过证据门(≥3 次验证、14 天内、未失效)——无导出。");
|
|
543
|
+
}
|
|
544
|
+
log(" 下一步:DSH 从项目根的 .dsh/skills 发现技能,catalog 里出现名字即生效(新会话无需重启)。");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Playbook + capsule counters for `status` (best effort; missing store → null). */
|
|
548
|
+
async function readPlaybookInsights(workspaceArg) {
|
|
549
|
+
try {
|
|
550
|
+
const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
551
|
+
const storage = join(home, "storages", "dsh_engram.json");
|
|
552
|
+
if (!(await exists(storage))) return null;
|
|
553
|
+
const unit = JSON.parse(await readFile(storage, "utf8"));
|
|
554
|
+
const table = unit?.tables?.memories ?? unit?.memories ?? {};
|
|
555
|
+
const all = Array.isArray(table) ? table : Object.values(table);
|
|
556
|
+
const key = workspaceKey(workspaceArg ?? process.cwd());
|
|
557
|
+
const rows = all.filter((m) => m?.workspace === key);
|
|
558
|
+
const config = { ...SKILL_GATE };
|
|
559
|
+
const procedures = rows.filter((m) => m?.kind === "procedure" && m.status !== "archived");
|
|
560
|
+
const capsules = rows.filter((m) => (m.tags ?? []).includes("subagent-capsule"));
|
|
561
|
+
const size = (m) => {
|
|
562
|
+
const match = /胶囊:.*? (\d+) → (\d+) 字符/.exec(String(m.text ?? ""));
|
|
563
|
+
return match === null ? null : [Number(match[1]), Number(match[2])];
|
|
564
|
+
};
|
|
565
|
+
let capsuleSource = 0;
|
|
566
|
+
let capsuleChars = 0;
|
|
567
|
+
for (const capsule of capsules) {
|
|
568
|
+
const parsed = size(capsule);
|
|
569
|
+
if (parsed !== null) {
|
|
570
|
+
capsuleSource += parsed[0];
|
|
571
|
+
capsuleChars += parsed[1];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
total: procedures.length,
|
|
576
|
+
promoted: procedures.filter((m) => isProvenProcedure(m, config)).length,
|
|
577
|
+
unproven: procedures.filter((m) => !isProvenProcedure(m, config) && !invalidatedProcedure(m)).length,
|
|
578
|
+
invalidated: procedures.filter((m) => invalidatedProcedure(m)).length,
|
|
579
|
+
capsules: capsules.length,
|
|
580
|
+
capsuleSource,
|
|
581
|
+
capsuleChars,
|
|
582
|
+
};
|
|
583
|
+
} catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
390
588
|
async function main() {
|
|
391
589
|
const [, , rawCommand, ...rest] = process.argv;
|
|
392
590
|
const command = rawCommand ?? "status";
|
|
@@ -394,7 +592,34 @@ async function main() {
|
|
|
394
592
|
const fileFlag = fileFlagIndex >= 0 ? rest[fileFlagIndex + 1] : undefined;
|
|
395
593
|
const presetsDirIndex = rest.indexOf("--presets-dir");
|
|
396
594
|
const presetsDirFlag = presetsDirIndex >= 0 ? rest[presetsDirIndex + 1] : undefined;
|
|
397
|
-
if (command === "
|
|
595
|
+
if (command === "skill") {
|
|
596
|
+
const sub = rest[0];
|
|
597
|
+
if (!["export", "approve", "revoke", "pending"].includes(sub ?? "")) {
|
|
598
|
+
throw new Error(`未知子命令 "skill ${sub ?? ""}"(用 skill export|pending|approve|revoke)`);
|
|
599
|
+
}
|
|
600
|
+
const flag = (name) => {
|
|
601
|
+
const at = rest.indexOf(name);
|
|
602
|
+
return at >= 0 ? rest[at + 1] : undefined;
|
|
603
|
+
};
|
|
604
|
+
const shared = {
|
|
605
|
+
workspace: flag("--workspace") ?? process.cwd(),
|
|
606
|
+
home: flag("--home") ?? process.env.DSH_HOME ?? join(homedir(), ".dsh"),
|
|
607
|
+
};
|
|
608
|
+
if (sub === "export") {
|
|
609
|
+
await runSkillExport({
|
|
610
|
+
...shared,
|
|
611
|
+
dir: flag("--dir"),
|
|
612
|
+
dryRun: rest.includes("--dry-run"),
|
|
613
|
+
force: rest.includes("--force"),
|
|
614
|
+
});
|
|
615
|
+
} else if (sub === "pending") {
|
|
616
|
+
await runSkillPending(shared);
|
|
617
|
+
} else {
|
|
618
|
+
const id = rest[1];
|
|
619
|
+
if (id === undefined || id.startsWith("--")) throw new Error(`skill ${sub} 需要一个记忆 id`);
|
|
620
|
+
await runSkillApprove({ ...shared, id, note: flag("--note"), revoke: sub === "revoke" });
|
|
621
|
+
}
|
|
622
|
+
} else if (command === "status") {
|
|
398
623
|
await printStatus(presetsDirFlag);
|
|
399
624
|
} else if (command === "doctor") {
|
|
400
625
|
await printDoctor(presetsDirFlag);
|