@lmzhen/dsh-evolution-merge 0.30.25 → 0.30.28

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
@@ -8217,7 +8217,7 @@ function buildValidationPlan(input) {
8217
8217
  const staged = new Set(files.map((file) => file.path));
8218
8218
  const manifest = new Set(manifestFiles);
8219
8219
  const forwardRefs = [...refs].filter((ref) => !staged.has(ref)).sort();
8220
- const reverseRefs = files.map((file) => file.path).filter((path) => !refs.has(path) && manifest.has(path)).sort();
8220
+ const reverseRefs = files.map((file) => file.path).filter((path) => path !== "SKILL.md" && !refs.has(path) && manifest.has(path)).sort();
8221
8221
  const orphanFiles = [...new Set(input.existingSupport ?? [])].filter((path) => !staged.has(path)).sort();
8222
8222
  const byPath = /* @__PURE__ */ new Map();
8223
8223
  const supportSources = input.supportSources ?? [];
@@ -8234,12 +8234,14 @@ function buildValidationPlan(input) {
8234
8234
  path,
8235
8235
  sources: value.sources
8236
8236
  }));
8237
- const sourceThreatFiles = (input.cards ?? []).flatMap((card) => (card.files ?? []).filter((file) => scanContentThreats(file.content) !== null).map((file) => card.name + "/" + file.path));
8237
+ const participants = input.participants === void 0 ? void 0 : new Set(input.participants);
8238
+ const participantCards = participants === void 0 ? input.cards ?? [] : (input.cards ?? []).filter((card) => participants.has(card.name));
8239
+ const sourceThreatFiles = participantCards.flatMap((card) => (card.files ?? []).filter((file) => scanContentThreats(file.content) !== null).map((file) => card.name + "/" + file.path));
8238
8240
  const threat = {
8239
- body: scanContentThreats(input.tree.body) !== null || (input.cards ?? []).some((card) => scanContentThreats(card.body) !== null),
8241
+ body: scanContentThreats(input.tree.body) !== null || participantCards.some((card) => scanContentThreats(card.body) !== null),
8240
8242
  files: [...files.filter((file) => scanContentThreats(file.content) !== null).map((file) => file.path), ...sourceThreatFiles]
8241
8243
  };
8242
- const sourceRevisions = input.sourceRevisions ?? Object.fromEntries((input.cards ?? []).map((card) => [card.name, digest(card.body)]));
8244
+ const sourceRevisions = input.sourceRevisions ?? Object.fromEntries(participantCards.map((card) => [card.name, digest(card.body)]));
8243
8245
  const revision = digest(JSON.stringify({
8244
8246
  target: input.target,
8245
8247
  body: digest(input.tree.body),
@@ -8377,7 +8379,8 @@ async function runAuthoring(options) {
8377
8379
  message: "target must be a bare skill name: " + options.target
8378
8380
  };
8379
8381
  const targetCard = options.cards.find((card) => card.name === options.target);
8380
- const mode = options.mode ?? (targetCard === void 0 ? "create-umbrella" : "merge-into-existing");
8382
+ const requestedMode = options.mode ?? (targetCard === void 0 ? "create-umbrella" : "merge-into-existing");
8383
+ const mode = requestedMode === "merge-into-existing" && targetCard === void 0 ? "create-umbrella" : requestedMode;
8381
8384
  const createMode = mode === "create-umbrella";
8382
8385
  if (!createMode && targetCard !== void 0) {
8383
8386
  if (targetCard.managed !== true) return {
@@ -8472,6 +8475,7 @@ async function runAuthoring(options) {
8472
8475
  source: "staged"
8473
8476
  })),
8474
8477
  manifestFiles,
8478
+ ...options.sources === void 0 ? {} : { participants: [...options.sources, options.target] },
8475
8479
  existingSupport: (options.cards.find((card) => card.name === options.target)?.files ?? []).map((file) => file.path)
8476
8480
  });
8477
8481
  const findings = [...validateStagedTree(tree.value, limitsSnapshot.skillLimits), ...validationPlan.findings];
@@ -8889,6 +8893,11 @@ async function runNewChildAuthoring(options, row, decision, signal) {
8889
8893
  source: "staged"
8890
8894
  })),
