@lmzhen/dsh-evolution-commands 0.32.3 → 0.32.5

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
@@ -1,34 +1,23 @@
1
1
  # @lmzhen/dsh-evolution-commands
2
2
 
3
- Human commands for the evolution family: it registers the `/evolution …` command surface
4
- and its help text, and drives the other rows from a command. It adds no automatic
5
- background behaviour of its own.
3
+ > `/evolution` 命令入口。
6
4
 
7
- ## Model surface
5
+ ## 你会看到
8
6
 
9
- - **Model-visible:** two direct tokens, both injected as user messages in this session — the `/evolution learn` injection (the full learning guidance) and the `maintain handoff` injection (a one-line POINTER to a finished maintenance report plus a report-only protocol: read the file, report per item, change nothing until the operator approves). **Command output never reaches model history**: `/evolution maintain` and every other subcommand answer in the client only. Everything else adds no tokens.
10
- - **Prompt prefix / KV cache:** independent of request-prefix construction — it does not alter the assembled prompt or tool list; family rules: `packages/README.md` §"Model-visible prompt prefix and the KV cache".
11
- - **Mount it?** yes — the `evolution-commands` row, carried by the `evolution-host`, `evolution-all` and `evolution-preset` bundles.
7
+ `/evolution doctor`、`pending`、`curator`、`merge`、`skill history` 等。
12
8
 
13
- ## Command reference
9
+ ## 什么时候会用到
14
10
 
15
- The full `/evolution` subcommand table is single-sourced in the family README's **Command reference** section (`packages/README.md` in the source repository; it is named here instead of linked because a repo-relative markdown link out of this package would be dead inside the npm published tree). It is rendered from the subcommand registry (`src/registry.ts`, the same single source as the `/evolution` input-declaration hint and the bare-command help output), and `tests/registry.spec.ts` (T-WD2) pins that rendered table byte-for-byte.
11
+ 日常管理和排障时。
16
12
 
17
- ## Known limitations
13
+ ## 默认 / 前提
18
14
 
19
- - No known durable consumer gaps at this time. Runtime contracts are covered by package and boundary tests.
20
- - **`/evolution maintain` runs in the BACKGROUND (0.19.0).** The command answers with a run id and the scan keeps working: read it with `/evolution maintain report [<id>]` (a run that produced nothing answers "no result" instead of an empty plan) or `/evolution maintain status [<id>]` (in flight plus the recent terminal states). Since 0.20.0 the id may be **omitted** on `report`, `cancel` and `handoff`: it then means the newest run in this home, and an unreadable `runs.json` answers "the run index is not readable" for that form rather than guessing. The run's lifetime belongs to this plugin through `evolution-core`'s run registry, **not** to the request that started it: the desktop shell ends a request at ~305 s (measured four times) while the same scan completes in 949.7 s on the web plane, and `maintainTimeoutMs` (default 600000) bounds the run itself. **Cancelling the request no longer stops the scan** — use `/evolution maintain cancel <id>`, or unmount the row. The plan is written once at settle to `<evolutionHome>/reports/maintain-<runId>.{json,md}`, under the same per-home reports window the curator's reports use.
21
- - **`maintain handoff` is a SOFT gate (0.20.0, accepted).** The injected protocol asks the model to report and wait for the user's reply, and "what was approved" lives only in that conversation — the family keeps no approval record of its own. Escalate to the existing `evolution-approval` channel when unattended bulk skill edits, a multi-person audit trail, or non-repudiation become requirements.
22
- - **`maintain handoff` reports a run whose state is not `succeeded` as "ready" when a report file exists (0.20.0, accepted).** A failed or cancelled run writes the same `.json`/`.md` pair (the failure text is the report), so the injection fires and the protocol's first line — read the file — is what tells the model what happened. Tightening it to `succeeded` only is a behavior choice, not a defect fix.
23
- - **The injected pointer names the `.md` digest without re-probing it (0.20.0, accepted).** `reportDigestPath` swaps the `.json` suffix; the writer emits both files in one settle, so a hand-deleted `.md` beside a live `.json` would name a missing file.
24
- - **The id-less forms repeat the same resolve skeleton three times (0.20.0, recorded).** `report`, `cancel` and `handoff` each run `syncRuns` → id-less `newestRun` → three-state error reporting; the wording is shared through `unreadableIndex`/`unreadableForId`, the skeleton is not. Extracting one resolver is the next batch's refactor.
25
- - **Cross-plane run state is eventual, and the shared index is the truth for runs this process does not own (0.19.1).** Two hosts sharing one `DSH_HOME` (the desktop app and a `dsh web` plane) read and write `evolution/runs.json` together. `begin()` resolves only after its row is in that index, so a plane reading the answer "started" finds the run; a run another plane owns is re-read at this plane's **next** read, and it stops being live in exactly two ways — its owner settled it (that terminal state is adopted) or its owner died (converged to `failed(orphan)`, with the reason spelled out). Runs this process owns keep memory as the truth. No timer and no heartbeat sits behind this: 0.19.0 skipped ids it already knew, which left an observing plane reporting `running` forever and refusing its own `maintain` until it restarted. The rule is monotonic on BOTH sides (0.19.2): the read side adopts a terminal row, and the write side never puts a row this process does not own back to `running` over the terminal state its owner wrote — so a settled run cannot be resurrected by this plane's own later write. Two hosts sharing one home must be on the SAME machine: `isProcessAlive(pid)` does not cross machines. Structurally (0.19.3) a row has **exactly one writer — the process that started it**: this process's rows live in its own map, every other row is whatever the index says (rebuilt on each read), and a settled row is **never** shown as running again — a stale writer that puts `running` back is refused on read and repaired on the next write.
26
- - **① variant form: the `/evolution …` command face is visible in every session** (accepted, not fixed). Commands are registered in scope layers, and the family registers once on the host plane, so a session on a platform original preset still lists the management-only subcommands even though no automatic family behaviour reaches it. The difference and its reasoning are single-sourced in `INSTALL.md` ("Known difference").
15
+ 随 all/host/preset 安装自动注册。
27
16
 
