@lmzhen/dsh-evolution-core 0.26.15 → 0.27.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 +16 -1
- package/lib/composition/index.js +3 -0
- package/lib/composition-D8Y3-ROd.js +604 -0
- package/lib/ensure-MEYMj9jn.js +222 -0
- package/lib/errors-Br2-uUSK.js +57 -0
- package/lib/http-routes-BP-S3KV-.js +149 -0
- package/lib/http-routes.js +2 -0
- package/lib/index.js +777 -840
- package/lib/restrictor.js +37 -0
- package/lib/subagent/index.js +186 -0
- package/lib/types/citations.js +192 -0
- package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +2 -2
- package/lib/types/composition/composer.js +681 -0
- package/lib/types/composition/ensure.d.ts +10 -0
- package/lib/types/composition/ensure.js +95 -0
- package/lib/types/composition/index.d.ts +5 -0
- package/lib/types/composition/index.js +7 -0
- package/lib/types/composition/preset-ids.d.ts +17 -0
- package/lib/types/composition/preset-ids.js +21 -0
- package/lib/types/composition/restricted-preset.d.ts +36 -0
- package/lib/types/composition/restricted-preset.js +42 -0
- package/lib/types/constants.js +223 -0
- package/lib/types/cost.js +67 -0
- package/lib/types/curator.d.ts +2 -0
- package/lib/types/curator.js +346 -0
- package/lib/types/diagnostics.d.ts +26 -0
- package/lib/types/diagnostics.js +23 -0
- package/lib/types/document-body.js +91 -0
- package/lib/types/drift-signals.js +319 -0
- package/lib/types/env.js +25 -0
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +59 -0
- package/lib/types/events.js +16 -0
- package/lib/types/evidence.js +65 -0
- package/lib/types/evolution-events.js +479 -0
- package/lib/types/fold-untrusted.js +44 -0
- package/lib/types/frontmatter.js +623 -0
- package/lib/types/fuzzy-match.js +135 -0
- package/lib/types/gates.js +51 -0
- package/lib/types/guards.js +27 -0
- package/lib/types/health-baseline.js +63 -0
- package/lib/types/http-routes.d.ts +67 -35
- package/lib/types/http-routes.js +135 -0
- package/lib/types/index.d.ts +13 -2
- package/lib/types/index.js +101 -0
- package/lib/types/instance-scope.js +69 -0
- package/lib/types/io.js +675 -0
- package/lib/types/learn-prompt.js +42 -0
- package/lib/types/limits.js +63 -0
- package/lib/types/live.js +107 -0
- package/lib/types/long-runs.js +96 -0
- package/lib/types/memory-store.d.ts +0 -16
- package/lib/types/memory-store.js +606 -0
- package/lib/types/mutations.js +95 -0
- package/lib/types/namespace-migration.js +156 -0
- package/lib/types/numeric.js +47 -0
- package/lib/types/opt-in.js +101 -0
- package/lib/types/package-identity.d.ts +14 -0
- package/lib/types/package-identity.js +47 -0
- package/lib/types/params.js +328 -0
- package/lib/types/payload.js +36 -0
- package/lib/types/platform-line.generated.d.ts +10 -0
- package/lib/types/platform-line.generated.js +12 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/platform-services.js +52 -0
- package/lib/types/probe.d.ts +9 -3
- package/lib/types/probe.js +122 -0
- package/lib/types/prompts.js +409 -0
- package/lib/types/quality.js +308 -0
- package/lib/types/reachability.d.ts +46 -0
- package/lib/types/reachability.js +122 -0
- package/lib/types/redact.js +257 -0
- package/lib/types/reference-rewrite.js +144 -0
- package/lib/types/reports.js +110 -0
- package/lib/types/restrictor.d.ts +11 -0
- package/lib/types/restrictor.js +48 -0
- package/lib/types/review-channel.js +59 -0
- package/lib/types/run-registry.js +345 -0
- package/lib/types/scope.js +7 -0
- package/lib/types/serial.js +17 -0
- package/lib/types/session-fold.d.ts +15 -0
- package/lib/types/session-fold.js +28 -0
- package/lib/types/session-projection.js +295 -0
- package/lib/types/signals.js +209 -0
- package/lib/types/skill-discovery.js +66 -0
- package/lib/types/skill-health.js +134 -0
- package/lib/types/skill-history.js +745 -0
- package/lib/types/skill-lint.js +112 -0
- package/lib/types/skill-reads.js +67 -0
- package/lib/types/skill-row-cells.js +123 -0
- package/lib/types/skill-store.d.ts +34 -3
- package/lib/types/skill-store.js +3528 -0
- package/lib/types/state-store.js +60 -0
- package/lib/types/store-ledger.d.ts +44 -0
- package/lib/types/store-ledger.js +117 -0
- package/lib/types/subagent/channel.d.ts +4 -0
- package/lib/types/subagent/channel.js +131 -0
- package/lib/types/subagent/index.d.ts +4 -0
- package/lib/types/subagent/index.js +5 -0
- package/lib/types/subagent/readiness.d.ts +6 -0
- package/lib/types/subagent/readiness.js +35 -0
- package/lib/types/subagent/types.d.ts +58 -0
- package/lib/types/subagent/types.js +2 -0
- package/lib/types/threats.js +338 -0
- package/lib/types/tool-dispatch.d.ts +5 -0
- package/lib/types/tool-dispatch.js +535 -0
- package/lib/types/tree-write.d.ts +53 -0
- package/lib/types/tree-write.js +40 -0
- package/lib/types/unit-members.js +121 -0
- package/lib/types/usage.js +474 -0
- package/lib/types/write-authority.d.ts +87 -0
- package/lib/types/write-authority.js +23 -0
- package/lib/types/write-gates.d.ts +111 -0
- package/lib/types/write-gates.js +344 -0
- package/lib/types/write-inventory.js +158 -0
- package/package.json +39 -11
package/lib/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as scopedPackageName, c as MAINTAINER_PRESET_ID, d as RESTRICTED_TOOLS_ROW_ID, f as RESTRICTOR_MODULE, g as scopeOfNearestManifest, p as REVIEWER_PRESET_ID, s as EVOLUTION_PRESET_IDS, u as RESTRICTED_PRESET_DISPLAY } from "./ensure-MEYMj9jn.js";
|
|
2
|
+
import { a as methodNotAllowed, c as asRecord, i as json, l as isRecord, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-BP-S3KV-.js";
|
|
3
|
+
import { w as allowRowCollisions } from "./composition-D8Y3-ROd.js";
|
|
4
|
+
import { n as errorText, t as EVOLUTION_ERRORS } from "./errors-Br2-uUSK.js";
|
|
5
|
+
import { createSubagentChannel, probeChannelState, subagentChannelState } from "./subagent/index.js";
|
|
6
|
+
import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
7
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { parse } from "yaml";
|
|
4
10
|
import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
5
11
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
6
12
|
import { homedir } from "node:os";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
13
|
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
9
14
|
import { z } from "zod";
|
|
10
15
|
//#region lib/types/citations.js
|
|
@@ -204,6 +209,313 @@ function scanBodyHooks(content) {
|
|
|
204
209
|
};
|
|
205
210
|
}
|
|
206
211
|
//#endregion
|
|
212
|
+
//#region lib/types/tree-write.js
|
|
213
|
+
/**
|
|
214
|
+
* G1/1.5 (S7-09): the TYPED critical section around a tree write.
|
|
215
|
+
*
|
|
216
|
+
* The control-plane lock used to wrap a whole pass, LLM call and all: an operator undo/restore waited up to
|
|
217
|
+
* one `curatorReviewTimeoutMs` (~120s) for a review that was merely thinking. The fix is to give the critical
|
|
218
|
+
* section a shape that a review cannot be squeezed into.
|
|
219
|
+
*
|
|
220
|
+
* WHAT THIS TYPE CAN AND CANNOT DO (the plan wording says "cannot be put inside the lock by TYPE" - that is
|
|
221
|
+
* only half true and the half that is false is the half people rely on): a callback is a CLOSURE, so even
|
|
222
|
+
* with `view` as its only parameter it can still reach `this.io` / `this.ctx` / `this.llm` from the enclosing
|
|
223
|
+
* scope. TypeScript cannot stop that, and a criterion claiming it would never go red.
|
|
224
|
+
*
|
|
225
|
+
* What the type DOES give: new code has no other entry - the callback is handed a `SnapshotView` and nothing
|
|
226
|
+
* else, so a phase that needs `io` or `llm` has to stay OUTSIDE and be passed its result in. What the type
|
|
227
|
+
* CANNOT give is covered by the companion source guard (see the family gates): an `io` / `llm` / `this.`
|
|
228
|
+
* / `ctx.` reference inside a `withTreeWrite` callback is a violation, and that guard is the executable form
|
|
229
|
+
* of "cannot be put inside the lock".
|
|
230
|
+
*
|
|
231
|
+
* @module @lmzhen/dsh-evolution-core/tree-write
|
|
232
|
+
*/
|
|
233
|
+
/**
|
|
234
|
+
* Run one tree-writing section under a caller-owned serial queue, exposing ONLY a {@link SnapshotView}.
|
|
235
|
+
*
|
|
236
|
+
* The queue is passed in rather than owned here: the lock belongs to whoever serializes the control plane
|
|
237
|
+
* (the curator), and a second lock would be a second answer to one question.
|
|
238
|
+
* @param acquire - the caller serial queue (e.g. the curator mutex), returning its release.
|
|
239
|
+
* @param view - what this section sees while it holds the lock.
|
|
240
|
+
* @param fn - the section. It may only use the view.
|
|
241
|
+
* @returns whatever the section returns.
|
|
242
|
+
*/
|
|
243
|
+
async function withTreeWrite(acquire, view, fn) {
|
|
244
|
+
const release = await acquire();
|
|
245
|
+
try {
|
|
246
|
+
return await fn(view);
|
|
247
|
+
} finally {
|
|
248
|
+
release();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region lib/types/write-authority.js
|
|
253
|
+
/** The mode a deployment that configures nothing gets. */
|
|
254
|
+
const DEFAULT_WRITE_CONFIRM_MODE = "auto";
|
|
255
|
+
/** Seconds a timeout-mode prompt waits before the write is cancelled. */
|
|
256
|
+
const DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS = 120;
|
|
257
|
+
/**
|
|
258
|
+
* The batch-one migration aid: every call site becomes ONE of these instead of hand-building the
|
|
259
|
+
* object - so the shape cannot drift between callers, and each site still has to say its own
|
|
260
|
+
* origin out loud (origin is required here on purpose: the whole point is that no caller inherits
|
|
261
|
+
* a default identity).
|
|
262
|
+
*/
|
|
263
|
+
function authorityFrom(input) {
|
|
264
|
+
return {
|
|
265
|
+
origin: input.origin,
|
|
266
|
+
threatExemptLabels: input.threatExemptLabels ?? [],
|
|
267
|
+
protectedNames: input.protectedNames,
|
|
268
|
+
readNames: input.readNames,
|
|
269
|
+
...input.confirm === void 0 ? {} : { confirm: input.confirm },
|
|
270
|
+
...input.confirmMode === void 0 ? {} : { confirmMode: input.confirmMode },
|
|
271
|
+
...input.confirmTimeoutSeconds === void 0 ? {} : { confirmTimeoutSeconds: input.confirmTimeoutSeconds },
|
|
272
|
+
warn: input.warn ?? (() => {})
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region lib/types/write-gates.js
|
|
277
|
+
/**
|
|
278
|
+
* The write-admission sequence: ONE ordered table of the reasons a skill write is refused.
|
|
279
|
+
*
|
|
280
|
+
* Two points read this table (design `dsh-evolution-write-gate-design.md` §3). The ADMISSION
|
|
281
|
+
* point (`execute`, BEFORE the approval seam) runs every gate that applies to `'admission'`, so a
|
|
282
|
+
* write that is refused is never staged and never spends the operator's attention twice. The
|
|
283
|
+
* EXECUTION point (the `executeCore` funnel, reached by the direct path and by the approval
|
|
284
|
+
* replay) runs the gates that apply to `'execution'`: the stored plan of a replayed write passed
|
|
285
|
+
* no schema, and the protected list can change while a record sits pending, so those verdicts are
|
|
286
|
+
* re-decided at the bytes.
|
|
287
|
+
*
|
|
288
|
+
* Order is part of the contract: a gate that can only refuse comes before one that needs a round
|
|
289
|
+
* trip with the operator, so a write that is refused anyway never asks anything.
|
|
290
|
+
*
|
|
291
|
+
* A gate decides whether a write may be ATTEMPTED. The library decides whether it can be
|
|
292
|
+
* PERFORMED — its own structured refusals (a restructure element that is not an object, a patch
|
|
293
|
+
* anchor that no longer matches, an `absorbed_into` that does not exist) stay where the bytes are
|
|
294
|
+
* read.
|
|
295
|
+
*
|
|
296
|
+
* Rule bodies live in `evolution-core` (the read-before-write rule in `skill-reads`, the
|
|
297
|
+
* required-argument table in `SKILL_ACTION_REQUIRED_FIELDS`); this module owns the ORDER and the
|
|
298
|
+
* applicability of each gate, which is an execution-point fact of this tool.
|
|
299
|
+
* @module @lmzhen/dsh-evolution-core/write-gates
|
|
300
|
+
*/
|
|
301
|
+
/** The confirm question's stable id — one question per write, so an answer to an earlier prompt
|
|
302
|
+
* can never be read as the answer to a later one. */
|
|
303
|
+
const CONFIRM_QUESTION_ID = "evolution-skill-write";
|
|
304
|
+
/** The cancel label every confirm question offers. */
|
|
305
|
+
const CANCEL_LABEL = "取消";
|
|
306
|
+
/** What a `timeout`-mode deadline settles with: comparable by identity against the seam's
|
|
307
|
+
* boolean, so the two outcomes stay distinguishable without a mutable flag (which a linter
|
|
308
|
+
* cannot follow through a timer callback, and a reader cannot either). */
|
|
309
|
+
const CONFIRM_TIMED_OUT = "confirm-timed-out";
|
|
310
|
+
/**
|
|
311
|
+
* How the ONE confirmation behaves (registry `skillWriteConfirm`).
|
|
312
|
+
*
|
|
313
|
+
* `auto` writes without asking — the default, because an unattended run (a background pass, a
|
|
314
|
+
* scheduled review, a headless session) must not park a tool call on a question nobody will answer.
|
|
315
|
+
* `ask` waits for as long as it takes, which is the deployment that wants the gate in the loop.
|
|
316
|
+
* `timeout` asks and cancels the write on its own deadline.
|
|
317
|
+
*/
|
|
318
|
+
/** A usable deadline: the configured value when it is a positive finite number, else the default. */
|
|
319
|
+
function timeoutSecondsOf(configured) {
|
|
320
|
+
return configured !== void 0 && Number.isFinite(configured) && configured >= 1 ? Math.floor(configured) : 120;
|
|
321
|
+
}
|
|
322
|
+
/** The scalar fields the tool schema types as strings; the replay channel has no schema.
|
|
323
|
+
* `action` is one of them: a non-string action used to fall through to the library as "Unknown
|
|
324
|
+
* action" while the read gate re-defaulted it to `patch` and refused with E-318 (review
|
|
325
|
+
* 2026-09-26, P2), so the shape gate refuses it by name instead. */
|
|
326
|
+
const SCALAR_ARG_FIELDS = [
|
|
327
|
+
"action",
|
|
328
|
+
"name",
|
|
329
|
+
"content",
|
|
330
|
+
"old_string",
|
|
331
|
+
"new_string",
|
|
332
|
+
"file_path",
|
|
333
|
+
"file_content",
|
|
334
|
+
"absorbed_into"
|
|
335
|
+
];
|
|
336
|
+
/** The scalar fields the tool schema types as BOOLEANS; the replay channel has no schema either.
|
|
337
|
+
* T2-V3/A28: `args.replace_all === true` answered "false" for anything else, so a staged
|
|
338
|
+
* `replace_all: 'yes'` silently patched only the FIRST occurrence — a wrong count with no signal.
|
|
339
|
+
* The plan channel already refuses a non-boolean `replace_all` by name (S1-C1); this is the same
|
|
340
|
+
* rule on the replay channel, so the two channels cannot disagree about one flag. */
|
|
341
|
+
const FLAG_ARG_FIELDS = ["replace_all"];
|
|
342
|
+
/**
|
|
343
|
+
* Assemble a gate context from a caller's services. The protected list is read HERE, so no caller can
|
|
344
|
+
* forget it or read it from a second place.
|
|
345
|
+
* @param services - the four required services plus the optional registry knobs.
|
|
346
|
+
* @param request - the point asking, the arguments under test, and the write origin.
|
|
347
|
+
* @returns a context the shared sequence accepts.
|
|
348
|
+
*/
|
|
349
|
+
function skillWriteGateContext(services, request) {
|
|
350
|
+
return {
|
|
351
|
+
point: request.point,
|
|
352
|
+
args: request.args,
|
|
353
|
+
origin: request.origin,
|
|
354
|
+
protectedNames: protectedSkillNamesOf(services.policy),
|
|
355
|
+
readNames: services.readNames,
|
|
356
|
+
confirm: services.confirm,
|
|
357
|
+
...services.confirmMode === void 0 ? {} : { confirmMode: services.confirmMode },
|
|
358
|
+
...services.confirmTimeoutSeconds === void 0 ? {} : { confirmTimeoutSeconds: services.confirmTimeoutSeconds },
|
|
359
|
+
...services.describeTarget === void 0 ? {} : { describeTarget: services.describeTarget },
|
|
360
|
+
warn: services.warn
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function protectedSkillNamesOf(policy) {
|
|
364
|
+
return (policy?.get?.())?.protectedSkillNames;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Narrow the arguments once for every gate.
|
|
368
|
+
*
|
|
369
|
+
* The admission point reaches the gates AFTER the schema, the replay channel BEFORE any
|
|
370
|
+
* validation, so a gate reads only what it can prove: anything else counts as absent, exactly as
|
|
371
|
+
* the missing-argument check already treated a non-string action.
|
|
372
|
+
* @param args - the arguments as the caller sent them.
|
|
373
|
+
* @returns the narrowed view.
|
|
374
|
+
*/
|
|
375
|
+
function gateArgsOf(args) {
|
|
376
|
+
const scalars = typeof args === "object" && args !== null ? args : {};
|
|
377
|
+
const action = scalars.action;
|
|
378
|
+
const name = scalars.name;
|
|
379
|
+
const absorbedInto = scalars.absorbed_into;
|
|
380
|
+
return {
|
|
381
|
+
action: typeof action === "string" ? action : void 0,
|
|
382
|
+
name: typeof name === "string" ? name : "",
|
|
383
|
+
absorbedInto: typeof absorbedInto === "string" && absorbedInto.trim() !== "" ? absorbedInto : void 0,
|
|
384
|
+
scalars
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/** The sequence, in order. A gate whose `appliesTo` omits the asking point is skipped. */
|
|
388
|
+
const GATES = [
|
|
389
|
+
{
|
|
390
|
+
id: "argument-shape",
|
|
391
|
+
appliesTo: ["admission", "execution"],
|
|
392
|
+
run: ({ view }) => {
|
|
393
|
+
for (const field of SCALAR_ARG_FIELDS) {
|
|
394
|
+
const value = view.scalars[field];
|
|
395
|
+
if (value === void 0 || value === null || typeof value === "string") continue;
|
|
396
|
+
return `skill_manage: "${field}" must be a string (got ${typeof value}); refusing the write.`;
|
|
397
|
+
}
|
|
398
|
+
for (const field of FLAG_ARG_FIELDS) {
|
|
399
|
+
const value = view.scalars[field];
|
|
400
|
+
if (value === void 0 || value === null || typeof value === "boolean") continue;
|
|
401
|
+
return `skill_manage: "${field}" must be a boolean (got ${typeof value}); refusing the write.`;
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: "missing-args",
|
|
408
|
+
appliesTo: ["admission", "execution"],
|
|
409
|
+
run: ({ view }) => {
|
|
410
|
+
const action = view.action;
|
|
411
|
+
const missing = (action === void 0 || !Object.hasOwn(SKILL_ACTION_REQUIRED_FIELDS, action) ? [] : SKILL_ACTION_REQUIRED_FIELDS[action] ?? []).filter((field) => view.scalars[field] === void 0 || view.scalars[field] === null);
|
|
412
|
+
if (missing.length === 0) return null;
|
|
413
|
+
return `skill_manage ${action} requires ${missing.join(", ")}; the tool description lists the arguments per action.`;
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "policy-protection",
|
|
418
|
+
appliesTo: ["admission", "execution"],
|
|
419
|
+
run: ({ view, origin, protectedNames }) => {
|
|
420
|
+
if (origin === "foreground") return null;
|
|
421
|
+
if (view.name === "" || !protectedNames?.includes(view.name)) return null;
|
|
422
|
+
return `skill_manage: "${view.name}" is protected by the current policy (protectedSkillNames); replayed/autonomous writes are refused.`;
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: "read-before-write",
|
|
427
|
+
appliesTo: ["admission"],
|
|
428
|
+
run: ({ view, origin, readNames, warn }) => {
|
|
429
|
+
if (origin === "foreground") return null;
|
|
430
|
+
if (readNames === void 0) {
|
|
431
|
+
warn("skill_manage: the read-before-write check could not run for this call — the session log is not readable here, so the write is allowed.");
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
if (view.action === void 0) return null;
|
|
435
|
+
if (!isUnreadWrite({
|
|
436
|
+
action: view.action,
|
|
437
|
+
name: view.name
|
|
438
|
+
}, readNames)) return null;
|
|
439
|
+
return errorText("e-318-skill-write-without-a-read", { a1: view.name });
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: "human-confirm",
|
|
444
|
+
appliesTo: ["admission"],
|
|
445
|
+
run: async ({ view, origin, confirm, warn, confirmMode, confirmTimeoutSeconds, describeTarget }) => {
|
|
446
|
+
const action = view.action;
|
|
447
|
+
if (action === void 0 || origin !== "foreground" || view.name === "") return null;
|
|
448
|
+
if (!(action === "create" || action === "delete" && view.absorbedInto === void 0)) return null;
|
|
449
|
+
const mode = confirmMode ?? "auto";
|
|
450
|
+
if (mode === "auto") return null;
|
|
451
|
+
if (confirm === void 0) {
|
|
452
|
+
warn("skill_manage: no confirmation channel is mounted, so the write proceeds unconfirmed.");
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
const confirmLabel = action === "create" ? "创建" : "删除";
|
|
456
|
+
const seconds = timeoutSecondsOf(confirmTimeoutSeconds);
|
|
457
|
+
const cancel = new AbortController();
|
|
458
|
+
let timer;
|
|
459
|
+
const deadline = mode === "timeout" ? new Promise((resolve) => {
|
|
460
|
+
timer = setTimeout(() => {
|
|
461
|
+
cancel.abort();
|
|
462
|
+
resolve(CONFIRM_TIMED_OUT);
|
|
463
|
+
}, seconds * 1e3);
|
|
464
|
+
timer.unref();
|
|
465
|
+
}) : void 0;
|
|
466
|
+
const context = await describeTarget?.();
|
|
467
|
+
const details = [context?.description === void 0 || context.description === "" ? null : "功能:" + context.description, context?.versions === void 0 ? null : "已记录的内容版本数:" + String(context.versions)].filter((line) => line !== null);
|
|
468
|
+
const consequence = action === "create" ? `创建技能 "${view.name}"?会在家族目录树里新建一个技能目录。` : `删除技能 "${view.name}"?它会被归档到 .archive 并离开目录。`;
|
|
469
|
+
const asked = confirm({
|
|
470
|
+
action,
|
|
471
|
+
name: view.name,
|
|
472
|
+
confirmLabel,
|
|
473
|
+
question: {
|
|
474
|
+
id: CONFIRM_QUESTION_ID,
|
|
475
|
+
header: "确认",
|
|
476
|
+
question: details.length === 0 ? consequence : consequence + " " + details.join(" · "),
|
|
477
|
+
options: [{ label: confirmLabel }, { label: CANCEL_LABEL }]
|
|
478
|
+
},
|
|
479
|
+
...mode === "timeout" ? { signal: cancel.signal } : {}
|
|
480
|
+
});
|
|
481
|
+
let outcome;
|
|
482
|
+
try {
|
|
483
|
+
outcome = deadline === void 0 ? await asked : await Promise.race([asked, deadline]);
|
|
484
|
+
} finally {
|
|
485
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
486
|
+
}
|
|
487
|
+
if (outcome === true) return null;
|
|
488
|
+
if (outcome === CONFIRM_TIMED_OUT || cancel.signal.aborted) return errorText("e-319-skill-write-confirm-timed-out", {
|
|
489
|
+
a1: view.name,
|
|
490
|
+
a2: action === "create" ? "创建" : "删除",
|
|
491
|
+
a3: String(seconds)
|
|
492
|
+
});
|
|
493
|
+
return errorText("e-317-skill-write-not-confirmed", {
|
|
494
|
+
a1: view.name,
|
|
495
|
+
a2: action === "create" ? "创建" : "删除"
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
];
|
|
500
|
+
/**
|
|
501
|
+
* Run the sequence for one write attempt.
|
|
502
|
+
* @param context - the asking point, the write, and the seams the human-facing gate needs.
|
|
503
|
+
* @returns the FIRST refusal, or null when every applicable gate passed. Both points return the
|
|
504
|
+
* message as the tool result unchanged, so the two entries refuse with identical wording.
|
|
505
|
+
*/
|
|
506
|
+
async function runWriteGates(context) {
|
|
507
|
+
const input = {
|
|
508
|
+
...context,
|
|
509
|
+
view: gateArgsOf(context.args)
|
|
510
|
+
};
|
|
511
|
+
for (const gate of GATES) {
|
|
512
|
+
if (!gate.appliesTo.includes(context.point)) continue;
|
|
513
|
+
const refusal = await gate.run(input);
|
|
514
|
+
if (refusal !== null) return refusal;
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
207
519
|
//#region lib/types/reference-rewrite.js
|
|
208
520
|
/**
|
|
209
521
|
* Reference re-homing plans (0.5.0 V1, design §16.7).
|
|
@@ -577,34 +889,6 @@ const MEMORY_GUIDANCE_SECTION_ORDER = 11e3;
|
|
|
577
889
|
/** Order of the `evolution-skills-guidance` section (last of the two). */
|
|
578
890
|
const SKILLS_GUIDANCE_SECTION_ORDER = 11100;
|
|
579
891
|
//#endregion
|
|
580
|
-
//#region lib/types/guards.js
|
|
581
|
-
/**
|
|
582
|
-
* The family's plain-object predicate, in ONE place (v46 G7.1 / audit A8).
|
|
583
|
-
*
|
|
584
|
-
* The audit found the same guard — "a non-null object that is not an array" — rewritten fifteen
|
|
585
|
-
* times across nine packages, four times inside core itself, and with two byte-identical `asRecord`
|
|
586
|
-
* copies in evolution-policy and evolution-threat. This module is the single definition; callers
|
|
587
|
-
* import it instead of spelling the clause again.
|
|
588
|
-
*
|
|
589
|
-
* @module @lmzhen/dsh-evolution-core
|
|
590
|
-
*/
|
|
591
|
-
/**
|
|
592
|
-
* Is this a non-null, non-array object (what JSON calls an object)?
|
|
593
|
-
* @param value - the value to test.
|
|
594
|
-
* @returns whether it is a plain object.
|
|
595
|
-
*/
|
|
596
|
-
function isRecord(value) {
|
|
597
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Read a value as a record, or undefined when it is not one.
|
|
601
|
-
* @param value - the value to read.
|
|
602
|
-
* @returns the value typed as a record, or undefined.
|
|
603
|
-
*/
|
|
604
|
-
function asRecord(value) {
|
|
605
|
-
return isRecord(value) ? value : void 0;
|
|
606
|
-
}
|
|
607
|
-
//#endregion
|
|
608
892
|
//#region lib/types/limits.js
|
|
609
893
|
/**
|
|
610
894
|
* Skill content limits: the byte/char budgets every write path validates against.
|
|
@@ -2481,6 +2765,7 @@ function buildCuratorRunReport(input) {
|
|
|
2481
2765
|
...input.snapshotPath === void 0 ? {} : { snapshotPath: input.snapshotPath },
|
|
2482
2766
|
...input.llmReviewEnabled === void 0 ? {} : { llmReviewEnabled: input.llmReviewEnabled },
|
|
2483
2767
|
...input.nominationsWarnings === void 0 ? {} : { nominationsWarnings: [...input.nominationsWarnings] },
|
|
2768
|
+
...input.scanWarnings === void 0 ? {} : { scanWarnings: [...input.scanWarnings] },
|
|
2484
2769
|
...input.wouldPrune === void 0 ? {} : { wouldPrune: input.wouldPrune === null ? null : [...input.wouldPrune] }
|
|
2485
2770
|
};
|
|
2486
2771
|
}
|
|
@@ -2505,7 +2790,8 @@ function renderCuratorReportMarkdown(report) {
|
|
|
2505
2790
|
...report.routeStubs === void 0 ? [] : [`- **Stale route stubs**: ${report.routeStubs.stubs}`],
|
|
2506
2791
|
...report.llmReviewEnabled === void 0 ? [] : [`- **llmReview**: ${report.llmReviewEnabled}`],
|
|
2507
2792
|
...report.wouldPrune === void 0 ? [] : [report.wouldPrune === null ? "- **Would prune (archiveRetention=report)**: unknown (archive listing unreadable)" : `- **Would prune (archiveRetention=report)**: ${report.wouldPrune.length}`],
|
|
2508
|
-
...report.nominationsWarnings === void 0 || report.nominationsWarnings.length === 0 ? [] : [`- **Nomination warnings**: ${report.nominationsWarnings.join("; ")}`]
|
|
2793
|
+
...report.nominationsWarnings === void 0 || report.nominationsWarnings.length === 0 ? [] : [`- **Nomination warnings**: ${report.nominationsWarnings.join("; ")}`],
|
|
2794
|
+
...report.scanWarnings === void 0 || report.scanWarnings.length === 0 ? [] : report.scanWarnings.map((warning) => "- **Dedup scan**: " + warning)
|
|
2509
2795
|
];
|
|
2510
2796
|
const section = (title, items) => items.length === 0 ? [] : [
|
|
2511
2797
|
"",
|
|
@@ -3179,194 +3465,6 @@ function fuzzyPatch(content, oldString, newString, replaceAll = false) {
|
|
|
3179
3465
|
}
|
|
3180
3466
|
/** Deterministic section-extraction plan facts; the caller owns the IO and the append semantics. */
|
|
3181
3467
|
//#endregion
|
|
3182
|
-
//#region lib/types/http-routes.js
|
|
3183
|
-
/**
|
|
3184
|
-
* The loopback HTTP plumbing every family route surface shares (E-302-era conventions).
|
|
3185
|
-
*
|
|
3186
|
-
* WHY THIS IS IN CORE: the fence, the bounded body reader and the JSON writer are the same for every
|
|
3187
|
-
* family route surface, and `evolution-core` is the one package all of them already depend on. The
|
|
3188
|
-
* canonical platform implementation (packages/client/connection/src/api-request-trust.ts) is not part
|
|
3189
|
-
* of that package's published surface, so the family keeps its own copy of the RULES here — once.
|
|
3190
|
-
*
|
|
3191
|
-
* Layering: pure transport. Nothing here knows about skills, approvals, curators or route tables.
|
|
3192
|
-
* @module @lmzhen/dsh-evolution-core/http-routes
|
|
3193
|
-
*/
|
|
3194
|
-
/** Largest request body a family route accepts; a bigger one reads as "no body" (400 from the handler). */
|
|
3195
|
-
const MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
3196
|
-
/** One header as a single string (a repeated header reads as absent rather than as its first value). */
|
|
3197
|
-
function header(request, name) {
|
|
3198
|
-
const value = request.headers[name];
|
|
3199
|
-
return typeof value === "string" ? value : void 0;
|
|
3200
|
-
}
|
|
3201
|
-
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
3202
|
-
function isIPv4Loopback(value) {
|
|
3203
|
-
const parts = value.split(".");
|
|
3204
|
-
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
3205
|
-
}
|
|
3206
|
-
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
3207
|
-
function isLoopbackAddress(address) {
|
|
3208
|
-
if (address === void 0) return false;
|
|
3209
|
-
const normalized = address.toLowerCase();
|
|
3210
|
-
if (normalized === "::1") return true;
|
|
3211
|
-
if (normalized.startsWith("::ffff:")) return isIPv4Loopback(normalized.slice(7));
|
|
3212
|
-
return isIPv4Loopback(normalized);
|
|
3213
|
-
}
|
|
3214
|
-
/** Whether a hostname names the loopback authority (localhost, [::1], 127/8). */
|
|
3215
|
-
function isLoopbackHostname(hostname) {
|
|
3216
|
-
if (hostname === "localhost" || hostname === "[::1]") return true;
|
|
3217
|
-
return isIPv4Loopback(hostname);
|
|
3218
|
-
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Whether one request may enter a family route.
|
|
3221
|
-
*
|
|
3222
|
-
* Rules, in order: the SOCKET must be loopback (authoritative — X-Forwarded-For is never trusted), the
|
|
3223
|
-
* Host header must name a loopback authority (DNS-rebinding defense), an explicit cross-site marker is
|
|
3224
|
-
* refused, and an attached Origin must be exactly this authority.
|
|
3225
|
-
* @param request - the request to judge.
|
|
3226
|
-
* @returns true when the request may proceed.
|
|
3227
|
-
*/
|
|
3228
|
-
function isLoopbackRequest(request) {
|
|
3229
|
-
if (!isLoopbackAddress(request.socket?.remoteAddress)) return false;
|
|
3230
|
-
const host = header(request, "host");
|
|
3231
|
-
if (host === void 0) return false;
|
|
3232
|
-
let hostUrl;
|
|
3233
|
-
try {
|
|
3234
|
-
hostUrl = new URL("http://" + host);
|
|
3235
|
-
} catch {
|
|
3236
|
-
return false;
|
|
3237
|
-
}
|
|
3238
|
-
if (!isLoopbackHostname(hostUrl.hostname)) return false;
|
|
3239
|
-
if (header(request, "sec-fetch-site") === "cross-site") return false;
|
|
3240
|
-
const origin = header(request, "origin");
|
|
3241
|
-
if (origin === void 0) return true;
|
|
3242
|
-
try {
|
|
3243
|
-
return new URL(origin).host === hostUrl.host;
|
|
3244
|
-
} catch {
|
|
3245
|
-
return false;
|
|
3246
|
-
}
|
|
3247
|
-
}
|
|
3248
|
-
/** Write one JSON response. The routes own their response lifecycle, so every path ends here. */
|
|
3249
|
-
function writeJson(res, status, value) {
|
|
3250
|
-
const body = JSON.stringify(value);
|
|
3251
|
-
res.writeHead(status, {
|
|
3252
|
-
"content-type": "application/json; charset=utf-8",
|
|
3253
|
-
"content-length": Buffer.byteLength(body)
|
|
3254
|
-
});
|
|
3255
|
-
res.end(body);
|
|
3256
|
-
}
|
|
3257
|
-
/**
|
|
3258
|
-
* Read one JSON request body, bounded.
|
|
3259
|
-
* @param req - the request.
|
|
3260
|
-
* @returns the parsed value, or null when the body is oversized, empty, or not JSON.
|
|
3261
|
-
*/
|
|
3262
|
-
async function readJsonBody(req) {
|
|
3263
|
-
const chunks = [];
|
|
3264
|
-
let size = 0;
|
|
3265
|
-
for await (const chunk of req) {
|
|
3266
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3267
|
-
size += buffer.byteLength;
|
|
3268
|
-
if (size > 65536) return null;
|
|
3269
|
-
chunks.push(buffer);
|
|
3270
|
-
}
|
|
3271
|
-
if (size === 0) return null;
|
|
3272
|
-
try {
|
|
3273
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3274
|
-
} catch {
|
|
3275
|
-
return null;
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
/**
|
|
3279
|
-
* Read one JSON request body that must be an OBJECT.
|
|
3280
|
-
*
|
|
3281
|
-
* The three route surfaces all take named fields, so "array or scalar" is a bad request rather than a
|
|
3282
|
-
* shape each handler has to re-check.
|
|
3283
|
-
* @param req - the request.
|
|
3284
|
-
* @returns the body as a plain object, or null when it is absent, oversized, not JSON, or not an object.
|
|
3285
|
-
*/
|
|
3286
|
-
async function readJsonObject(req) {
|
|
3287
|
-
const body = await readJsonBody(req);
|
|
3288
|
-
return isRecord(body) ? body : null;
|
|
3289
|
-
}
|
|
3290
|
-
//#endregion
|
|
3291
|
-
//#region lib/types/env.js
|
|
3292
|
-
/**
|
|
3293
|
-
* Evolution environment-variable reading, single-source (WC, 0.3.56).
|
|
3294
|
-
*
|
|
3295
|
-
* Every `DSH_EVOLUTION_*` value the PLUGIN code reads goes through this module
|
|
3296
|
-
* so the trim/whitelist patterns (v10 P2-12/13) live in exactly one place and
|
|
3297
|
-
* the generated env reference can point at one behavior. Configuration-layer
|
|
3298
|
-
* reads inside patch YAML `!!js` expressions (session-query path/openAt) stay
|
|
3299
|
-
* in the profile config evaluation — they are NOT migrated (they are resolved
|
|
3300
|
-
* at cordis config time, not plugin time) but are documented in the README
|
|
3301
|
-
* env table. `EVOLUTION_SCOPE` is read by the source installers only
|
|
3302
|
-
* (`packages/scripts/install-layered.mjs`, `packages/test-support/row-contract.ts`),
|
|
3303
|
-
* never by plugin runtime code.
|
|
3304
|
-
*
|
|
3305
|
-
* v14 P3-1: the former `EVOLUTION_ENV_KEYS` export was deleted — nothing read
|
|
3306
|
-
* it, so the "generated env reference" it claimed to source was never
|
|
3307
|
-
* generated (the README table is maintained by hand and now lists every key).
|
|
3308
|
-
*/
|
|
3309
|
-
const ALLOW_ROW_COLLISIONS = "1";
|
|
3310
|
-
/** N-5 escape: `DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1` downgrades a delta-row
|
|
3311
|
-
* collision from fail-loud to warn+keep-both. Any other value (including an
|
|
3312
|
-
* EMPTY/whitespace string — a set-but-unset variable) keeps the fail-loud. */
|
|
3313
|
-
function allowRowCollisions(env = process.env) {
|
|
3314
|
-
return env.DSH_EVOLUTION_ALLOW_ROW_COLLISIONS === ALLOW_ROW_COLLISIONS;
|
|
3315
|
-
}
|
|
3316
|
-
//#endregion
|
|
3317
|
-
//#region lib/types/errors.js
|
|
3318
|
-
/**
|
|
3319
|
-
* THE error-code table: E-301…E-322 with the exact text each one emits (E-317…E-319 are the
|
|
3320
|
-
* write-admission gates' own refusals).
|
|
3321
|
-
*
|
|
3322
|
-
* The codes used to be spelled out at 33 call sites across two packages, so one
|
|
3323
|
-
* code's wording could drift between the branches that answer it and nothing could
|
|
3324
|
-
* see the drift. Keys name the SCENARIO rather than the code, because a code
|
|
3325
|
-
* legitimately carries more than one text (E-301 answers pending / approve / reject
|
|
3326
|
-
* with different guidance); the code still leads every string, so model- and
|
|
3327
|
-
* user-facing output is unchanged. Interpolations are written `%aN%` and filled by
|
|
3328
|
-
* `errorText` from the values the call site used to interpolate inline.
|
|
3329
|
-
* @module @lmzhen/dsh-evolution-core
|
|
3330
|
-
*/
|
|
3331
|
-
const EVOLUTION_ERRORS = Object.freeze({
|
|
3332
|
-
"e-305-this-invocation-carries-no": "E-305: 本次调用没有携带 agent —— `%a1%` 需要由会话支撑的调用(请在 GUI 或 CLI 的会话里运行)。",
|
|
3333
|
-
"e-306-this-deployment-stages-foreground": "E-306: 本部署会暂存前台写入,但 `/evolution %a1%` 无法通过技能运行器回放 —— 没有可暂存的内容。请在批准策略为 'never' 的会话里运行,或在 evolution-approval 行上设置 `stageForeground: false`,然后重试该命令。",
|
|
3334
|
-
"e-301-approval-service-not-mounted": "E-301: 批准服务未挂载:待批准写入无法列出或回放。挂载 evolution-approval 行(随 evolution-host/evolution-all 提供)后,运行 /evolution doctor 可查看已挂载的服务。",
|
|
3335
|
-
"e-301-approval-service-not-mounted-2": "E-301: 批准服务未挂载。下一步:evolution-approval 行随 evolution-host/evolution-all 一起提供 —— 运行 /evolution doctor 查看已挂载的服务。",
|
|
3336
|
-
"e-304-the-mounted-approval-service": "E-304: 已挂载的批准服务早于本能力发布 —— 请改为驳回该记录。",
|
|
3337
|
-
"e-302-curator-service-not-mounted": "E-302: 策展服务未挂载。下一步:挂载 evolution-curator 行(evolution-host/evolution-all),并运行 /evolution doctor。",
|
|
3338
|
-
"e-305-the-invocation-agent-exposes": "E-305: 调用 agent 既没有暴露 `followup` 也没有 `inject` —— %a1% 没有投递通道。",
|
|
3339
|
-
"e-306-this-deployment-stages-foreground-2": "E-306: 本部署会暂存前台写入,但本次调用没有 agent 会话 —— `/evolution restructure` 会暂存一条没有会话归属的记录。请在 GUI 或 CLI 的会话里运行,或在 `stageForeground: false` 行上设置,然后重试该命令。",
|
|
3340
|
-
"e-303-replay-service-not-mounted": "E-303: 回放服务未挂载。下一步:挂载 evolution-replay 行(evolution-host/evolution-all),并运行 /evolution doctor。",
|
|
3341
|
-
"e-307-the-settings-service-could": "E-307: 设置服务无法报告它的分区(%a1%)。/evolution params 需要用户层来区分覆盖值与部署值;运行 /evolution doctor 查看已挂载的服务。",
|
|
3342
|
-
"e-308-unknown-parameter-group-group": "E-308: 未知的参数分组「%a1%」—— 已知分组:%a2%。",
|
|
3343
|
-
"e-311-no-settings-service-is": "E-311: 没有挂载设置服务,因此没有可写入的用户层。请挂载 settings 行(packages/settings/settings-file)后重试;本命令从不修改 cordis.yml —— 部署值属于部署。/evolution doctor 会列出已挂载的服务。",
|
|
3344
|
-
"e-312-error-instanceof-Error-error": "E-312: %a1% —— /evolution params 会列出规范 id。",
|
|
3345
|
-
"e-313-unknown-parameter-id-evolution": "E-313: 未知参数「%a1%」—— /evolution params 会列出每个已注册 id 及其层级与用户层。",
|
|
3346
|
-
"e-314-id-is-a-deployment": "E-314: 「%a1%」是部署参数(层级 %a2%,归属 %a3%)—— 它写在 cordis.yml 的插件行或策略行上,而不是从会话写入。/evolution params 会显示每一行由谁可写。",
|
|
3347
|
-
"e-315-id-has-no-user": "E-315: 「%a1%」没有用户层 —— 归属 %a2% 没有发布设置命名空间,因此该值留在部署层。",
|
|
3348
|
-
"e-307-the-settings-service-could-2": "E-307: 设置服务无法报告它的分区(%a1%)。/evolution policy set 需要当前修订号才能安全写入;运行 /evolution doctor 查看已挂载的服务。",
|
|
3349
|
-
"e-316-namespace-namespace-is-not": "E-316: namespace \"%a1%\" 未注册 —— 归属 %a2% 在本组合中未挂载,因此无法从这里写入它的参数。 /evolution params 会把那些行标为 'unregistered'.",
|
|
3350
|
-
"e-309-revision-conflict-you-sent": "E-309: 修订冲突 —— 你提交的是 %a1%,文档当前为 %a2%。请用 /evolution params --json 重新读取后重试。",
|
|
3351
|
-
"e-309-revision-conflict-message-Re": "E-309: 修订冲突 —— %a1%。请用 /evolution params --json 重新读取后重试。",
|
|
3352
|
-
"e-310-the-settings-service-refused": "E-310: 设置服务拒绝了这次写入:%a1% —— 归属插件的规则优先(跨字段成对约束,或只能收紧的上限)。/evolution params 会显示当前值。",
|
|
3353
|
-
"e-317-skill-write-not-confirmed": "E-317: 技能 \"%a1%\" 未被%a2% —— 确认提示被拒绝或关闭,因此没有写入任何内容。每次创建与裸删除都会出现该提示;只有操作者要求时才重复调用。",
|
|
3354
|
-
"e-318-skill-write-without-a-read": "E-318: 本会话没有读取技能 \"%a1%\",因此拒绝这次写入。请先用 `skill` 工具读取它(失败的读取不算),然后重复该写入。",
|
|
3355
|
-
"e-320-the-profile-home-is-not": "E-320: 本宿主没有报告 profile home,因此无法定位旧设置文档。/evolution migrate 读取 `<profile home>/settings.yaml.imported`;请在由 profile 启动的会话里运行。",
|
|
3356
|
-
"e-322-a-parameter-would-break-a": "E-322: %a1% 至少应为 %a2%,但这次写入把它设成 %a3%。未写入任何内容。",
|
|
3357
|
-
"e-321-no-io-provider-is": "E-321: 没有挂载 evolution-io 提供方,因此无法读取旧设置文档。请挂载 evolution-io 行(evolution-host/evolution-all)后重试。",
|
|
3358
|
-
"e-319-skill-write-confirm-timed-out": "E-319: 技能 \"%a1%\" 未被%a2% —— 确认提示在 %a3% 秒内无人应答,因此该写入被取消。没有人在场决定:把 `skillWriteConfirm` 设为 `auto` 可免提示直接写入,保留 `ask` 则一直等到有人应答,或提高 `skillWriteConfirmTimeoutSeconds`。"
|
|
3359
|
-
});
|
|
3360
|
-
/**
|
|
3361
|
-
* Render one table entry, substituting its interpolation slots.
|
|
3362
|
-
* @param id - the scenario key.
|
|
3363
|
-
* @param args - values for the entry's `%aN%` slots.
|
|
3364
|
-
* @returns the message text the call site used to spell out inline.
|
|
3365
|
-
*/
|
|
3366
|
-
function errorText(id, args = {}) {
|
|
3367
|
-
return EVOLUTION_ERRORS[id].replace(/%a(\d+)%/g, (_match, n) => String(args["a" + n] ?? ""));
|
|
3368
|
-
}
|
|
3369
|
-
//#endregion
|
|
3370
3468
|
//#region lib/types/learn-prompt.js
|
|
3371
3469
|
/**
|
|
3372
3470
|
* Open-ended `/evolution learn` prompt builder.
|
|
@@ -3946,24 +4044,6 @@ function render(entries) {
|
|
|
3946
4044
|
function stripDatePrefix(entry) {
|
|
3947
4045
|
return entry.replace(/^## \d{4}-\d{2}-\d{2}\n/, "");
|
|
3948
4046
|
}
|
|
3949
|
-
/**
|
|
3950
|
-
* Neutralize the platform's `{{name}}` prompt-variable syntax in text that is
|
|
3951
|
-
* about to be registered as a `systemPrompt.context` contribution.
|
|
3952
|
-
*
|
|
3953
|
-
* The platform interpolates every context/section text once per model step and
|
|
3954
|
-
* THROWS on an unknown or malformed reference — so one stored memory entry
|
|
3955
|
-
* containing `{{...}}` (a CI expression, a Jinja/Helm/Vue template, a doc
|
|
3956
|
-
* placeholder) used to fail `assemble()` on EVERY later pre-step of every
|
|
3957
|
-
* session under this DSH_HOME, while a registered name such as `{{cwd}}` was
|
|
3958
|
-
* silently substituted into the "memory" the model reads. Only the INJECTED
|
|
3959
|
-
* text is neutralized: stored entries keep their original bytes.
|
|
3960
|
-
*
|
|
3961
|
-
* @param text - rendered context text about to leave for the prompt surface.
|
|
3962
|
-
* @returns the same text with every `{{` split so it cannot start a reference.
|
|
3963
|
-
*/
|
|
3964
|
-
function neutralizePromptVariables(text) {
|
|
3965
|
-
return text.includes("{{") ? text.replaceAll("{{", "{ {") : text;
|
|
3966
|
-
}
|
|
3967
4047
|
/** F-201: does `content` carry the on-disk entry delimiter or a trailing
|
|
3968
4048
|
* `\n§` fragment that would combine with the render terminator into a real
|
|
3969
4049
|
* delimiter boundary? Both split the fact into multiple entries on read-back
|
|
@@ -4530,7 +4610,7 @@ var MemoryStore = class {
|
|
|
4530
4610
|
parts.push(`## ${label} (${safe.length} entries)${usage}${note}\n${body}`);
|
|
4531
4611
|
} else if (entries.length > 0) parts.push(`## ${label} — ${entries.length} entries withheld by the security scan; none injected`);
|
|
4532
4612
|
}
|
|
4533
|
-
return
|
|
4613
|
+
return parts.join("\n\n");
|
|
4534
4614
|
}
|
|
4535
4615
|
/**
|
|
4536
4616
|
* Detect on-disk drift for a caller that holds no locked view: `true` when the
|
|
@@ -4613,563 +4693,6 @@ async function recordMutation(root, io, record, cap = 500) {
|
|
|
4613
4693
|
});
|
|
4614
4694
|
}
|
|
4615
4695
|
//#endregion
|
|
4616
|
-
//#region lib/types/preset-composition.js
|
|
4617
|
-
/**
|
|
4618
|
-
* Build the user-root Evolution preset composition from the RUNTIME platform's
|
|
4619
|
-
* `standard` preset rows plus the evolution delta rows (P1-1 follow-up,
|
|
4620
|
-
* 0.3.15): the agent-preset registry mounts ONE composition file verbatim, so
|
|
4621
|
-
* a delta-only `agent.cordis.yml` would produce an agent carrying only the
|
|
4622
|
-
* delta rows.
|
|
4623
|
-
*
|
|
4624
|
-
* Same contract as `install-layered.mjs` `generateAgentPreset` (the source
|
|
4625
|
-
* install path) — installer.spec pins byte parity between the two, and both
|
|
4626
|
-
* apply the identical row-collision contract: a delta id that overlaps a
|
|
4627
|
-
* standard row id fails loud by default, and `DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1`
|
|
4628
|
-
* downgrades it to a warning that keeps both (the row mounts twice).
|
|
4629
|
-
*
|
|
4630
|
-
* V10-14 / 0.3.53 (P1-2): BOTH composers now inject the Hermes 60-char catalog
|
|
4631
|
-
* cap onto the standard-sourced `- id: tool-skill` row of the composed preset
|
|
4632
|
-
* (see injectCatalogDescriptionCap) — the session-visible tool-skill instance
|
|
4633
|
-
* mounts in the preset's own standing scope, where no profile-root patch can
|
|
4634
|
-
* reach it. 0.3.53 moved the injection INTO the composer so
|
|
4635
|
-
* `/evolution preset install` (the npm user's only preset path) gets it too;
|
|
4636
|
-
* install-layered applies the byte-identical rule, pinned by installer.spec.
|
|
4637
|
-
*
|
|
4638
|
-
* Row ids are read from `- id:` lines; an id present in both fragments would
|
|
4639
|
-
* mount twice and could shadow the platform row, so it fails loud.
|
|
4640
|
-
* @param standardComposition - the runtime `standard` preset composition.
|
|
4641
|
-
* @param deltaComposition - the evolution delta fragment.
|
|
4642
|
-
* @returns the composed preset composition (standard rows first, then delta).
|
|
4643
|
-
*/
|
|
4644
|
-
function composePresetComposition(standardComposition, deltaComposition) {
|
|
4645
|
-
const standardIds = compositionRowIds(standardComposition);
|
|
4646
|
-
const collisions = [...compositionRowIds(deltaComposition)].filter((id) => standardIds.has(id)).sort();
|
|
4647
|
-
if (collisions.length > 0 && !allowRowCollisions()) throw new Error(`evolution preset composition: delta rows collide with runtime standard rows: ${collisions.join(", ")}`);
|
|
4648
|
-
if (collisions.length > 0) console.warn(`evolution preset composition: warning — delta rows collide with standard rows (${collisions.join(", ")}); keeping both (DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1)`);
|
|
4649
|
-
return applyRowOverrides(`${standardComposition.replace(/\s+$/, "")}\n\n${deltaComposition.trim()}\n`);
|
|
4650
|
-
}
|
|
4651
|
-
/**
|
|
4652
|
-
* One table, two generation paths: `row-overrides.json` at the PACKAGE ROOT
|
|
4653
|
-
* states what a generated preset must carry beyond the platform composition,
|
|
4654
|
-
* and the source installer (`scripts/install-layered.mjs`) reads the same file.
|
|
4655
|
-
* The path resolves from both `src/` and the built `lib/`, which is why the
|
|
4656
|
-
* file sits at the root and ships in `files`.
|
|
4657
|
-
*
|
|
4658
|
-
* Before 0.3.77 each side carried its own hand-kept copy of this table; they
|
|
4659
|
-
* happened to stay byte-identical, which is exactly the kind of agreement no
|
|
4660
|
-
* test can keep — the entries are now data, and a divergence is impossible.
|
|
4661
|
-
*/
|
|
4662
|
-
const ROW_OVERRIDES_URL = new URL("../row-overrides.json", import.meta.url);
|
|
4663
|
-
/** One cache per PATH: the installer reads the same file through its own root, and a single-slot
|
|
4664
|
-
* cache would hand the second caller the first caller's bytes. */
|
|
4665
|
-
const overridesCache = /* @__PURE__ */ new Map();
|
|
4666
|
-
/**
|
|
4667
|
-
* Read and validate the shared override table.
|
|
4668
|
-
* G6.1/M1: the path is a PARAMETER (default: the table beside this module). The source installer
|
|
4669
|
-
* runs the generated port from `scripts/`, so its own root has to be passed in — a module-relative
|
|
4670
|
-
* read would look for `scripts/../row-overrides.json` and fail.
|
|
4671
|
-
* @param path - the table to read.
|
|
4672
|
-
* @returns the entries, in file order. A malformed table fails LOUD: a composer
|
|
4673
|
-
* that silently skipped an entry would ship a preset running the platform
|
|
4674
|
-
* default while nothing downstream reported it.
|
|
4675
|
-
*/
|
|
4676
|
-
function loadRowOverrides(path = fileURLToPath(ROW_OVERRIDES_URL)) {
|
|
4677
|
-
const cached = overridesCache.get(path);
|
|
4678
|
-
if (cached !== void 0) return cached;
|
|
4679
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
4680
|
-
if (!Array.isArray(parsed)) throw new Error("evolution-core: row-overrides.json must be an array of override entries");
|
|
4681
|
-
const entries = [];
|
|
4682
|
-
for (const [index, entry] of parsed.entries()) {
|
|
4683
|
-
const candidate = entry;
|
|
4684
|
-
if (candidate === null || typeof candidate !== "object" || typeof candidate.row !== "string" || candidate.row === "" || typeof candidate.key !== "string" || candidate.key === "" || !Array.isArray(candidate.lines) || candidate.lines.some((line) => typeof line !== "string") || typeof candidate.missingReason !== "string") throw new Error(`evolution-core: row-overrides.json entry ${index} is malformed (row/key/lines/missingReason are required)`);
|
|
4685
|
-
entries.push({
|
|
4686
|
-
row: candidate.row,
|
|
4687
|
-
key: candidate.key,
|
|
4688
|
-
lines: candidate.lines,
|
|
4689
|
-
missingReason: candidate.missingReason
|
|
4690
|
-
});
|
|
4691
|
-
}
|
|
4692
|
-
overridesCache.set(path, entries);
|
|
4693
|
-
return entries;
|
|
4694
|
-
}
|
|
4695
|
-
/**
|
|
4696
|
-
* Ensure every {@link RowOverride} inside its target row.
|
|
4697
|
-
*
|
|
4698
|
-
* Key-level by construction: we INSERT the override's own lines and leave a row
|
|
4699
|
-
* that already carries the key byte-identical, so a re-run never doubles a key.
|
|
4700
|
-
* This is the deliberate opposite of the platform's patch layers, where an
|
|
4701
|
-
* override REPLACES `config` wholesale — the difference is why the composer can
|
|
4702
|
-
* add a key without erasing the platform's own config defaults.
|
|
4703
|
-
*/
|
|
4704
|
-
function applyRowOverrides(composition, overrides = loadRowOverrides()) {
|
|
4705
|
-
let lines = composition.split("\n");
|
|
4706
|
-
for (const override of overrides) lines = applyOneOverride(lines, override);
|
|
4707
|
-
return lines.join("\n");
|
|
4708
|
-
}
|
|
4709
|
-
function applyOneOverride(lines, override) {
|
|
4710
|
-
const rowRe = new RegExp("^- id:\\s*" + override.row.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*$");
|
|
4711
|
-
let found = false;
|
|
4712
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
4713
|
-
if (!rowRe.test(lines[i] ?? "")) continue;
|
|
4714
|
-
found = true;
|
|
4715
|
-
let end = i;
|
|
4716
|
-
let hasConfig = false;
|
|
4717
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
4718
|
-
const next = lines[j] ?? "";
|
|
4719
|
-
if (next.trim() === "") break;
|
|
4720
|
-
if (!/^\s/.test(next)) break;
|
|
4721
|
-
if (new RegExp("^ {2}" + override.key + ":(\\s|$)").test(next)) hasConfig = true;
|
|
4722
|
-
end = j;
|
|
4723
|
-
}
|
|
4724
|
-
if (hasConfig) continue;
|
|
4725
|
-
lines.splice(end + 1, 0, ...override.lines);
|
|
4726
|
-
i = end + override.lines.length;
|
|
4727
|
-
}
|
|
4728
|
-
if (!found) console.warn("evolution preset composition: warning — " + override.missingReason);
|
|
4729
|
-
return lines;
|
|
4730
|
-
}
|
|
4731
|
-
/**
|
|
4732
|
-
* Row ids of a composition fragment (line scan, no YAML library).
|
|
4733
|
-
*
|
|
4734
|
-
* PLAN S5.9 (2026-09-16, audit P2-27): the id extraction accepts INDENTED
|
|
4735
|
-
* `- id:` rows too, so a collision hidden in a nested group is still caught —
|
|
4736
|
-
* the old `^- id:` anchored at column 0 and was blind to exactly the rows an
|
|
4737
|
-
* upstream group nesting would produce. Twin of `rowIds` in
|
|
4738
|
-
* `scripts/install-layered.mjs` (installer.spec pins detection parity).
|
|
4739
|
-
* Boundary (current, deliberate): DETECTION covers nested rows, while the
|
|
4740
|
-
* override INJECTION anchors (`applyOneOverride` below) still match top-level
|
|
4741
|
-
* rows only — the injection indent contract (`^ {2}key:`) is defined against a
|
|
4742
|
-
* column-0 row.
|
|
4743
|
-
*/
|
|
4744
|
-
function compositionRowIds(composition) {
|
|
4745
|
-
const ids = /* @__PURE__ */ new Set();
|
|
4746
|
-
for (const line of composition.split("\n")) {
|
|
4747
|
-
const id = /^\s*- id:\s*(\S+)/.exec(line)?.[1];
|
|
4748
|
-
if (id) ids.add(id);
|
|
4749
|
-
}
|
|
4750
|
-
return ids;
|
|
4751
|
-
}
|
|
4752
|
-
/** The platform package a DECLARATIVE preset row names (0.2.x replaced preset directories with it). */
|
|
4753
|
-
const AGENT_PRESET_PACKAGE = "@deepseek-ai/dsh-agent-preset";
|
|
4754
|
-
/**
|
|
4755
|
-
* Compose the DECLARATIVE preset row from the platform base composition and the family delta.
|
|
4756
|
-
*
|
|
4757
|
-
* 0.2.x removed the directory mechanism: `dsh-agent-preset` is an ordinary composition row whose
|
|
4758
|
-
* `config.plugins` carries the preset's complete child plugin list
|
|
4759
|
-
* (`packages/preset/agent-preset/src/index.ts`, `agent-preset-registry/src/definition.ts` — the
|
|
4760
|
-
* definition has NO base/extends field, which is why the platform base is merged in here).
|
|
4761
|
-
*
|
|
4762
|
-
* The composition itself is {@link composePresetComposition}: same row-collision contract and the
|
|
4763
|
-
* same `row-overrides.json` injection, applied while the composed rows are still at column 0 — the
|
|
4764
|
-
* override anchors are defined against a column-0 `- id:` row, so the indentation that puts them
|
|
4765
|
-
* under `plugins:` must come last.
|
|
4766
|
-
* @param baseComposition - the platform base preset patch/composition rows.
|
|
4767
|
-
* @param deltaComposition - the family delta rows.
|
|
4768
|
-
* @param identity - row id, preset id, display fields and order.
|
|
4769
|
-
* @returns the row text, newline-terminated, ready to merge into a profile patch.
|
|
4770
|
-
*/
|
|
4771
|
-
/**
|
|
4772
|
-
* Drop the blank lines that merely separate block-sequence items, keeping the ones INSIDE a block
|
|
4773
|
-
* literal (`key: |` / `>`): there a blank line is part of the value, and filtering it flattened a
|
|
4774
|
-
* platform base preset's multi-paragraph prompt into one paragraph (G6 review, P1).
|
|
4775
|
-
* @param text - the composition rows.
|
|
4776
|
-
* @returns the rows with the separating blank lines removed.
|
|
4777
|
-
*/
|
|
4778
|
-
function denseBlockSequence(text) {
|
|
4779
|
-
const out = [];
|
|
4780
|
-
let scalarIndent = -1;
|
|
4781
|
-
for (const line of text.split("\n")) {
|
|
4782
|
-
const trimmed = line.trim();
|
|
4783
|
-
const indent = line.length - line.trimStart().length;
|
|
4784
|
-
if (scalarIndent >= 0 && trimmed !== "" && indent <= scalarIndent) scalarIndent = -1;
|
|
4785
|
-
if (scalarIndent < 0) {
|
|
4786
|
-
const match = /^(\s*)[\w".-]+:\s*[|>]/.exec(line);
|
|
4787
|
-
if (match !== null) scalarIndent = (match[1] ?? "").length;
|
|
4788
|
-
}
|
|
4789
|
-
if (trimmed === "" && scalarIndent < 0) continue;
|
|
4790
|
-
out.push(line);
|
|
4791
|
-
}
|
|
4792
|
-
return out.join("\n");
|
|
4793
|
-
}
|
|
4794
|
-
function composePresetRow(baseComposition, deltaComposition, identity) {
|
|
4795
|
-
const plugins = denseBlockSequence(composePresetComposition(baseComposition, deltaComposition)).split("\n").map((line) => line === "" ? "" : " " + line).join("\n");
|
|
4796
|
-
return [
|
|
4797
|
-
"- id: " + identity.rowId,
|
|
4798
|
-
" name: '@deepseek-ai/dsh-agent-preset'",
|
|
4799
|
-
" config:",
|
|
4800
|
-
" id: " + identity.id,
|
|
4801
|
-
" name: " + JSON.stringify(identity.name),
|
|
4802
|
-
" description: " + JSON.stringify(identity.description),
|
|
4803
|
-
" order: " + String(identity.order),
|
|
4804
|
-
" plugins:",
|
|
4805
|
-
plugins,
|
|
4806
|
-
""
|
|
4807
|
-
].join("\n");
|
|
4808
|
-
}
|
|
4809
|
-
/** The patch-entry key that ADDS a row: a plain entry overrides the row with the same id, so a
|
|
4810
|
-
* preset the platform does not ship has to arrive inside an `insert` entry
|
|
4811
|
-
* (the platform's own `user-patches.spec.ts` proves the override-vs-insert split). */
|
|
4812
|
-
const PATCH_INSERT_KEY = "insert";
|
|
4813
|
-
/**
|
|
4814
|
-
* Wrap a composed row as the patch entry a profile patch needs.
|
|
4815
|
-
*
|
|
4816
|
-
* The row {@link composePresetRow} returns starts at column 0; a patch entry's list nests it four
|
|
4817
|
-
* spaces deeper. Wrapping is a separate step because the same row text is also what
|
|
4818
|
-
* {@link mergePresetRow} matches by id, and because the composer's override anchors are defined
|
|
4819
|
-
* against column-0 rows.
|
|
4820
|
-
* @param row - the row text, as {@link composePresetRow} returns it.
|
|
4821
|
-
* @returns the patch entry text, newline-terminated.
|
|
4822
|
-
*/
|
|
4823
|
-
function composePresetInsert(row) {
|
|
4824
|
-
return `- ${PATCH_INSERT_KEY}:\n${row.replace(/\s+$/, "").split("\n").map((line) => " " + line).join("\n")}\n`;
|
|
4825
|
-
}
|
|
4826
|
-
/**
|
|
4827
|
-
* The row list a platform base preset patch carries under `config.plugins`, dedented to column 0.
|
|
4828
|
-
*
|
|
4829
|
-
* A base preset arrives as a whole patch entry — `- insert:` / `- id: preset-standard` /
|
|
4830
|
-
* `config:` / `plugins:` — because that is how the platform ships it
|
|
4831
|
-
* (`packages/bundle/web-app/presets/standard.patch.yml`). The preset's own list is the
|
|
4832
|
-
* SHALLOWEST `plugins:` line: a child row may carry a key by that name far deeper.
|
|
4833
|
-
* @param patchText - the base preset patch file's text.
|
|
4834
|
-
* @returns the plugin rows, dedented and newline-terminated.
|
|
4835
|
-
* @throws {Error} when the patch carries no `plugins:` list.
|
|
4836
|
-
*/
|
|
4837
|
-
function basePresetPlugins(patchText) {
|
|
4838
|
-
const lines = patchText.split("\n");
|
|
4839
|
-
let at = -1;
|
|
4840
|
-
let indent = Number.POSITIVE_INFINITY;
|
|
4841
|
-
for (const [index, line] of lines.entries()) {
|
|
4842
|
-
const width = /^(\s*)plugins:\s*$/.exec(line)?.[1]?.length;
|
|
4843
|
-
if (width === void 0) continue;
|
|
4844
|
-
if (width < indent) {
|
|
4845
|
-
indent = width;
|
|
4846
|
-
at = index;
|
|
4847
|
-
}
|
|
4848
|
-
}
|
|
4849
|
-
if (at < 0) throw new Error("evolution preset composition: the base preset patch carries no `plugins:` list");
|
|
4850
|
-
const block = [];
|
|
4851
|
-
for (const line of lines.slice(at + 1)) {
|
|
4852
|
-
if (line.trim() === "") {
|
|
4853
|
-
block.push(line);
|
|
4854
|
-
continue;
|
|
4855
|
-
}
|
|
4856
|
-
if (line.length - line.trimStart().length <= indent) break;
|
|
4857
|
-
block.push(line);
|
|
4858
|
-
}
|
|
4859
|
-
while (block.length > 0 && block[block.length - 1]?.trim() === "") block.pop();
|
|
4860
|
-
const widths = block.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
|
|
4861
|
-
const dedent = widths.length === 0 ? 0 : Math.min(...widths);
|
|
4862
|
-
return block.map((line) => line.slice(0, dedent).trim() === "" ? line.slice(dedent) : line).join("\n") + "\n";
|
|
4863
|
-
}
|
|
4864
|
-
/** The row id a family preset occupies: the platform ships \`preset-standard\` for id `standard`
|
|
4865
|
-
* (`packages/bundle/web-app/presets/standard.patch.yml`), so a family preset id mirrors it. */
|
|
4866
|
-
function presetRowId(id) {
|
|
4867
|
-
return `preset-${id}`;
|
|
4868
|
-
}
|
|
4869
|
-
/**
|
|
4870
|
-
* The patch entry a profile patch receives for one preset: base rows plus the family delta, wrapped
|
|
4871
|
-
* as \`- insert:\`.
|
|
4872
|
-
*
|
|
4873
|
-
* The two halves are also exposed on their own — {@link composePresetRow} for the row, so
|
|
4874
|
-
* {@link mergePresetRow} can match it by id, and {@link basePresetPlugins} to read the platform
|
|
4875
|
-
* base out of a shipped patch.
|
|
4876
|
-
* @param basePatchText - the platform base preset patch's text.
|
|
4877
|
-
* @param deltaComposition - the family delta rows.
|
|
4878
|
-
* @param identity - row id, preset id, display fields and order.
|
|
4879
|
-
* @returns the patch entry text, newline-terminated.
|
|
4880
|
-
*/
|
|
4881
|
-
function composePresetEntry(basePatchText, deltaComposition, identity) {
|
|
4882
|
-
return composePresetInsert(composePresetRow(basePresetPlugins(basePatchText), deltaComposition, identity));
|
|
4883
|
-
}
|
|
4884
|
-
/**
|
|
4885
|
-
* The text of one row's own patch item, or null when the patch carries no such row.
|
|
4886
|
-
*
|
|
4887
|
-
* The comparison unit for a freshness check: this is byte-for-byte what {@link composePresetEntry}
|
|
4888
|
-
* writes for the same row id, whether the row sits inside an \`- insert:\` entry (installer-written)
|
|
4889
|
-
* or at column 0 (saved by the Web editor).
|
|
4890
|
-
* @param patchText - the patch text.
|
|
4891
|
-
* @param rowId - the row id to read.
|
|
4892
|
-
* @returns the item's text, newline-terminated, or null.
|
|
4893
|
-
*/
|
|
4894
|
-
function presetRowBody(block) {
|
|
4895
|
-
const lines = block.split("\n");
|
|
4896
|
-
if (!/^- insert:\s*$/.test(lines[0] ?? "")) return block;
|
|
4897
|
-
return lines.slice(1).map((line) => line.startsWith(" ") ? line.slice(4) : line).join("\n");
|
|
4898
|
-
}
|
|
4899
|
-
/**
|
|
4900
|
-
* The row BODY of a patch block, whichever form it is in.
|
|
4901
|
-
*
|
|
4902
|
-
* {@link presetRowBlock} returns the enclosing item verbatim: an installer-written row sits inside
|
|
4903
|
-
* `- insert:` while a row the platform's Web editor saved IS that column-0 item. A comparison must
|
|
4904
|
-
* compare like with like — comparing the raw block against `composePresetEntry()` (which always
|
|
4905
|
-
* starts with `- insert:`) reported DIFFERS for every editor-saved row, forever (G6 review, P2), and
|
|
4906
|
-
* the remedy it printed (`re-run the installer`) was refused by the `exists; use --force` guard.
|
|
4907
|
-
* @param block - a block as {@link presetRowBlock} returns it.
|
|
4908
|
-
* @returns the row text, `- insert:` unwrapped and its four-space indent stripped.
|
|
4909
|
-
*/
|
|
4910
|
-
function presetRowBlock(patchText, rowId) {
|
|
4911
|
-
const lines = patchText.split("\n");
|
|
4912
|
-
const span = rowBlockSpan(lines, rowId);
|
|
4913
|
-
if (span === null) return null;
|
|
4914
|
-
const block = [...lines.slice(span.start, span.end)];
|
|
4915
|
-
while (block.length > 0 && (block[block.length - 1] ?? "").trim() === "") block.pop();
|
|
4916
|
-
return block.join("\n") + "\n";
|
|
4917
|
-
}
|
|
4918
|
-
/** The package every shipped base preset patch declares its row with
|
|
4919
|
-
* (`packages/bundle/web-app/presets/standard.patch.yml`). The platform's only bundle carrying
|
|
4920
|
-
* preset patches today; a bundle a caller mounts is probed beside it. */
|
|
4921
|
-
const PLATFORM_BUNDLE_PACKAGE = "@deepseek-ai/dsh-web-app";
|
|
4922
|
-
/**
|
|
4923
|
-
* What `DSH_AGENT_PRESET_ROOT` must name: a directory holding the platform base preset patches
|
|
4924
|
-
* under their shipped file names (`<base>.patch.yml`) — e.g. a copy of a same-version platform
|
|
4925
|
-
* tree's `packages/bundle/web-app/presets/`. The desktop application is the shape that needs it:
|
|
4926
|
-
* its platform packages live inside `resources/app.asar`, so there is no plain
|
|
4927
|
-
* `node_modules/@deepseek-ai/dsh-web-app/presets/` on disk for a source installer to read.
|
|
4928
|
-
*/
|
|
4929
|
-
const AGENT_PRESET_ROOT_HINT = "point DSH_AGENT_PRESET_ROOT at a directory holding <base>.patch.yml (copy it out of a same-version dsh tree: packages/bundle/web-app/presets/)";
|
|
4930
|
-
/**
|
|
4931
|
-
* Resolve the platform patch file one `--base` names.
|
|
4932
|
-
*
|
|
4933
|
-
* 0.2.x ships each base preset as a bundle patch layer
|
|
4934
|
-
* (`packages/bundle/web-app/presets/<base>.patch.yml`, declared in that package's
|
|
4935
|
-
* `dsh.bundle.patch`), so the FILE — not a preset directory — is the base a family row is composed
|
|
4936
|
-
* from. Candidates, in order: an explicit `DSH_AGENT_PRESET_ROOT`; a module-graph probe when the
|
|
4937
|
-
* caller supplies one (a desktop/Electron host resolves the bundle inside `app.asar`, where no file
|
|
4938
|
-
* walk can reach); the target profile's own `node_modules`; then, walking up from the caller,
|
|
4939
|
-
* every `presets/` directory of a checkout's bundle packages, then every
|
|
4940
|
-
* `@deepseek-ai/dsh-<name>/presets/` directory of an installed tree.
|
|
4941
|
-
*
|
|
4942
|
-
* A base with no patch file is REFUSED with every probed candidate listed: composing a family preset
|
|
4943
|
-
* from another base's rows would mount rows the user did not ask for, with nothing saying so.
|
|
4944
|
-
* @param base - the base name, already validated against `evolution-agent/bases.json`.
|
|
4945
|
-
* @param sources - the roots to probe.
|
|
4946
|
-
* @returns the patch file's absolute path.
|
|
4947
|
-
* @throws {Error} when no candidate exists.
|
|
4948
|
-
*/
|
|
4949
|
-
function resolveBasePresetPatch(base, sources) {
|
|
4950
|
-
const file = `${base}.patch.yml`;
|
|
4951
|
-
const explicit = sources.root?.trim();
|
|
4952
|
-
if (explicit !== void 0 && explicit !== "") {
|
|
4953
|
-
const path = join(explicit, file);
|
|
4954
|
-
if (!existsSync(path)) throw new Error(`evolution preset composition: DSH_AGENT_PRESET_ROOT is set but ${path} does not exist — ${AGENT_PRESET_ROOT_HINT}`);
|
|
4955
|
-
return path;
|
|
4956
|
-
}
|
|
4957
|
-
const probes = [];
|
|
4958
|
-
const probe = (path) => {
|
|
4959
|
-
probes.push(path);
|
|
4960
|
-
return existsSync(path) ? path : void 0;
|
|
4961
|
-
};
|
|
4962
|
-
if (sources.resolve !== void 0) {
|
|
4963
|
-
const probeSpecifier = `${PLATFORM_BUNDLE_PACKAGE}/presets/${file}`;
|
|
4964
|
-
try {
|
|
4965
|
-
const found = probe(sources.resolve(probeSpecifier));
|
|
4966
|
-
if (found !== void 0) return found;
|
|
4967
|
-
} catch {
|
|
4968
|
-
probes.push(`node module graph: ${probeSpecifier}`);
|
|
4969
|
-
}
|
|
4970
|
-
}
|
|
4971
|
-
if (sources.profileDir !== void 0 && sources.profileDir !== "") for (const name of new Set([PLATFORM_BUNDLE_PACKAGE, ...sources.bundles ?? []])) {
|
|
4972
|
-
const found = probe(join(sources.profileDir, "node_modules", ...name.split("/"), "presets", file));
|
|
4973
|
-
if (found !== void 0) return found;
|
|
4974
|
-
}
|
|
4975
|
-
for (let level = sources.fromDir; level !== dirname(level); level = dirname(level)) {
|
|
4976
|
-
const bundleRoot = join(level, "packages", "bundle");
|
|
4977
|
-
for (const name of listDirectories(bundleRoot)) {
|
|
4978
|
-
const found = probe(join(bundleRoot, name, "presets", file));
|
|
4979
|
-
if (found !== void 0) return found;
|
|
4980
|
-
}
|
|
4981
|
-
const scopeRoot = join(level, "node_modules", "@deepseek-ai");
|
|
4982
|
-
for (const name of listDirectories(scopeRoot)) {
|
|
4983
|
-
if (!name.startsWith("dsh-")) continue;
|
|
4984
|
-
const found = probe(join(scopeRoot, name, "presets", file));
|
|
4985
|
-
if (found !== void 0) return found;
|
|
4986
|
-
}
|
|
4987
|
-
}
|
|
4988
|
-
for (const root of installRoots(sources.fromDir)) for (const shape of DESKTOP_RESOURCE_SHAPES) {
|
|
4989
|
-
const found = probe(join(root, ...shape, "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", file));
|
|
4990
|
-
if (found !== void 0) return found;
|
|
4991
|
-
const scanned = probeDesktopScope(join(root, ...shape), file, probes);
|
|
4992
|
-
if (scanned !== void 0) return scanned;
|
|
4993
|
-
}
|
|
4994
|
-
throw new Error(`evolution preset composition: cannot find the runtime platform '${base}' preset patch — no candidate carries presets/${file}. A base with no runtime patch is refused rather than composed from another base: the generated preset must follow the platform it runs on. Probed (in order): ${probes.join(", ")}. On a desktop install the platform packages live inside resources/app.asar and no plain file exists — ${AGENT_PRESET_ROOT_HINT}`);
|
|
4995
|
-
}
|
|
4996
|
-
/** The Electron resource layouts a desktop installation exposes its unpacked packages under. */
|
|
4997
|
-
const DESKTOP_RESOURCE_SHAPES = [["resources", "app.asar.unpacked"], [
|
|
4998
|
-
"resources",
|
|
4999
|
-
"app",
|
|
5000
|
-
"resources",
|
|
5001
|
-
"app.asar.unpacked"
|
|
5002
|
-
]];
|
|
5003
|
-
/**
|
|
5004
|
-
* Installation directories a desktop/Electron tree may sit at, probed before the source walk.
|
|
5005
|
-
*
|
|
5006
|
-
* The default Windows desktop location plus every ancestor that carries an
|
|
5007
|
-
* `resources/app.asar.unpacked` directory (a script run from inside an installation).
|
|
5008
|
-
* @param fromDir - the caller's own directory.
|
|
5009
|
-
* @returns the candidate installation roots.
|
|
5010
|
-
*/
|
|
5011
|
-
function installRoots(fromDir) {
|
|
5012
|
-
const roots = [];
|
|
5013
|
-
const localAppData = process.env.LOCALAPPDATA;
|
|
5014
|
-
if (process.platform === "win32" && localAppData !== void 0 && localAppData !== "") roots.push(join(localAppData, "Programs", "DeepSeek Harness"));
|
|
5015
|
-
for (let level = fromDir; level !== dirname(level); level = dirname(level)) if (existsSync(join(level, "resources", "app.asar.unpacked"))) roots.push(level);
|
|
5016
|
-
return roots;
|
|
5017
|
-
}
|
|
5018
|
-
/**
|
|
5019
|
-
* Probe every `@deepseek-ai/dsh-*` package under one unpacked resource root.
|
|
5020
|
-
* @param resourceRoot - e.g. `<install>/resources/app.asar.unpacked`.
|
|
5021
|
-
* @param file - the patch file name to look for.
|
|
5022
|
-
* @param probes - the caller's probe log, appended to for the failure report.
|
|
5023
|
-
* @returns the patch path when one exists.
|
|
5024
|
-
*/
|
|
5025
|
-
function probeDesktopScope(resourceRoot, file, probes) {
|
|
5026
|
-
const scopeRoot = join(resourceRoot, "dsh", "node_modules", "@deepseek-ai");
|
|
5027
|
-
for (const name of listDirectories(scopeRoot)) {
|
|
5028
|
-
if (!name.startsWith("dsh-")) continue;
|
|
5029
|
-
const path = join(scopeRoot, name, "presets", file);
|
|
5030
|
-
probes.push(path);
|
|
5031
|
-
if (existsSync(path)) return path;
|
|
5032
|
-
}
|
|
5033
|
-
}
|
|
5034
|
-
/**
|
|
5035
|
-
* Subdirectories of one directory, or none when it does not exist: a missing level is an ordinary
|
|
5036
|
-
* miss on a candidate walk, not a failure.
|
|
5037
|
-
* @param dir - the directory to list.
|
|
5038
|
-
* @returns the directory names.
|
|
5039
|
-
*/
|
|
5040
|
-
function listDirectories(dir) {
|
|
5041
|
-
try {
|
|
5042
|
-
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
5043
|
-
} catch {
|
|
5044
|
-
return [];
|
|
5045
|
-
}
|
|
5046
|
-
}
|
|
5047
|
-
/**
|
|
5048
|
-
* The block of one row id: the enclosing column-0 list item and the row's own first line.
|
|
5049
|
-
*
|
|
5050
|
-
* An installer-written row sits inside `- insert:` (four spaces deeper); a row the Web editor
|
|
5051
|
-
* saved is that column-0 item itself. Both must be replaced whole, so the span reaches back to the
|
|
5052
|
-
* enclosing item and forward to the next one.
|
|
5053
|
-
* @param lines - the patch text's lines.
|
|
5054
|
-
* @param rowId - the row id to locate.
|
|
5055
|
-
* @returns the span, or null when the patch carries no such row.
|
|
5056
|
-
*/
|
|
5057
|
-
function rowBlockSpan(lines, rowId) {
|
|
5058
|
-
const rowRe = new RegExp("^\\s*- id:\\s*" + rowId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*$");
|
|
5059
|
-
const rowStart = lines.findIndex((line) => rowRe.test(line));
|
|
5060
|
-
if (rowStart < 0) return null;
|
|
5061
|
-
let start = rowStart;
|
|
5062
|
-
while (start > 0 && !/^- /.test(lines[start] ?? "")) start -= 1;
|
|
5063
|
-
let end = lines.length;
|
|
5064
|
-
for (let index = start + 1; index < lines.length; index += 1) if (/^- /.test(lines[index] ?? "")) {
|
|
5065
|
-
end = index;
|
|
5066
|
-
break;
|
|
5067
|
-
}
|
|
5068
|
-
return {
|
|
5069
|
-
start,
|
|
5070
|
-
end,
|
|
5071
|
-
rowStart
|
|
5072
|
-
};
|
|
5073
|
-
}
|
|
5074
|
-
/**
|
|
5075
|
-
* Remove one row from a patch, taking its `- insert:` item with it when that item holds nothing
|
|
5076
|
-
* else (an empty insert entry is left-over structure the platform would keep parsing).
|
|
5077
|
-
* @param patchText - the patch text to edit.
|
|
5078
|
-
* @param rowId - the row id to remove.
|
|
5079
|
-
* @returns the patch text, newline-terminated; byte-identical when the id is absent.
|
|
5080
|
-
*/
|
|
5081
|
-
function removePresetRow(patchText, rowId) {
|
|
5082
|
-
const lines = patchText.split("\n");
|
|
5083
|
-
const span = rowBlockSpan(lines, rowId);
|
|
5084
|
-
if (span === null) return patchText;
|
|
5085
|
-
const rowLine = lines[span.rowStart] ?? "";
|
|
5086
|
-
const rowIndent = rowLine.length - rowLine.trimStart().length;
|
|
5087
|
-
const nested = rowIndent > 0;
|
|
5088
|
-
const siblings = lines.slice(span.start + 1, span.end).filter((line) => {
|
|
5089
|
-
return line.length - line.trimStart().length === rowIndent && line.trimStart().startsWith("- ");
|
|
5090
|
-
}).length;
|
|
5091
|
-
const cut = nested && siblings > 1 ? {
|
|
5092
|
-
start: span.rowStart,
|
|
5093
|
-
end: rowSiblingEnd(lines, span.rowStart, span.end)
|
|
5094
|
-
} : span;
|
|
5095
|
-
const kept = [...lines.slice(0, cut.start), ...lines.slice(cut.end)];
|
|
5096
|
-
while (kept.length > 1 && (kept[kept.length - 1] ?? "") === "") kept.pop();
|
|
5097
|
-
const text = kept.join("\n");
|
|
5098
|
-
if (text === "") return "";
|
|
5099
|
-
return text.endsWith("\n") ? text : text + "\n";
|
|
5100
|
-
}
|
|
5101
|
-
/**
|
|
5102
|
-
* The end of one nested row's own block inside a patch entry.
|
|
5103
|
-
* @param lines - the patch text's lines.
|
|
5104
|
-
* @param rowStart - the row's `- id:` line.
|
|
5105
|
-
* @param itemEnd - the enclosing item's end.
|
|
5106
|
-
* @returns the index of the next sibling row, or the item's end.
|
|
5107
|
-
*/
|
|
5108
|
-
function rowSiblingEnd(lines, rowStart, itemEnd) {
|
|
5109
|
-
const indent = (lines[rowStart] ?? "").length - (lines[rowStart] ?? "").trimStart().length;
|
|
5110
|
-
for (let index = rowStart + 1; index < itemEnd; index += 1) {
|
|
5111
|
-
const line = lines[index] ?? "";
|
|
5112
|
-
if (line.trim() === "") continue;
|
|
5113
|
-
const width = line.length - line.trimStart().length;
|
|
5114
|
-
if (width === indent && line.trimStart().startsWith("- ")) return index;
|
|
5115
|
-
if (width < indent) return index;
|
|
5116
|
-
}
|
|
5117
|
-
return itemEnd;
|
|
5118
|
-
}
|
|
5119
|
-
/**
|
|
5120
|
-
* Merge one generated row into a composition (a profile patch) by row id.
|
|
5121
|
-
*
|
|
5122
|
-
* Idempotent by construction: an existing row with the same `- id:` is REPLACED whole — its block
|
|
5123
|
-
* is the enclosing column-0 list item, so an installer-written row inside `- insert:` and a
|
|
5124
|
-
* Web-editor-saved row at column 0 are both matched by {@link rowBlockSpan} — and anything else in
|
|
5125
|
-
* the patch stays byte-identical, because the patch usually carries rows this installer knows
|
|
5126
|
-
* nothing about. A missing row is appended.
|
|
5127
|
-
*
|
|
5128
|
-
* The platform seeds a fresh profile patch with the empty list `[]`
|
|
5129
|
-
* (`packages/boot/app-boot/src/profile.ts`, `PROFILE_PATCH_FILENAME`), which is a COMPLETE YAML
|
|
5130
|
-
* document: a block sequence appended after it is not parsable. That seed is therefore replaced, not
|
|
5131
|
-
* extended.
|
|
5132
|
-
* @param patchText - the current patch text (`''` for a fresh one).
|
|
5133
|
-
* @param row - the row text, as {@link composePresetRow} returns it.
|
|
5134
|
-
* @param rowId - the row id to replace or append.
|
|
5135
|
-
* @returns the merged patch text, newline-terminated.
|
|
5136
|
-
*/
|
|
5137
|
-
function mergePresetRow(patchText, row, rowId) {
|
|
5138
|
-
const lines = patchText.split("\n");
|
|
5139
|
-
const block = row.replace(/\s+$/, "").split("\n");
|
|
5140
|
-
const span = rowBlockSpan(lines, rowId);
|
|
5141
|
-
if (span === null) {
|
|
5142
|
-
const seedIndex = lines.findIndex((line) => EMPTY_PATCH_SEED_RE.test(line));
|
|
5143
|
-
const kept = trimTrailingBlankLines(seedIndex >= 0 ? [...lines.slice(0, seedIndex), ...lines.slice(seedIndex + 1)] : [...lines]);
|
|
5144
|
-
return (kept.length === 0 ? block : [...kept, ...block]).join("\n") + "\n";
|
|
5145
|
-
}
|
|
5146
|
-
return trimTrailingBlankLines([
|
|
5147
|
-
...lines.slice(0, span.start),
|
|
5148
|
-
...block,
|
|
5149
|
-
...lines.slice(span.end)
|
|
5150
|
-
]).join("\n") + "\n";
|
|
5151
|
-
}
|
|
5152
|
-
/** The empty patch list the platform seeds a profile patch with; see {@link mergePresetRow}. */
|
|
5153
|
-
const EMPTY_PATCH_SEED = "[]";
|
|
5154
|
-
/** The seed in any spelling the platform accepts (`[]`, `[ ]`, `[] # empty`). */
|
|
5155
|
-
const EMPTY_PATCH_SEED_RE = /^\s*\[\s*\](\s*#.*)?\s*$/;
|
|
5156
|
-
/** Drop trailing blank lines only: a kept line's own terminator (a CRLF file's `\r`) is never touched. */
|
|
5157
|
-
function trimTrailingBlankLines(lines) {
|
|
5158
|
-
const out = [...lines];
|
|
5159
|
-
while (out.length > 0 && (out[out.length - 1] ?? "").trim() === "") out.pop();
|
|
5160
|
-
return out;
|
|
5161
|
-
}
|
|
5162
|
-
/**
|
|
5163
|
-
* The text to write when a patch is left with no entries.
|
|
5164
|
-
*
|
|
5165
|
-
* A zero-byte patch file is not what the platform writes or parses; its own seed is the empty list.
|
|
5166
|
-
* @param patchText - the text {@link removePresetRow} returned.
|
|
5167
|
-
* @returns the platform's empty-list seed when nothing is left, the text otherwise.
|
|
5168
|
-
*/
|
|
5169
|
-
function presetPatchText(patchText) {
|
|
5170
|
-
return patchText.trim() === "" ? `[]\n` : patchText;
|
|
5171
|
-
}
|
|
5172
|
-
//#endregion
|
|
5173
4696
|
//#region lib/types/prompts.js
|
|
5174
4697
|
/**
|
|
5175
4698
|
* Review and curation prompts adapted from Hermes Agent
|
|
@@ -6482,7 +6005,7 @@ function renderLongRunTable() {
|
|
|
6482
6005
|
/**
|
|
6483
6006
|
* Every platform service the family reaches for with `ctx.get`, and what degrades without it.
|
|
6484
6007
|
*
|
|
6485
|
-
* O-2 (v46 audit): the family probes
|
|
6008
|
+
* O-2 (v46 audit): the family probes 25 platform services optionally — the platform has no
|
|
6486
6009
|
* formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
|
|
6487
6010
|
* surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
|
|
6488
6011
|
* them; this table is the single source it judges the rest from, and the arch guard cross-checks it
|
|
@@ -6492,7 +6015,7 @@ function renderLongRunTable() {
|
|
|
6492
6015
|
* `client` probes run in the browser half, where the host has no view of the seats.
|
|
6493
6016
|
* @module @lmzhen/dsh-evolution-core
|
|
6494
6017
|
*/
|
|
6495
|
-
/** The
|
|
6018
|
+
/** The 25 probes, in service-name order (the guard checks this list against the tree). */
|
|
6496
6019
|
const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
6497
6020
|
{
|
|
6498
6021
|
service: "agents",
|
|
@@ -6529,11 +6052,6 @@ const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
|
6529
6052
|
feature: "every durable read and write (the IO seam)",
|
|
6530
6053
|
side: "host"
|
|
6531
6054
|
},
|
|
6532
|
-
{
|
|
6533
|
-
service: "fs",
|
|
6534
|
-
feature: "the platform filesystem the evolution-io-fs backend maps the seam onto (locks + writeText(expected) version CAS)",
|
|
6535
|
-
side: "host"
|
|
6536
|
-
},
|
|
6537
6055
|
{
|
|
6538
6056
|
service: "hmr",
|
|
6539
6057
|
feature: "the preset install read-merge-write commit (the platform HMR lock serializes it)",
|
|
@@ -6614,11 +6132,6 @@ const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
|
6614
6132
|
feature: "the storage-domain state provider",
|
|
6615
6133
|
side: "host"
|
|
6616
6134
|
},
|
|
6617
|
-
{
|
|
6618
|
-
service: "subagents",
|
|
6619
|
-
feature: "spawning review and maintenance subagents",
|
|
6620
|
-
side: "host"
|
|
6621
|
-
},
|
|
6622
6135
|
{
|
|
6623
6136
|
service: "systemPrompt",
|
|
6624
6137
|
feature: "the protected-skills prompt variable",
|
|
@@ -6628,11 +6141,6 @@ const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
|
6628
6141
|
service: "tools",
|
|
6629
6142
|
feature: "tool registration and the guard seam",
|
|
6630
6143
|
side: "host"
|
|
6631
|
-
},
|
|
6632
|
-
{
|
|
6633
|
-
service: "webServer",
|
|
6634
|
-
feature: "the skill-history panel routes",
|
|
6635
|
-
side: "host"
|
|
6636
6144
|
}
|
|
6637
6145
|
]);
|
|
6638
6146
|
/**
|
|
@@ -7019,6 +6527,15 @@ const DISPATCH_EVENT_TYPES = [
|
|
|
7019
6527
|
PTC_DISPATCH_EVENT
|
|
7020
6528
|
];
|
|
7021
6529
|
/**
|
|
6530
|
+
* Model-visible session frames: the message types plus the ONE dispatch vocabulary above.
|
|
6531
|
+
* Consumers classify through this list instead of naming event literals (rule N11).
|
|
6532
|
+
*/
|
|
6533
|
+
const MODEL_SESSION_EVENT_TYPES = [
|
|
6534
|
+
"user/message",
|
|
6535
|
+
"assistant/message",
|
|
6536
|
+
...DISPATCH_EVENT_TYPES
|
|
6537
|
+
];
|
|
6538
|
+
/**
|
|
7022
6539
|
* \`TypeError\` thrown when a payload that claims to be a dispatch event
|
|
7023
6540
|
* violates the platform's event declaration. Named so a deployment can tell this
|
|
7024
6541
|
* refusal apart from a generic failure in a log line.
|
|
@@ -9352,6 +8869,16 @@ function staleRouteStubs(indexes) {
|
|
|
9352
8869
|
* re-probes after its copies so a mid-copy writer demotes to `skipped`
|
|
9353
8870
|
* (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
|
|
9354
8871
|
*/
|
|
8872
|
+
/**
|
|
8873
|
+
* The CLASS boundary default authority (G1/1.1, Stage 2).
|
|
8874
|
+
*
|
|
8875
|
+
* The FACTORY (newSkillLibrary) requires an explicit authority and the v35 A6 register pins every
|
|
8876
|
+
* production construction, so a production caller cannot omit one. The class is the INTERNAL
|
|
8877
|
+
* boundary: arch rule N7 forbids constructing it directly in production, while ~200 test fixtures do
|
|
8878
|
+
* exactly that and ARE the operator, so an omitted authority reads as foreground here - named and
|
|
8879
|
+
* stated instead of silent.
|
|
8880
|
+
*/
|
|
8881
|
+
const CLASS_DEFAULT_AUTHORITY = authorityFrom({ origin: "foreground" });
|
|
9355
8882
|
/** 0.3.16 (S1.13, T-6): the pointer-line prefix written into a body when a
|
|
9356
8883
|
* section is moved to references/ — single literal, both restructure and
|
|
9357
8884
|
* append-mode consolidation emit the same discoverability line. */
|
|
@@ -9721,6 +9248,28 @@ function planRestructureSections(body, moves) {
|
|
|
9721
9248
|
}))
|
|
9722
9249
|
};
|
|
9723
9250
|
}
|
|
9251
|
+
/**
|
|
9252
|
+
* WHY MODULE SCOPE (S4.4): the serial queue must be shared ACROSS SkillLibrary instances. Six services
|
|
9253
|
+
* each build their own store over the SAME skill root (tool face / curator face / review face), and a
|
|
9254
|
+
* per-instance queue - which is what `this.serial = makeSerialQueue()` was - let two of them run
|
|
9255
|
+
* read->plan->commit against one tree at the same time. The mutex cannot live on an instance because
|
|
9256
|
+
* instances share nothing (S2-02, G1/1.4); it cannot be a file lock either, because the point is to
|
|
9257
|
+
* order IN-PROCESS phases before the io transact layer sees them. Keyed by root: two roots are
|
|
9258
|
+
* independent trees and must not wait on each other.
|
|
9259
|
+
*/
|
|
9260
|
+
const ROOT_SERIAL = /* @__PURE__ */ new Map();
|
|
9261
|
+
/** The ONE queue for a skill root, created on first use (module scope, see above).
|
|
9262
|
+
* @param root - the resolved skill root; the key, so two spellings of one directory share a queue.
|
|
9263
|
+
* @returns the queue every instance over this root serializes through.
|
|
9264
|
+
*/
|
|
9265
|
+
function serialForRoot(root) {
|
|
9266
|
+
const key = resolve(root);
|
|
9267
|
+
const existing = ROOT_SERIAL.get(key);
|
|
9268
|
+
if (existing !== void 0) return existing;
|
|
9269
|
+
const created = makeSerialQueue();
|
|
9270
|
+
ROOT_SERIAL.set(key, created);
|
|
9271
|
+
return created;
|
|
9272
|
+
}
|
|
9724
9273
|
var SkillLibrary = class {
|
|
9725
9274
|
root;
|
|
9726
9275
|
limits;
|
|
@@ -9739,20 +9288,24 @@ var SkillLibrary = class {
|
|
|
9739
9288
|
/** V10-03 (P2-18): see the constructor's threatExemptLabels. Empty by
|
|
9740
9289
|
* default — the strict ANY-hit-blocks policy is unchanged. */
|
|
9741
9290
|
threatExemptLabels;
|
|
9291
|
+
/** G1/1.1: default authority; a call may override (origin is per-invocation). */
|
|
9292
|
+
/** G1/1.1/Stage 2: the ONE default every mutator reads. Per-call overrides still win. */
|
|
9293
|
+
authority;
|
|
9742
9294
|
/** skill-history.ts: the content history is best-effort, so a failure must not be SILENT —
|
|
9743
9295
|
* but it is a deployment condition, not a per-write event, so it warns once per instance. */
|
|
9744
9296
|
historyWarned = false;
|
|
9745
|
-
constructor(root = skillsRoot(), io = nodeEvolutionIo(), limits = DEFAULT_SKILL_LIMITS, onMutation, transact, threatExemptLabels) {
|
|
9297
|
+
constructor(root = skillsRoot(), io = nodeEvolutionIo(), limits = DEFAULT_SKILL_LIMITS, onMutation, transact, threatExemptLabels, authority = CLASS_DEFAULT_AUTHORITY) {
|
|
9746
9298
|
this.root = root;
|
|
9747
9299
|
this.io = io;
|
|
9748
9300
|
this.limits = limits;
|
|
9749
9301
|
this.onMutation = onMutation;
|
|
9750
9302
|
this.threatExemptLabels = threatExemptLabels ?? [];
|
|
9303
|
+
this.authority = authority;
|
|
9751
9304
|
this.transact = transact ?? (io.transact ? (ioLike, path, task) => {
|
|
9752
9305
|
const t = ioLike.transact;
|
|
9753
9306
|
return t ? t(path, task) : transactIo(ioLike, path, task);
|
|
9754
9307
|
} : void 0);
|
|
9755
|
-
this.serial =
|
|
9308
|
+
this.serial = serialForRoot(this.root);
|
|
9756
9309
|
}
|
|
9757
9310
|
/**
|
|
9758
9311
|
* Run one single-file read-modify-write for a mutator. When `transact` was
|
|
@@ -9867,6 +9420,32 @@ var SkillLibrary = class {
|
|
|
9867
9420
|
* a swallowed error would silently downgrade the anchor to
|
|
9868
9421
|
* last-writer-wins.
|
|
9869
9422
|
*/
|
|
9423
|
+
/**
|
|
9424
|
+
* S4-01 (G0/0.7): the ONE three-state read the staging/replay anchors use.
|
|
9425
|
+
*
|
|
9426
|
+
* The two absorbing reads below absorb ENOENT/EISDIR and rethrow everything else, and the callers then
|
|
9427
|
+
* each wrote their own try/catch to turn a rethrow into a decision - three near-copies of one posture,
|
|
9428
|
+
* and the newest of them silently dropped the anchor instead of refusing. A read failure is its own
|
|
9429
|
+
* state, so it gets its own name here: `unknown` with the reason, next to `present` and `absent`.
|
|
9430
|
+
* Failing closed is then the CALLER decision (they refuse on unknown), not something each call site has
|
|
9431
|
+
* to re-derive from a thrown error.
|
|
9432
|
+
*/
|
|
9433
|
+
async readSkillProbe(rawName) {
|
|
9434
|
+
return await this.probeOf(() => this.read(rawName));
|
|
9435
|
+
}
|
|
9436
|
+
/** The support-file face of {@link readSkillProbe}. */
|
|
9437
|
+
async readSupportFileProbe(rawName, filePath) {
|
|
9438
|
+
return await this.probeOf(() => this.readSupportFile(rawName, filePath));
|
|
9439
|
+
}
|
|
9440
|
+
/** One place that turns the absorbing reads into three states. `null` is absent; a throw is unknown. */
|
|
9441
|
+
async probeOf(read) {
|
|
9442
|
+
try {
|
|
9443
|
+
const value = await read();
|
|
9444
|
+
return value === null ? probeAbsent() : probePresent(value);
|
|
9445
|
+
} catch (error) {
|
|
9446
|
+
return probeUnknown(probeReason(error));
|
|
9447
|
+
}
|
|
9448
|
+
}
|
|
9870
9449
|
async readSupportFile(name, filePath) {
|
|
9871
9450
|
const bad = this.badName(name, { allowReserved: true });
|
|
9872
9451
|
if (bad) throw new Error(bad);
|
|
@@ -9899,7 +9478,9 @@ var SkillLibrary = class {
|
|
|
9899
9478
|
}
|
|
9900
9479
|
async list(options = {}) {
|
|
9901
9480
|
const summaries = [];
|
|
9902
|
-
for (const
|
|
9481
|
+
for (const entry of await this.io.list(this.root)) {
|
|
9482
|
+
if (entry.startsWith(".")) continue;
|
|
9483
|
+
const name = entry;
|
|
9903
9484
|
const dir = this.dirOf(name);
|
|
9904
9485
|
let md;
|
|
9905
9486
|
try {
|
|
@@ -9996,7 +9577,7 @@ var SkillLibrary = class {
|
|
|
9996
9577
|
* (P1-1/v14 — the name guard lives HERE as well as at every entry point, so
|
|
9997
9578
|
* no caller can build a path from an unvalidated name).
|
|
9998
9579
|
*/
|
|
9999
|
-
async writeProtection(rawName, origin =
|
|
9580
|
+
async writeProtection(rawName, origin = this.authority.origin) {
|
|
10000
9581
|
const name = rawName.trim();
|
|
10001
9582
|
const badName = this.badName(name);
|
|
10002
9583
|
if (badName) return badName;
|
|
@@ -10274,7 +9855,7 @@ var SkillLibrary = class {
|
|
|
10274
9855
|
* protective mutation, so the autonomous pipeline may never call it. The
|
|
10275
9856
|
* marker write is the only state change; content is untouched.
|
|
10276
9857
|
*/
|
|
10277
|
-
async setPinned(name, pinned, origin =
|
|
9858
|
+
async setPinned(name, pinned, origin = this.authority.origin) {
|
|
10278
9859
|
return await this.serial(() => this.setPinnedCore(name, pinned, origin));
|
|
10279
9860
|
}
|
|
10280
9861
|
async setPinnedCore(name, pinned, origin) {
|
|
@@ -10329,7 +9910,7 @@ var SkillLibrary = class {
|
|
|
10329
9910
|
path: dir
|
|
10330
9911
|
};
|
|
10331
9912
|
}
|
|
10332
|
-
async create(name, content, origin =
|
|
9913
|
+
async create(name, content, origin = this.authority.origin) {
|
|
10333
9914
|
const normalized = name.trim();
|
|
10334
9915
|
return await this.serial(() => this.createCore(normalized, content, origin));
|
|
10335
9916
|
}
|
|
@@ -10456,7 +10037,7 @@ var SkillLibrary = class {
|
|
|
10456
10037
|
...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
|
|
10457
10038
|
};
|
|
10458
10039
|
}
|
|
10459
|
-
async update(rawName, content, origin =
|
|
10040
|
+
async update(rawName, content, origin = this.authority.origin, anchor) {
|
|
10460
10041
|
const name = rawName.trim();
|
|
10461
10042
|
return await this.serial(() => this.updateCore(name, content, origin, anchor));
|
|
10462
10043
|
}
|
|
@@ -10545,7 +10126,7 @@ var SkillLibrary = class {
|
|
|
10545
10126
|
};
|
|
10546
10127
|
}, anchor !== void 0 ? () => anchorUnverifiable(name, null) : (error) => unreadableTarget(name, error));
|
|
10547
10128
|
}
|
|
10548
|
-
async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin =
|
|
10129
|
+
async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = this.authority.origin) {
|
|
10549
10130
|
const name = rawName.trim();
|
|
10550
10131
|
return await this.serial(() => this.patchCore(name, oldString, newString, filePath, replaceAll, origin));
|
|
10551
10132
|
}
|
|
@@ -10767,7 +10348,7 @@ var SkillLibrary = class {
|
|
|
10767
10348
|
if (typeof name !== "string") return false;
|
|
10768
10349
|
return name !== "" && name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") && basename(name) === name;
|
|
10769
10350
|
}
|
|
10770
|
-
async archive(rawName, options = {}) {
|
|
10351
|
+
async archive(rawName, options = {}, origin = this.authority.origin) {
|
|
10771
10352
|
const name = rawName.trim();
|
|
10772
10353
|
const badName = this.badName(name, { allowReserved: true });
|
|
10773
10354
|
if (badName) return {
|
|
@@ -10834,7 +10415,7 @@ var SkillLibrary = class {
|
|
|
10834
10415
|
try {
|
|
10835
10416
|
await this.io.writeText(join(dest, ".archive-name"), `${name}\n`);
|
|
10836
10417
|
} catch {}
|
|
10837
|
-
await this.audit(name, "archive", md, null, reason);
|
|
10418
|
+
await this.audit(name, "archive", md, null, reason, void 0, void 0, void 0, origin);
|
|
10838
10419
|
this.notifyMutation({
|
|
10839
10420
|
action: "archive",
|
|
10840
10421
|
name,
|
|
@@ -10934,7 +10515,7 @@ var SkillLibrary = class {
|
|
|
10934
10515
|
* still refuses, and the refusal names it. The archived source keeps whatever
|
|
10935
10516
|
* the body did not need.
|
|
10936
10517
|
*/
|
|
10937
|
-
async consolidate(target, sources, origin =
|
|
10518
|
+
async consolidate(target, sources, origin = this.authority.origin, options = {}) {
|
|
10938
10519
|
const targetName = target.trim();
|
|
10939
10520
|
const normalizedSources = [...new Set(sources.map((name) => name.trim()))].filter((name) => name !== targetName);
|
|
10940
10521
|
const mode = options.mode ?? "append";
|
|
@@ -11128,7 +10709,7 @@ var SkillLibrary = class {
|
|
|
11128
10709
|
* support-directory links is refused (those links' files stay behind in the
|
|
11129
10710
|
* same package; the moved text belongs in references/ beside them).
|
|
11130
10711
|
*/
|
|
11131
|
-
async restructure(rawName, moves, origin =
|
|
10712
|
+
async restructure(rawName, moves, origin = this.authority.origin) {
|
|
11132
10713
|
const name = rawName.trim();
|
|
11133
10714
|
return await this.serial(() => this.restructureCore(name, moves, origin));
|
|
11134
10715
|
}
|
|
@@ -11345,7 +10926,7 @@ var SkillLibrary = class {
|
|
|
11345
10926
|
};
|
|
11346
10927
|
return {
|
|
11347
10928
|
ok: false,
|
|
11348
|
-
message: `目录树变更失败:${reason}。回滚无法恢复 ${stuck.join(", ")}
|
|
10929
|
+
message: `目录树变更失败:${reason}。回滚无法恢复 ${stuck.join(", ")}:可恢复性由当前快照决定:${await this.snapshotRecoveryAvailability()} —— 若上面不是「最新快照」,请重新应用这次变更。`
|
|
11349
10930
|
};
|
|
11350
10931
|
}
|
|
11351
10932
|
const unitId = plan.unitId ?? "u-" + Date.now().toString(36) + "-" + (nextUnitSeq += 1).toString(36);
|
|
@@ -11384,7 +10965,7 @@ var SkillLibrary = class {
|
|
|
11384
10965
|
* recoverability: archival never deletes, and this is the control-plane
|
|
11385
10966
|
* path back. The `.archive-reason` marker is dropped on restore.
|
|
11386
10967
|
*/
|
|
11387
|
-
async restoreFromArchive(rawName) {
|
|
10968
|
+
async restoreFromArchive(rawName, origin = this.authority.origin) {
|
|
11388
10969
|
const name = rawName.trim();
|
|
11389
10970
|
const bad = this.badName(name, { allowReserved: true });
|
|
11390
10971
|
if (bad) return {
|
|
@@ -11450,7 +11031,7 @@ var SkillLibrary = class {
|
|
|
11450
11031
|
};
|
|
11451
11032
|
return {
|
|
11452
11033
|
ok: false,
|
|
11453
|
-
message: `.archive 里没有「${name}」对应的 frontmatter 名,但有 ${unreadable.length} 个名字相似的归档项:${unreadable.join(", ")}。这些归档项没有可读的 SKILL.md
|
|
11034
|
+
message: `.archive 里没有「${name}」对应的 frontmatter 名,但有 ${unreadable.length} 个名字相似的归档项:${unreadable.join(", ")}。这些归档项没有可读的 SKILL.md(或名字对不上),按名字恢复无法区分它们:请把目录改成技能名后再次恢复;快照这条路的当前状态是:${await this.snapshotRecoveryAvailability()}。`
|
|
11454
11035
|
};
|
|
11455
11036
|
}
|
|
11456
11037
|
const source = join(archiveRoot, chosen);
|
|
@@ -11472,7 +11053,7 @@ var SkillLibrary = class {
|
|
|
11472
11053
|
for (const marker of [".archive-reason", ".archive-name"]) if (await this.io.exists(join(dest, marker))) await this.io.remove(join(dest, marker));
|
|
11473
11054
|
const restoredProbe = await probeText(this.io, join(dest, "SKILL.md"));
|
|
11474
11055
|
if (isUnknown(restoredProbe)) console.warn(`evolution-skill-store: the restored SKILL.md of "${name}" could not be read back (${restoredProbe.reason}); the restore is recorded without after-bytes`);
|
|
11475
|
-
await this.audit(name, "restore", null, isPresent(restoredProbe) ? restoredProbe.value : null, `restored from ${source}
|
|
11056
|
+
await this.audit(name, "restore", null, isPresent(restoredProbe) ? restoredProbe.value : null, `restored from ${source}`, void 0, void 0, void 0, origin);
|
|
11476
11057
|
this.notifyMutation({
|
|
11477
11058
|
action: "restore",
|
|
11478
11059
|
name,
|
|
@@ -11484,7 +11065,7 @@ var SkillLibrary = class {
|
|
|
11484
11065
|
path: dest
|
|
11485
11066
|
};
|
|
11486
11067
|
}
|
|
11487
|
-
async writeSupportFile(rawName, filePath, content, origin =
|
|
11068
|
+
async writeSupportFile(rawName, filePath, content, origin = this.authority.origin, anchor) {
|
|
11488
11069
|
const name = rawName.trim();
|
|
11489
11070
|
return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin, anchor));
|
|
11490
11071
|
}
|
|
@@ -11568,7 +11149,7 @@ var SkillLibrary = class {
|
|
|
11568
11149
|
};
|
|
11569
11150
|
}, anchor !== void 0 ? () => anchorUnverifiable(name, filePath) : (error) => unreadableTarget(name + "/" + filePath, error));
|
|
11570
11151
|
}
|
|
11571
|
-
async removeSupportFile(rawName, filePath, origin =
|
|
11152
|
+
async removeSupportFile(rawName, filePath, origin = this.authority.origin, anchor, unitId) {
|
|
11572
11153
|
const name = rawName.trim();
|
|
11573
11154
|
return await this.serial(() => this.removeSupportFileCore(name, filePath, origin, anchor, unitId));
|
|
11574
11155
|
}
|
|
@@ -11827,6 +11408,22 @@ var SkillLibrary = class {
|
|
|
11827
11408
|
}
|
|
11828
11409
|
}
|
|
11829
11410
|
/** Keep only the newest N snapshots (Hermes keep=5 parity); older ones are removed outright. */
|
|
11411
|
+
/**
|
|
11412
|
+
* G3/3.4: can a snapshot actually be used right now?
|
|
11413
|
+
*
|
|
11414
|
+
* The recovery messages used to promise "restore it from a snapshot" without asking whether a usable
|
|
11415
|
+
* snapshot exists (audit S1-01: the message promised recoverability from a chain that is five deep and
|
|
11416
|
+
* whose newest member held 9 of 35 skills). Three states, like every other reading here: a fragment
|
|
11417
|
+
* naming the usable snapshot, a fragment saying there is none, or a fragment saying the directory could
|
|
11418
|
+
* not be read - the caller embeds whichever it gets, so the sentence is generated by the capability.
|
|
11419
|
+
*/
|
|
11420
|
+
async snapshotRecoveryAvailability() {
|
|
11421
|
+
const listed = await this.listSnapshots();
|
|
11422
|
+
if (isUnknown(listed)) return "快照目录读不出来(" + listed.reason + ")";
|
|
11423
|
+
const latest = isPresent(listed) ? listed.value[0] : void 0;
|
|
11424
|
+
if (!latest) return "当前没有可用快照";
|
|
11425
|
+
return "最新快照 " + latest.path;
|
|
11426
|
+
}
|
|
11830
11427
|
async retainSnapshots(keep) {
|
|
11831
11428
|
const listed = await this.listSnapshots();
|
|
11832
11429
|
if (isUnknown(listed)) {
|
|
@@ -11990,12 +11587,15 @@ var SkillLibrary = class {
|
|
|
11990
11587
|
}
|
|
11991
11588
|
}
|
|
11992
11589
|
const restoresSuppressed = manifest.sidecars.includes(".curator-suppressed.json");
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
if (entry === ".
|
|
11996
|
-
if (entry === ".
|
|
11997
|
-
|
|
11998
|
-
|
|
11590
|
+
const declared = new Set([...manifest.skills, ...manifest.sidecars]);
|
|
11591
|
+
const doomed = rootEntries.filter((entry) => {
|
|
11592
|
+
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") return false;
|
|
11593
|
+
if (entry === ".history") return false;
|
|
11594
|
+
if (entry === ".curator-suppressed.json" && restoresSuppressed) return false;
|
|
11595
|
+
return !declared.has(entry);
|
|
11596
|
+
});
|
|
11597
|
+
if (doomed.length > 0) console.warn("skill-store: snapshot restore will delete " + String(doomed.length) + " live entr(y|ies) the snapshot does not declare: " + doomed.join(", "));
|
|
11598
|
+
for (const entry of doomed) await this.io.remove(join(this.root, entry));
|
|
11999
11599
|
for (const name of manifest.skills) await this.io.copy(join(snapshotPath, name), join(this.root, name));
|
|
12000
11600
|
for (const sidecar of manifest.sidecars) await this.io.copy(join(snapshotPath, sidecar), join(this.root, sidecar));
|
|
12001
11601
|
{
|
|
@@ -12009,10 +11609,300 @@ var SkillLibrary = class {
|
|
|
12009
11609
|
}
|
|
12010
11610
|
};
|
|
12011
11611
|
function newSkillLibrary(options) {
|
|
12012
|
-
const { config, io, limits, ctx, transact, threatExemptLabels } = options;
|
|
11612
|
+
const { config, io, limits, ctx, transact, threatExemptLabels, authority } = options;
|
|
12013
11613
|
return new SkillLibrary(resolveSkillsRoot(config ?? {}), io, limits, ctx ? (event) => {
|
|
12014
11614
|
ctx.emit("evolution/skill-mutated", event);
|
|
12015
|
-
} : void 0, transact, [...threatExemptLabels ?? []]);
|
|
11615
|
+
} : void 0, transact, [...threatExemptLabels ?? []], authority);
|
|
11616
|
+
}
|
|
11617
|
+
//#endregion
|
|
11618
|
+
//#region lib/types/store-ledger.js
|
|
11619
|
+
/**
|
|
11620
|
+
* G3/3.2 (audit 06-performance.md 接缝 C): ONE declaration for every bounded store.
|
|
11621
|
+
*
|
|
11622
|
+
* Before this file the family had eight retention rules scattered across four packages and no place
|
|
11623
|
+
* that could answer "how long does this record live under the current policy" (S1.md 4.3): six caps
|
|
11624
|
+
* were named constants, three lived inline (snapshots 5, reports 20/20/10, archive 365d). Each entry
|
|
11625
|
+
* below states its bound AND the code that declares it, so a gate can compare the two sets.
|
|
11626
|
+
*
|
|
11627
|
+
* `declare` is an ARRAY because one store may have several declaration sites (the report buckets are
|
|
11628
|
+
* kept by two packages). The `inline` branch matches a CODE SNIPPET rather than a line number: line
|
|
11629
|
+
* numbers rot on every edit above them, a snippet survives (and a snippet that stops matching is the
|
|
11630
|
+
* detector the gate wants). No naming convention is assumed anywhere - that is the whole point of a
|
|
11631
|
+
* ledger: the list IS the declaration.
|
|
11632
|
+
*/
|
|
11633
|
+
/** The one list. A new bounded store that is not registered here is what the G3 gate refuses. */
|
|
11634
|
+
const STORE_LEDGER = [
|
|
11635
|
+
{
|
|
11636
|
+
id: "archive-entries",
|
|
11637
|
+
owner: "evolution-core",
|
|
11638
|
+
unit: "entries",
|
|
11639
|
+
bound: "365 days, report-only by default (the removal policy is report, not delete)",
|
|
11640
|
+
cutoff: "archivedAt older than the retention window",
|
|
11641
|
+
trigger: "run-end",
|
|
11642
|
+
artifact: "count of .archive entries and their ages",
|
|
11643
|
+
declare: [{
|
|
11644
|
+
kind: "inline",
|
|
11645
|
+
file: "packages/evolution-core/src/skill-store.ts",
|
|
11646
|
+
contains: "ARCHIVE_RETENTION_DAYS = 365",
|
|
11647
|
+
value: "365 days"
|
|
11648
|
+
}, {
|
|
11649
|
+
kind: "named",
|
|
11650
|
+
module: "evolution-core/src/limits.ts",
|
|
11651
|
+
name: "DEFAULT_ARCHIVE_RETENTION_POLICY",
|
|
11652
|
+
value: "report"
|
|
11653
|
+
}]
|
|
11654
|
+
},
|
|
11655
|
+
{
|
|
11656
|
+
id: "skill-snapshots",
|
|
11657
|
+
owner: "evolution-core",
|
|
11658
|
+
unit: "entries",
|
|
11659
|
+
bound: "5 snapshots",
|
|
11660
|
+
cutoff: "oldest snapshot beyond the keep window",
|
|
11661
|
+
trigger: "run-end",
|
|
11662
|
+
artifact: "snapshot count and total bytes",
|
|
11663
|
+
declare: [{
|
|
11664
|
+
kind: "inline",
|
|
11665
|
+
file: "packages/evolution-core/src/skill-store.ts",
|
|
11666
|
+
contains: "retainSnapshots(5)",
|
|
11667
|
+
value: "5"
|
|
11668
|
+
}]
|
|
11669
|
+
},
|
|
11670
|
+
{
|
|
11671
|
+
id: "skill-versions",
|
|
11672
|
+
owner: "evolution-core",
|
|
11673
|
+
unit: "entries",
|
|
11674
|
+
bound: "20 versions per skill",
|
|
11675
|
+
cutoff: "history index entries beyond versionKeep",
|
|
11676
|
+
trigger: "write",
|
|
11677
|
+
artifact: "history index length per skill",
|
|
11678
|
+
declare: [{
|
|
11679
|
+
kind: "named",
|
|
11680
|
+
module: "evolution-core/src/constants.ts",
|
|
11681
|
+
name: "DEFAULT_SKILL_VERSION_KEEP",
|
|
11682
|
+
value: 20
|
|
11683
|
+
}]
|
|
11684
|
+
},
|
|
11685
|
+
{
|
|
11686
|
+
id: "pending-resolved",
|
|
11687
|
+
owner: "evolution-state-storage",
|
|
11688
|
+
unit: "entries",
|
|
11689
|
+
bound: "200 resolved pending records",
|
|
11690
|
+
cutoff: "resolved records beyond the cap",
|
|
11691
|
+
trigger: "write",
|
|
11692
|
+
artifact: "resolved record count",
|
|
11693
|
+
declare: [{
|
|
11694
|
+
kind: "named",
|
|
11695
|
+
module: "evolution-state-storage/src/constants.ts",
|
|
11696
|
+
name: "PENDING_RESOLVED_CAP",
|
|
11697
|
+
value: 200
|
|
11698
|
+
}]
|
|
11699
|
+
},
|
|
11700
|
+
{
|
|
11701
|
+
id: "review-session",
|
|
11702
|
+
owner: "evolution-state-storage",
|
|
11703
|
+
unit: "entries",
|
|
11704
|
+
bound: "500 review session rows",
|
|
11705
|
+
cutoff: "rows beyond the session cap",
|
|
11706
|
+
trigger: "write",
|
|
11707
|
+
artifact: "review state row count",
|
|
11708
|
+
declare: [{
|
|
11709
|
+
kind: "named",
|
|
11710
|
+
module: "evolution-state-storage/src/constants.ts",
|
|
11711
|
+
name: "REVIEW_STATE_SESSION_CAP",
|
|
11712
|
+
value: 500
|
|
11713
|
+
}]
|
|
11714
|
+
},
|
|
11715
|
+
{
|
|
11716
|
+
id: "reports",
|
|
11717
|
+
owner: "evolution-commands",
|
|
11718
|
+
unit: "files",
|
|
11719
|
+
bound: "20/20/10 files across the three report buckets",
|
|
11720
|
+
cutoff: "reports beyond each bucket keep",
|
|
11721
|
+
trigger: "run-end",
|
|
11722
|
+
artifact: "report file count per bucket",
|
|
11723
|
+
declare: [{
|
|
11724
|
+
kind: "inline",
|
|
11725
|
+
file: "packages/evolution-commands/src/index.ts",
|
|
11726
|
+
contains: "maintain-",
|
|
11727
|
+
value: "20"
|
|
11728
|
+
}, {
|
|
11729
|
+
kind: "inline",
|
|
11730
|
+
file: "packages/evolution-curator/src/index.ts",
|
|
11731
|
+
contains: "errorKeep",
|
|
11732
|
+
value: "10"
|
|
11733
|
+
}]
|
|
11734
|
+
},
|
|
11735
|
+
{
|
|
11736
|
+
id: "event-archives",
|
|
11737
|
+
owner: "evolution-core",
|
|
11738
|
+
unit: "files",
|
|
11739
|
+
bound: "10 archived event logs",
|
|
11740
|
+
cutoff: "archives beyond the retention count",
|
|
11741
|
+
trigger: "write",
|
|
11742
|
+
artifact: "event archive count",
|
|
11743
|
+
declare: [{
|
|
11744
|
+
kind: "named",
|
|
11745
|
+
module: "evolution-core/src/evolution-events.ts",
|
|
11746
|
+
name: "EVENT_LOG_RETAIN_ARCHIVES",
|
|
11747
|
+
value: 10
|
|
11748
|
+
}]
|
|
11749
|
+
},
|
|
11750
|
+
{
|
|
11751
|
+
id: "run-records",
|
|
11752
|
+
owner: "evolution-core",
|
|
11753
|
+
unit: "entries",
|
|
11754
|
+
bound: "50 run records",
|
|
11755
|
+
cutoff: "runs beyond the record cap",
|
|
11756
|
+
trigger: "write",
|
|
11757
|
+
artifact: "runs.json record count",
|
|
11758
|
+
declare: [{
|
|
11759
|
+
kind: "named",
|
|
11760
|
+
module: "evolution-core/src/run-registry.ts",
|
|
11761
|
+
name: "DEFAULT_RUN_RECORDS",
|
|
11762
|
+
value: 50
|
|
11763
|
+
}]
|
|
11764
|
+
},
|
|
11765
|
+
{
|
|
11766
|
+
id: "support-reads",
|
|
11767
|
+
owner: "evolution-core",
|
|
11768
|
+
unit: "entries",
|
|
11769
|
+
bound: "200 support_reads keys",
|
|
11770
|
+
cutoff: "keys beyond the read-path cap",
|
|
11771
|
+
trigger: "write",
|
|
11772
|
+
artifact: "usage sidecar key count",
|
|
11773
|
+
declare: [{
|
|
11774
|
+
kind: "named",
|
|
11775
|
+
module: "evolution-core/src/usage.ts",
|
|
11776
|
+
name: "MAX_SUPPORT_READ_PATHS",
|
|
11777
|
+
value: 200
|
|
11778
|
+
}]
|
|
11779
|
+
}
|
|
11780
|
+
];
|
|
11781
|
+
//#endregion
|
|
11782
|
+
//#region lib/types/reachability.js
|
|
11783
|
+
/**
|
|
11784
|
+
* G3/3.4: the READ-ONLY reachability reconciliation.
|
|
11785
|
+
*
|
|
11786
|
+
* Three resource domains have grown past their owners' view: content blobs whose index entries were
|
|
11787
|
+
* trimmed away (skill-history: "Trimming drops INDEX entries; orphan blobs stay"), report files no run
|
|
11788
|
+
* record points at, and snapshot directories whose manifest holds almost nothing. The audit measured
|
|
11789
|
+
* them once (233 blobs / 11.40 MiB / 75.4% unreachable; 52 orphan report .md; 4/5 empty snapshot trees,
|
|
11790
|
+
* the newest holding 9 of 35 skills); this module answers the same three questions, from the tree in
|
|
11791
|
+
* front of it, and NOTHING ELSE: it deletes nothing, repairs nothing, and adds no timer.
|
|
11792
|
+
*
|
|
11793
|
+
* Every path is a parameter. The reconciliation must not grow its own idea of where a domain lives -
|
|
11794
|
+
* the callers already know (historyRoot / the reports directory / the snapshot root), and a second
|
|
11795
|
+
* guess here would be a second owner of that fact.
|
|
11796
|
+
*/
|
|
11797
|
+
const HISTORY_BASELINE = "233 blobs / 11.40 MiB unreachable (75.4%)";
|
|
11798
|
+
const REPORTS_BASELINE = "52 orphan report .md";
|
|
11799
|
+
const BACKUPS_BASELINE = "4/5 empty snapshot trees; the newest held 9/35 skills";
|
|
11800
|
+
/** Every sha256 any skill index references, across the whole history root. */
|
|
11801
|
+
async function referencedBlobs(io, historyRoot) {
|
|
11802
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
11803
|
+
const skillsDir = join(historyRoot, "skills");
|
|
11804
|
+
const namesProbe = await probeList(io, skillsDir);
|
|
11805
|
+
if (isUnknown(namesProbe)) return void 0;
|
|
11806
|
+
const names = isPresent(namesProbe) ? namesProbe.value : [];
|
|
11807
|
+
for (const name of names) {
|
|
11808
|
+
const probe = await probeText(io, join(skillsDir, name, "index.json"));
|
|
11809
|
+
if (isUnknown(probe)) return void 0;
|
|
11810
|
+
if (!isPresent(probe)) continue;
|
|
11811
|
+
for (const match of probe.value.matchAll(/[a-f0-9]{64}/g)) referenced.add(match[0]);
|
|
11812
|
+
}
|
|
11813
|
+
return referenced;
|
|
11814
|
+
}
|
|
11815
|
+
/**
|
|
11816
|
+
* The history domain: blobs on disk minus the blobs the indexes still reference.
|
|
11817
|
+
* @param io - the tree reader.
|
|
11818
|
+
* @param historyRoot - the history root (`.history`), passed in, never guessed.
|
|
11819
|
+
* @returns the reading, with the audit baseline quoted for comparison.
|
|
11820
|
+
*/
|
|
11821
|
+
async function historyReachability(io, historyRoot) {
|
|
11822
|
+
const blobsProbe = await probeList(io, join(historyRoot, "blobs"));
|
|
11823
|
+
const referenced = await referencedBlobs(io, historyRoot);
|
|
11824
|
+
if (isUnknown(blobsProbe) || referenced === void 0) return {
|
|
11825
|
+
domain: "history.blobs",
|
|
11826
|
+
unit: "bytes",
|
|
11827
|
+
total: 0,
|
|
11828
|
+
reachable: 0,
|
|
11829
|
+
unreachable: 0,
|
|
11830
|
+
baseline: HISTORY_BASELINE,
|
|
11831
|
+
unmeasured: "a blob listing or an index read failed; this domain was not measured"
|
|
11832
|
+
};
|
|
11833
|
+
const blobs = isPresent(blobsProbe) ? blobsProbe.value : [];
|
|
11834
|
+
const reachable = blobs.filter((blob) => referenced.has(blob)).length;
|
|
11835
|
+
return {
|
|
11836
|
+
domain: "history.blobs",
|
|
11837
|
+
unit: "bytes",
|
|
11838
|
+
total: blobs.length,
|
|
11839
|
+
reachable,
|
|
11840
|
+
unreachable: blobs.length - reachable,
|
|
11841
|
+
baseline: HISTORY_BASELINE
|
|
11842
|
+
};
|
|
11843
|
+
}
|
|
11844
|
+
/**
|
|
11845
|
+
* The reports domain: files no run record points at. A record WITHOUT a `resultRef` still owns the
|
|
11846
|
+
* conventional path (`reports/maintain-<id>.json`), so that path counts as reachable - counting only
|
|
11847
|
+
* explicit refs would report compliant files as orphans and the number could never match the baseline.
|
|
11848
|
+
* @param io - the tree reader.
|
|
11849
|
+
* @param reportsDir - the reports directory, passed in.
|
|
11850
|
+
* @param runIds - every run id the run records carry.
|
|
11851
|
+
* @param resultRefs - every explicit resultRef those records carry.
|
|
11852
|
+
*/
|
|
11853
|
+
async function reportsReachability(io, reportsDir, runIds, resultRefs) {
|
|
11854
|
+
const listed = await probeList(io, reportsDir);
|
|
11855
|
+
if (isUnknown(listed)) return {
|
|
11856
|
+
domain: "reports.files",
|
|
11857
|
+
unit: "files",
|
|
11858
|
+
total: 0,
|
|
11859
|
+
reachable: 0,
|
|
11860
|
+
unreachable: 0,
|
|
11861
|
+
baseline: REPORTS_BASELINE,
|
|
11862
|
+
unmeasured: "the reports directory could not be listed; this domain was not measured"
|
|
11863
|
+
};
|
|
11864
|
+
const files = isPresent(listed) ? listed.value : [];
|
|
11865
|
+
const reachableNames = /* @__PURE__ */ new Set();
|
|
11866
|
+
for (const ref of resultRefs) reachableNames.add(ref.slice(ref.lastIndexOf("/") + 1));
|
|
11867
|
+
for (const id of runIds) reachableNames.add("maintain-" + id + ".json");
|
|
11868
|
+
const reachable = files.filter((file) => reachableNames.has(file)).length;
|
|
11869
|
+
return {
|
|
11870
|
+
domain: "reports.files",
|
|
11871
|
+
unit: "files",
|
|
11872
|
+
total: files.length,
|
|
11873
|
+
reachable,
|
|
11874
|
+
unreachable: files.length - reachable,
|
|
11875
|
+
baseline: REPORTS_BASELINE
|
|
11876
|
+
};
|
|
11877
|
+
}
|
|
11878
|
+
/**
|
|
11879
|
+
* The backups domain: snapshot trees and how much each holds. `skillsPerSnapshot` is measured by the
|
|
11880
|
+
* caller (it already reads manifests); an empty tree is one whose manifest lists no skill.
|
|
11881
|
+
* @param io - the tree reader.
|
|
11882
|
+
* @param backupsRoot - the snapshot root, passed in.
|
|
11883
|
+
* @param skillsPerSnapshot - the caller's manifest reading, one entry per snapshot directory.
|
|
11884
|
+
*/
|
|
11885
|
+
async function backupsReachability(io, backupsRoot, skillsPerSnapshot) {
|
|
11886
|
+
const listed = await probeList(io, backupsRoot);
|
|
11887
|
+
if (isUnknown(listed)) return {
|
|
11888
|
+
domain: "backups.trees",
|
|
11889
|
+
unit: "files",
|
|
11890
|
+
total: 0,
|
|
11891
|
+
reachable: 0,
|
|
11892
|
+
unreachable: 0,
|
|
11893
|
+
baseline: BACKUPS_BASELINE,
|
|
11894
|
+
unmeasured: "the snapshot root could not be listed; this domain was not measured"
|
|
11895
|
+
};
|
|
11896
|
+
const dirs = isPresent(listed) ? listed.value : [];
|
|
11897
|
+
const empty = skillsPerSnapshot.filter((count) => count === 0).length;
|
|
11898
|
+
return {
|
|
11899
|
+
domain: "backups.trees",
|
|
11900
|
+
unit: "files",
|
|
11901
|
+
total: dirs.length,
|
|
11902
|
+
reachable: dirs.length - empty,
|
|
11903
|
+
unreachable: empty,
|
|
11904
|
+
baseline: BACKUPS_BASELINE
|
|
11905
|
+
};
|
|
12016
11906
|
}
|
|
12017
11907
|
//#endregion
|
|
12018
11908
|
//#region lib/types/params.js
|
|
@@ -13983,4 +13873,51 @@ function foldUntrustedLines(text, limit) {
|
|
|
13983
13873
|
return limit > 0 ? marked.slice(0, limit) : marked;
|
|
13984
13874
|
}
|
|
13985
13875
|
//#endregion
|
|
13986
|
-
|
|
13876
|
+
//#region lib/types/diagnostics.js
|
|
13877
|
+
const DIAGNOSTICS_VIEW_SCHEMA_VERSION = 2;
|
|
13878
|
+
const DIAGNOSTICS_FACT_KEYS = [
|
|
13879
|
+
"platform-line",
|
|
13880
|
+
"family-version",
|
|
13881
|
+
"install-form",
|
|
13882
|
+
"preset-ids",
|
|
13883
|
+
"readonly-contract",
|
|
13884
|
+
"upgrade-order",
|
|
13885
|
+
"fallback-map"
|
|
13886
|
+
];
|
|
13887
|
+
const DIAGNOSTICS_RUNTIME_KEYS = [
|
|
13888
|
+
"subagent-channel",
|
|
13889
|
+
"restricted-preset",
|
|
13890
|
+
"host-surface",
|
|
13891
|
+
"session-fold",
|
|
13892
|
+
"platform-services",
|
|
13893
|
+
"runs",
|
|
13894
|
+
"platform-pairing",
|
|
13895
|
+
"install-form-detected"
|
|
13896
|
+
];
|
|
13897
|
+
//#endregion
|
|
13898
|
+
//#region lib/types/platform-line.generated.js
|
|
13899
|
+
/** The frozen platform line this release targets (peers/engines/anchors derived from version+floor). */
|
|
13900
|
+
const PLATFORM_LINE = {
|
|
13901
|
+
version: "0.2.1-rc.2",
|
|
13902
|
+
floor: "0.2.1-rc.1",
|
|
13903
|
+
previousAnchor: "0.2.0-rc.2",
|
|
13904
|
+
peersRange: "^0.2.1-rc.1",
|
|
13905
|
+
enginesRange: ">=0.2.1-rc.1 <0.2.2-0",
|
|
13906
|
+
upstreamRef: "dsh-v0.2.1-rc.2"
|
|
13907
|
+
};
|
|
13908
|
+
//#endregion
|
|
13909
|
+
//#region lib/types/session-fold.js
|
|
13910
|
+
/** Frames the model authored/saw (the only frames evidence, reads and usage accounting may consume). */
|
|
13911
|
+
const MODEL_EVENT_TYPES = new Set(MODEL_SESSION_EVENT_TYPES);
|
|
13912
|
+
/** Classify one session event WITHOUT interpreting it (pure). */
|
|
13913
|
+
function classifySessionEvent(event) {
|
|
13914
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
13915
|
+
if (type.startsWith("plugin:")) return event.ignorable === true ? "plugin-record" : "unknown";
|
|
13916
|
+
return MODEL_EVENT_TYPES.has(type) ? "model" : "unknown";
|
|
13917
|
+
}
|
|
13918
|
+
/** True when the event must be skipped by evidence/usage readers (tolerant, never fatal). */
|
|
13919
|
+
function isIgnorableEvent(event) {
|
|
13920
|
+
return classifySessionEvent(event) !== "model";
|
|
13921
|
+
}
|
|
13922
|
+
//#endregion
|
|
13923
|
+
export { AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ADMISSION, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DEFAULT_WRITE_CONFIRM_MODE, DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS, DIAGNOSTICS_FACT_KEYS, DIAGNOSTICS_RUNTIME_KEYS, DIAGNOSTICS_VIEW_SCHEMA_VERSION, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_PRESET_IDS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, HOST_ROUTES, INSTANCE_KEYS, KEEP_LINE_RE, LONG_RUNS, LOW_QUALITY_THRESHOLD, MAINTAINER_PRESET_ID, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MODEL_SESSION_EVENT_TYPES as MODEL_SESSION_EVENTS, MODEL_SESSION_EVENT_TYPES, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_LINE, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRICTED_PRESET_DISPLAY, RESTRICTED_TOOLS_ROW_ID, RESTRICTOR_MODULE, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, REVIEWER_PRESET_ID, RULES, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, STORE_LEDGER, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPORT_SPRAWL_FILES, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, admissionVerdict, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, atomicWriteFiles, authoringFeedback, authorityFrom, backupsReachability, blobPath, bodyCost, buildCuratorRunReport, buildHealthBaseline, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, candidateSkills, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, classifySessionEvent, clearReviewChannel, collectReadSkillNames, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, countReferenceFiles, createGateSet, createSubagentChannel, daysSinceIso, declaredReportStamp, describeReferenceRewrite, diffFindings, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, h2Sections, historyIndexFile, historyReachability, historyRoot, hostSurface, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isIgnorableEvent, isLandmark, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isRecord, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, json, landmarkVerdict, latestActivityAt, latestVersionAt, lifecycleCandidate, lintBody, lintSkill, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, methodNotAllowed, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, probeAbsent, probeChannelState, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, protectedSkillNamesOf, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, referencedBlobs, registerHostRoutes, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsReachability, reportsSweepLockTarget, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, resolveUnitMembers, resolveUnitMembersFromIndexes, restructureCandidates, retainEventArchives, retirementReport, reviewPrompt, runWriteGates, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopeOfNearestManifest, scopedPackageName, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillWriteGateContext, skillsRoot, staleRouteStubs, subagentChannelState, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, unhookedSupportPointers, unitMembersAreComplete, unwrapLive, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, withTreeWrite, writeDurableTmp, writePathSymbols, yamlPlainScalarNeedsQuotes };
|