@lmzhen/dsh-evolution-core 0.30.30 → 0.30.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -4,9 +4,9 @@ import { w as allowRowCollisions } from "./composition-DSesmTvf.js";
4
4
  import { n as errorText, t as EVOLUTION_ERRORS } from "./errors-Br2-uUSK.js";
5
5
  import { createSubagentChannel, probeChannelState, subagentChannelState } from "./subagent/index.js";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
- import { basename, dirname, join, resolve } from "node:path";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
8
8
  import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
9
- import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
9
+ import { cp, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { parse } from "yaml";
12
12
  import { homedir } from "node:os";
@@ -2251,6 +2251,40 @@ function describeReferenceRewrite(plan) {
2251
2251
  ].filter((part) => part.length > 0).join("; ")}`;
2252
2252
  }
2253
2253
  //#endregion
2254
+ //#region lib/types/support-refs.js
2255
+ /**
2256
+ * Support-directory references in a markdown body (009 kernel): references/…,
2257
+ * templates/…, scripts/…, assets/… relative links — any extension and nested
2258
+ * paths. Pure: who checks them and what the verdict is belongs to the caller.
2259
+ * `..` traversal is a different defect class (path validation owns it) and is
2260
+ * not a support link.
2261
+ */
2262
+ function supportRefs(content) {
2263
+ const refs = [];
2264
+ for (const match of content.matchAll(/(?<![A-Za-z0-9._/-])(?:references|templates|scripts|assets)\/[A-Za-z0-9._/-]+/g)) {
2265
+ const ref = match[0];
2266
+ if (ref.includes("..")) continue;
2267
+ refs.push(ref);
2268
+ }
2269
+ return refs;
2270
+ }
2271
+ /**
2272
+ * References that leave the skill root: parent traversals or absolute support paths.
2273
+ * Extract must not silently treat these as relative support files; the caller reports
2274
+ * them (bounded external copy is a separate, explicit policy).
2275
+ * @param content - markdown body.
2276
+ * @returns external-looking support references, de-duplicated in first-seen order.
2277
+ */
2278
+ function externalRefs(content) {
2279
+ const refs = [];
2280
+ for (const match of content.matchAll(/(?:^|[\s("'`])((?:\.\.\/)+[A-Za-z0-9._/-]+|\/[A-Za-z0-9._/-]+)/g)) {
2281
+ const ref = match[1]?.replace(/[.,;:]+$/, "");
2282
+ if (ref === void 0 || ref.length === 0 || refs.includes(ref)) continue;
2283
+ refs.push(ref);
2284
+ }
2285
+ return refs;
2286
+ }
2287
+ //#endregion
2254
2288
  //#region lib/types/constants.js
2255
2289
  /**
2256
2290
  * Shared constants for the dsh-evolution plugin family.
@@ -3047,6 +3081,786 @@ function authoringFeedback(frontmatter) {
3047
3081
  * v28 G2.1 (EVO-IO-05): this is a delegation to the seam's own
3048
3082
  * `isCommittedWarning` — the marker predicate has exactly one definition. */
3049
3083
  //#endregion
