@lmzhen/dsh-evolution-commands 0.3.80 → 0.3.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
4
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { MAX_TIMER_DELAY_MS, appendEvolutionEvent, assertSkillsRootAliasRetired, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, newSkillLibrary, resolveRootConfig } from "@lmzhen/dsh-evolution-core";
5
+ import { MAX_TIMER_DELAY_MS, appendEvolutionEvent, assertSkillsRootAliasRetired, buildLearnPrompt, clampedNumber, composePresetComposition, eventsFile, evolutionRoot, isMissingPath, newSkillLibrary, resolveRootConfig, scopedProbeReport } from "@lmzhen/dsh-evolution-core";
6
6
  import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
7
7
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
@@ -323,8 +323,9 @@ async function diagnose(ctx, options = {}) {
323
323
  if (host && preset) conflicts.push("evolution-host and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
324
324
  if (full && presetDirInstalled) conflicts.push(`evolution-all and the layered Evolution preset (${layeredArtifactLabel}) are both present — the model rows double-mount. Keep ONE (use layered without all, or drop the preset).`);
325
325
  if (preset && presetDirInstalled) conflicts.push(`evolution-preset and the layered Evolution preset (${layeredArtifactLabel}) are both present — the model rows double-mount (the preset bundle carries the same model rows as all). Keep ONE (drop the preset bundle, or remove the layered preset).`);
326
+ const reviewMounted = full || host || preset;
326
327
  const services = {
327
- review: full || host || preset,
328
+ review: reviewMounted,
328
329
  curator: has("evolutionCurator"),
329
330
  approval: has("evolutionApproval"),
330
331
  skillUsage: has("skillUsage"),
@@ -333,10 +334,13 @@ async function diagnose(ctx, options = {}) {
333
334
  let pendingCount = null;
334
335
  let executingCount = null;
335
336
  const approvalService = ctx.get("evolutionApproval");
336
- if (approvalService?.list) try {
337
- const rows = await approvalService.list("pending");
337
+ const approvalList = approvalService?.list?.bind(approvalService);
338
+ if (approvalList !== void 0) try {
339
+ const pendingProbe = await probeBounded(() => approvalList("pending"), "approval pending listing");
340
+ const rows = pendingProbe.ok ? pendingProbe.value : void 0;
338
341
  pendingCount = Array.isArray(rows) ? rows.length : null;
339
- const executing = await approvalService.list("executing");
342
+ const executingProbe = await probeBounded(() => approvalList("executing"), "approval executing listing");
343
+ const executing = executingProbe.ok ? executingProbe.value : void 0;
340
344
  executingCount = Array.isArray(executing) ? executing.length : null;
341
345
  } catch {
342
346
  pendingCount = null;
@@ -346,6 +350,10 @@ async function diagnose(ctx, options = {}) {
346
350
  const deploymentForm = installForm === "layered" ? "variant" : installForm === "full" || installForm === "preset" ? "attach" : installForm === "host" ? "host-only" : installForm === "preset-only" ? "preset-only" : "none";
347
351
  const mountConflicts = conflicts.filter((row) => !row.includes("DEGRADED"));
348
352
  const undecidable = mountConflicts.length !== conflicts.length;
353
+ const scopedProbe = {
354
+ rows: undecidable ? null : reviewMounted ? ["evolution-review", "skill-usage"] : [],
355
+ ...scopedProbeReport()
356
+ };
349
357
  const actions = [];
350
358
  if (mountConflicts.length > 0) actions.push("Resolve the conflict first: keep exactly one of evolution-all / evolution-host / evolution-preset / layered.");
351
359
  else if (undecidable) actions.push("The install form is UNDECIDABLE (see the DEGRADED row above): an unreadable profiles directory or profile manifest hides whatever is installed, so this report must not add or remove a bundle. Fix the reported read failure, then re-run /evolution doctor.");
@@ -357,7 +365,12 @@ async function diagnose(ctx, options = {}) {
357
365
  if (services.review && !services.curator) actions.push("Curator service is not mounted — automatic curation is off; verify the host/all bundle row set is complete.");
358
366
  if (pendingCount === null && services.approval) actions.push("Approval service is mounted but pending listing failed — check the evolution state service.");
359
367
  const memoryIssues = memoryInterpolationIssues(home);
368
+ const budgetIssues = memoryBudgetIssues(ctx);
369
+ const queryIssues = await sessionQueryIssues(ctx);
370
+ if (queryIssues.length > 0) actions.push("Session search is degraded: isolate the session named above (or wait for the platform fix described in the family maintenance notes) before retrying the same query");
371
+ if (budgetIssues.length > 0) actions.push("Align the memory budget: leave memory-files memoryCharLimit/userCharLimit UNSET so the store follows evolution-policy, or set both surfaces to the same value (the review plans against the policy value while the store enforces its own)");
360
372
  if (memoryIssues.length > 0) actions.push("Rewrite the memory entries listed above (or run a build with the render-time neutralization) — they broke prompt assembly on older builds.");
373
+ if (scopedProbe.rows !== null && scopedProbe.rows.length > 0 && scopedProbe.verdict === "never-hit") actions.push("The session-scoped rows are mounted but the family-tool probe has never matched in this process — review injection and skill-usage telemetry skipped every session observed. HOST-ONLY install: the model rows (tool-memory / tool-skill-manage) sit in evolution-host devDependencies, so no session carries them; install a bundle that mounts them (see INSTALL.md). VARIANT install: only a session on the Evolution preset matches, so open one — a session on a platform original preset is the intended skip, not a fault. Either way, check that no profile overlay disables those two rows.");
361
374
  if ((executingCount ?? 0) > 0) actions.push(`${executingCount} staged write(s) are EXECUTING (an approve crashed mid-run — or one is still in flight). Inspect with /evolution pending: if you started the approve, verify the landed write and do not reject it; only reject after verifying no write is intended. For a verified orphan (this build: S2-P2-22), /evolution release <id> returns it to the pending window instead.`);
362
375
  for (const row of presetFreshness) {
363
376
  if (row.status !== "differs") continue;
@@ -370,18 +383,126 @@ async function diagnose(ctx, options = {}) {
370
383
  conflicts,
371
384
  envIssues: env,
372
385
  memoryIssues,
386
+ budgetIssues,
387
+ queryIssues,
373
388
  services,
374
389
  pendingCount,
375
390
  executingCount,
376
391
  presetFreshness,
392
+ scopedProbe,
377
393
  actions
378
394
  };
379
395
  }
396
+ /**
397
+ * S2-12③ (FLOW5-4): the memory budget is configured in TWO places —
398
+ * `memory-files`' `memoryCharLimit`/`userCharLimit` (what the STORE enforces)
399
+ * and `evolution-policy`'s `memoryChars`/`userChars` (what the review PLANS
400
+ * against; `memory-files` follows the policy only for an UNSET limit). A
401
+ * disagreement therefore means an explicit contradiction, or a `memory-files`
402
+ * row mounted before the policy existed — either way the reviewer budgets ops
403
+ * the store will refuse. Read-only, like every other doctor row.
404
+ *
405
+ * @param ctx - the runtime service view (`get(name)`).
406
+ * @returns one message per disagreeing surface; empty when not comparable.
407
+ */
408
+ /** S4 review (source-first pass) R-1: how long a diagnostic probe may wait.
409
+ * Doctor must ANSWER even when the thing it probes is blocked — the failure it
410
+ * reports (a concurrent writer stalling the corpus) is exactly the one that
411
+ * would otherwise hang the command forever. */
412
+ const PROBE_TIMEOUT_MS = 5e3;
413
+ /**
414
+ * Bound a diagnostic probe. A never-settling service is a FINDING, not a hang:
415
+ * the probe reports it like any other failure.
416
+ *
417
+ * Takes a THUNK, not a promise (review R-2): a mounted-but-broken service can
418
+ * throw synchronously, and `probeBounded(Promise.resolve(service.list()))` would
419
+ * let that throw escape the probe — crashing the command instead of reporting.
420
+ *
421
+ * @param work - the probe's work, invoked inside the guard.
422
+ * @param label - the surface being probed (named in the finding).
423
+ * @returns the value, or a message describing the failure/timeout.
424
+ */
425
+ async function probeBounded(work, label) {
426
+ let started;
427
+ try {
428
+ started = work();
429
+ } catch (error) {
430
+ return {
431
+ ok: false,
432
+ message: `${label} failed: ${error instanceof Error ? error.message : String(error)}`
433
+ };
434
+ }
435
+ let timer;
436
+ const timeout = new Promise((resolve) => {
437
+ timer = setTimeout(() => {
438
+ resolve({
439
+ ok: false,
440
+ message: `${label} did not answer within ${PROBE_TIMEOUT_MS}ms — the corpus is blocked (a concurrent writer can stall the observation) or the service is wedged; isolate the most recently written session and re-run`
441
+ });
442
+ }, PROBE_TIMEOUT_MS);
443
+ });
444
+ const settled = started.then((value) => ({
445
+ ok: true,
446
+ value
447
+ }), (error) => ({
448
+ ok: false,
449
+ message: `${label} failed: ${error instanceof Error ? error.message : String(error)}`
450
+ }));
451
+ const outcome = await Promise.race([settled, timeout]);
452
+ if (timer !== void 0) clearTimeout(timer);
453
+ return outcome;
454
+ }
455
+ /**
456
+ * S4/P-1+P-2 (platform report P-1/P-2, family mitigation) — the session-query
457
+ * index can fail AS A WHOLE: a concurrent writer leaves the persistence
458
+ * observation unstable ('did not stabilize after one retry') and ONE
459
+ * header-conflicting session aborts the same try block, which the tool surface
460
+ * folds into 'storage is unavailable'. Doctor cannot read the index's row count
461
+ * or the last stabilization result (the platform exposes no such API — recorded
462
+ * in the platform report), but it CAN exercise the corpus listing and report the
463
+ * verbatim failure with the actionable half: isolate the offending session.
464
+ *
465
+ * @param ctx - the runtime service view (`get(name)`).
466
+ * @returns one or two messages; empty when the service is absent or healthy.
467
+ */
468
+ async function sessionQueryIssues(ctx) {
469
+ const service = ctx.get("sessionQuery");
470
+ const list = service?.listSessions?.bind(service);
471
+ if (list === void 0) return [];
472
+ const outcome = await probeBounded(() => list(), "session-query listing");
473
+ if (!outcome.ok) return [outcome.message, "isolate the offending session — move it OUT of the corpus with a manifest (never delete it) and re-run; the platform already retries the observation once, so retrying first is not the answer"];
474
+ return Array.isArray(outcome.value) ? [] : ["session-query answered a listing with a non-list value — the service is mounted but not honouring its read contract"];
475
+ }
476
+ function memoryBudgetIssues(ctx) {
477
+ const budget = ctx.get("evolutionMemoryBudget");
478
+ const policy = ctx.get("evolutionPolicy")?.get?.();
479
+ if (budget === void 0 || policy === void 0) return [];
480
+ const issues = [];
481
+ if (budget.memoryCharLimit !== void 0 && policy.memoryChars !== void 0 && budget.memoryCharLimit !== policy.memoryChars) issues.push(`store enforces memoryCharLimit=${budget.memoryCharLimit} (source: ${budget.memorySource ?? "unknown"}) while evolution-policy declares memoryChars=${policy.memoryChars} — the review plans against the policy value and the store refuses what exceeds its own`);
482
+ if (budget.userCharLimit !== void 0 && policy.userChars !== void 0 && budget.userCharLimit !== policy.userChars) issues.push(`store enforces userCharLimit=${budget.userCharLimit} (source: ${budget.userSource ?? "unknown"}) while evolution-policy declares userChars=${policy.userChars} — same divergence as the memory limit`);
483
+ return issues;
484
+ }
485
+ /** S0-4 (v43 G-1 / J-1): the scoped-row reconciliation line. The verdict is the
486
+ * runtime witness, the row set is what this home installs, and the two forms are
487
+ * spelled out because they read identically in the numbers (`never-hit`) while
488
+ * needing opposite responses: under the host-only form nothing can ever match,
489
+ * under the variant form the match arrives with the first Evolution-preset
490
+ * session.
491
+ * @param check - the report's scoped-row check.
492
+ * @returns one line, or the earlier diagnostic when either half is unknown. */
493
+ function scopedProbeLine(check) {
494
+ if (check.rows === null) return `scoped rows: (undecidable), probe=${check.verdict} — a DEGRADED bundle read hides which bundles are installed, so the scoped rows cannot be reconciled with the probe`;
495
+ if (check.rows.length === 0) return `scoped rows: (none mounted), probe=${check.verdict} — no installed bundle carries the session-scoped rows (evolution-review / skill-usage), so the gate is inactive here`;
496
+ const rows = check.rows.map((row) => `${row}=on`).join("/");
497
+ const detail = check.verdict === "hit" ? `${check.hits} scoped evaluation(s) carried the family tools` : check.verdict === "never-hit" ? `${check.misses} scoped evaluation(s) resolved false — review injection and skill-usage telemetry are inert for every session observed; HOST-ONLY installs reach this because tool-memory / tool-skill-manage are not dependencies of evolution-host, VARIANT installs only match once a session runs the Evolution preset` : "the gate has not evaluated a session in this process yet — no session event has reached it since startup";
498
+ return `scoped rows: ${rows}, probe=${check.verdict} (${detail})`;
499
+ }
380
500
  function renderDoctorText(report) {
381
501
  const lines = [
382
502
  `Evolution doctor — install form: ${report.installForm} (deployment: ${report.deploymentForm})`,
383
503
  `bundles (all profiles): ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
384
504
  `services: review=${report.services.review} (inferred from bundles, all profiles) curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
505
+ scopedProbeLine(report.scopedProbe),
385
506
  `pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
386
507
  `executing: ${report.executingCount === null ? "unknown" : report.executingCount}`
387
508
  ];
@@ -393,6 +514,8 @@ function renderDoctorText(report) {
393
514
  if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
394
515
  if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
395
516
  if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
517
+ if (report.budgetIssues.length > 0) lines.push("memory budget:", ...report.budgetIssues.map((line) => ` ! ${line}`));
518
+ if (report.queryIssues.length > 0) lines.push("session search:", ...report.queryIssues.map((line) => ` ! ${line}`));
396
519
  if (report.actions.length > 0) lines.push("next steps:", ...report.actions.map((line) => ` → ${line}`));
397
520
  return lines.join("\n");
398
521
  }
@@ -516,6 +639,15 @@ function apply(ctx, rawConfig = {}) {
516
639
  kind: "error",
517
640
  text
518
641
  });
642
+ const invocationAgent = invocation.agent;
643
+ const agentMissing = (need) => invocationAgent === void 0 ? err(`E-305: this invocation carries no agent — \`${need}\` needs a session-backed call (run it from a session in the GUI or the CLI).`) : void 0;
644
+ const unreplayableWriteRefusal = (what) => {
645
+ if (approval === void 0) return void 0;
646
+ const session = invocationAgent?.session;
647
+ const sessionPolicy = effectiveSessionPolicy(ctx, session);
648
+ if (!(approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false)) return void 0;
649
+ return err(`E-306: this deployment stages foreground writes, but \`/evolution ${what}\` is not replayable through the skill runner — there is nothing to stage. Run it from a session whose approval policy is 'never', or set \`stageForeground: false\` on the evolution-approval row, then repeat the command.`);
650
+ };
519
651
  const approval = ctx.get("evolutionApproval");
520
652
  const pendingMatch = /^pending(?: --detail)?$/.exec(input);
521
653
  if (pendingMatch) {
@@ -628,6 +760,8 @@ function apply(ctx, rawConfig = {}) {
628
760
  if (input === "restore" || input.startsWith("restore ")) {
629
761
  const tail = input.slice(7).trim();
630
762
  if (tail !== "") return err(`\`restore\` takes no arguments (got "${tail}"). Use \`restore\` for a whole-tree rollback, or \`skill restore <name>\` for one skill.`);
763
+ const restoreRefusal = unreplayableWriteRefusal("restore");
764
+ if (restoreRefusal) return restoreRefusal;
631
765
  const curator = ctx.get("evolutionCurator");
632
766
  const result = curator ? await curator.restoreSnapshot() : {
633
767
  ok: false,
@@ -636,6 +770,8 @@ function apply(ctx, rawConfig = {}) {
636
770
  return result.ok ? ok(result.message) : err(result.message);
637
771
  }
638
772
  if (input.startsWith("consolidate ")) {
773
+ const consolidateRefusal = unreplayableWriteRefusal("consolidate");
774
+ if (consolidateRefusal) return consolidateRefusal;
639
775
  const planTail = /\s--plan\s+(\S+)\s*$/.exec(input) ?? null;
640
776
  const planRunId = planTail?.[1] ?? void 0;
641
777
  const [target, ...sources] = (planTail ? input.slice(0, planTail.index) : input).slice(12).trim().split(/\s+/).filter(Boolean);
@@ -651,6 +787,8 @@ function apply(ctx, rawConfig = {}) {
651
787
  if (input.startsWith("skill restore ")) {
652
788
  const name = input.slice(14).trim();
653
789
  if (!name) return err("Usage: /evolution skill restore <name>");
790
+ const skillRestoreRefusal = unreplayableWriteRefusal("skill restore");
791
+ if (skillRestoreRefusal) return skillRestoreRefusal;
654
792
  const curator = ctx.get("evolutionCurator");
655
793
  const result = curator ? await curator.restore(name) : {
656
794
  ok: false,
@@ -685,9 +823,15 @@ function apply(ctx, rawConfig = {}) {
685
823
  summary: "learn request"
686
824
  }
687
825
  });
688
- const woke = typeof invocation.agent.followup === "function";
689
- if (woke) invocation.agent.followup(message);
690
- else invocation.agent.inject(message);
826
+ const missingAgent = agentMissing("learn");
827
+ if (missingAgent) return missingAgent;
828
+ const agent = invocationAgent;
829
+ let woke = false;
830
+ if (typeof agent.followup === "function") {
831
+ agent.followup(message);
832
+ woke = true;
833
+ } else if (typeof agent.inject === "function") agent.inject(message);
834
+ else return err("E-305: the invocation agent exposes neither `followup` nor `inject` — this learn request has no delivery channel.");
691
835
  const eventIo = ctx.get("evolutionIo")?.provider();
692
836
  if (eventIo) appendEvolutionEvent(eventIo, eventsFile(evolutionRoot()), {
693
837
  type: "learn",
@@ -848,9 +992,9 @@ function apply(ctx, rawConfig = {}) {
848
992
  const ioRegistry = ctx.get("evolutionIo");
849
993
  if (!ioRegistry) return err("Evolution IO registry not mounted — restructure unavailable.");
850
994
  if (approval) {
851
- const session = invocation.agent.session;
995
+ const session = invocationAgent?.session;
852
996
  const sessionPolicy = effectiveSessionPolicy(ctx, session);
853
- if (approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false && !approval.hasRunner("skill")) return err("Restructure cannot be staged: no skill replay runner is registered — mount the tool-skill-manage row (evolution-agent preset, or evolution-all) or disable evolution-approval.");
997
+ if (session !== void 0 && approval.isEnabled !== false && sessionPolicy !== "never" && approval.stageForeground !== false && !approval.hasRunner("skill")) return err("Restructure cannot be staged: no skill replay runner is registered — mount the tool-skill-manage row (evolution-agent preset, or evolution-all) or disable evolution-approval.");
854
998
  const decision = await approval.request({
855
999
  kind: "skill",
856
1000
  summary: `/evolution restructure ${name}${planRunId ? ` (plan ${planRunId})` : ""}`,
@@ -867,8 +1011,10 @@ function apply(ctx, rawConfig = {}) {
867
1011
  libraryOrigin: "foreground"
868
1012
  },
869
1013
  origin: "foreground",
870
- ...session.id ? { sessionId: session.id } : {},
871
- session,
1014
+ ...session !== void 0 ? {
1015
+ sessionId: session.id,
1016
+ session
1017
+ } : {},
872
1018
  ...sessionPolicy !== void 0 ? { sessionPolicy } : {}
873
1019
  });
874
1020
  if (decision.action === "staged") return ok(`${decision.message}${planRunId ? `\n[audit] plan=${planRunId}` : ""}`);
@@ -966,7 +1112,7 @@ const defaultFs = {
966
1112
  try {
967
1113
  return statSync(path).mtimeMs;
968
1114
  } catch (error) {
969
- if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
1115
+ if (isMissingPath(error)) return null;
970
1116
  throw error;
971
1117
  }
972
1118
  }
@@ -1,3 +1,4 @@
1
+ import { type ScopedProbeReport } from '@lmzhen/dsh-evolution-core';
1
2
  /** G3-② (B2): one delivered preset variant compared against a fresh generation.
2
3
  * "absent" is the user simply not using that base (never an action); "unknown"
3
4
  * is a comparison that could not run, kept distinct from a verified "fresh". */
@@ -10,6 +11,29 @@ export interface PresetFreshnessRow {
10
11
  /** Why the comparison could not run — set on unknown rows only. */
11
12
  detail?: string;
12
13
  }
14
+ /** S0-4 (v43 G-1 / J-1): the session-scoped rows against the runtime witness.
15
+ *
16
+ * The gate has two halves nobody reconciled: the bundle turns `sessionScoped` on
17
+ * for `evolution-review` and `skill-usage`, and the runtime witness knows
18
+ * whether ANY session ever saw the family's model tools. `never-hit` with the
19
+ * rows mounted is a finding — the host-only form mounts no model row at all
20
+ * (tool-memory / tool-skill-manage are devDependencies of evolution-host) and an
21
+ * overlay may disable either row, so review injection and skill-usage telemetry
22
+ * run for nobody. `idle` claims nothing: the gate simply has not been asked yet
23
+ * in this process. Read-only, like every other row of this report. */
24
+ export interface ScopedProbeCheck {
25
+ /** The scoped rows the installed bundles mount (`evolution-review`,
26
+ * `skill-usage`); empty when no bundle is installed, and null when a DEGRADED
27
+ * bundle read makes the set undecidable — an unreadable manifest is not
28
+ * evidence of absence (the INST-01 / S2.1 discipline). */
29
+ rows: string[] | null;
30
+ /** The runtime witness, verbatim (`scopedProbeReport()`). */
31
+ verdict: ScopedProbeReport['verdict'];
32
+ /** Scoped gate evaluations that resolved true. */
33
+ hits: number;
34
+ /** Scoped gate evaluations that resolved false. */
35
+ misses: number;
36
+ }
13
37
  export interface DoctorReport {
14
38
  /** OPT-23 (2026-09): `preset-only` — the delivered Evolution preset
15
39
  * artifact exists but NO profile carries an evolution bundle (e.g. the
@@ -39,6 +63,14 @@ export interface DoctorReport {
39
63
  * pre-step of every session under this home, and an operator may want to clean
40
64
  * the file regardless. */
41
65
  memoryIssues: string[];
66
+ /** S4/P-1+P-2 (platform gap, family mitigation): the session-query corpus
67
+ * listing failed — reported verbatim, with the isolation recipe. Empty when the
68
+ * service is absent or answering. */
69
+ queryIssues: string[];
70
+ /** S2-12③ (FLOW5-4): the memory budget's two configuration surfaces disagree —
71
+ * `memory-files`' store limit vs `evolution-policy`'s planning value. Empty
72
+ * when either surface is absent (nothing to compare) or they agree. */
73
+ budgetIssues: string[];
42
74
  services: {
43
75
  review: boolean;
44
76
  curator: boolean;
@@ -56,6 +88,10 @@ export interface DoctorReport {
56
88
  * upgrade leaves a file describing a platform that no longer exists. Read-only:
57
89
  * a stale snapshot is reported, never repaired. */
58
90
  presetFreshness: PresetFreshnessRow[];
91
+ /** S0-4 (v43 G-1 / J-1): the scoped rows × the probe witness, so a deployment
92
+ * whose cross-session consumers are inert for every session stops looking
93
+ * healthy. See {@link ScopedProbeCheck}. */
94
+ scopedProbe: ScopedProbeCheck;
59
95
  actions: string[];
60
96
  }
61
97
  /** Profile bundle rows for evolution-family packages across all profiles.
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.3.80",
4
+ "version": "0.3.82",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,12 +27,12 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-approval": "^0.3.80",
31
- "@lmzhen/dsh-evolution-core": "^0.3.80",
32
- "@lmzhen/dsh-evolution-maintenance": "^0.3.80"
30
+ "@lmzhen/dsh-evolution-approval": "^0.3.82",
31
+ "@lmzhen/dsh-evolution-core": "^0.3.82",
32
+ "@lmzhen/dsh-evolution-maintenance": "^0.3.82"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@lmzhen/dsh-evolution-agent-preset": "^0.3.80"
35
+ "@lmzhen/dsh-evolution-agent-preset": "^0.3.82"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",