8891
8895
  manifestFiles,
8896
+ participants: [
8897
+ row.a,
8898
+ row.b,
8899
+ decision.target
8900
+ ],
8892
8901
  existingSupport: (options.cards.find((card) => card.name === decision.target)?.files ?? []).map((file) => file.path),
8893
8902
  supportSources: options.cards.filter((card) => card.name === row.a || card.name === row.b).flatMap((card) => (card.files ?? []).map((file) => ({
8894
8903
  path: file.path,
@@ -9080,6 +9089,11 @@ function failRun(stage, hint, next, result) {
9080
9089
  text: formatMergeError(result.error)
9081
9090
  };
9082
9091
  }
9092
+ async function hostOnlyFailure(verb, capabilities) {
9093
+ if (capabilities === void 0) return void 0;
9094
+ const state = await capabilities.hostOnly();
9095
+ return state === void 0 ? void 0 : fail(verb, "e-323", "host data plane unavailable: " + state.code + (state.detail === void 0 ? "" : " (" + state.detail + ")"), "run /evolution merge doctor");
9096
+ }
9083
9097
  /**
9084
9098
  * The approved/fuse row that already holds a target. Pair/approve move a row to
9085
9099
  * approved/fuse BEFORE their run starts, so this row IS the in-flight claim a second
@@ -9162,6 +9176,8 @@ function createMergeRuntime(deps) {
9162
9176
  text: "merge: 技能名不能包含 provider/name 或 provider:name 语法:" + command.a + " / " + command.b + " (provider-conflict)\nusage: /evolution merge pair <a> <b>"
9163
9177
  };
9164
9178
  if (agentless) return fail("pair", "e-305", errorText("e-305-this-invocation-carries-no", { a1: "pair" }), "在 GUI 或 CLI 的会话里运行 /evolution merge pair " + command.a + " " + command.b);
9179
+ const pairHost = await hostOnlyFailure("pair", deps.hostCapabilities);
9180
+ if (pairHost !== void 0) return pairHost;
9165
9181
  const pairKeyValue = pairKey(command.a, command.b);
9166
9182
  const inFlight = inFlightPairRun(await readRunMetas(deps.io, deps.home), pairKeyValue);
9167
9183
  let ownerActive = false;
@@ -9239,6 +9255,8 @@ function createMergeRuntime(deps) {
9239
9255
  }
9240
9256
  if (command.verb === "scan") {
9241
9257
  if (agentless) return fail("scan", "e-305", errorText("e-305-this-invocation-carries-no", { a1: "scan" }), "在 GUI 或 CLI 的会话里运行 /evolution merge scan");
9258
+ const scanHost = await hostOnlyFailure("scan", deps.hostCapabilities);
9259
+ if (scanHost !== void 0) return scanHost;
9242
9260
  const signal = new AbortController().signal;
9243
9261
  const cards = await deps.cards(signal);
9244
9262
  const blocked = /* @__PURE__ */ new Set();
@@ -9756,6 +9774,31 @@ function createMergeRuntime(deps) {
9756
9774
  } };
9757
9775
  }
9758
9776
  //#endregion
9777
+ //#region lib/types/host-capabilities.js
9778
+ /**
9779
+ * The host's data-plane capability as ONE value shared by snapshot, doctor, HTTP and run preflight.
9780
+ * The probe is injected so the Cordis wiring stays in index.ts; a rejected probe is the same E-323
9781
+ * answer as an unavailable one, never an unhandled promise.
9782
+ * @module @lmzhen/dsh-evolution-merge
9783
+ */
9784
+ /**
9785
+ * Wrap one probe with the shared error normalization.
9786
+ * @param probe - the platform-specific availability check.
9787
+ * @returns the capability value consumed by snapshot and runtime.
9788
+ */
9789
+ function createHostCapabilities(probe) {
9790
+ return { hostOnly: async () => {
9791
+ try {
9792
+ return await probe();
9793
+ } catch (error) {
9794
+ return {
9795
+ code: "E-323",
9796
+ detail: error instanceof Error ? error.message : String(error)
9797
+ };
9798
+ }
9799
+ } };
9800
+ }
9801
+ //#endregion
9759
9802
  //#region lib/types/params.js
9760
9803
  /**
9761
9804
  * Merge parameter resolution. E2/E3 values arrive as one JSON object; this is the only
@@ -10534,6 +10577,16 @@ function apply(ctx, rawConfig = {}) {
10534
10577
  });
10535
10578
  const platformChannel = createSubagentChannel(ctx);
10536
10579
  const channel = subagentChannelAdapter(platformChannel);
10580
+ const hostCapabilities = createHostCapabilities(async () => {
10581
+ const state = await platformChannel.prepare({
10582
+ kind: "reviewer",
10583
+ allow: ["skill"]
10584
+ });
10585
+ return state.kind === "unavailable" ? {
10586
+ code: "E-323",
10587
+ detail: state.reason
10588
+ } : void 0;
10589
+ });
10537
10590
  const stage = new StageToolRegistry();
10538
10591
  const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
10539
10592
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
@@ -10597,23 +10650,7 @@ function apply(ctx, rawConfig = {}) {
10597
10650
  runs: runs.runs("merge"),
10598
10651
  approval: params.mergeApproval,
10599
10652
  platformWrite,
10600
- hostOnly: async () => {
10601
- try {
10602
- const state = await platformChannel.prepare({
10603
- kind: "reviewer",
10604
- allow: ["skill"]
10605
- });
10606
- return state.kind === "unavailable" ? {
10607
- code: "E-323",
10608
- detail: state.reason
10609
- } : void 0;
10610
- } catch (error) {
10611
- return {
10612
- code: "E-323",
10613
- detail: error instanceof Error ? error.message : String(error)
10614
- };
10615
- }
10616
- },
10653
+ hostOnly: hostCapabilities.hostOnly,
10617
10654
  live: async () => liveRowFactsOf(await readSkillCards(skills)),
10618
10655
  history: async () => {
10619
10656
  const items = /* @__PURE__ */ new Map();
@@ -10707,6 +10744,7 @@ function apply(ctx, rawConfig = {}) {
10707
10744
  parent: void 0,
10708
10745
  allow: ["merge_stage"],
10709
10746
  timeoutMinutes: params.mergeRunTimeoutMinutes,
10747
+ hostCapabilities,
10710
10748
  isBlocked: async (name) => {
10711
10749
  return await ctx.get("evolutionCurator")?.isBlocked?.(name) === true;
10712
10750
  },
@@ -10,7 +10,12 @@ export async function runAuthoring(options) {
10
10
  if (options.target.includes('/') || options.target.includes(':'))
11
11
  return { ok: false, code: 'target-invalid', message: 'target must be a bare skill name: ' + options.target };
12
12
  const targetCard = options.cards.find(card => card.name === options.target);
13
- const mode = options.mode ?? (targetCard === undefined ? 'create-umbrella' : 'merge-into-existing');
13
+ // A decision can name a fresh target yet say merge-into-existing (the model's vocabulary is
14
+ // about intent, not existence). The target card is the authoritative existence fact: when it is
15
+ // absent, the only safe mode is create-umbrella; otherwise commit-assembly correctly refuses to
16
+ // re-create a target that disappeared later.
17
+ const requestedMode = options.mode ?? (targetCard === undefined ? 'create-umbrella' : 'merge-into-existing');
18
+ const mode = requestedMode === 'merge-into-existing' && targetCard === undefined ? 'create-umbrella' : requestedMode;
14
19
  const createMode = mode === 'create-umbrella';
15
20
  if (!createMode && targetCard !== undefined) {
16
21
  if (targetCard.managed !== true)
@@ -60,7 +65,7 @@ export async function runAuthoring(options) {
60
65
  const limitsSnapshot = limitsSnapshotOf(options.limits ?? DEFAULT_SKILL_LIMITS);
61
66
  const participants = new Set(options.sources ?? []);
62
67
  const supportSources = options.cards.filter(card => participants.has(card.name)).flatMap(card => (card.files ?? []).map(file => ({ path: file.path, content: file.content, source: card.name })));
63
- const validationPlan = buildValidationPlan({ tree: tree.value, target: options.target, limits: limitsSnapshot, cards: options.cards, supportSources, ...(options.promptVersion === undefined ? {} : { promptVersion: options.promptVersion }), ...(options.promptHash === undefined ? {} : { promptHash: options.promptHash }), supportFiles: tree.value.files.map(file => ({ path: file.path, content: file.content, source: 'staged' })), manifestFiles, existingSupport: (options.cards.find(card => card.name === options.target)?.files ?? []).map(file => file.path) });
68
+ const validationPlan = buildValidationPlan({ tree: tree.value, target: options.target, limits: limitsSnapshot, cards: options.cards, supportSources, ...(options.promptVersion === undefined ? {} : { promptVersion: options.promptVersion }), ...(options.promptHash === undefined ? {} : { promptHash: options.promptHash }), supportFiles: tree.value.files.map(file => ({ path: file.path, content: file.content, source: 'staged' })), manifestFiles, ...(options.sources === undefined ? {} : { participants: [...options.sources, options.target] }), existingSupport: (options.cards.find(card => card.name === options.target)?.files ?? []).map(file => file.path) });
64
69
  const findings = [...validateStagedTree(tree.value, limitsSnapshot.skillLimits), ...validationPlan.findings];
65
70
  const lengths = { bodyBytes: Buffer.byteLength(tree.value.body, 'utf8'), fileBytes: tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0), totalBytes: Buffer.byteLength(tree.value.body, 'utf8') + tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) };
66
71
  if (hasBlockingFinding(findings))
@@ -61,5 +61,7 @@ export declare function buildValidationPlan(input: {
61
61
  }[];
62
62
  readonly promptVersion?: string;
63
63
  readonly promptHash?: string;
64
+ /** Skills that participate in this merge (pair sources and target); omit for legacy all-card behavior. */
65
+ readonly participants?: readonly string[];
64
66
  }): ValidationPlan;
65
67
  //# sourceMappingURL=validation-plan.d.ts.map
@@ -10,7 +10,9 @@ export function buildValidationPlan(input) {
10
10
  const staged = new Set(files.map(file => file.path));
11
11
  const manifest = new Set(manifestFiles);
12
12
  const forwardRefs = [...refs].filter(ref => !staged.has(ref)).sort();
13
- const reverseRefs = files.map(file => file.path).filter(path => !refs.has(path) && manifest.has(path)).sort();
13
+ // SKILL.md is the body itself, never a support reference; a child may list it in the
14
+ // authoring manifest, but it must not require the body to reference itself.
15
+ const reverseRefs = files.map(file => file.path).filter(path => path !== 'SKILL.md' && !refs.has(path) && manifest.has(path)).sort();
14
16
  const existing = new Set(input.existingSupport ?? []);
15
17
  const orphanFiles = [...existing].filter(path => !staged.has(path)).sort();
16
18
  const byPath = new Map();
@@ -25,9 +27,11 @@ export function buildValidationPlan(input) {
25
27
  byPath.set(file.path, entry);
26
28
  }
27
29
  const supportConflicts = [...byPath.entries()].filter(([, value]) => value.contents.size > 1).map(([path, value]) => ({ path, sources: value.sources }));
28
- const sourceThreatFiles = (input.cards ?? []).flatMap(card => (card.files ?? []).filter(file => scanContentThreats(file.content) !== null).map(file => card.name + '/' + file.path));
29
- const threat = { body: scanContentThreats(input.tree.body) !== null || (input.cards ?? []).some(card => scanContentThreats(card.body) !== null), files: [...files.filter(file => scanContentThreats(file.content) !== null).map(file => file.path), ...sourceThreatFiles] };
30
- const sourceRevisions = input.sourceRevisions ?? Object.fromEntries((input.cards ?? []).map(card => [card.name, digest(card.body)]));
30
+ const participants = input.participants === undefined ? undefined : new Set(input.participants);
31
+ const participantCards = participants === undefined ? (input.cards ?? []) : (input.cards ?? []).filter(card => participants.has(card.name));
32
+ const sourceThreatFiles = participantCards.flatMap(card => (card.files ?? []).filter(file => scanContentThreats(file.content) !== null).map(file => card.name + '/' + file.path));
33
+ const threat = { body: scanContentThreats(input.tree.body) !== null || participantCards.some(card => scanContentThreats(card.body) !== null), files: [...files.filter(file => scanContentThreats(file.content) !== null).map(file => file.path), ...sourceThreatFiles] };
34
+ const sourceRevisions = input.sourceRevisions ?? Object.fromEntries(participantCards.map(card => [card.name, digest(card.body)]));
31
35
  const revision = digest(JSON.stringify({ target: input.target, body: digest(input.tree.body), files: files.map(file => [file.path, file.digest]), q09: { forwardRefs, reverseRefs, orphanFiles }, supportConflicts, threat, sourceRevisions, targetRevision: input.targetRevision ?? null }));
32
36
  const findings = [];
33
37
  const stageDigest = digest(JSON.stringify(files.map(file => [file.path, file.digest])));
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The host's data-plane capability as ONE value shared by snapshot, doctor, HTTP and run preflight.
3
+ * The probe is injected so the Cordis wiring stays in index.ts; a rejected probe is the same E-323
4
+ * answer as an unavailable one, never an unhandled promise.
5
+ * @module @lmzhen/dsh-evolution-merge
6
+ */
7
+ /** The one unavailable answer the merge surface understands. */
8
+ export interface HostOnlyUnavailable {
9
+ readonly code: 'E-323';
10
+ readonly detail?: string;
11
+ }
12
+ /** A host capability probe; undefined means the data plane is usable. */
13
+ export interface HostCapabilities {
14
+ readonly hostOnly: () => Promise<HostOnlyUnavailable | undefined>;
15
+ }
16
+ /**
17
+ * Wrap one probe with the shared error normalization.
18
+ * @param probe - the platform-specific availability check.
19
+ * @returns the capability value consumed by snapshot and runtime.
20
+ */
21
+ export declare function createHostCapabilities(probe: () => Promise<HostOnlyUnavailable | undefined>): HostCapabilities;
22
+ //# sourceMappingURL=host-capabilities.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The host's data-plane capability as ONE value shared by snapshot, doctor, HTTP and run preflight.
3
+ * The probe is injected so the Cordis wiring stays in index.ts; a rejected probe is the same E-323
4
+ * answer as an unavailable one, never an unhandled promise.
5
+ * @module @lmzhen/dsh-evolution-merge
6
+ */
7
+ /**
8
+ * Wrap one probe with the shared error normalization.
9
+ * @param probe - the platform-specific availability check.
10
+ * @returns the capability value consumed by snapshot and runtime.
11
+ */
12
+ export function createHostCapabilities(probe) {
13
+ return {
14
+ hostOnly: async () => {
15
+ try {
16
+ return await probe();
17
+ }
18
+ catch (error) {
19
+ return { code: 'E-323', detail: error instanceof Error ? error.message : String(error) };
20
+ }
21
+ },
22
+ };
23
+ }
24
+ //# sourceMappingURL=host-capabilities.js.map
@@ -31,6 +31,7 @@ import { pairKey, rowIdOf } from "./contracts.js";
31
31
  import { makeMergeRoutes } from "./merge-routes.js";
32
32
  import { createMergeCommand, runMergeCommand } from "./merge-command.js";
33
33
  import { createMergeRuntime } from "./runtime.js";
34
+ import { createHostCapabilities } from "./host-capabilities.js";
34
35
  import { resolveMergeParams } from "./params.js";
35
36
  import { subagentChannelAdapter } from "./adapters/channel.js";
36
37
  import { StageToolRegistry } from "./stage-tool-registry.js";
@@ -234,6 +235,10 @@ export function apply(ctx, rawConfig = {}) {
234
235
  }).catch((error) => { ctx.logger.warn('merge state readiness failed: ' + (error instanceof Error ? error.message : String(error))); });
235
236
  const platformChannel = createSubagentChannel(ctx);
236
237
  const channel = subagentChannelAdapter(platformChannel);
238
+ const hostCapabilities = createHostCapabilities(async () => {
239
+ const state = await platformChannel.prepare({ kind: 'reviewer', allow: ['skill'] });
240
+ return state.kind === 'unavailable' ? { code: 'E-323', detail: state.reason } : undefined;
241
+ });
237
242
  const stage = new StageToolRegistry();
238
243
  const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
239
244
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
@@ -276,15 +281,7 @@ export function apply(ctx, rawConfig = {}) {
276
281
  });
277
282
  source = {
278
283
  async snapshot() {
279
- return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite, hostOnly: async () => {
280
- try {
281
- const state = await platformChannel.prepare({ kind: 'reviewer', allow: ['skill'] });
282
- return state.kind === 'unavailable' ? { code: 'E-323', detail: state.reason } : undefined;
283
- }
284
- catch (error) {
285
- return { code: 'E-323', detail: error instanceof Error ? error.message : String(error) };
286
- }
287
- }, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
284
+ return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite, hostOnly: hostCapabilities.hostOnly, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
288
285
  const items = new Map();
289
286
  const receipts = [...await readCommitReceipts(io, home)].sort((a, b) => a.at - b.at);
290
287
  const generationOf = new Map(receipts.map((receipt, index) => [receipt.runId, index + 1]));
@@ -331,6 +328,7 @@ export function apply(ctx, rawConfig = {}) {
331
328
  io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite,
332
329
  ...(defaultModel === undefined ? {} : { model: defaultModel.currentSelection().model }),
333
330
  channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
331
+ hostCapabilities,
334
332
  isBlocked: async (name) => {
335
333
  const curator = ctx.get('evolutionCurator');
336
334
  return (await curator?.isBlocked?.(name)) === true;
@@ -90,7 +90,7 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
90
90
  if (missing !== null)
91
91
  return { kind: 'failed', failure: { stage: 'author', code: 'merge-manifest-missing', message: 'manifest file missing from staged tree: ' + missing, retryable: true, retryFrom: 'agent2' } };
92
92
  const limitsSnapshot = limitsSnapshotOf(options.limits ?? DEFAULT_SKILL_LIMITS);
93
- const validationPlan = buildValidationPlan({ tree: tree.value, target: decision.target, limits: limitsSnapshot, cards: options.cards, promptVersion: MERGE_PROMPT_VERSION, promptHash: promptHashOf(buildRound2Prompt(row, decision)), supportFiles: tree.value.files.map(file => ({ path: file.path, content: file.content, source: 'staged' })), manifestFiles, existingSupport: (options.cards.find(card => card.name === decision.target)?.files ?? []).map(file => file.path), supportSources: options.cards.filter(card => card.name === row.a || card.name === row.b).flatMap(card => (card.files ?? []).map(file => ({ path: file.path, content: file.content, source: card.name }))) });
93
+ const validationPlan = buildValidationPlan({ tree: tree.value, target: decision.target, limits: limitsSnapshot, cards: options.cards, promptVersion: MERGE_PROMPT_VERSION, promptHash: promptHashOf(buildRound2Prompt(row, decision)), supportFiles: tree.value.files.map(file => ({ path: file.path, content: file.content, source: 'staged' })), manifestFiles, participants: [row.a, row.b, decision.target], existingSupport: (options.cards.find(card => card.name === decision.target)?.files ?? []).map(file => file.path), supportSources: options.cards.filter(card => card.name === row.a || card.name === row.b).flatMap(card => (card.files ?? []).map(file => ({ path: file.path, content: file.content, source: card.name }))) });
94
94
  const findings = [...validateStagedTree(tree.value, limitsSnapshot.skillLimits), ...validationPlan.findings];
95
95
  if (hasBlockingFinding(findings)) {
96
96
  const lengths = { bodyBytes: Buffer.byteLength(tree.value.body, 'utf8'), fileBytes: tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0), totalBytes: Buffer.byteLength(tree.value.body, 'utf8') + tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) };
@@ -6,6 +6,7 @@ import type { MergeRegistryPort } from './run-shell.ts';
6
6
  import type { MergeCommandService } from './merge-command.ts';
7
7
  import type { StageToolContext } from './stage-tool.ts';
8
8
  import type { ConversationChannelLike } from './channel-port.ts';
9
+ import type { HostCapabilities } from './host-capabilities.ts';
9
10
  export declare function forcedDecisionFor(a: string, b: string): MergeDecision;
10
11
  export interface MergeStageBindingPort {
11
12
  bind(context: StageToolContext): void;
@@ -20,6 +21,7 @@ export interface MergeRuntimeDeps {
20
21
  readonly params: MergeParams;
21
22
  readonly platformWrite: boolean;
22
23
  readonly channel: ConversationChannelLike;
24
+ readonly hostCapabilities?: HostCapabilities;
23
25
  readonly parent: unknown;
24
26
  readonly allow: readonly string[];
25
27
  readonly cwd?: string;
@@ -38,6 +38,12 @@ function fail(stage, code, hint, next, runId) {
38
38
  function failRun(stage, hint, next, result) {
39
39
  return result.error === undefined ? fail(stage, 'run-failed', hint, next, result.runId) : { ok: false, text: formatMergeError(result.error) };
40
40
  }
41
+ async function hostOnlyFailure(verb, capabilities) {
42
+ if (capabilities === undefined)
43
+ return undefined;
44
+ const state = await capabilities.hostOnly();
45
+ return state === undefined ? undefined : fail(verb, 'e-323', 'host data plane unavailable: ' + state.code + (state.detail === undefined ? '' : ' (' + state.detail + ')'), 'run /evolution merge doctor');
46
+ }
41
47
  /**
42
48
  * The approved/fuse row that already holds a target. Pair/approve move a row to
43
49
  * approved/fuse BEFORE their run starts, so this row IS the in-flight claim a second
@@ -100,6 +106,9 @@ export function createMergeRuntime(deps) {
100
106
  // the three-line shape instead of opening a run that is guaranteed to fail.
101
107
  if (agentless)
102
108
  return fail('pair', 'e-305', errorText('e-305-this-invocation-carries-no', { a1: 'pair' }), '在 GUI 或 CLI 的会话里运行 /evolution merge pair ' + command.a + ' ' + command.b);
109
+ const pairHost = await hostOnlyFailure('pair', deps.hostCapabilities);
110
+ if (pairHost !== undefined)
111
+ return pairHost;
103
112
  // FR-EXEC-17/NFR-04: two pair runs for one pair must not race for the same row.
104
113
  const pairKeyValue = pairKey(command.a, command.b);
105
114
  const inFlight = inFlightPairRun(await readRunMetas(deps.io, deps.home), pairKeyValue);
@@ -179,6 +188,9 @@ export function createMergeRuntime(deps) {
179
188
  if (command.verb === 'scan') {
180
189
  if (agentless)
181
190
  return fail('scan', 'e-305', errorText('e-305-this-invocation-carries-no', { a1: 'scan' }), '在 GUI 或 CLI 的会话里运行 /evolution merge scan');
191
+ const scanHost = await hostOnlyFailure('scan', deps.hostCapabilities);
192
+ if (scanHost !== undefined)
193
+ return scanHost;
182
194
  const signal = new AbortController().signal;
183
195
  const cards = await deps.cards(signal);
184
196
  const blocked = new Set();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-merge",
3
3
  "description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
4
- "version": "0.30.25",
4
+ "version": "0.30.28",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,14 +32,14 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@deepseek-ai/schemastery": "^3.18.5",
35
- "@lmzhen/dsh-evolution-core": "^0.30.25",
36
- "@lmzhen/dsh-evolution-commands": "^0.30.25",
37
- "@lmzhen/dsh-evolution-merge-engine": "0.30.25"
35
+ "@lmzhen/dsh-evolution-core": "^0.30.28",
36
+ "@lmzhen/dsh-evolution-commands": "^0.30.28",
37
+ "@lmzhen/dsh-evolution-merge-engine": "0.30.28"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.5"
41
41
  },
42
42
  "engines": {
43
- "dsh": ">=0.2.1-rc.1 <0.2.2-0"
43
+ "dsh": ">=0.2.1-rc.2 <0.2.2-0"
44
44
  }
45
45
  }