3084
+ //#region lib/types/document-body.js
3085
+ /**
3086
+ * What the panel shows when it renders one version as a document.
3087
+ *
3088
+ * Two decisions, both made here so the preview and the rendered diff cannot drift apart:
3089
+ *
3090
+ * - FRONTMATTER IS NOT DOCUMENT TEXT. A SKILL.md opens with a YAML block; the renderer read its
3091
+ * closing `---` as a setext underline, so the metadata line became the document's biggest heading
3092
+ * and the real title sat below it (measured on the installed panel 2026-09-28). The block is
3093
+ * stripped for DISPLAY only: the raw view and the body route still carry every byte, and the
3094
+ * panel already shows the name and description on the row and in the left column.
3095
+ * - STRIPPING FAILS OPEN. Only a block that the family's own reader accepts is removed. A document
3096
+ * whose text merely starts with a thematic break, a block with no closing line, or a frontmatter
3097
+ * whose body is empty is returned UNCHANGED — showing a document's first line is always better
3098
+ * than showing nothing.
3099
+ *
3100
+ * `readingStats` exists for the other half of the reader's honesty: a preview that stops early has
3101
+ * to be able to say HOW much it is not showing.
3102
+ * @module @lmzhen/dsh-evolution-core/document-body
3103
+ */
3104
+ /**
3105
+ * The document a reader sees: the body without its leading frontmatter block.
3106
+ * @param content - the stored bytes of one version.
3107
+ * @returns the text to render; unchanged when there is no frontmatter the reader accepts.
3108
+ */
3109
+ function displayBodyOf(content) {
3110
+ const block = frontmatterBlock(content);
3111
+ if (block === null) return content;
3112
+ if (parseFrontmatter(content) === null) return content;
3113
+ return block.lines.slice(block.end + 1).join("\n").replace(/^\s*\n/, "");
3114
+ }
3115
+ /**
3116
+ * How much of a version the panel is showing.
3117
+ * @param full - the whole stored text.
3118
+ * @param shown - the part the route handed over.
3119
+ * @returns the counts, so the panel can say what it is not showing.
3120
+ */
3121
+ function readingStats(full, shown) {
3122
+ return {
3123
+ totalChars: full.length,
3124
+ shownChars: shown.length,
3125
+ truncated: shown.length < full.length
3126
+ };
3127
+ }
3128
+ /**
3129
+ * The H2 sections of a body, in document order — the ONE scan (G3).
3130
+ *
3131
+ * Both the restructure candidate list and the store anchor validation call this function, so a
3132
+ * candidate can only name a heading that exists EXACTLY ONCE and has content: the two rejections a
3133
+ * hand-written anchor used to hit ("no such heading", "ambiguous anchor") stop being reachable.
3134
+ * H2 is the section boundary (a section spans to the next H2 or EOF), matching the store.
3135
+ * @param body - the markdown after the frontmatter block.
3136
+ * @returns one row per H2 heading, with `count` filled in a second pass.
3137
+ */
3138
+ function h2Sections(body) {
3139
+ const lines = body.split(String.fromCharCode(10));
3140
+ const rows = [];
3141
+ for (let i = 0; i < lines.length; i += 1) {
3142
+ const match = /^#{2}\s+(.+?)\s*$/.exec(lines[i] ?? "");
3143
+ if (match === null) continue;
3144
+ const title = (match[1] ?? "").trim();
3145
+ let end = lines.length;
3146
+ for (let j = i + 1; j < lines.length; j += 1) if (/^#{2}\s/.test(lines[j] ?? "")) {
3147
+ end = j;
3148
+ break;
3149
+ }
3150
+ rows.push({
3151
+ title,
3152
+ start: i,
3153
+ end,
3154
+ count: 0
3155
+ });
3156
+ }
3157
+ for (const row of rows) row.count = rows.filter((other) => other.title === row.title).length;
3158
+ return rows;
3159
+ }
3160
+ /**
3161
+ * The sections a restructure proposal may name: heading unique, section non-empty. Derived from
3162
+ * `h2Sections`, i.e. from the SAME scan the store validates against.
3163
+ * @param body - the markdown after the frontmatter block.
3164
+ * @returns move-ready pairs in document order; empty when nothing qualifies.
3165
+ */
3166
+ function restructureCandidates(body) {
3167
+ return h2Sections(body).filter((section) => section.count === 1 && section.end > section.start + 1).map((section) => ({
3168
+ heading: section.title,
3169
+ toFile: "references/" + section.title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "") + ".md"
3170
+ }));
3171
+ }
3172
+ //#endregion
3173
+ //#region lib/types/section-selection.js
3174
+ const LF$1 = String.fromCharCode(10);
3175
+ /**
3176
+ * Select whole H2 sections or exact content sub-blocks from one body.
3177
+ *
3178
+ * The H2 scan is document-body's h2Sections, so restructure and extract share
3179
+ * one boundary rule. A start/end pair selects an inclusive, contiguous run of
3180
+ * exact lines inside that heading; it cannot select the heading itself, overlap
3181
+ * another request, or leave a selected range empty.
3182
+ * @param body - markdown after frontmatter.
3183
+ * @param requests - exact heading plus optional start/end line pairs.
3184
+ * @returns selected ranges in request order, or the first refusal.
3185
+ */
3186
+ function selectSections(body, requests) {
3187
+ const lines = body.split(LF$1);
3188
+ const sections = h2Sections(body);
3189
+ const selected = [];
3190
+ for (let requestIndex = 0; requestIndex < requests.length; requestIndex += 1) {
3191
+ const request = requests[requestIndex];
3192
+ if (request === void 0) continue;
3193
+ const wanted = request.heading.trim();
3194
+ const matching = sections.filter((section) => section.title === wanted);
3195
+ if (matching.length === 0) return {
3196
+ ok: false,
3197
+ code: "heading-missing",
3198
+ message: "no \"## " + wanted + "\" heading in the body"
3199
+ };
3200
+ if (matching.length > 1) return {
3201
+ ok: false,
3202
+ code: "heading-ambiguous",
3203
+ message: "heading \"## " + wanted + "\" appears " + matching.length + " times (ambiguous anchor)"
3204
+ };
3205
+ const section = matching[0];
3206
+ if (section === void 0) return {
3207
+ ok: false,
3208
+ code: "heading-missing",
3209
+ message: "no \"## " + wanted + "\" heading in the body"
3210
+ };
3211
+ if (selected.some((entry) => entry.start === section.start)) return {
3212
+ ok: false,
3213
+ code: "heading-duplicate",
3214
+ message: "heading \"## " + wanted + "\" is moved twice"
3215
+ };
3216
+ if (!(request.start !== void 0 || request.end !== void 0)) {
3217
+ if (section.end === section.start + 1) return {
3218
+ ok: false,
3219
+ code: "heading-empty",
3220
+ message: "heading \"## " + wanted + "\" has an empty section"
3221
+ };
3222
+ selected.push({
3223
+ heading: wanted,
3224
+ start: section.start,
3225
+ end: section.end,
3226
+ text: lines.slice(section.start, section.end).join(LF$1),
3227
+ requestIndex
3228
+ });
3229
+ continue;
3230
+ }
3231
+ if (request.start === void 0 || request.end === void 0) return {
3232
+ ok: false,
3233
+ code: "part-anchor-incomplete",
3234
+ message: "heading \"## " + wanted + "\" needs both start and end anchors"
3235
+ };
3236
+ const startMatches = [];
3237
+ const endMatches = [];
3238
+ for (let index = 0; index < lines.length; index += 1) {
3239
+ if (index <= section.start || index >= section.end) continue;
3240
+ if (lines[index] === request.start) startMatches.push(index);
3241
+ if (lines[index] === request.end) endMatches.push(index);
3242
+ }
3243
+ if (startMatches.length !== 1) return {
3244
+ ok: false,
3245
+ code: "part-anchor-not-found",
3246
+ message: "heading \"## " + wanted + "\" start anchor must match exactly one content line"
3247
+ };
3248
+ if (endMatches.length !== 1) return {
3249
+ ok: false,
3250
+ code: "part-anchor-not-found",
3251
+ message: "heading \"## " + wanted + "\" end anchor must match exactly one content line"
3252
+ };
3253
+ const start = startMatches[0];
3254
+ const end = endMatches[0];
3255
+ if (start === void 0 || end === void 0 || start > end) return {
3256
+ ok: false,
3257
+ code: "part-anchor-order",
3258
+ message: "heading \"## " + wanted + "\" start anchor is after end anchor"
3259
+ };
3260
+ if (selected.some((entry) => start < entry.end && end + 1 > entry.start)) return {
3261
+ ok: false,
3262
+ code: "part-anchor-overlaps",
3263
+ message: "heading \"## " + wanted + "\" selection overlaps another selected block"
3264
+ };
3265
+ selected.push({
3266
+ heading: wanted,
3267
+ start,
3268
+ end: end + 1,
3269
+ text: lines.slice(start, end + 1).join(LF$1),
3270
+ requestIndex,
3271
+ anchorStart: request.start,
3272
+ anchorEnd: request.end
3273
+ });
3274
+ }
3275
+ return {
3276
+ ok: true,
3277
+ sections: selected
3278
+ };
3279
+ }
3280
+ //#endregion
3281
+ //#region lib/types/extract-plan.js
3282
+ const LF = String.fromCharCode(10);
3283
+ function sha256$1(text) {
3284
+ return createHash("sha256").update(text).digest("hex");
3285
+ }
3286
+ function unique(values) {
3287
+ return [...new Set(values)];
3288
+ }
3289
+ function computeSupport(parts, residual, sourceName, sourceFiles, targetFiles, externalFiles) {
3290
+ const selectedText = parts.map((part) => part.sourceText).join("\n\n");
3291
+ const selectedRefs = unique(supportRefs(selectedText));
3292
+ const residualRefs = new Set(supportRefs(residual));
3293
+ const unresolved = selectedRefs.filter((ref) => !sourceFiles.includes(ref));
3294
+ if (unresolved.length > 0) return { error: "support-unresolved: " + unresolved.join(", ") };
3295
+ const candidates = selectedRefs.filter((ref) => sourceFiles.includes(ref));
3296
+ const moves = planRehoming(candidates, targetFiles, sourceName).moves;
3297
+ const byFrom = new Map(moves.map((move) => [move.from, move.to]));
3298
+ const exclusive = [];
3299
+ const shared = [];
3300
+ for (const from of candidates) {
3301
+ const to = byFrom.get(from);
3302
+ if (to === void 0) continue;
3303
+ if (residualRefs.has(from)) shared.push({
3304
+ from,
3305
+ to,
3306
+ provenance: sourceName + ":" + from
3307
+ });
3308
+ else exclusive.push({
3309
+ from,
3310
+ to
3311
+ });
3312
+ }
3313
+ const targetFilesWithMoves = unique([...targetFiles, ...moves.map((move) => move.to)]);
3314
+ const external = unique(externalRefs(selectedText));
3315
+ const suppliedRefs = new Set(externalFiles.map((file) => file.ref));
3316
+ const missingExternal = external.filter((ref) => !suppliedRefs.has(ref));
3317
+ const warnings = [];
3318
+ if (missingExternal.length > 0) warnings.push("external-support-not-copied: " + missingExternal.join(", "));
3319
+ const externalClosure = [];
3320
+ for (const file of externalFiles) {
3321
+ if (!external.includes(file.ref)) return { error: "external-ref-not-selected: " + file.ref };
3322
+ if (file.targetPath.startsWith("/") || file.targetPath.includes("..") || !/^(?:references|templates|scripts|assets)\//.test(file.targetPath)) return { error: "external-target-unsafe: " + file.targetPath };
3323
+ if (targetFilesWithMoves.includes(file.targetPath)) return { error: "external-collision: " + file.targetPath };
3324
+ externalClosure.push({
3325
+ from: file.ref,
3326
+ to: file.targetPath,
3327
+ provenance: file.provenance,
3328
+ originalPath: file.originalPath,
3329
+ content: file.content,
3330
+ sha256: file.sha256
3331
+ });
3332
+ }
3333
+ const rewritten = /* @__PURE__ */ new Map();
3334
+ for (const part of parts) {
3335
+ const partRefs = supportRefs(part.sourceText);
3336
+ const rewrite = planReferenceRewrite({
3337
+ content: part.sourceText,
3338
+ files: sourceFiles,
3339
+ moves: moves.filter((move) => partRefs.includes(move.from)),
3340
+ targetFiles: targetFilesWithMoves
3341
+ });
3342
+ rewritten.set(part.heading, applyReferenceRewrite(part.sourceText, rewrite.edits));
3343
+ warnings.push(...rewrite.unresolved.map((ref) => "support-unresolved: " + ref));
3344
+ warnings.push(...rewrite.residualDangling.map((ref) => "support-dangling: " + ref));
3345
+ }
3346
+ for (const part of parts) {
3347
+ let text = rewritten.get(part.heading) ?? part.sourceText;
3348
+ for (const file of externalClosure) text = text.split(file.from).join(file.to);
3349
+ part.movedText = text;
3350
+ }
3351
+ const exclusiveSet = new Set(exclusive.map((entry) => entry.from));
3352
+ const parentExclusive = supportRefs(residual).filter((ref) => exclusiveSet.has(ref));
3353
+ if (parentExclusive.length > 0) return { error: "support-exclusive-referenced-by-parent: " + parentExclusive.join(", ") };
3354
+ const dangling = unique(warnings.filter((item) => item.startsWith("support-dangling: ")).map((item) => item.slice(18)));
3355
+ return {
3356
+ support: {
3357
+ exclusive,
3358
+ shared,
3359
+ external: externalClosure,
3360
+ unresolved: warnings.filter((item) => item.startsWith("support-unresolved: ")),
3361
+ dangling
3362
+ },
3363
+ warnings
3364
+ };
3365
+ }
3366
+ /**
3367
+ * Build a pure extract plan. No IO, no writes: selection, residual, support
3368
+ * closure and the moved text are computed from the supplied bodies/file lists.
3369
+ * @param input - source body, target identity, owner decision and support lists.
3370
+ * @returns a plan, or a refusal with a stable code.
3371
+ */
3372
+ function planExtract(input) {
3373
+ if (input.outcome === "owner-gap") {
3374
+ if (input.owner.mode !== "owner-gap") return {
3375
+ ok: false,
3376
+ code: "owner-gap-mismatch",
3377
+ message: "outcome owner-gap requires an owner-gap decision"
3378
+ };
3379
+ if (input.owner.neededOwner === void 0 || input.owner.neededOwner.trim() === "") return {
3380
+ ok: false,
3381
+ code: "owner-gap-needed-owner",
3382
+ message: "owner-gap requires neededOwner"
3383
+ };
3384
+ } else {
3385
+ if (input.owner.mode !== "covered" || !input.owner.triggerCovers || !input.owner.outcomeCovers) return {
3386
+ ok: false,
3387
+ code: "owner-not-covered",
3388
+ message: "extract/mixed requires a covering owner"
3389
+ };
3390
+ if (!input.targetExists) return {
3391
+ ok: false,
3392
+ code: "target-missing",
3393
+ message: "covered owner target does not exist"
3394
+ };
3395
+ }
3396
+ const selection = selectSections(input.body, input.requests);
3397
+ if (!selection.ok) return {
3398
+ ok: false,
3399
+ code: selection.code,
3400
+ message: selection.message
3401
+ };
3402
+ if (selection.sections.length === 0) return {
3403
+ ok: false,
3404
+ code: "part-empty",
3405
+ message: "at least one part is required"
3406
+ };
3407
+ const parts = selection.sections.map((section) => ({
3408
+ heading: section.heading,
3409
+ anchor: {
3410
+ heading: section.heading,
3411
+ ...section.anchorStart === void 0 ? {} : { start: section.anchorStart },
3412
+ ...section.anchorEnd === void 0 ? {} : { end: section.anchorEnd }
3413
+ },
3414
+ sourceText: section.text,
3415
+ movedText: section.text,
3416
+ bytes: Buffer.byteLength(section.text, "utf8"),
3417
+ sha256: sha256$1(section.text)
3418
+ }));
3419
+ const pointer = "This capability now lives in " + (input.owner.mode === "covered" ? input.owner.name ?? input.targetName : input.owner.neededOwner ?? input.targetName) + "; project-specific facts stay here.";
3420
+ const ordered = [...selection.sections].sort((a, b) => a.start - b.start);
3421
+ const first = ordered[0];
3422
+ if (first === void 0) return {
3423
+ ok: false,
3424
+ code: "part-empty",
3425
+ message: "at least one part is required"
3426
+ };
3427
+ const byStart = new Map(ordered.map((section) => [section.start, section]));
3428
+ const lines = input.body.split(LF);
3429
+ const rebuilt = [];
3430
+ for (let index = 0; index < lines.length; index += 1) {
3431
+ const section = byStart.get(index);
3432
+ if (section !== void 0) {
3433
+ if (index === first.start) rebuilt.push(pointer);
3434
+ index = section.end - 1;
3435
+ } else rebuilt.push(lines[index] ?? "");
3436
+ }
3437
+ const residual = rebuilt.join(LF);
3438
+ if (residual.trim().length === 0) return {
3439
+ ok: false,
3440
+ code: "parent-empty",
3441
+ message: "parent residual is empty"
3442
+ };
3443
+ if (residual.split(LF).filter((line) => line.trim() !== "" && line !== pointer && !line.trimStart().startsWith("#")).length === 0) return {
3444
+ ok: false,
3445
+ code: "parent-hollow-heading",
3446
+ message: "parent residual has no substantive content"
3447
+ };
3448
+ if (input.outcome === "owner-gap") return {
3449
+ ok: true,
3450
+ plan: {
3451
+ source: input.sourceName,
3452
+ target: {
3453
+ name: input.targetName,
3454
+ exists: input.targetExists
3455
+ },
3456
+ outcome: input.outcome,
3457
+ parts,
3458
+ residual,
3459
+ pointer,
3460
+ owner: input.owner,
3461
+ support: {
3462
+ exclusive: [],
3463
+ shared: [],
3464
+ external: [],
3465
+ unresolved: [],
3466
+ dangling: []
3467
+ },
3468
+ warnings: [],
3469
+ ...input.anchors === void 0 ? {} : { anchors: input.anchors }
3470
+ }
3471
+ };
3472
+ const supportResult = computeSupport(parts, residual, input.sourceName, input.sourceFiles, input.targetFiles, input.external ?? []);
3473
+ if ("error" in supportResult) return {
3474
+ ok: false,
3475
+ code: supportResult.error.split(":")[0] ?? "support-invalid",
3476
+ message: supportResult.error
3477
+ };
3478
+ if (supportResult.support.dangling.length > 0) return {
3479
+ ok: false,
3480
+ code: "support-dangling",
3481
+ message: "support-dangling: " + supportResult.support.dangling.join(", ")
3482
+ };
3483
+ return {
3484
+ ok: true,
3485
+ plan: {
3486
+ source: input.sourceName,
3487
+ target: {
3488
+ name: input.targetName,
3489
+ exists: input.targetExists
3490
+ },
3491
+ outcome: input.outcome,
3492
+ parts,
3493
+ residual,
3494
+ pointer,
3495
+ owner: input.owner,
3496
+ support: supportResult.support,
3497
+ warnings: unique(supportResult.warnings),
3498
+ ...input.anchors === void 0 ? {} : { anchors: input.anchors }
3499
+ }
3500
+ };
3501
+ }
3502
+ /**
3503
+ * Project one plan onto the current source/target SKILL.md bytes.
3504
+ *
3505
+ * This is the one place that composes frontmatter + residual + target additions;
3506
+ * preview and the write adapter both call it so they cannot advertise different
3507
+ * bytes. Pure: no IO.
3508
+ * @param plan - plan from planExtract.
3509
+ * @param sourceMd - current source SKILL.md.
3510
+ * @param targetMd - current target SKILL.md.
3511
+ * @returns both next documents, or the refusal that makes projection impossible.
3512
+ */
3513
+ function projectExtractBodies(plan, sourceMd, targetMd) {
3514
+ const sourceBlock = frontmatterBlock(sourceMd);
3515
+ const targetBlock = frontmatterBlock(targetMd);
3516
+ if (!sourceBlock) return {
3517
+ ok: false,
3518
+ message: "source 没有有效 frontmatter"
3519
+ };
3520
+ if (!targetBlock) return {
3521
+ ok: false,
3522
+ message: "target 没有有效 frontmatter"
3523
+ };
3524
+ const sourceHeader = sourceBlock.lines.slice(0, sourceBlock.end + 1).join(LF);
3525
+ const targetNormalized = targetMd.replace(/\r\n/g, LF);
3526
+ const targetLines = targetNormalized.split(LF);
3527
+ for (const part of plan.parts) if (targetLines.some((line) => line.trim() === "## " + part.heading)) return {
3528
+ ok: false,
3529
+ message: "target 已有同名 heading「" + part.heading + "」。"
3530
+ };
3531
+ const additions = plan.parts.map((part) => part.movedText).join("\n\n");
3532
+ const targetRaw = targetNormalized.trimEnd() + "\n\n" + additions + LF;
3533
+ const targetAfter = targetBlock.nl === "\r\n" ? targetRaw.replace(/\n/g, "\r\n") : targetRaw;
3534
+ const sourceRaw = (sourceHeader + plan.residual).replace(/\r\n/g, LF);
3535
+ return {
3536
+ ok: true,
3537
+ projection: {
3538
+ sourceAfter: sourceBlock.nl === "\r\n" ? sourceRaw.replace(/\n/g, "\r\n") : sourceRaw,
3539
+ targetAfter
3540
+ }
3541
+ };
3542
+ }
3543
+ //#endregion
3544
+ //#region lib/types/owner-resolver.js
3545
+ /**
3546
+ * Project the current library listing into owner candidates.
3547
+ *
3548
+ * The source is excluded; a skill with an empty description is named in
3549
+ * excluded rather than silently dropped. Protected candidates remain visible
3550
+ * (so a caller can explain why it refused), but are marked for the writer's
3551
+ * protection gate. Headings reuse h2Sections; no second heading scanner.
3552
+ * @param source - skill being extracted from.
3553
+ * @param summaries - SkillLibrary.list({ withContent: true, withFrontmatter: true }).
3554
+ * @returns stable-name-ordered candidates plus explicit exclusions.
3555
+ */
3556
+ function ownerCandidatesFromSummaries(source, summaries) {
3557
+ const normalized = source.trim();
3558
+ const candidates = [];
3559
+ const excluded = [];
3560
+ for (const summary of summaries) {
3561
+ if (summary.name === normalized) {
3562
+ excluded.push({
3563
+ name: summary.name,
3564
+ reason: "source"
3565
+ });
3566
+ continue;
3567
+ }
3568
+ if (summary.description.trim() === "") {
3569
+ excluded.push({
3570
+ name: summary.name,
3571
+ reason: "unreadable"
3572
+ });
3573
+ continue;
3574
+ }
3575
+ candidates.push({
3576
+ name: summary.name,
3577
+ description: summary.description,
3578
+ ...summary.whenToUse === void 0 ? {} : { whenToUse: summary.whenToUse },
3579
+ headings: h2Sections(summary.content ?? "").map((section) => section.title),
3580
+ protectedBy: summary.protectedBy,
3581
+ protectionUnknown: summary.protectionUnknown,
3582
+ managed: summary.managed
3583
+ });
3584
+ }
3585
+ candidates.sort((left, right) => left.name.localeCompare(right.name));
3586
+ excluded.sort((left, right) => left.name.localeCompare(right.name));
3587
+ return {
3588
+ source: normalized,
3589
+ candidates,
3590
+ excluded
3591
+ };
3592
+ }
3593
+ /**
3594
+ * Find one named candidate in a lookup.
3595
+ * @param lookup - projection from ownerCandidatesFromSummaries.
3596
+ * @param name - exact skill name.
3597
+ * @returns the candidate, or undefined when absent.
3598
+ */
3599
+ function findOwnerCandidate(lookup, name) {
3600
+ return lookup.candidates.find((candidate) => candidate.name === name);
3601
+ }
3602
+ //#endregion
3603
+ //#region lib/types/proposal-view.js
3604
+ /**
3605
+ * Project a pure ExtractPlan into the proposal record every read-only surface consumes.
3606
+ *
3607
+ * This is a projection, not a second evaluator: it keeps the plan's owner decision,
3608
+ * support closure and warnings. It never writes and never invents coverage.
3609
+ * @param plan - plan produced by planExtract.
3610
+ * @param provenance - run id and timestamp for the proposing pass.
3611
+ * @returns a proposal with status proposed.
3612
+ */
3613
+ function proposalFromPlan(plan, provenance) {
3614
+ return {
3615
+ version: 1,
3616
+ id: "extract-" + provenance.runId + "-" + plan.source,
3617
+ source: plan.source,
3618
+ outcome: plan.outcome,
3619
+ parts: plan.parts.map((part) => ({
3620
+ heading: part.heading,
3621
+ anchor: part.anchor,
3622
+ reason: part.sourceText.slice(0, 160)
3623
+ })),
3624
+ owner: plan.owner,
3625
+ support: plan.support,
3626
+ warnings: plan.warnings,
3627
+ provenance: {
3628
+ promptVersion: "v1",
3629
+ runId: provenance.runId,
3630
+ at: provenance.at ?? Date.now()
3631
+ },
3632
+ status: "proposed"
3633
+ };
3634
+ }
3635
+ //#endregion
3636
+ //#region lib/types/state-store.js
3637
+ /**
3638
+ * Evolution home path helpers: the DSH root and `$DSH_HOME/evolution` for
3639
+ * plugin-owned sidecar state (reports, activity store, feedback file,
3640
+ * state-domain data).
3641
+ *
3642
+ * C-10: PATH HELPERS ONLY — despite the file name there is no store
3643
+ * here. Durable evolution state lives in the state stack (evolution-state over
3644
+ * evolution-state-json / -domain); skills and memories live in skill-store.ts
3645
+ * / memory-store.ts. The file name is kept deliberately: renaming it would
3646
+ * touch every family import for zero behavior change, and the audit records
3647
+ * the mismatch as known naming debt.
3648
+ */
3649
+ /**
3650
+ * DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
3651
+ * fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
3652
+ * home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
3653
+ * V8-06 (0.3.47) extends the guard to whitespace.
3654
+ * C-11 correction (S2.2, PLAN 2026-09-16): upstream `resolveDshHome`
3655
+ * (util/home-paths) uses `trim()` ONLY as the ADOPTION test and then uses the
3656
+ * RAW env value — the earlier "same trimmed source" form returned the trimmed
3657
+ * text, so `DSH_HOME=" /x "` produced `/x` where upstream produced the literal
3658
+ * padded path. The value semantics now match upstream line for line:
3659
+ * `const selected = configured ?? (fromEnv !== undefined &&
3660
+ * fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())`.
3661
+ * OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
3662
+ * resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
3663
+ * load-bearing when the skill-catalog shadow made "same tree as the upstream
3664
+ * `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
3665
+ * absolute `<home>/skills` while this value fed a literal `~/x` (a directory
3666
+ * named `~` under the host CWD) or a CWD-relative path — split-brain skill
3667
+ * trees, preset installs the platform never reads, doctor probes of a
3668
+ * directory nothing serves.
3669
+ * S2.2 (PLAN 2026-09-16), final correction: the result is ALWAYS
3670
+ * `resolve(expandHomePath(selected))` — the earlier form returned an
3671
+ * already-absolute value VERBATIM, so a trailing slash or `..` segment
3672
+ * landed unnormalized while upstream normalizes every value. Relative
3673
+ * `DSH_HOME` values change landing spot; a CLEAN absolute home stays
3674
+ * byte-identical (resolve is a no-op on it).
3675
+ */
3676
+ function evolutionRoot(env = process.env) {
3677
+ const fromEnv = env.DSH_HOME;
3678
+ const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), ".dsh");
3679
+ return resolve(selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected);
3680
+ }
3681
+ /** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
3682
+ * state (reports, activity store, feedback file, state-domain data). */
3683
+ function evolutionHome(env = process.env) {
3684
+ return join(evolutionRoot(env), "evolution");
3685
+ }
3686
+ //#endregion
3687
+ //#region lib/types/proposal-report.js
3688
+ /** Directory for the one proposal lane; reports are retention-swept with the other run reports. */
3689
+ function proposalReportDir() {
3690
+ return join(evolutionHome(), "reports");
3691
+ }
3692
+ /** Stable report filename for one proposal; the id may carry path separators. */
3693
+ function proposalReportName(proposal) {
3694
+ return "extract-proposal-" + encodeURIComponent(proposal.id) + ".json";
3695
+ }
3696
+ /**
3697
+ * Persist one proposal as a report. Best-effort: a failed write never blocks the extract.
3698
+ * @param io - evolution IO seam.
3699
+ * @param proposal - proposal to persist.
3700
+ * @param dir - reports directory (tests pass a temp dir).
3701
+ * @returns the written path.
3702
+ */
3703
+ async function writeProposalReport(io, proposal, dir = proposalReportDir()) {
3704
+ const path = join(dir, proposalReportName(proposal));
3705
+ await io.writeText(path, JSON.stringify(proposal));
3706
+ return path;
3707
+ }
3708
+ function isProposal(value) {
3709
+ if (value === null || typeof value !== "object") return false;
3710
+ const candidate = value;
3711
+ return candidate.version === 1 && typeof candidate.id === "string" && typeof candidate.source === "string" && (candidate.outcome === "keep" || candidate.outcome === "extract" || candidate.outcome === "mixed" || candidate.outcome === "owner-gap");
3712
+ }
3713
+ /**
3714
+ * Read the newest proposals. Missing/unreadable reports are skipped, never guessed.
3715
+ * @param io - evolution IO seam.
3716
+ * @param dir - reports directory.
3717
+ * @param limit - newest rows to return.
3718
+ * @returns valid proposals, newest filename first.
3719
+ */
3720
+ async function readProposalReports(io, dir = proposalReportDir(), limit = 10) {
3721
+ const listed = await probeList(io, dir);
3722
+ if (isUnknown(listed) || !isPresent(listed)) return [];
3723
+ const names = listed.value.filter((name) => name.startsWith("extract-proposal-") && name.endsWith(".json"));
3724
+ names.sort((a, b) => b.localeCompare(a));
3725
+ const proposals = [];
3726
+ for (const name of names.slice(0, limit)) {
3727
+ const raw = await probeText(io, join(dir, name));
3728
+ if (!isPresent(raw)) continue;
3729
+ let parsed;
3730
+ try {
3731
+ parsed = JSON.parse(raw.value);
3732
+ } catch {
3733
+ continue;
3734
+ }
3735
+ if (isProposal(parsed)) proposals.push(parsed);
3736
+ }
3737
+ return proposals;
3738
+ }
3739
+ //#endregion
3740
+ //#region lib/types/external-support.js
3741
+ const MAX_FILE_BYTES = 1024 * 1024;
3742
+ const MAX_TOTAL_BYTES = 4 * 1024 * 1024;
3743
+ function targetPathFor(ref) {
3744
+ let value = ref.replace(/^\/+/, "");
3745
+ while (value.startsWith("../")) value = value.slice(3);
3746
+ const support = value.match(/(?:^|\/)(references|templates|scripts|assets)\/(.+)$/);
3747
+ if (support !== null) return support[1] + "/" + support[2];
3748
+ return "references/" + basename(value);
3749
+ }
3750
+ function annotate(content, originalPath) {
3751
+ const note = "external source: " + originalPath;
3752
+ const extension = originalPath.slice(originalPath.lastIndexOf(".")).toLowerCase();
3753
+ if (extension === ".md") return "<!-- " + note + " -->\n\n" + content;
3754
+ const prefix = extension === ".py" || extension === ".sh" || extension === ".rb" ? "# " : "// ";
3755
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs" || extension === ".ts" || extension === ".tsx" || extension === ".py" || extension === ".sh" || extension === ".rb") {
3756
+ if (content.startsWith("#!")) {
3757
+ const end = content.indexOf(String.fromCharCode(10));
3758
+ if (end >= 0) return content.slice(0, end + 1) + prefix + note + String.fromCharCode(10) + content.slice(end + 1);
3759
+ }
3760
+ return prefix + note + String.fromCharCode(10) + content;
3761
+ }
3762
+ return content;
3763
+ }
3764
+ function inside(root, candidate) {
3765
+ const rel = relative(root, candidate);
3766
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
3767
+ }
3768
+ /**
3769
+ * Read and annotate external support files under one explicit root. The caller owns
3770
+ * the root choice; this module only enforces containment, target-path safety, size
3771
+ * limits and determinism.
3772
+ * @param body - selected source markdown body.
3773
+ * @param sourceDir - source skill directory.
3774
+ * @param allowedRoot - canonical root external files must stay under.
3775
+ * @param targetFiles - existing target support paths.
3776
+ * @returns resolved external entries, or a refusal.
3777
+ */
3778
+ async function readExternalSupport(body, sourceDir, allowedRoot, targetFiles) {
3779
+ const refs = externalRefs(body);
3780
+ if (refs.length === 0) return {
3781
+ ok: true,
3782
+ files: []
3783
+ };
3784
+ let root;
3785
+ try {
3786
+ root = await realpath(allowedRoot);
3787
+ } catch {
3788
+ return {
3789
+ ok: false,
3790
+ message: "external support 根目录不可读:" + allowedRoot
3791
+ };
3792
+ }
3793
+ const files = [];
3794
+ const targets = new Set(targetFiles);
3795
+ let total = 0;
3796
+ for (const ref of refs) {
3797
+ const candidate = isAbsolute(ref) ? resolve(ref) : resolve(sourceDir, ref);
3798
+ let canonical;
3799
+ try {
3800
+ canonical = await realpath(candidate);
3801
+ } catch {
3802
+ return {
3803
+ ok: false,
3804
+ message: "external support 文件不存在:" + ref
3805
+ };
3806
+ }
3807
+ if (!inside(root, canonical)) return {
3808
+ ok: false,
3809
+ message: "external support 越界:" + ref
3810
+ };
3811
+ const info = await stat(canonical);
3812
+ if (!info.isFile()) return {
3813
+ ok: false,
3814
+ message: "external support 不是普通文件:" + ref
3815
+ };
3816
+ if (info.size > MAX_FILE_BYTES) return {
3817
+ ok: false,
3818
+ message: "external support 超过单文件上限:" + ref
3819
+ };
3820
+ total += info.size;
3821
+ if (total > MAX_TOTAL_BYTES) return {
3822
+ ok: false,
3823
+ message: "external support 超过总上限:" + ref
3824
+ };
3825
+ const targetPath = targetPathFor(ref);
3826
+ if (targets.has(targetPath)) return {
3827
+ ok: false,
3828
+ message: "external support 目标冲突:" + targetPath
3829
+ };
3830
+ targets.add(targetPath);
3831
+ const annotated = annotate(await readFile(canonical, "utf8"), canonical);
3832
+ files.push({
3833
+ ref,
3834
+ originalPath: canonical,
3835
+ targetPath,
3836
+ content: annotated,
3837
+ sha256: createHash("sha256").update(annotated).digest("hex"),
3838
+ provenance: "external:" + canonical
3839
+ });
3840
+ }
3841
+ return {
3842
+ ok: true,
3843
+ files
3844
+ };
3845
+ }
3846
+ //#endregion
3847
+ //#region lib/types/extract-prompt.js
3848
+ /**
3849
+ * The frozen semantic prompt for skill extraction. One constant, consumed by
3850
+ * explicit extract agents and tests; it is deliberately not part of the review
3851
+ * prompt bundle, and no review path injects it.
3852
+ */
3853
+ const EXTRACT_PROMPT_VERSION = "v1";
3854
+ const EXTRACT_PROMPT_V1 = `Inspect the source skill, its linked support files, and the candidate owners
3855
+ in the library. A part of the skill (one or more sections) is a separate
3856
+ capability when it has its own trigger and outcome, can be used without the
3857
+ parent context, and leaves the parent coherent. If it is separate and a
3858
+ class-level owner covers it, extract that part. If no owner covers it, report
3859
+ the owner gap and name the class-level owner that would be needed. If it mixes
3860
+ a reusable capability with parent-specific facts, return mixed and name only
3861
+ the reusable part. Otherwise keep it. Keep, extract, mixed, and owner-gap are
3862
+ all valid outcomes; no split is required.`;
3863
+ //#endregion
3050
3864
  //#region lib/types/cost.js
