@lmzhen/dsh-evolution-commands 0.19.3 → 0.21.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 +7 -3
- package/lib/index.js +245 -38
- package/lib/types/index.d.ts +12 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ background behaviour of its own.
|
|
|
6
6
|
|
|
7
7
|
## Model surface
|
|
8
8
|
|
|
9
|
-
- **Model-visible:**
|
|
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
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
11
|
- **Mount it?** yes — the `evolution-commands` row, carried by the `evolution-host`, `evolution-all` and `evolution-preset` bundles.
|
|
12
12
|
|
|
@@ -17,7 +17,11 @@ The full `/evolution` subcommand table is single-sourced in the family README's
|
|
|
17
17
|
## Known limitations
|
|
18
18
|
|
|
19
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
|
|
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.
|
|
21
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.
|
|
22
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").
|
|
23
27
|
|
|
@@ -25,6 +29,6 @@ The full `/evolution` subcommand table is single-sourced in the family README's
|
|
|
25
29
|
|
|
26
30
|
## Notes and history
|
|
27
31
|
|
|
28
|
-
- P2-22 (v11) correction: this package's
|
|
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.
|
|
29
33
|
- **① variant form: the `/evolution …` command face is visible in every session** (accepted, not fixed: 0.3.78).
|
|
30
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).
|
package/lib/index.js
CHANGED
|
@@ -380,8 +380,10 @@ function presetIssues(presets) {
|
|
|
380
380
|
* build-time guards cannot. Three classes, each with its own move:
|
|
381
381
|
* - a user override (the effective value is no longer the deployment's),
|
|
382
382
|
* - a deprecated alias still written in a user section (writes refuse it),
|
|
383
|
-
* - a declared user-writable parameter whose owner publishes no
|
|
384
|
-
*
|
|
383
|
+
* - a declared user-writable parameter whose owner publishes no settings namespace AT ALL
|
|
384
|
+
* (a registry fact: writes answer E-315), and
|
|
385
|
+
* - a declared user-writable parameter whose owning row this runtime does not mount
|
|
386
|
+
* (the registry promises a face this composition never carries).
|
|
385
387
|
* G5: the two layers and the live rows come from the platform (`configEditor.configuration()`,
|
|
386
388
|
* `pluginManager.listPlugins()`); the registry side (which spellings are deprecated, which rows
|
|
387
389
|
* declare an E3 parameter) stays the family's own table. A surface that is not mounted is
|
|
@@ -411,14 +413,20 @@ function paramDivergences(platform) {
|
|
|
411
413
|
return issues;
|
|
412
414
|
}
|
|
413
415
|
const live = liveRowTails(platform.plugins);
|
|
414
|
-
const
|
|
416
|
+
const noUserLayer = [];
|
|
417
|
+
const unmounted = [];
|
|
418
|
+
for (const entry of PARAM_EXPOSURE) {
|
|
419
|
+
if (entry.tier !== "E3") continue;
|
|
415
420
|
const namespace = paramSettingsId(entry.owner);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
421
|
+
if (namespace === void 0) noUserLayer.push(entry.id);
|
|
422
|
+
else if (!live.has(namespace)) unmounted.push(entry.id);
|
|
423
|
+
}
|
|
424
|
+
for (const [ids, sentence] of [[noUserLayer, "declared user-writable but the owner publishes no settings namespace"], [unmounted, "declared user-writable but unreachable here"]]) {
|
|
425
|
+
if (ids.length === 0) continue;
|
|
426
|
+
const shown = ids.slice(0, 6).join(", ");
|
|
427
|
+
const more = ids.length > 6 ? ` (+${ids.length - 6} more)` : "";
|
|
428
|
+
const why = sentence.includes("no settings namespace") ? "those parameters have no user layer at all (writes answer E-315) — register the owner in PARAM_NAMESPACES or move its rows to E2" : "the owning row is not mounted in this runtime";
|
|
429
|
+
issues.push(`${sentence}: ${shown}${more} — ${why}`);
|
|
422
430
|
}
|
|
423
431
|
return issues;
|
|
424
432
|
}
|
|
@@ -1318,6 +1326,14 @@ const COMMAND_ENTRIES = [
|
|
|
1318
1326
|
usage: "skill undo <name> [--to v<N>]",
|
|
1319
1327
|
summary: "把某个技能的内容回退到某一版(默认上一版;只动内容,撤销本身也记为一版)"
|
|
1320
1328
|
},
|
|
1329
|
+
{
|
|
1330
|
+
usage: "skill pin <name>",
|
|
1331
|
+
summary: "钉住一个技能:免于删除、后台审查与生命周期整理(立即生效、随时可解钉;与 skill_manage 的 pin 动作同一语义,不走审批)"
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
usage: "skill unpin <name>",
|
|
1335
|
+
summary: "解钉一个技能:撤销上面的冻结(立即生效、不走审批)"
|
|
1336
|
+
},
|
|
1321
1337
|
{
|
|
1322
1338
|
usage: "skills health",
|
|
1323
1339
|
summary: "技能库结构体检"
|
|
@@ -1339,12 +1355,16 @@ const COMMAND_ENTRIES = [
|
|
|
1339
1355
|
summary: "看维护扫描:在飞的那次(含已跑多久)或某个 run 的终态"
|
|
1340
1356
|
},
|
|
1341
1357
|
{
|
|
1342
|
-
usage: "maintain report <id>",
|
|
1343
|
-
summary: "读某个 run
|
|
1358
|
+
usage: "maintain report [<id>]",
|
|
1359
|
+
summary: "读某个 run 的结果正文(省略 id 取本 home 最新一条;没有结果就明说,不装成空结果)"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
usage: "maintain handoff [<id>]",
|
|
1363
|
+
summary: "把某次维护报告交给模型:注入指针与汇报协议,模型只汇报、等你确认后再动手"
|
|
1344
1364
|
},
|
|
1345
1365
|
{
|
|
1346
|
-
usage: "maintain cancel <id>",
|
|
1347
|
-
summary: "
|
|
1366
|
+
usage: "maintain cancel [<id>]",
|
|
1367
|
+
summary: "停掉在飞的那次维护扫描(省略 id 仅当最新一条是本进程在跑的那次)"
|
|
1348
1368
|
},
|
|
1349
1369
|
{
|
|
1350
1370
|
usage: "migrate",
|
|
@@ -1396,6 +1416,7 @@ const Config = z.object({
|
|
|
1396
1416
|
skillsRoot: z.string().default(""),
|
|
1397
1417
|
maintainCooldownMs: z.number().min(0).default(3e4),
|
|
1398
1418
|
maintainTimeoutMs: z.number().min(1).default(6e5),
|
|
1419
|
+
maintainHandoffOnSettle: z.boolean().default(false).volatile(),
|
|
1399
1420
|
threatExemptLabels: z.array(z.string()).default([])
|
|
1400
1421
|
});
|
|
1401
1422
|
/** How one run reads in `maintain status` output. */
|
|
@@ -1406,6 +1427,65 @@ function renderRunRecord(record) {
|
|
|
1406
1427
|
if (record.resultRef !== void 0) parts.push(`→ ${record.resultRef}`);
|
|
1407
1428
|
return parts.join(" ");
|
|
1408
1429
|
}
|
|
1430
|
+
/** The newest MAINTENANCE run this home knows — the default when a subcommand omits its id
|
|
1431
|
+
* (G2/0.20.0). The registry view is newest-first and already carries the cross-plane rows. The
|
|
1432
|
+
* index is a file a user can hand-edit, so a row with a blank id is skipped rather than answered
|
|
1433
|
+
* from (review P2-7: it produced text like `Run (succeeded) …`). `RunKind` has one member today,
|
|
1434
|
+
* so kind needs no filter — a second kind must add one here when it lands. */
|
|
1435
|
+
function newestRun(registry) {
|
|
1436
|
+
return registry.runs().find((record) => record.id.trim() !== "");
|
|
1437
|
+
}
|
|
1438
|
+
/** Sentence F, ONE wording for the three id-less forms (design `10` cell 13): an index that could
|
|
1439
|
+
* not be read must never answer as "there is no recent run" (G2/0.20.0). */
|
|
1440
|
+
function unreadableIndex(note) {
|
|
1441
|
+
return `The home's run index is not readable (${note ?? "unknown reason"}) — this is NOT "there is no recent run".`;
|
|
1442
|
+
}
|
|
1443
|
+
/** The explicit-id twin of {@link unreadableIndex}, worded like `maintain status` so the four
|
|
1444
|
+
* readers cannot drift (review P1-1). */
|
|
1445
|
+
function unreadableForId(id, note) {
|
|
1446
|
+
return `Run ${id} is not in this process's history, and the home's run index is not readable (${note ?? "unknown reason"}) — that is NOT proof the run never happened.`;
|
|
1447
|
+
}
|
|
1448
|
+
/** The reader-facing report path: the `.md` digest beside the run's `.json` result. */
|
|
1449
|
+
function reportDigestPath(record) {
|
|
1450
|
+
return (record.resultRef ?? join(evolutionHome(), "reports", `maintain-${record.id}.json`)).replace(/\.json$/, ".md");
|
|
1451
|
+
}
|
|
1452
|
+
/** Deliver one handoff message through the waking primitive, called ON the receiver
|
|
1453
|
+
* (family checklist new-consumer: N13b fails a wake primitive read into a local — the
|
|
1454
|
+
* 0.3.73 incident ate review prompts for six days). Falls back to the non-waking
|
|
1455
|
+
* `inject`, exactly like `/evolution learn`. @returns true when the receiver woke,
|
|
1456
|
+
* false when the message was only queued, `undefined` when neither primitive exists. */
|
|
1457
|
+
function deliverHandoff(agent, message) {
|
|
1458
|
+
if (typeof agent.followup === "function") {
|
|
1459
|
+
agent.followup(message);
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
if (typeof agent.inject === "function") {
|
|
1463
|
+
agent.inject(message);
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
/** The model-facing handoff message (G2/0.20.0): a POINTER to the report plus the
|
|
1468
|
+
* report-only protocol. ONE builder shared by `maintain handoff` and the settle-time
|
|
1469
|
+
* automatic handoff, so the two cannot drift. Missing counts are omitted, never guessed. */
|
|
1470
|
+
function handoffPrompt(input) {
|
|
1471
|
+
const counts = [
|
|
1472
|
+
input.verdict === void 0 ? void 0 : `verdict=${input.verdict}`,
|
|
1473
|
+
input.recommendationCount === void 0 ? void 0 : `${input.recommendationCount} recommendations`,
|
|
1474
|
+
input.needsHumanCount === void 0 ? void 0 : `${input.needsHumanCount} of them need your user's confirmation`
|
|
1475
|
+
].filter((part) => part !== void 0);
|
|
1476
|
+
return [
|
|
1477
|
+
`${`[evolution maintain] The maintenance report for run ${input.runId} is ready: ${input.reportPath}`}${counts.length === 0 ? "" : ` (${counts.join(", ")}; each item has a stable [n] id)`}.`,
|
|
1478
|
+
"",
|
|
1479
|
+
"This turn is REPORT-ONLY. Read the file, then tell your user what it found and what it proposes — one line",
|
|
1480
|
+
"per item: [n] | what would change | which skills | why (the finding) | undo | needs-your-confirmation?",
|
|
1481
|
+
"Lead with the items that need confirmation. Do NOT edit, restructure, archive or rename anything in this",
|
|
1482
|
+
"turn, and start no other skill work: your user must approve which items to carry out. When they reply by [n]",
|
|
1483
|
+
"ids, carry out ONLY the approved items, one at a time, and read each item's undo line before you touch",
|
|
1484
|
+
"anything; if an item has no undo line, treat it as irreversible and ask first. Apply every change through",
|
|
1485
|
+
"the skill-writing tool (skill_manage) — never edit SKILL.md or its support files with generic file tools:",
|
|
1486
|
+
"an out-of-band edit leaves no version in skill history and no audit record."
|
|
1487
|
+
].join("\n");
|
|
1488
|
+
}
|
|
1409
1489
|
/**
|
|
1410
1490
|
* Write one run's result to its ONE home: `<evolutionHome>/reports/maintain-<id>.json`
|
|
1411
1491
|
* plus the `.md` digest, then sweep that window.
|
|
@@ -1475,7 +1555,8 @@ async function readRunReport(io, record) {
|
|
|
1475
1555
|
const parsed = JSON.parse(probed.value);
|
|
1476
1556
|
return {
|
|
1477
1557
|
kind: "present",
|
|
1478
|
-
text: `${`Maintenance run ${record.id} — ${record.state}${record.failure === void 0 ? "" : ` (${record.failure})`}`}\n${typeof parsed.text === "string" && parsed.text.trim() !== "" ? parsed.text : renderRunRecord(record)}
|
|
1558
|
+
text: `${`Maintenance run ${record.id} — ${record.state}${record.failure === void 0 ? "" : ` (${record.failure})`}`}\n${typeof parsed.text === "string" && parsed.text.trim() !== "" ? parsed.text : renderRunRecord(record)}`,
|
|
1559
|
+
report: parsed
|
|
1479
1560
|
};
|
|
1480
1561
|
} catch (error) {
|
|
1481
1562
|
return {
|
|
@@ -1484,10 +1565,29 @@ async function readRunReport(io, record) {
|
|
|
1484
1565
|
};
|
|
1485
1566
|
}
|
|
1486
1567
|
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Read a switch that may be a live settings reference.
|
|
1570
|
+
*
|
|
1571
|
+
* The loader hands a `.volatile()` field as `{ get() }` and the value is read at USE time; a
|
|
1572
|
+
* directly constructed plugin passes a plain boolean (the schema is the loader's face — the same
|
|
1573
|
+
* split the numeric knobs document as their second clamp layer). A single-form read is wrong
|
|
1574
|
+
* either way: `=== true` is never true for a reference, and `?.get()` ignores the plain value.
|
|
1575
|
+
* @param value - the configured switch, in either form, or undefined.
|
|
1576
|
+
* @returns the switch; undefined reads as off.
|
|
1577
|
+
*/
|
|
1578
|
+
function liveFlag(value) {
|
|
1579
|
+
return typeof value === "boolean" ? value : value?.get() ?? false;
|
|
1580
|
+
}
|
|
1487
1581
|
function apply(ctx, rawConfig = {}) {
|
|
1488
1582
|
const config = rawConfig;
|
|
1489
1583
|
assertSkillsRootAliasRetired(rawConfig);
|
|
1490
1584
|
const skillsRootValue = resolveRootConfig(rawConfig).root;
|
|
1585
|
+
ctx.inject(["settings"], (scope) => {
|
|
1586
|
+
const settings = scope.settings;
|
|
1587
|
+
if (typeof settings?.configure !== "function") return;
|
|
1588
|
+
const disposer = settings.configure({ auto: false }, ctx.fiber);
|
|
1589
|
+
if (typeof disposer === "function") scope.effect(() => disposer, "evolution-commands: settings presentation");
|
|
1590
|
+
});
|
|
1491
1591
|
let runs;
|
|
1492
1592
|
ctx.inject(["evolutionIo"], (ioCtx) => {
|
|
1493
1593
|
const ioRegistry = ioCtx.get("evolutionIo");
|
|
@@ -1717,6 +1817,23 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1717
1817
|
};
|
|
1718
1818
|
return result.ok ? ok(result.message) : err(result.message);
|
|
1719
1819
|
}
|
|
1820
|
+
if (input === "skill pin" || input.startsWith("skill pin ") || input === "skill unpin" || input.startsWith("skill unpin ")) {
|
|
1821
|
+
const pinned = !input.startsWith("skill unpin");
|
|
1822
|
+
const name = input.slice(pinned ? 10 : 12).trim();
|
|
1823
|
+
if (!name) return err(`Usage: /evolution skill ${pinned ? "pin" : "unpin"} <name>`);
|
|
1824
|
+
const ioRegistry = ctx.get("evolutionIo");
|
|
1825
|
+
if (!ioRegistry) return err("Evolution IO registry not mounted — skill pinning unavailable.");
|
|
1826
|
+
const result = await newSkillLibrary({
|
|
1827
|
+
config: { root: skillsRootValue },
|
|
1828
|
+
io: ioRegistry.provider(),
|
|
1829
|
+
ctx,
|
|
1830
|
+
limits: {
|
|
1831
|
+
...DEFAULT_SKILL_LIMITS,
|
|
1832
|
+
...policyStageLimits(ctx.get("evolutionPolicy")?.get?.())
|
|
1833
|
+
}
|
|
1834
|
+
}).setPinned(name, pinned);
|
|
1835
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
1836
|
+
}
|
|
1720
1837
|
if (input === "skills health") {
|
|
1721
1838
|
const curator = ctx.get("evolutionCurator");
|
|
1722
1839
|
if (!curator) return err(errorText("e-302-curator-service-not-mounted"));
|
|
@@ -1751,7 +1868,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1751
1868
|
agent.followup(message);
|
|
1752
1869
|
woke = true;
|
|
1753
1870
|
} else if (typeof agent.inject === "function") agent.inject(message);
|
|
1754
|
-
else return err(errorText("e-305-the-invocation-agent-exposes"));
|
|
1871
|
+
else return err(errorText("e-305-the-invocation-agent-exposes", { a1: "this learn request" }));
|
|
1755
1872
|
const eventIo = ctx.get("evolutionIo")?.provider();
|
|
1756
1873
|
if (eventIo) appendEvolutionEvent(eventIo, eventsFile(evolutionRoot()), {
|
|
1757
1874
|
type: "learn",
|
|
@@ -1812,6 +1929,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1812
1929
|
}
|
|
1813
1930
|
const runHome = evolutionHome();
|
|
1814
1931
|
const runRoot = evolutionRoot();
|
|
1932
|
+
const runAgent = invocationAgent;
|
|
1815
1933
|
const handle = await registry.begin("maintain");
|
|
1816
1934
|
(async () => {
|
|
1817
1935
|
try {
|
|
@@ -1842,7 +1960,11 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1842
1960
|
runId: handle.id,
|
|
1843
1961
|
...outcome.ok ? {
|
|
1844
1962
|
verdict: outcome.verdict,
|
|
1845
|
-
recommendationCount: outcome.recommendationCount
|
|
1963
|
+
recommendationCount: outcome.recommendationCount,
|
|
1964
|
+
items: outcome.items,
|
|
1965
|
+
notes: outcome.notes,
|
|
1966
|
+
forcedHuman: outcome.forcedHuman,
|
|
1967
|
+
needsHumanCount: outcome.needsHumanCount
|
|
1846
1968
|
} : {},
|
|
1847
1969
|
...outcome.ok ? {} : { failure: handle.signal.aborted ? "cancelled: the scan was stopped before it settled" : outcome.error ?? "Maintenance scan failed." },
|
|
1848
1970
|
startedAt: handle.startedAt,
|
|
@@ -1857,16 +1979,44 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1857
1979
|
...resultRef === void 0 ? {} : { resultRef },
|
|
1858
1980
|
...outcome.ok ? {} : { failure: handle.signal.aborted ? "cancelled: the scan was stopped before it settled" : outcome.error ?? "Maintenance scan failed." }
|
|
1859
1981
|
});
|
|
1860
|
-
if (outcome.ok)
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1982
|
+
if (outcome.ok) {
|
|
1983
|
+
appendEvolutionEvent(ioRegistry.provider(), eventsFile(runRoot), {
|
|
1984
|
+
type: "maintain",
|
|
1985
|
+
source: "manual",
|
|
1986
|
+
runId: handle.id,
|
|
1987
|
+
verdict: outcome.verdict,
|
|
1988
|
+
recommendations: outcome.recommendationCount
|
|
1989
|
+
}).catch((error) => {
|
|
1990
|
+
ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
|
|
1991
|
+
});
|
|
1992
|
+
if (liveFlag(config.maintainHandoffOnSettle) && resultRef !== void 0 && runAgent !== void 0) try {
|
|
1993
|
+
if (deliverHandoff(runAgent, createUserMessage({
|
|
1994
|
+
content: [{
|
|
1995
|
+
type: "text",
|
|
1996
|
+
text: handoffPrompt({
|
|
1997
|
+
runId: handle.id,
|
|
1998
|
+
reportPath: reportDigestPath({
|
|
1999
|
+
id: handle.id,
|
|
2000
|
+
resultRef
|
|
2001
|
+
}),
|
|
2002
|
+
verdict: outcome.verdict,
|
|
2003
|
+
recommendationCount: outcome.recommendationCount,
|
|
2004
|
+
needsHumanCount: outcome.needsHumanCount
|
|
2005
|
+
})
|
|
2006
|
+
}],
|
|
2007
|
+
source: {
|
|
2008
|
+
kind: "evolution-commands",
|
|
2009
|
+
form: "notice",
|
|
2010
|
+
summary: "maintenance report handoff (auto)"
|
|
2011
|
+
}
|
|
2012
|
+
})) === void 0) ctx.logger.warn(`evolution-commands: the automatic report handoff for run ${handle.id} could not be delivered — this host exposes neither followup nor inject; the report is at ${reportDigestPath({
|
|
2013
|
+
id: handle.id,
|
|
2014
|
+
resultRef
|
|
2015
|
+
})}`);
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
ctx.logger.warn(`evolution-commands: automatic report handoff failed (${String(error)})`);
|
|
2018
|
+
}
|
|
2019
|
+
} else ctx.logger.warn(`evolution-commands: maintenance run ${handle.id} produced no plan (${outcome.error ?? "unknown reason"})`);
|
|
1870
2020
|
} catch (error) {
|
|
1871
2021
|
const aborted = handle.signal.aborted;
|
|
1872
2022
|
const failure = aborted ? "cancelled: the scan was stopped before it settled" : `Maintenance scan failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -1905,15 +2055,22 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1905
2055
|
const recent = all.filter((record) => record.state !== "running").slice(0, 5);
|
|
1906
2056
|
return ok([live.length === 0 ? "No maintenance run in flight." : `In flight:\n${live.map(renderRunRecord).join("\n")}`, recent.length === 0 ? "" : `Recent:\n${recent.map(renderRunRecord).join("\n")}`].filter((part) => part !== "").join("\n"));
|
|
1907
2057
|
}
|
|
1908
|
-
const maintainReport = /^maintain\s+report
|
|
2058
|
+
const maintainReport = /^maintain\s+report(?:\s+(\S+))?\s*$/.exec(input);
|
|
1909
2059
|
if (maintainReport) {
|
|
1910
2060
|
const registry = runs;
|
|
1911
2061
|
if (!registry) return err("Evolution IO registry not mounted — run results unavailable.");
|
|
1912
|
-
await syncRuns(registry);
|
|
2062
|
+
const synced = await syncRuns(registry);
|
|
1913
2063
|
const wanted = maintainReport[1];
|
|
1914
|
-
if (wanted === void 0) return err(
|
|
1915
|
-
const record = registry.find(wanted);
|
|
1916
|
-
if (!record)
|
|
2064
|
+
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2065
|
+
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2066
|
+
if (!record) {
|
|
2067
|
+
if (wanted !== void 0) {
|
|
2068
|
+
if (!synced.ok) return err(unreadableForId(wanted, synced.note));
|
|
2069
|
+
return err(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
|
|
2070
|
+
}
|
|
2071
|
+
return err("No maintenance run recorded in this home yet — /evolution maintain starts one.");
|
|
2072
|
+
}
|
|
2073
|
+
if (wanted === void 0 && record.state === "running") return err(`The newest run ${record.id} is still running — its report appears when it settles; /evolution maintain status ${record.id} shows where it is.`);
|
|
1917
2074
|
const ioRegistry = ctx.get("evolutionIo");
|
|
1918
2075
|
if (!ioRegistry) return err("Evolution IO registry not mounted — run results unavailable.");
|
|
1919
2076
|
const probed = await readRunReport(ioRegistry.provider(), record);
|
|
@@ -1921,22 +2078,72 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1921
2078
|
if (probed.kind === "unknown") return err(`Run ${record.id} has a report that could not be read (${probed.reason}) — this is not "no result".`);
|
|
1922
2079
|
return ok(probed.text);
|
|
1923
2080
|
}
|
|
1924
|
-
const maintainCancel = /^maintain\s+cancel
|
|
2081
|
+
const maintainCancel = /^maintain\s+cancel(?:\s+(\S+))?\s*$/.exec(input);
|
|
1925
2082
|
if (maintainCancel) {
|
|
1926
2083
|
const registry = runs;
|
|
1927
2084
|
if (!registry) return err("Evolution IO registry not mounted — run cancellation unavailable.");
|
|
1928
|
-
await syncRuns(registry);
|
|
2085
|
+
const synced = await syncRuns(registry);
|
|
1929
2086
|
const wanted = maintainCancel[1];
|
|
1930
|
-
if (wanted === void 0) return err(
|
|
1931
|
-
const record = registry.find(wanted);
|
|
1932
|
-
if (!record)
|
|
2087
|
+
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2088
|
+
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2089
|
+
if (!record) {
|
|
2090
|
+
if (wanted !== void 0) {
|
|
2091
|
+
if (!synced.ok) return err(unreadableForId(wanted, synced.note));
|
|
2092
|
+
return err(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
|
|
2093
|
+
}
|
|
2094
|
+
return err("No maintenance run recorded in this home yet — /evolution maintain starts one.");
|
|
2095
|
+
}
|
|
2096
|
+
const idlessHint = wanted === void 0 ? " Pass an id to cancel a specific run: /evolution maintain status lists them." : "";
|
|
1933
2097
|
if (!registry.cancel(record.id)) {
|
|
1934
2098
|
if (record.state === "running") return err(`Run ${record.id} is running in ANOTHER process sharing this home — it can only be cancelled there.`);
|
|
1935
|
-
return err(`Run ${record.id} is already ${record.state} — nothing to cancel
|
|
2099
|
+
return err(`Run ${record.id} is already ${record.state} — nothing to cancel.${idlessHint}`);
|
|
1936
2100
|
}
|
|
1937
2101
|
return ok(`Maintenance run ${record.id} cancelled — the scan stops at its next checkpoint. /evolution maintain status ${record.id} shows the terminal state.`);
|
|
1938
2102
|
}
|
|
1939
|
-
|
|
2103
|
+
const maintainHandoff = /^maintain\s+handoff(?:\s+(\S+))?\s*$/.exec(input);
|
|
2104
|
+
if (maintainHandoff) {
|
|
2105
|
+
const registry = runs;
|
|
2106
|
+
if (!registry) return err("Evolution IO registry not mounted — the report cannot be handed over.");
|
|
2107
|
+
const synced = await syncRuns(registry);
|
|
2108
|
+
const wanted = maintainHandoff[1];
|
|
2109
|
+
if (wanted === void 0 && !synced.ok) return err(unreadableIndex(synced.note));
|
|
2110
|
+
const record = wanted === void 0 ? newestRun(registry) : registry.find(wanted);
|
|
2111
|
+
if (!record) {
|
|
2112
|
+
if (wanted !== void 0) {
|
|
2113
|
+
if (!synced.ok) return err(unreadableForId(wanted, synced.note));
|
|
2114
|
+
return err(`No run ${wanted} in this home — /evolution maintain status lists the recent ones.`);
|
|
2115
|
+
}
|
|
2116
|
+
return err("No maintenance run recorded in this home yet — /evolution maintain starts one.");
|
|
2117
|
+
}
|
|
2118
|
+
if (wanted === void 0 && record.state === "running") return err(`The newest run ${record.id} is still running — its report appears when it settles; /evolution maintain status ${record.id} shows where it is.`);
|
|
2119
|
+
const ioRegistry = ctx.get("evolutionIo");
|
|
2120
|
+
if (!ioRegistry) return err("Evolution IO registry not mounted — the report cannot be handed over.");
|
|
2121
|
+
const probed = await readRunReport(ioRegistry.provider(), record);
|
|
2122
|
+
if (probed.kind === "missing") return err(`Run ${record.id} (${record.state}) has NO result: no report was written${record.failure === void 0 ? "" : ` (${record.failure})`}.`);
|
|
2123
|
+
if (probed.kind === "unknown") return err(`Run ${record.id} has a report that could not be read (${probed.reason}) — this is not "no result".`);
|
|
2124
|
+
const missingAgent = agentMissing("handoff");
|
|
2125
|
+
if (missingAgent) return missingAgent;
|
|
2126
|
+
const woke = deliverHandoff(invocationAgent, createUserMessage({
|
|
2127
|
+
content: [{
|
|
2128
|
+
type: "text",
|
|
2129
|
+
text: handoffPrompt({
|
|
2130
|
+
runId: record.id,
|
|
2131
|
+
reportPath: reportDigestPath(record),
|
|
2132
|
+
verdict: probed.report.verdict,
|
|
2133
|
+
recommendationCount: probed.report.recommendationCount,
|
|
2134
|
+
needsHumanCount: probed.report.needsHumanCount
|
|
2135
|
+
})
|
|
2136
|
+
}],
|
|
2137
|
+
source: {
|
|
2138
|
+
kind: "evolution-commands",
|
|
2139
|
+
form: "notice",
|
|
2140
|
+
summary: "maintenance report handoff"
|
|
2141
|
+
}
|
|
2142
|
+
}));
|
|
2143
|
+
if (woke === void 0) return err(errorText("e-305-the-invocation-agent-exposes", { a1: "this maintenance handoff" }));
|
|
2144
|
+
return ok(woke ? `Handed run ${record.id} to the model — report: ${reportDigestPath(record)}. It will read the file and follow the action lines.` : `Handed run ${record.id} to the model, queued — this host exposes no wake-up channel, so it is read on your next message.`);
|
|
2145
|
+
}
|
|
2146
|
+
if (/^maintain\b/.test(input)) return err("Unknown maintain arguments: expected `maintain`, `maintain --timeout <ms>` / `maintain --timeout=<ms>`, `maintain status [<id>]`, `maintain report [<id>]`, `maintain handoff [<id>]` or `maintain cancel [<id>]`. Got: " + input);
|
|
1940
2147
|
const presetInstall = /^preset install(?: --base (\S+))?$/.exec(input);
|
|
1941
2148
|
if (presetInstall !== null || /^preset\b/.test(input)) {
|
|
1942
2149
|
if (presetInstall === null) return err("Unknown preset arguments: expected `preset install` or `preset install --base <name>[,<name>...]` (the names live in the agent package's bases.json). Got: " + input);
|
package/lib/types/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Human commands for the evolution family: /evolution learn|pending|curator|restore|consolidate|skills refresh.
|
|
3
3
|
* @module @lmzhen/dsh-evolution-commands
|
|
4
4
|
*/
|
|
5
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
6
6
|
import z from '@deepseek-ai/schemastery';
|
|
7
7
|
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
declare module '@deepseek-ai/dsh-llm' {
|
|
@@ -36,6 +36,15 @@ export interface Config {
|
|
|
36
36
|
* 4-8 min — the 13:38 successful run used --timeout 600000; the old 120s
|
|
37
37
|
* default deadlined bare runs mid-analysis (14:37 run aborted at 119.94s). */
|
|
38
38
|
maintainTimeoutMs?: number | undefined;
|
|
39
|
+
/** G2 (0.20.0): when a scan settles successfully, hand its report to the model
|
|
40
|
+
* automatically (the same pointer+protocol injection `maintain handoff` performs).
|
|
41
|
+
* Default false: the injection wakes a turn, which costs a model call.
|
|
42
|
+
*
|
|
43
|
+
* `.volatile()` (0.21.0, G3): the field carries a USER LAYER, so the loader hands the live
|
|
44
|
+
* reference and the switch is read at every settle — a settings write turns it on or off without
|
|
45
|
+
* a reload. A directly constructed plugin still passes a plain boolean (the schema's default is
|
|
46
|
+
* the loader's face), so the reader accepts both. */
|
|
47
|
+
maintainHandoffOnSettle?: boolean | Volatile<boolean> | undefined;
|
|
39
48
|
/** Threat-scan exemption labels for WRITE-path skill mutations (P2-18).
|
|
40
49
|
* Threaded into the SkillLibrary this package constructs for `restructure`
|
|
41
50
|
* (its only write path); the core-side constructor option carries the same
|
|
@@ -51,12 +60,14 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
51
60
|
skillsRoot: z<string, string, "defined">;
|
|
52
61
|
maintainCooldownMs: z<number, number, "defined">;
|
|
53
62
|
maintainTimeoutMs: z<number, number, "defined">;
|
|
63
|
+
maintainHandoffOnSettle: z<boolean, boolean, "volatile-defined">;
|
|
54
64
|
threatExemptLabels: z<string[], string[], "defined">;
|
|
55
65
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
56
66
|
root: z<string, string, "defined">;
|
|
57
67
|
skillsRoot: z<string, string, "defined">;
|
|
58
68
|
maintainCooldownMs: z<number, number, "defined">;
|
|
59
69
|
maintainTimeoutMs: z<number, number, "defined">;
|
|
70
|
+
maintainHandoffOnSettle: z<boolean, boolean, "volatile-defined">;
|
|
60
71
|
threatExemptLabels: z<string[], string[], "defined">;
|
|
61
72
|
}>>, "plain">;
|
|
62
73
|
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
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.
|
|
4
|
+
"version": "0.21.0",
|
|
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.4",
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
32
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.21.0",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.21.0",
|
|
32
|
+
"@lmzhen/dsh-evolution-maintenance": "^0.21.0"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@lmzhen/dsh-evolution-agent-preset": "^0.
|
|
35
|
+
"@lmzhen/dsh-evolution-agent-preset": "^0.21.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|