28
- **Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
17
+ ## 边界
29
18
 
30
- ## Notes and history
19
+ 没有 `commands` 服务时整条命令不存在。
31
20
 
32
- - P2-22 (v11) correction: this package's direct model-visible tokens are the `/evolution learn` injection (the full learning guidance) and — since 0.20.0 — the `maintain handoff` injection (a report pointer plus the report-only protocol). Both are injected as user messages in this session; see **Model surface** above for the current count.
33
- - **① variant form: the `/evolution …` command face is visible in every session** (accepted, not fixed: 0.3.78).
34
- - The injection goes through the agent's waking primitive (`followup`, falling back to `inject` when the host lacks it), **called on the agent instance**: the platform's `Agent.followup` is a prototype method (`this.send(...)`), so a detached reference throws and queues nothing (0.3.73 fix; the same shape is pinned by rule N13b in `packages/scripts/verify-arch-guards.mjs`, which masks comments and string literals before matching and self-tests its detector at startup).
21
+ 分层/variant 的命令面差异单源在 [INSTALL.md](../INSTALL.md)。
22
+
23
+ > 旧的 consolidate 已从用户面退役;合并请用 `merge`。
package/lib/index.js CHANGED
@@ -73,7 +73,7 @@ const RUNTIME_LABELS = {
73
73
  */
74
74
  const FORM_WORDS = Object.freeze({
75
75
  M1: "完整功能包",
76
- M2: "后台自动化包",
76
+ M2: "人工审批包",
77
77
  M3: "会话预设包",
78
78
  M4: "分层安装"
79
79
  });
@@ -238,7 +238,7 @@ function runtimeOf(inputs) {
238
238
  label: RUNTIME_LABELS.runs ?? "运行登记",
239
239
  status: "warn",
240
240
  detail: "running=" + String(inputs.runs.running) + ",orphaned=" + String(inputs.runs.orphaned),
241
- remedy: "运行 /evolution runs 收敛遗留运行。"
241
+ remedy: "运行 /evolution maintain status 查看并收敛遗留运行。"
242
242
  } : {
243
243
  key: "runs",
244
244
  label: RUNTIME_LABELS.runs ?? "运行登记",
@@ -392,9 +392,10 @@ async function assembleDoctorInputs(ctx, extras = {}) {
392
392
  const hasPreset = bundles.some((name) => name.endsWith("dsh-evolution-preset"));
393
393
  const hasHost = bundles.some((name) => name.endsWith("dsh-evolution-host"));
394
394
  const hasFamilyPreset = listed.some((entry) => typeof entry.id === "string" && EVOLUTION_PRESET_IDS.includes(entry.id));
395
+ const humanGated = get("evolutionApproval")?.isEnabled === true;
395
396
  const detectedForm = hasAll || hasPreset ? {
396
- id: "M1",
397
- evidence: [hasAll ? "bundle dsh-evolution-all installed" : "bundle dsh-evolution-preset installed"],
397
+ id: humanGated ? "M2" : "M1",
398
+ evidence: [hasAll ? "bundle dsh-evolution-all installed" : "bundle dsh-evolution-preset installed", ...humanGated ? ["approval.enabled: true"] : []],
398
399
  confidence: "declared"
399
400
  } : hasHost ? hasFamilyPreset ? {
400
401
  id: "M4",
@@ -1446,8 +1447,9 @@ async function diagnose(ctx, options = {}) {
1446
1447
  pendingCount = null;
1447
1448
  executingCount = null;
1448
1449
  }
1449
- const installForm = full ? "full" : preset ? "preset" : layered ? "layered" : host ? "host" : presetDirInstalled ? "preset-only" : "none";
1450
- const deploymentForm = installForm === "layered" ? "variant" : installForm === "full" || installForm === "preset" ? "attach" : installForm === "host" ? "host-only" : installForm === "preset-only" ? "preset-only" : "none";
1450
+ const humanGated = ctx.get("evolutionApproval")?.isEnabled === true;
1451
+ const installForm = full || preset ? humanGated ? "human-gated" : full ? "full" : "preset" : layered ? "layered" : host ? "host" : presetDirInstalled ? "preset-only" : "none";
1452
+ const deploymentForm = installForm === "layered" ? "variant" : installForm === "full" || installForm === "preset" || installForm === "human-gated" ? "attach" : installForm === "host" ? "host-only" : installForm === "preset-only" ? "preset-only" : "none";
1451
1453
  const mountConflicts = conflicts.filter((row) => !row.includes("DEGRADED"));
1452
1454
  const undecidable = mountConflicts.length !== conflicts.length;
1453
1455
  const scopedProbe = {
@@ -1483,7 +1485,7 @@ async function diagnose(ctx, options = {}) {
1483
1485
  if (row.status !== "differs") continue;
1484
1486
  actions.push(`Preset row for base "${row.base}" in ${row.destination} DIFFERS from a fresh generation — it is an install-time snapshot; re-run the installer (/evolution preset install) to regenerate`);
1485
1487
  }
1486
- const view = buildDoctorView(await assembleDoctorInputs(ctx));
1488
+ const view = buildDoctorView(await assembleDoctorInputs(ctx, options.runs === void 0 ? {} : { runs: options.runs }));
1487
1489
  const merge = await mergeDoctorLine(ctx);
1488
1490
  if (merge.status !== "ok" && merge.remedy !== void 0) actions.push("合并行:" + merge.remedy);
1489
1491
  return {
@@ -2270,7 +2272,10 @@ function parseCommand(defs, argv) {
2270
2272
  help: true
2271
2273
  };
2272
2274
  const candidates = defs.filter((item) => item.words.includes(verb));
2273
- const def = candidates.find((item) => rest.filter((token) => token.startsWith("--")).every((token) => item.flags.some((flag) => flag.name === token.slice(2)))) ?? candidates[0];
2275
+ const def = candidates.find((item) => rest.filter((token) => token.startsWith("--")).every((token) => {
2276
+ const name = token.slice(2).split("=")[0] ?? "";
2277
+ return item.flags.some((flag) => flag.name === name);
2278
+ })) ?? candidates[0];
2274
2279
  if (def === void 0) return {
2275
2280
  ok: false,
2276
2281
  code: "usage",
@@ -2284,15 +2289,27 @@ function parseCommand(defs, argv) {
2284
2289
  positional.push(token);
2285
2290
  continue;
2286
2291
  }
2287
- const name = token.slice(2);
2292
+ const eq = token.indexOf("=");
2293
+ const name = (eq < 0 ? token : token.slice(0, eq)).slice(2);
2294
+ const inline = eq < 0 ? void 0 : token.slice(eq + 1);
2288
2295
  const flag = def.flags.find((item) => item.name === name);
2289
2296
  if (flag === void 0) return {
2290
2297
  ok: false,
2291
2298
  code: "usage",
2292
2299
  message: "unknown flag: --" + name
2293
2300
  };
2294
- if (flag.kind === "boolean") values[name] = true;
2295
- else {
2301
+ if (flag.kind === "boolean") {
2302
+ if (inline !== void 0) return {
2303
+ ok: false,
2304
+ code: "usage",
2305
+ message: "flag does not take a value: --" + name
2306
+ };
2307
+ values[name] = true;
2308
+ } else {
2309
+ if (inline !== void 0) {
2310
+ values[name] = inline;
2311
+ continue;
2312
+ }
2296
2313
  const value = rest[index + 1];
2297
2314
  if (value === void 0 || value.startsWith("--")) return {
2298
2315
  ok: false,
@@ -2573,7 +2590,7 @@ function apply(ctx, rawConfig = {}) {
2573
2590
  };
2574
2591
  const approval = ctx.get("evolutionApproval");
2575
2592
  const pendingParsed = parseSubcommand(input);
2576
- if (!isParseFailure(pendingParsed) && pendingParsed.name === "pending") {
2593
+ if (!isParseFailure(pendingParsed) && pendingParsed.name === "pending" && pendingParsed.positionals.length === 0 && [...pendingParsed.flags.keys()].every((flag) => flag === "detail")) {
2577
2594
  if (!approval) return err(errorText("e-301-approval-service-not-mounted"));
2578
2595
  const listed = [...await approval.list("pending"), ...await approval.list("executing")];
2579
2596
  const pending = [...new Map(listed.map((row) => [row.id, row])).values()];
@@ -3331,7 +3348,7 @@ function apply(ctx, rawConfig = {}) {
3331
3348
  return ok(replay.compare().report);
3332
3349
  }
3333
3350
  const paramsParsed = parseSubcommand(input);
3334
- if (!isParseFailure(paramsParsed) && paramsParsed.name === "params") {
3351
+ if (!isParseFailure(paramsParsed) && paramsParsed.name === "params" && paramsParsed.positionals.length === 0 && [...paramsParsed.flags.keys()].every((flag) => flag === "group" || flag === "json")) {
3335
3352
  const provider = ctx.get("settings");
3336
3353
  const sections = /* @__PURE__ */ new Map();
3337
3354
  if (provider?.describe !== void 0) try {
@@ -3446,7 +3463,8 @@ function apply(ctx, rawConfig = {}) {
3446
3463
  } };
3447
3464
  const report = await diagnose(ctx, {
3448
3465
  home: evolutionRoot(),
3449
- ...mounted
3466
+ ...mounted,
3467
+ runs: doctorRuns()
3450
3468
  });
3451
3469
  if (input === "doctor --json") return ok(JSON.stringify(report.view, null, 2));
3452
3470
  return ok(renderDoctorText(report));
@@ -73,7 +73,7 @@ export interface DoctorReport {
73
73
  * per-session install reported `none` and the action ladder advised
74
74
  * installing `all` — which double-mounts the preset's model rows (the
75
75
  * exact conflict this report flags). */
76
- installForm: 'full' | 'host' | 'preset' | 'layered' | 'preset-only' | 'none';
76
+ installForm: 'full' | 'human-gated' | 'host' | 'preset' | 'layered' | 'preset-only' | 'none';
77
77
  /** The PRODUCT form the install form implies (0.3.77, C axis). `variant` =
78
78
  * session-level opt-in: the model tools live in the Evolution preset, so a
79
79
  * session on any platform original preset carries none of the family and the
@@ -166,6 +166,10 @@ export declare function diagnose(ctx: {
166
166
  }, options?: {
167
167
  home?: string;
168
168
  mounted?: MountedProfile;
169
+ runs?: {
170
+ readonly running: number;
171
+ readonly orphaned: number;
172
+ };
169
173
  }): Promise<DoctorReport>;
170
174
  /**
171
175
  * W8-S7: the doctor's text face, VIEW-FIRST.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-commands",
3
3
  "description": "Human commands for the evolution family (/evolution pending|curator|maintain|doctor) (community build)",
4
- "version": "0.32.3",
4
+ "version": "0.32.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,13 +27,13 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.5",
30
- "@lmzhen/dsh-evolution-approval": "^0.32.3",
31
- "@lmzhen/dsh-evolution-core": "^0.32.3",
32
- "@lmzhen/dsh-evolution-maintenance": "^0.32.3",
33
- "@lmzhen/dsh-evolution-state-storage": "^0.32.3"
30
+ "@lmzhen/dsh-evolution-approval": "^0.32.5",
31
+ "@lmzhen/dsh-evolution-core": "^0.32.5",
32
+ "@lmzhen/dsh-evolution-maintenance": "^0.32.5",
33
+ "@lmzhen/dsh-evolution-state-storage": "^0.32.5"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@lmzhen/dsh-evolution-agent-preset": "^0.32.3"
36
+ "@lmzhen/dsh-evolution-agent-preset": "^0.32.5"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.5",