3051
3865
  /**
3052
3866
  * Context-cost accounting for skill bodies (design §5.2).
@@ -4469,78 +5283,27 @@ function fuzzyPatch(content, oldString, newString, replaceAll = false) {
4469
5283
  * Build the agent prompt for an open-ended `/evolution learn` request.
4470
5284
  *
4471
5285
  * @param userRequest free-text the user gave after `/evolution learn`; an
4472
- * empty string falls back to "the workflow we just went through".
4473
- * @returns a complete instruction the agent runs as a normal turn.
4474
- */
4475
- function buildLearnPrompt(userRequest) {
4476
- return [
4477
- "[/learn] The user wants you to learn a reusable skill from the request below, and save it.",
4478
- "",
4479
- "THE REQUEST:",
4480
- userRequest.trim() || "the workflow we just went through in this conversation — review the steps taken and distill them into a reusable skill",
4481
- "",
4482
- "The request is open-ended and may mix two kinds of content, in any order: SOURCES to gather (directories, file paths, URLs, \"what we just did\", pasted notes) AND REQUIREMENTS that shape the skill (what to focus on, what to leave out, scope, naming, the angle to take). Treat EVERY part of the request as load-bearing. In particular, prose that comes after a path or link is NOT incidental — it is the user telling you what they want from that source. A request like `<url> focus on the auth flow, skip the deprecated endpoints` means: gather the URL AND honor \"focus on auth, skip deprecated\" as authoring requirements. Never fetch the first source and ignore the rest.",
4483
- "",
4484
- "Do this:",
4485
- "1. Gather every source the user named, using the tools you already have — reads and searches for local files or directories, web access for URLs, this conversation history if they referred to something you just did, and the text they pasted as-is. If the request is ambiguous about scope, make a reasonable choice and note it; do not stall.",
4486
- "2. Author ONE SKILL.md, applying every requirement, focus, and constraint in the request — these govern what the SKILL.md covers and emphasizes, not just which sources you read.",
4487
- "3. Save it with the `skill_manage` tool (action=\"create\"). Pick a sensible category. If the procedure needs a non-trivial script, add it under the skill's `scripts/` with `skill_manage` write_file and reference it by relative path.",
4488
- "",
4489
- "Apply the skill-authoring standards (frontmatter, prose, file layout) carried by the `skill_manage` tool description — that is the single copy; do not restate them here.",
4490
- "",
4491
- "When done, tell the user the skill name, its category, and a one-line summary of what it captured."
4492
- ].join("\n");
4493
- }
4494
- //#endregion
4495
- //#region lib/types/state-store.js
4496
- /**
4497
- * Evolution home path helpers: the DSH root and `$DSH_HOME/evolution` for
4498
- * plugin-owned sidecar state (reports, activity store, feedback file,
4499
- * state-domain data).
4500
- *
4501
- * C-10: PATH HELPERS ONLY — despite the file name there is no store
4502
- * here. Durable evolution state lives in the state stack (evolution-state over
4503
- * evolution-state-json / -domain); skills and memories live in skill-store.ts
4504
- * / memory-store.ts. The file name is kept deliberately: renaming it would
4505
- * touch every family import for zero behavior change, and the audit records
4506
- * the mismatch as known naming debt.
4507
- */
4508
- /**
4509
- * DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
4510
- * fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
4511
- * home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
4512
- * V8-06 (0.3.47) extends the guard to whitespace.
4513
- * C-11 correction (S2.2, PLAN 2026-09-16): upstream `resolveDshHome`
4514
- * (util/home-paths) uses `trim()` ONLY as the ADOPTION test and then uses the
4515
- * RAW env value — the earlier "same trimmed source" form returned the trimmed
4516
- * text, so `DSH_HOME=" /x "` produced `/x` where upstream produced the literal
4517
- * padded path. The value semantics now match upstream line for line:
4518
- * `const selected = configured ?? (fromEnv !== undefined &&
4519
- * fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())`.
4520
- * OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
4521
- * resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
4522
- * load-bearing when the skill-catalog shadow made "same tree as the upstream
4523
- * `USER_DSH_RANK` provider" a hard contract: upstream watches the EXPANDED
4524
- * absolute `<home>/skills` while this value fed a literal `~/x` (a directory
4525
- * named `~` under the host CWD) or a CWD-relative path — split-brain skill
4526
- * trees, preset installs the platform never reads, doctor probes of a
4527
- * directory nothing serves.
4528
- * S2.2 (PLAN 2026-09-16), final correction: the result is ALWAYS
4529
- * `resolve(expandHomePath(selected))` — the earlier form returned an
4530
- * already-absolute value VERBATIM, so a trailing slash or `..` segment
4531
- * landed unnormalized while upstream normalizes every value. Relative
4532
- * `DSH_HOME` values change landing spot; a CLEAN absolute home stays
4533
- * byte-identical (resolve is a no-op on it).
5286
+ * empty string falls back to "the workflow we just went through".
5287
+ * @returns a complete instruction the agent runs as a normal turn.
4534
5288
  */
4535
- function evolutionRoot(env = process.env) {
4536
- const fromEnv = env.DSH_HOME;
4537
- const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), ".dsh");
4538
- return resolve(selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected);
4539
- }
4540
- /** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
4541
- * state (reports, activity store, feedback file, state-domain data). */
4542
- function evolutionHome(env = process.env) {
4543
- return join(evolutionRoot(env), "evolution");
5289
+ function buildLearnPrompt(userRequest) {
5290
+ return [
5291
+ "[/learn] The user wants you to learn a reusable skill from the request below, and save it.",
5292
+ "",
5293
+ "THE REQUEST:",
5294
+ userRequest.trim() || "the workflow we just went through in this conversation — review the steps taken and distill them into a reusable skill",
5295
+ "",
5296
+ "The request is open-ended and may mix two kinds of content, in any order: SOURCES to gather (directories, file paths, URLs, \"what we just did\", pasted notes) AND REQUIREMENTS that shape the skill (what to focus on, what to leave out, scope, naming, the angle to take). Treat EVERY part of the request as load-bearing. In particular, prose that comes after a path or link is NOT incidental — it is the user telling you what they want from that source. A request like `<url> focus on the auth flow, skip the deprecated endpoints` means: gather the URL AND honor \"focus on auth, skip deprecated\" as authoring requirements. Never fetch the first source and ignore the rest.",
5297
+ "",
5298
+ "Do this:",
5299
+ "1. Gather every source the user named, using the tools you already have — reads and searches for local files or directories, web access for URLs, this conversation history if they referred to something you just did, and the text they pasted as-is. If the request is ambiguous about scope, make a reasonable choice and note it; do not stall.",
5300
+ "2. Author ONE SKILL.md, applying every requirement, focus, and constraint in the request — these govern what the SKILL.md covers and emphasizes, not just which sources you read.",
5301
+ "3. Save it with the `skill_manage` tool (action=\"create\"). Pick a sensible category. If the procedure needs a non-trivial script, add it under the skill's `scripts/` with `skill_manage` write_file and reference it by relative path.",
5302
+ "",
5303
+ "Apply the skill-authoring standards (frontmatter, prose, file layout) carried by the `skill_manage` tool description — that is the single copy; do not restate them here.",
5304
+ "",
5305
+ "When done, tell the user the skill name, its category, and a one-line summary of what it captured."
5306
+ ].join("\n");
4544
5307
  }
4545
5308
  //#endregion
4546
5309
  //#region lib/types/serial.js
@@ -5710,8 +6473,8 @@ async function recordMutation(root, io, record, cap = 500) {
5710
6473
  * changes semantically: the bundle digest is the fail-closed signal for
5711
6474
  * review workers, so a stale id across deployments must be distinguishable.
5712
6475
  */
5713
- const PROMPT_BUNDLE_VERSION = 21;
5714
- const PROMPT_BUNDLE_ID = `dsh-evolution@21`;
6476
+ const PROMPT_BUNDLE_VERSION = 22;
6477
+ const PROMPT_BUNDLE_ID = `dsh-evolution@22`;
5715
6478
  const MEMORY_REVIEW_PROMPT = `[Auto-review — Memory]
5716
6479
  Review the conversation above and consider saving to memory if appropriate.
5717
6480
 
@@ -5722,10 +6485,12 @@ Focus on:
5722
6485
  If something stands out, save it using the memory tool.
5723
6486
  If nothing is worth saving, just say "Nothing to save." and stop.`;
5724
6487
  const SKILL_REVIEW_PROMPT = `[Auto-review — Skills]
5725
- Review the conversation above and update the skill library. Be ACTIVE — most sessions produce at least one skill update, even if small. A pass that does nothing is a missed learning opportunity, not a neutral outcome.
6488
+ Review the conversation above and update the skill library. Be ACTIVE — inspect the session and the existing library thoroughly, and prefer a small, precise update when the session teaches something durable. Activity is not a quota: a precise no-op after checking is better than a vague or forced entry.
5726
6489
 
5727
6490
  Target shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.
5728
6491
 
6492
+ A skill body is a reusable decision rule (when / do / check); dates, versions, incident narratives, and one-off evidence belong in references when they would otherwise dominate the body.
6493
+
5729
6494
  Signals to look for (any one of these warrants action):
5730
6495
  • User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.
5731
6496
  • User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.
@@ -5743,7 +6508,7 @@ Preference order — prefer the earliest action that fits, but do pick one when
5743
6508
  • scripts/<name>.<ext> — statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).
5744
6509
  Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.
5745
6510
  4. RESTRUCTURE a loaded skill whose body grew log-like — rc/sha/date-dense sections, session-detail spirals, or a fat body with no support files. Use skill_manage action=restructure with restructure: [{"heading": "<the exact ## heading text>", "to_file": "references/<topic>.md"}] — the ENTIRE ## section (from that heading to the next heading) moves into the support file and its position becomes a pointer line. The skill's name and directory never change. Only propose headings that exist verbatim in the body; never invent one, and never restructure a healthy small skill.
5746
- 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL when no existing skill covers the class. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong — fall back to (1), (2), or (3).
6511
+ 5. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL only when no existing class-level skill covers the class and no existing umbrella can absorb the learning. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong — fall back to (1), (2), or (3).
5747
6512
 
5748
6513
  User-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.
5749
6514
 
@@ -5767,16 +6532,18 @@ Do NOT capture (these become persistent self-imposed constraints that bite you l
5767
6532
 
5768
6533
  If a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill — never 'this tool does not work' as a standalone constraint.
5769
6534
 
5770
- 'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, just say 'Nothing to save.' and stop. Otherwise, act.`;
6535
+ 'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, state what you checked and stop. Otherwise, act.`;
5771
6536
  const COMBINED_REVIEW_PROMPT = `[Auto-review]
5772
6537
  Review the conversation above and update two things:
5773
6538
 
5774
6539
  **Memory**: who the user is. Did the user reveal persona, desires, preferences, personal details, or expectations about how you should behave? Save facts about the user and durable preferences with the memory tool.
5775
6540
 
5776
- **Skills**: how to do this class of task. Be ACTIVE — most sessions produce at least one skill update. A pass that does nothing is a missed learning opportunity, not a neutral outcome.
6541
+ **Skills**: how to do this class of task. Be ACTIVE — inspect the session and the existing library thoroughly, and prefer a small, precise update when the session teaches something durable. Activity is not a quota: a precise no-op after checking is better than a vague or forced entry.
5777
6542
 
5778
6543
  Target shape of the skill library: CLASS-LEVEL skills with a rich SKILL.md and a references/ directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries.
5779
6544
 
6545
+ A skill body is a reusable decision rule (when / do / check); dates, versions, incident narratives, and one-off evidence belong in references when they would otherwise dominate the body.
6546
+
5780
6547
  Signals that warrant a skill update (any one is enough):
5781
6548
  • User corrected your style, tone, format, legibility, verbosity, or approach. Frustration is a FIRST-CLASS skill signal, not just a memory signal. 'stop doing X', 'don't format like this', 'I hate when you Y' — embed the lesson in the skill that governs that task so the next session starts fixed.
5782
6549
  • Non-trivial technique, fix, workaround, or debugging path emerged.
@@ -5789,7 +6556,7 @@ Preference order for skills — pick the earliest that fits:
5789
6556
  2. UPDATE AN EXISTING UMBRELLA. Patch it.
5790
6557
  3. ADD A SUPPORT FILE under an existing umbrella via skill_manage action=write_file. Three kinds: references/<topic>.md for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes) written concise and task-focused; templates/<name>.<ext> for starter files meant to be copied and modified; scripts/<name>.<ext> for statically re-runnable actions (verification, fixture generators, probes). Add a one-line pointer in SKILL.md so future agents find them.
5791
6558
  4. RESTRUCTURE a loaded skill whose body grew log-like (rc/sha/date-dense sections, session-detail spirals, fat body with no support files) via skill_manage action=restructure with restructure: [{"heading": "<the exact ## heading text>", "to_file": "references/<topic>.md"}] — the ENTIRE ## section moves into the support file and its position becomes a pointer line; the skill's name and directory never change. Only propose headings that exist verbatim in the body.
5792
- 5. CREATE A NEW CLASS-LEVEL UMBRELLA when nothing exists. Name at the class level — NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).
6559
+ 5. CREATE A NEW CLASS-LEVEL UMBRELLA only when no existing class-level skill covers the class and no existing umbrella can absorb the learning. Name at the class level — NOT a PR number, error string, codename, library-alone name, or 'fix-X / debug-Y' session artifact. If the name only fits today's task, fall back to (1), (2), or (3).
5793
6560
 
5794
6561
  Two-tier deposition discipline (DSH addition): classify before writing — PATTERN (symptom → mechanism → fix → verification) goes in the SKILL.md body; LOG (commit SHAs, npm/profile states, this release's change list, this session's narrative) goes in a references/ file. Body density IS reuse rate; a pattern fits in 2-8 physical lines.
5795
6562
 
@@ -5876,11 +6643,48 @@ Your output IS the deliverable: produce the exact same human-readable summary an
5876
6643
 
5877
6644
  If you accidentally take a mutating action, say so explicitly in the summary.`;
5878
6645
  const COMPLETION_SKILL_REVIEW_PROMPT = `[Auto-review — Skills · task complete]
5879
- Your current task now appears complete. Before wrapping up, review the approach and update the skill library via skill_manage.
6646
+ Review the conversation above and update the skill library. Be ACTIVE — inspect the session and the existing library thoroughly, and prefer a small, precise update when the session teaches something durable. Activity is not a quota: a precise no-op after checking is better than a vague or forced entry.
6647
+
6648
+ Target shape of the library: CLASS-LEVEL skills, each with a rich SKILL.md and a \`references/\` directory for session-specific detail. Not a long flat list of narrow one-session-one-skill entries. This shapes HOW you update, not WHETHER you update.
5880
6649
 
5881
- Follow the skills review policy: be ACTIVE, prefer class-level umbrellas, patch ONLY skills loaded or read this session, and capture non-trivial techniques and user corrections. Do NOT capture environment-dependent failures, negative claims about tools, or one-off task narratives.
6650
+ A skill body is a reusable decision rule (when / do / check); dates, versions, incident narratives, and one-off evidence belong in references when they would otherwise dominate the body.
5882
6651
 
5883
- Do NOT modify output files or re-run the task. If you are still mid-task, ignore this.`;
6652
+ Signals to look for (any one of these warrants action):
6653
+ • User corrected your style, tone, format, legibility, or verbosity. Frustration signals like 'stop doing X', 'this is too verbose', 'don't format like this', 'why are you explaining', 'just give me the answer', 'you always do Y and I hate it', or an explicit 'remember this' are FIRST-CLASS skill signals, not just memory signals. Update the relevant skill(s) to embed the preference so the next session starts already knowing.
6654
+ • User corrected your workflow, approach, or sequence of steps. Encode the correction as a pitfall or explicit step in the skill that governs that class of task.
6655
+ • Non-trivial technique, fix, workaround, debugging path, or tool-usage pattern emerged that a future session would benefit from. Capture it.
6656
+ • A skill that got loaded or consulted this session turned out to be wrong, missing a step, or outdated. Patch it NOW.
6657
+
6658
+ Preference order — prefer the earliest action that fits, but do pick one when a signal above fired:
6659
+ 1. UPDATE A CURRENTLY-LOADED SKILL. Look back through the conversation for skills the user loaded via /skill-name or you read via skill. If any of them covers the territory of the new learning, PATCH that one first. It is the skill that was in play, so it's the right one to extend.
6660
+ 2. UPDATE AN EXISTING UMBRELLA (via the skill catalog + skill). If no loaded skill fits but an existing class-level skill does, patch it. Add a subsection, a pitfall, or broaden a trigger.
6661
+ 3. ADD A SUPPORT FILE under an existing umbrella. Skills can be packaged with three kinds of support files — use the right directory per kind:
6662
+ • \`references/<topic>.md\` — session-specific detail (error transcripts, reproduction recipes, provider quirks) AND condensed knowledge banks: quoted research, API docs, external authoritative excerpts, or domain notes you found while working on the problem. Write it concise and for the value of the task, not as a full mirror of upstream docs.
6663
+ • \`templates/<name>.<ext>\` — starter files meant to be copied and modified (boilerplate configs, scaffolding, a known-good example the agent can \`reproduce with modifications\`).
6664
+ • \`scripts/<name>.<ext>\` — statically re-runnable actions the skill can invoke directly (verification scripts, fixture generators, deterministic probes, anything the agent should run rather than hand-type each time).
6665
+ Add support files via skill_manage action=write_file with file_path starting 'references/', 'templates/', or 'scripts/'. The umbrella's SKILL.md should gain a one-line pointer to any new support file so future agents know it exists.
6666
+ 4. CREATE A NEW CLASS-LEVEL UMBRELLA SKILL only when no existing class-level skill covers the class and no existing umbrella can absorb the learning. The name MUST be at the class level. The name MUST NOT be a specific PR number, error string, feature codename, library-alone name, or 'fix-X / debug-Y / audit-Z-today' session artifact. If the proposed name only makes sense for today's task, it's wrong — fall back to (1), (2), or (3).
6667
+
6668
+ User-preference embedding (important): when the user expressed a style/format/workflow preference, the update belongs in the SKILL.md body, not just in memory. Memory captures 'who the user is and what the current situation and state of your operations are'; skills capture 'how to do this class of task for this user'. When they complain about how you handled a task, the skill that governs that task needs to carry the lesson.
6669
+
6670
+ If you notice two existing skills that overlap, note it in your reply — the background curator handles consolidation at scale.
6671
+
6672
+ Protected skills (DO NOT edit these):
6673
+ • Bundled skills (shipped with the platform).
6674
+ • Hub-installed skills (installed via the platform hub).
6675
+ Pinned skills (marked via skill_manage pin) are read-only to THIS background review pass — the pinned write guard refuses background changes, so this pass may not update them. They also cannot be archived by any writer (the foreground included): remove the .pinned marker first. Foreground and delegated-subagent update/patch writes to pinned skills remain allowed.
6676
+ If the only skills that need updating are protected, say
6677
+ 'Nothing to save.' and stop.
6678
+
6679
+ Do NOT capture (these become persistent self-imposed constraints that bite you later when the environment changes):
6680
+ • Environment-dependent failures: missing binaries, fresh-install errors, post-migration path mismatches, 'command not found', unconfigured credentials, uninstalled packages. The user can fix these — they are not durable rules.
6681
+ • Negative claims about tools or features ('browser tools do not work', 'X tool is broken', 'cannot use Y from execute_code'). These harden into refusals the agent cites against itself for months after the actual problem was fixed.
6682
+ • Session-specific transient errors that resolved before the conversation ended. If retrying worked, the lesson is the retry pattern, not the original failure.
6683
+ • One-off task narratives. A user asking 'summarize today's market' or 'analyze this PR' is not a class of work that warrants a skill.
6684
+
6685
+ If a tool failed because of setup state, capture the FIX (install command, config step, env var to set) under an existing setup or troubleshooting skill — never 'this tool does not work' as a standalone constraint.
6686
+
6687
+ 'Nothing to save.' is a real option but should NOT be the default. If the session ran smoothly with no corrections and produced no new technique, state what you checked and stop. Otherwise, act.`;
5884
6688
  /**
5885
6689
  * Maintenance-subagent persona (design 011 §6). Constants pin the persona
5886
6690
  * with `{signal:id}` placeholders; the renderer substitutes names from the
@@ -6002,12 +6806,12 @@ function sha256(text) {
6002
6806
  function createPromptBundle(prompts) {
6003
6807
  const canonical = JSON.stringify({
6004
6808
  id: PROMPT_BUNDLE_ID,
6005
- version: 21,
6809
+ version: 22,
6006
6810
  prompts: Object.fromEntries(Object.entries(prompts).sort())
6007
6811
  });
6008
6812
  return Object.freeze({
6009
6813
  id: PROMPT_BUNDLE_ID,
6010
- version: 21,
6814
+ version: 22,
6011
6815
  prompts: Object.freeze({ ...prompts }),
6012
6816
  sha256: sha256(canonical)
6013
6817
  });
@@ -6026,10 +6830,10 @@ const PROMPT_BUNDLE = createPromptBundle({
6026
6830
  skillsGuidance: SKILLS_GUIDANCE
6027
6831
  });
6028
6832
  function verifyPromptBundle(bundle = PROMPT_BUNDLE) {
6029
- if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 21) return false;
6833
+ if (bundle.id !== PROMPT_BUNDLE_ID || bundle.version !== 22) return false;
6030
6834
  const canonical = JSON.stringify({
6031
6835
  id: PROMPT_BUNDLE_ID,
6032
- version: 21,
6836
+ version: 22,
6033
6837
  prompts: Object.fromEntries(Object.entries(bundle.prompts).sort())
6034
6838
  });
6035
6839
  return bundle.sha256 === sha256(canonical);
@@ -7012,6 +7816,7 @@ const CONTENT_ACTIONS = [
7012
7816
  "patch",
7013
7817
  "consolidate",
7014
7818
  "restructure",
7819
+ "extract",
7015
7820
  "restore",
7016
7821
  "merge"
7017
7822
  ];
@@ -7419,6 +8224,7 @@ function versionActionKind(action) {
7419
8224
  case "archive": return "archive";
7420
8225
  case "consolidate": return "consolidate";
7421
8226
  case "restructure": return "restructure";
8227
+ case "extract": return "extract";
7422
8228
  case "merge": return "merge";
7423
8229
  case "write_file": return "support-write";
7424
8230
  case "remove_file": return "support-remove";
@@ -9483,7 +10289,8 @@ const READ_REQUIRED_ACTIONS = Object.freeze([
9483
10289
  "delete",
9484
10290
  "write_file",
9485
10291
  "remove_file",
9486
- "restructure"
10292
+ "restructure",
10293
+ "extract"
9487
10294
  ]);
9488
10295
  /**
9489
10296
  * Does this one op write a skill the caller never read?
@@ -9694,95 +10501,6 @@ function skillRowCells(skill) {
9694
10501
  };
9695
10502
  }
9696
10503
  //#endregion
9697
- //#region lib/types/document-body.js
9698
- /**
9699
- * What the panel shows when it renders one version as a document.
9700
- *
9701
- * Two decisions, both made here so the preview and the rendered diff cannot drift apart:
9702
- *
9703
- * - FRONTMATTER IS NOT DOCUMENT TEXT. A SKILL.md opens with a YAML block; the renderer read its
9704
- * closing `---` as a setext underline, so the metadata line became the document's biggest heading
9705
- * and the real title sat below it (measured on the installed panel 2026-09-28). The block is
9706
- * stripped for DISPLAY only: the raw view and the body route still carry every byte, and the
9707
- * panel already shows the name and description on the row and in the left column.
9708
- * - STRIPPING FAILS OPEN. Only a block that the family's own reader accepts is removed. A document
9709
- * whose text merely starts with a thematic break, a block with no closing line, or a frontmatter
9710
- * whose body is empty is returned UNCHANGED — showing a document's first line is always better
9711
- * than showing nothing.
9712
- *
9713
- * `readingStats` exists for the other half of the reader's honesty: a preview that stops early has
9714
- * to be able to say HOW much it is not showing.
9715
- * @module @lmzhen/dsh-evolution-core/document-body
9716
- */
9717
- /**
9718
- * The document a reader sees: the body without its leading frontmatter block.
9719
- * @param content - the stored bytes of one version.
9720
- * @returns the text to render; unchanged when there is no frontmatter the reader accepts.
9721
- */
9722
- function displayBodyOf(content) {
9723
- const block = frontmatterBlock(content);
9724
- if (block === null) return content;
9725
- if (parseFrontmatter(content) === null) return content;
9726
- return block.lines.slice(block.end + 1).join("\n").replace(/^\s*\n/, "");
9727
- }
9728
- /**
9729
- * How much of a version the panel is showing.
9730
- * @param full - the whole stored text.
9731
- * @param shown - the part the route handed over.
9732
- * @returns the counts, so the panel can say what it is not showing.
9733
- */
9734
- function readingStats(full, shown) {
9735
- return {
9736
- totalChars: full.length,
9737
- shownChars: shown.length,
9738
- truncated: shown.length < full.length
9739
- };
9740
- }
9741
- /**
9742
- * The H2 sections of a body, in document order — the ONE scan (G3).
9743
- *
9744
- * Both the restructure candidate list and the store anchor validation call this function, so a
9745
- * candidate can only name a heading that exists EXACTLY ONCE and has content: the two rejections a
9746
- * hand-written anchor used to hit ("no such heading", "ambiguous anchor") stop being reachable.
9747
- * H2 is the section boundary (a section spans to the next H2 or EOF), matching the store.
9748
- * @param body - the markdown after the frontmatter block.
9749
- * @returns one row per H2 heading, with `count` filled in a second pass.
9750
- */
9751
- function h2Sections(body) {
9752
- const lines = body.split(String.fromCharCode(10));
9753
- const rows = [];
9754
- for (let i = 0; i < lines.length; i += 1) {
9755
- const match = /^#{2}\s+(.+?)\s*$/.exec(lines[i] ?? "");
9756
- if (match === null) continue;
9757
- const title = (match[1] ?? "").trim();
9758
- let end = lines.length;
9759
- for (let j = i + 1; j < lines.length; j += 1) if (/^#{2}\s/.test(lines[j] ?? "")) {
9760
- end = j;
9761
- break;
9762
- }
9763
- rows.push({
9764
- title,
9765
- start: i,
9766
- end,
9767
- count: 0
9768
- });
9769
- }
9770
- for (const row of rows) row.count = rows.filter((other) => other.title === row.title).length;
9771
- return rows;
9772
- }
9773
- /**
9774
- * The sections a restructure proposal may name: heading unique, section non-empty. Derived from
9775
- * `h2Sections`, i.e. from the SAME scan the store validates against.
9776
- * @param body - the markdown after the frontmatter block.
9777
- * @returns move-ready pairs in document order; empty when nothing qualifies.
9778
- */
9779
- function restructureCandidates(body) {
9780
- return h2Sections(body).filter((section) => section.count === 1 && section.end > section.start + 1).map((section) => ({
9781
- heading: section.title,
9782
- toFile: "references/" + section.title.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "") + ".md"
9783
- }));
9784
- }
9785
- //#endregion
9786
10504
  //#region lib/types/drift-signals.js
9787
10505
  /**
9788
10506
  * Library-level drift signals for the maintenance subagent (design 011).
@@ -10585,48 +11303,20 @@ function validateSupportPath(filePath) {
10585
11303
  }
10586
11304
  return null;
10587
11305
  }
10588
- /**
10589
- * Support-directory references in a markdown body (009 kernel): `references/…`,
10590
- * `templates/…`, `scripts/…`, `assets/…` relative links — any extension and
10591
- * nested paths (v7 audit P3-1: `.md`-only matching missed `scripts/run.sh` and
10592
- * `references/sub/x.md`, both of which dangle just like a `.md` link once the
10593
- * source package is archived). Pure — who checks them and what the verdict is
10594
- * belongs to the caller's context (a moved/appended body whose references
10595
- * travel with an archived source is a dangling link; a restructure pointer is
10596
- * a fresh link to a file written in the same plan). `..` traversal is a
10597
- * different defect class (path validation owns it) and is not a support link.
10598
- */
10599
- function supportRefs(content) {
10600
- const refs = [];
10601
- for (const match of content.matchAll(/\b(?:references|templates|scripts|assets)\/[A-Za-z0-9._/-]+/g)) {
10602
- const ref = match[0];
10603
- if (ref.includes("..")) continue;
10604
- refs.push(ref);
10605
- }
10606
- return refs;
10607
- }
10608
11306
  function planRestructureSections(body, moves) {
11307
+ const selection = selectSections(body, moves.map((move) => ({ heading: move.heading })));
11308
+ if (!selection.ok) return { error: selection.message };
10609
11309
  const lines = body.split("\n");
10610
- const sections = h2Sections(body);
10611
- const spans = [];
10612
- for (const move of moves) {
10613
- const wanted = move.heading.trim();
10614
- const matching = sections.filter((section) => section.title === wanted);
10615
- if (matching.length === 0) return { error: `no "## ${wanted}" heading in the body` };
10616
- if (matching.length > 1) return { error: `heading "## ${wanted}" appears ${matching.length} times (ambiguous anchor)` };
10617
- const section = matching[0];
10618
- if (section === void 0) return { error: `no "## ${wanted}" heading in the body` };
10619
- const { start, end } = section;
10620
- if (end === start + 1) return { error: `heading "## ${wanted}" has an empty section` };
10621
- if (spans.some((span) => start === span.start)) return { error: `heading "## ${wanted}" is moved twice` };
10622
- spans.push({
10623
- start,
10624
- end,
10625
- rel: move.toFile,
10626
- heading: wanted,
10627
- text: lines.slice(start, end).join("\n")
10628
- });
10629
- }
11310
+ const spans = selection.sections.map((section) => {
11311
+ const move = moves[section.requestIndex];
11312
+ return {
11313
+ start: section.start,
11314
+ end: section.end,
11315
+ rel: move?.toFile ?? "",
11316
+ heading: section.heading,
11317
+ text: section.text
11318
+ };
11319
+ });
10630
11320
  const byStart = new Map(spans.map((span) => [span.start, span]));
10631
11321
  const rebuilt = [];
10632
11322
  for (let i = 0; i < lines.length; i += 1) {
@@ -12721,48 +13411,245 @@ var SkillLibrary = class {
12721
13411
  * restructure (and future reference-mode consolidations) never implement
12722
13412
  * two-phase commit themselves.
12723
13413
  */
12724
- async applyTreeChange(plan) {
12725
- const name = plan.name.trim();
12726
- const badName = this.badName(name, { allowReserved: true });
12727
- if (badName) return {
13414
+ /**
13415
+ * Apply one G1 ExtractPlan as one cross-skill unit. This is a thin adapter:
13416
+ * selection, residual, pointer and support closure were already decided by
13417
+ * planExtract; this method only projects them into ordered kernel operations.
13418
+ * @param plan - pure plan from extract-plan.ts.
13419
+ * @param origin - write face recorded in the audit ledger.
13420
+ * @returns the kernel result; a refusal writes nothing.
13421
+ */
13422
+ async extract(plan, origin = this.authority.origin) {
13423
+ if (plan.outcome === "owner-gap" || plan.owner.mode !== "covered") return {
12728
13424
  ok: false,
12729
- message: badName
13425
+ message: "提取被拒绝:owner-gap 不是写操作。"
12730
13426
  };
12731
- const dir = this.dirOf(name);
12732
- if (!await this.io.readText(join(dir, "SKILL.md"))) return {
13427
+ if (plan.support.unresolved.length > 0 || plan.support.dangling.length > 0) return {
12733
13428
  ok: false,
12734
- message: `技能「${name}」不存在。`
13429
+ message: "提取被拒绝:support closure 存在 unresolved/dangling。"
12735
13430
  };
12736
- const protection = plan.protection === "write" ? await this.writeProtection(name, plan.origin) : plan.protection === "delete" ? await this.deleteProtection(name) : null;
12737
- if (protection) return {
13431
+ if (plan.parts.length === 0) return {
12738
13432
  ok: false,
12739
- message: `技能「${name}」受保护(${protection})。`
13433
+ message: "提取被拒绝:没有 part。"
13434
+ };
13435
+ const source = plan.source.trim();
13436
+ const target = plan.target.name.trim();
13437
+ if (source === target) return {
13438
+ ok: false,
13439
+ message: "提取被拒绝:source 与 target 相同。"
13440
+ };
13441
+ if (plan.target.exists !== true) return {
13442
+ ok: false,
13443
+ message: "提取被拒绝:target 不存在;owner-gap 不自动 create。"
13444
+ };
13445
+ const sourceDir = this.dirOf(source);
13446
+ const targetDir = this.dirOf(target);
13447
+ const sourceMd = await this.io.readText(join(sourceDir, "SKILL.md"));
13448
+ const targetMd = await this.io.readText(join(targetDir, "SKILL.md"));
13449
+ if (sourceMd === null) return {
13450
+ ok: false,
13451
+ message: "技能「" + source + "」不存在。"
13452
+ };
13453
+ if (targetMd === null) return {
13454
+ ok: false,
13455
+ message: "技能「" + target + "」不存在。"
13456
+ };
13457
+ if (plan.anchors !== void 0) {
13458
+ const sourceHash = contentHash(sourceMd);
13459
+ const targetHash = contentHash(targetMd);
13460
+ if (sourceHash !== plan.anchors.sourceMdSha256 || targetHash !== plan.anchors.targetMdSha256) return {
13461
+ ok: false,
13462
+ message: "提取被拒绝:source/target 在计划之后发生了变化;请重新计划。"
13463
+ };
13464
+ }
13465
+ const projected = projectExtractBodies(plan, sourceMd, targetMd);
13466
+ if (!projected.ok) return {
13467
+ ok: false,
13468
+ message: "提取被拒绝:" + projected.message
13469
+ };
13470
+ const sourceNew = projected.projection.sourceAfter;
13471
+ const targetNew = projected.projection.targetAfter;
13472
+ const ops = [];
13473
+ for (const move of [...plan.support.shared, ...plan.support.exclusive]) {
13474
+ const fromPath = join(sourceDir, ...move.from.split("/"));
13475
+ const toPath = join(targetDir, ...move.to.split("/"));
13476
+ const content = await this.io.readText(fromPath);
13477
+ if (content === null) return {
13478
+ ok: false,
13479
+ message: "提取被拒绝:support 文件不存在 " + move.from
13480
+ };
13481
+ const previous = await this.readForCas(toPath);
13482
+ ops.push({
13483
+ kind: "write",
13484
+ target: toPath,
13485
+ content,
13486
+ expected: previous,
13487
+ skill: target
13488
+ });
13489
+ }
13490
+ for (const file of plan.support.external) {
13491
+ if (file.content === void 0) return {
13492
+ ok: false,
13493
+ message: "提取被拒绝:external support 缺少已验证的内容 " + file.from
13494
+ };
13495
+ if (file.to.startsWith("/") || file.to.includes("..") || !/^(?:references|templates|scripts|assets)\//.test(file.to)) return {
13496
+ ok: false,
13497
+ message: "提取被拒绝:external target 不安全 " + file.to
13498
+ };
13499
+ const toPath = join(targetDir, ...file.to.split("/"));
13500
+ const previous = await this.readForCas(toPath);
13501
+ ops.push({
13502
+ kind: "write",
13503
+ target: toPath,
13504
+ content: file.content,
13505
+ expected: previous,
13506
+ skill: target
13507
+ });
13508
+ }
13509
+ ops.push({
13510
+ kind: "write",
13511
+ target: join(targetDir, "SKILL.md"),
13512
+ content: targetNew,
13513
+ expected: targetMd,
13514
+ skill: target
13515
+ });
13516
+ for (const move of plan.support.exclusive) {
13517
+ const sourcePath = join(sourceDir, ...move.from.split("/"));
13518
+ const previous = await this.readForCas(sourcePath);
13519
+ if (previous === null) return {
13520
+ ok: false,
13521
+ message: "提取被拒绝:待删除 support 不存在 " + move.from
13522
+ };
13523
+ ops.push({
13524
+ kind: "delete",
13525
+ target: sourcePath,
13526
+ expected: previous,
13527
+ skill: source
13528
+ });
13529
+ }
13530
+ ops.push({
13531
+ kind: "write",
13532
+ target: join(sourceDir, "SKILL.md"),
13533
+ content: sourceNew,
13534
+ expected: sourceMd,
13535
+ skill: source
13536
+ });
13537
+ return this.applyTreeChange({
13538
+ name: source,
13539
+ origin,
13540
+ protection: "write",
13541
+ roots: [{
13542
+ name: source,
13543
+ protection: "write"
13544
+ }, {
13545
+ name: target,
13546
+ protection: "write"
13547
+ }],
13548
+ writes: [],
13549
+ ops,
13550
+ auditAction: "extract",
13551
+ auditSummary: "extract " + plan.parts.map((part) => part.heading).join(", ") + " from " + source + " to " + target,
13552
+ eventAction: "extract"
13553
+ });
13554
+ }
13555
+ async applyTreeChange(plan) {
13556
+ const primaryName = plan.name.trim();
13557
+ const declared = plan.roots ?? [{
13558
+ name: primaryName,
13559
+ protection: plan.protection
13560
+ }];
13561
+ const roots = [];
13562
+ for (const root of declared) {
13563
+ const name = root.name.trim();
13564
+ if (roots.some((entry) => entry.name === name)) return {
13565
+ ok: false,
13566
+ message: "目录树变更声明了重复的根「" + name + "」。"
13567
+ };
13568
+ const badName = this.badName(name, { allowReserved: true });
13569
+ if (badName) return {
13570
+ ok: false,
13571
+ message: badName
13572
+ };
13573
+ const dir = this.dirOf(name);
13574
+ if (!await this.io.readText(join(dir, "SKILL.md"))) return {
13575
+ ok: false,
13576
+ message: "技能「" + name + "」不存在。"
13577
+ };
13578
+ const protection = root.protection === "write" ? await this.writeProtection(name, plan.origin) : root.protection === "delete" ? await this.deleteProtection(name) : null;
13579
+ if (protection) return {
13580
+ ok: false,
13581
+ message: "技能「" + name + "」受保护(" + protection + ")。"
13582
+ };
13583
+ roots.push({
13584
+ name,
13585
+ protection: root.protection,
13586
+ dir
13587
+ });
13588
+ }
13589
+ if (!roots.some((root) => root.name === primaryName)) return {
13590
+ ok: false,
13591
+ message: "目录树变更的 primary skill「" + primaryName + "」不在 roots 中。"
13592
+ };
13593
+ const provided = plan.ops ?? [...plan.writes.map((write) => ({
13594
+ kind: "write",
13595
+ ...write
13596
+ })), ...(plan.deletes ?? []).map((del) => ({
13597
+ kind: "delete",
13598
+ ...del
13599
+ }))];
13600
+ if (provided.length === 0) return {
13601
+ ok: false,
13602
+ message: "目录树变更为空。"
12740
13603
  };
12741
13604
  const landing = [];
12742
- for (const write of plan.writes) {
13605
+ for (const op of provided) {
13606
+ const owner = roots.find((root) => {
13607
+ const rel = relative(root.dir, op.target);
13608
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
13609
+ });
13610
+ if (owner === void 0) return {
13611
+ ok: false,
13612
+ message: "目标不属于任何声明的技能根:" + op.target
13613
+ };
13614
+ const skill = op.skill ?? owner.name;
13615
+ if (!roots.some((root) => root.name === skill)) return {
13616
+ ok: false,
13617
+ message: "目标声明的技能「" + skill + "」不在 roots 中:" + op.target
13618
+ };
12743
13619
  let previous;
12744
13620
  try {
12745
- previous = await this.io.readText(write.target);
13621
+ previous = await this.io.readText(op.target);
12746
13622
  } catch (error) {
12747
13623
  return {
12748
13624
  ok: false,
12749
- message: `拒绝目录树变更:无法安全预读 ${write.target}(${error instanceof Error ? error.message : String(error)});未执行任何写入`
13625
+ message: "拒绝目录树变更:无法安全预读 " + op.target + "(" + (error instanceof Error ? error.message : String(error)) + ");未执行任何写入"
12750
13626
  };
12751
13627
  }
12752
- if (Buffer.byteLength(write.content, "utf8") > this.limits.maxSkillFileBytes) return {
12753
- ok: false,
12754
- message: `写入超过 ${this.limits.maxSkillFileBytes} 字节:${write.target}`
12755
- };
12756
- const threat = this.contentThreatBlock(write.content);
12757
- if (threat) return {
13628
+ if (op.kind === "delete" && previous === null) return {
12758
13629
  ok: false,
12759
- message: threat
13630
+ message: "拒绝目录树变更:删除目标不存在 " + op.target
12760
13631
  };
13632
+ if (op.kind === "write") {
13633
+ if (Buffer.byteLength(op.content, "utf8") > this.limits.maxSkillFileBytes) return {
13634
+ ok: false,
13635
+ message: "写入超过 " + this.limits.maxSkillFileBytes + " 字节:" + op.target
13636
+ };
13637
+ const threat = this.contentThreatBlock(op.content);
13638
+ if (threat) return {
13639
+ ok: false,
13640
+ message: threat
13641
+ };
13642
+ }
13643
+ const rel = relative(owner.dir, op.target).split(String.fromCharCode(92)).join("/");
12761
13644
  landing.push({
12762
- target: write.target,
12763
- content: write.content,
13645
+ kind: op.kind,
13646
+ target: op.target,
13647
+ content: op.kind === "write" ? op.content : null,
12764
13648
  previous,
12765
- expected: write.expected
13649
+ expected: op.expected,
13650
+ skill,
13651
+ dir: owner.dir,
13652
+ rel
12766
13653
  });
12767
13654
  }
12768
13655
  const written = [];
@@ -12781,11 +13668,12 @@ var SkillLibrary = class {
12781
13668
  drift.seen = true;
12782
13669
  return current;
12783
13670
  }
12784
- return entry.content;
13671
+ return entry.kind === "delete" ? null : entry.content;
12785
13672
  });
12786
- if (!ran.done) throw new Error(`internal error: the write transaction for ${entry.target} did not invoke the task; nothing was written`);
12787
- if (drift.seen) throw new Error(`concurrent modification detected: ${entry.target} changed after the plan was computed (a concurrent writer won the race); no further writes were performed`);
12788
- } else await this.io.writeText(entry.target, entry.content);
13673
+ if (!ran.done) throw new Error("internal error: the write transaction for " + entry.target + " did not invoke the task; nothing was written");
13674
+ if (drift.seen) throw new Error("concurrent modification detected: " + entry.target + " changed after the plan was computed (a concurrent writer won the race); no further writes were performed");
13675
+ } else if (entry.kind === "delete") await this.io.remove(entry.target);
13676
+ else await this.io.writeText(entry.target, entry.content ?? "");
12789
13677
  } catch (error) {
12790
13678
  if (!isCommittedOnly(error)) throw error;
12791
13679
  durabilityWarning = error instanceof Error ? error.message : String(error);
@@ -12806,42 +13694,43 @@ var SkillLibrary = class {
12806
13694
  const reason = error instanceof Error ? error.message : String(error);
12807
13695
  if (stuck.length === 0) return {
12808
13696
  ok: false,
12809
- message: `目录树变更失败,已回滚:${reason}`
13697
+ message: "目录树变更失败,已回滚:" + reason
12810
13698
  };
12811
13699
  return {
12812
13700
  ok: false,
12813
- message: `目录树变更失败:${reason}。回滚无法恢复 ${stuck.join(", ")}:可恢复性由当前快照决定:${await this.snapshotRecoveryAvailability()} —— 若上面不是「最新快照」,请重新应用这次变更。`
13701
+ message: "目录树变更失败:" + reason + "。回滚无法恢复 " + stuck.join(", ") + ":可恢复性由当前快照决定:" + await this.snapshotRecoveryAvailability() + " —— 若上面不是「最新快照」,请重新应用这次变更。"
12814
13702
  };
12815
13703
  }
12816
13704
  const unitId = plan.unitId ?? "u-" + Date.now().toString(36) + "-" + (nextUnitSeq += 1).toString(36);
12817
- for (const land of landing) {
12818
- const rel = land.target.split(/[\\/]/).pop() === "SKILL.md" ? void 0 : land.target.slice(dir.length + 1).split("\\").join("/");
12819
- const unitHead = land === landing[0] ? plan.unitHead ?? {
12820
- moves: landing.filter((l) => l.target.split(/[\\/]/).pop() !== "SKILL.md").map((l) => {
12821
- const p = l.target.slice(dir.length + 1).split(String.fromCharCode(92)).join(String.fromCharCode(47));
12822
- return {
12823
- path: p,
12824
- kind: l.previous === null ? "created" : "moved",
12825
- to: p,
12826
- ...l.previous === null ? {} : { beforeRef: contentHash(l.previous) },
12827
- afterRef: contentHash(l.content)
12828
- };
12829
- }).filter((m, idx, all) => all.map((x) => x.path).lastIndexOf(m.path) === idx),
12830
- sources: [{ skill: name }]
12831
- } : void 0;
12832
- const auditedBefore = land.previous;
12833
- const auditedAfter = land.content;
12834
- await this.audit(name, plan.auditAction, auditedBefore, auditedAfter, plan.auditSummary, rel, unitId, unitHead, plan.origin);
13705
+ const defaultHead = {
13706
+ moves: landing.filter((entry) => basename(entry.target) !== "SKILL.md").map((entry) => ({
13707
+ path: entry.rel,
13708
+ kind: entry.kind === "delete" ? "removed" : entry.previous === null ? "created" : "moved",
13709
+ ...entry.previous === null ? {} : { beforeRef: contentHash(entry.previous) },
13710
+ ...entry.kind === "delete" || entry.content === null ? {} : { afterRef: contentHash(entry.content) }
13711
+ })),
13712
+ sources: roots.map((root) => ({ skill: root.name }))
13713
+ };
13714
+ const affected = /* @__PURE__ */ new Set();
13715
+ for (let index = 0; index < landing.length; index += 1) {
13716
+ const entry = landing[index];
13717
+ if (entry === void 0) continue;
13718
+ await this.audit(entry.skill, plan.auditAction, entry.previous, entry.kind === "delete" ? null : entry.content, plan.auditSummary, entry.rel === "SKILL.md" ? void 0 : entry.rel, unitId, index === 0 ? plan.unitHead ?? defaultHead : void 0, plan.origin);
13719
+ affected.add(entry.skill);
12835
13720
  }
12836
- this.notifyMutation({
12837
- action: plan.eventAction,
12838
- name,
12839
- skillDir: dir
12840
- });
13721
+ for (const skill of affected) {
13722
+ const dir = roots.find((root) => root.name === skill)?.dir;
13723
+ if (dir !== void 0) this.notifyMutation({
13724
+ action: plan.eventAction,
13725
+ name: skill,
13726
+ skillDir: dir
13727
+ });
13728
+ }
13729
+ const primaryDir = roots.find((root) => root.name === primaryName)?.dir ?? roots[0]?.dir ?? "";
12841
13730
  return {
12842
13731
  ok: true,
12843
- message: durabilityWarning === "" ? `${plan.eventAction} "${name}" succeeded.` : `${plan.eventAction} "${name}" succeeded (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:${durabilityWarning})`,
12844
- path: dir
13732
+ message: durabilityWarning === "" ? plan.eventAction + " \"" + primaryName + "\" succeeded." : plan.eventAction + " \"" + primaryName + "\" succeeded (警告:写入已落盘,但目录 fsync 失败 —— 持久性未确认:" + durabilityWarning + ")",
13733
+ path: primaryDir
12845
13734
  };
12846
13735
  }
12847
13736
  /**
@@ -15892,4 +16781,4 @@ function isIgnorableEvent(event) {
15892
16781
  return classifySessionEvent(event) !== "model";
15893
16782
  }
15894
16783
  //#endregion
15895
- 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, UNIT_JOURNAL_PHASES, UNIT_JOURNAL_SCHEMA_VERSION, UNIT_STORAGE_SCHEMA_VERSION, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, UnitTransactions, VERSION_SUMMARY_MAX_CHARS, absentHostServices, acquireTreeWriteLock, admissionVerdict, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assertUnitJournalTransition, 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, ensureSkillFrontmatter, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, executeUnitUndo, 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, planUnitUndoAcross, policyStageLimits, probeAbsent, probeChannelState, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, protectedSkillNamesOf, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonObject, readNumberParam, readParam, readTreeLock, readUnitEvidence, readUnitIndex, readUnitJournal, readUnitReceipt, readUnitSnapshot, 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, revisionAnchor, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runWriteGates, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopeOfNearestManifest, scopedPackageName, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillLibraryUnitBackend, skillReadNameOf, skillRowCells, skillStateKey, skillWriteGateContext, skillsRoot, staleRouteStubs, subagentChannelState, supportFileReadOf, supportRefs, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, treeLockFile, trimPatternBoundaries, unhookedSupportPointers, unitEvidenceDigest, unitEvidencePath, unitIndexPath, unitJournalPath, unitJournalPhaseRank, unitMembersAreComplete, unitReceiptPath, unitSnapshotPath, unitSnapshotsDir, unitStorageKey, unwrapLive, updateSuppressedNames, upsertUnitIndex, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, validateSupportPath, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, withTreeWrite, withTreeWriteLock, writeDurableTmp, writePathSymbols, writeUnitEvidence, writeUnitJournal, writeUnitReceipt, writeUnitSnapshot, yamlPlainScalarNeedsQuotes };
16784
+ 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, EXTRACT_PROMPT_V1, EXTRACT_PROMPT_VERSION, 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, UNIT_JOURNAL_PHASES, UNIT_JOURNAL_SCHEMA_VERSION, UNIT_STORAGE_SCHEMA_VERSION, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, UnitTransactions, VERSION_SUMMARY_MAX_CHARS, absentHostServices, acquireTreeWriteLock, admissionVerdict, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assertUnitJournalTransition, 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, ensureSkillFrontmatter, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, executeUnitUndo, externalRefs, filterUnreadSkillOps, findDriftSignal, findOwnerCandidate, 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, ownerCandidatesFromSummaries, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planExtract, planNamespaceMigration, planReferenceRewrite, planRehoming, planUnitUndoAcross, policyStageLimits, probeAbsent, probeChannelState, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectExtractBodies, projectionText, proposalFromPlan, proposalReportDir, proposalReportName, protectedSkillNamesOf, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readExternalSupport, readHistoryIndex, readJsonObject, readNumberParam, readParam, readProposalReports, readTreeLock, readUnitEvidence, readUnitIndex, readUnitJournal, readUnitReceipt, readUnitSnapshot, 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, revisionAnchor, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runWriteGates, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopeOfNearestManifest, scopedPackageName, scopedProbeReport, selectSections, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillLibraryUnitBackend, skillReadNameOf, skillRowCells, skillStateKey, skillWriteGateContext, skillsRoot, staleRouteStubs, subagentChannelState, supportFileReadOf, supportRefs, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, treeLockFile, trimPatternBoundaries, unhookedSupportPointers, unitEvidenceDigest, unitEvidencePath, unitIndexPath, unitJournalPath, unitJournalPhaseRank, unitMembersAreComplete, unitReceiptPath, unitSnapshotPath, unitSnapshotsDir, unitStorageKey, unwrapLive, updateSuppressedNames, upsertUnitIndex, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, validateSupportPath, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, withTreeWrite, withTreeWriteLock, writeDurableTmp, writePathSymbols, writeProposalReport, writeUnitEvidence, writeUnitJournal, writeUnitReceipt, writeUnitSnapshot, yamlPlainScalarNeedsQuotes };