@lmzhen/dsh-evolution-core 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,8 +77,8 @@ const HOST_ROUTES = {
77
77
  merge: {
78
78
  view: "/api/dsh-evolution/merge/view",
79
79
  rows: "/api/dsh-evolution/merge/rows",
80
- run: "/api/dsh-evolution/merge/run",
81
- runs: "/api/dsh-evolution/merge/runs"
80
+ body: "/api/dsh-evolution/merge/body",
81
+ history: "/api/dsh-evolution/merge/history"
82
82
  },
83
83
  diagnostics: { view: "/api/dsh-evolution/diagnostics" }
84
84
  };
@@ -1,2 +1,2 @@
1
- import { a as methodNotAllowed, i as json, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-w09FdvOp.js";
1
+ import { a as methodNotAllowed, i as json, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-ifJKGnu6.js";
2
2
  export { HOST_ROUTES, MAX_REQUEST_BODY_BYTES, hostSurface, json, methodNotAllowed, readJsonObject, registerHostRoutes };
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _ as scopedPackageName, c as MAINTAINER_PRESET_ID, d as RESTRICTED_TOOLS_ROW_ID, f as RESTRICTOR_MODULE, g as scopeOfNearestManifest, p as REVIEWER_PRESET_ID, s as EVOLUTION_PRESET_IDS, u as RESTRICTED_PRESET_DISPLAY } from "./ensure-MEYMj9jn.js";
2
- import { a as methodNotAllowed, c as asRecord, i as json, l as isRecord, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-w09FdvOp.js";
2
+ import { a as methodNotAllowed, c as asRecord, i as json, l as isRecord, n as MAX_REQUEST_BODY_BYTES, o as readJsonObject, r as hostSurface, s as registerHostRoutes, t as HOST_ROUTES } from "./http-routes-ifJKGnu6.js";
3
3
  import { w as allowRowCollisions } from "./composition-D8Y3-ROd.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";
@@ -5608,7 +5608,11 @@ function parseIndex(raw) {
5608
5608
  ...typeof record.resultRef === "string" ? { resultRef: record.resultRef } : {},
5609
5609
  ...typeof record.failure === "string" ? { failure: record.failure } : {},
5610
5610
  ...typeof record.supersededAt === "number" ? { supersededAt: record.supersededAt } : {},
5611
- ...typeof record.parentRunId === "string" ? { parentRunId: record.parentRunId } : {}
5611
+ ...typeof record.parentRunId === "string" ? { parentRunId: record.parentRunId } : {},
5612
+ ...record.mergeStatus === "running" || record.mergeStatus === "succeeded" || record.mergeStatus === "failed" || record.mergeStatus === "cancelled" || record.mergeStatus === "awaiting-approval" || record.mergeStatus === "undone" || record.mergeStatus === "superseded" || record.mergeStatus === "interrupted" || record.mergeStatus === "orphan" ? { mergeStatus: record.mergeStatus } : {},
5613
+ ...typeof record.stagingRef === "string" ? { stagingRef: record.stagingRef } : {},
5614
+ ...typeof record.pairKey === "string" ? { pairKey: record.pairKey } : {},
5615
+ ...Array.isArray(record.pairs) ? { pairs: record.pairs } : {}
5612
5616
  });
5613
5617
  }
5614
5618
  return {
@@ -5642,6 +5646,7 @@ function newRunRegistry(options) {
5642
5646
  const orphanOf = (row) => ({
5643
5647
  ...row,
5644
5648
  state: "failed",
5649
+ mergeStatus: "orphan",
5645
5650
  endedAt: now(),
5646
5651
  failure: ORPHAN_FAILURE
5647
5652
  });
@@ -5780,6 +5785,10 @@ function newRunRegistry(options) {
5780
5785
  pid: process.pid,
5781
5786
  ...seed?.command !== void 0 ? { command: seed.command } : {},
5782
5787
  ...seed?.parentRunId !== void 0 ? { parentRunId: seed.parentRunId } : {},
5788
+ ...seed?.mergeStatus !== void 0 ? { mergeStatus: seed.mergeStatus } : {},
5789
+ ...seed?.stagingRef !== void 0 ? { stagingRef: seed.stagingRef } : {},
5790
+ ...seed?.pairKey !== void 0 ? { pairKey: seed.pairKey } : {},
5791
+ ...seed?.pairs !== void 0 ? { pairs: seed.pairs } : {},
5783
5792
  ...seed?.total !== void 0 ? { progress: {
5784
5793
  done: 0,
5785
5794
  total: seed.total
@@ -5811,6 +5820,7 @@ function newRunRegistry(options) {
5811
5820
  const row = owned.get(id);
5812
5821
  if (row === void 0 || row.supersededAt !== void 0) continue;
5813
5822
  row.supersededAt = now();
5823
+ if (row.kind === "merge") row.mergeStatus = "superseded";
5814
5824
  changed = true;
5815
5825
  }
5816
5826
  if (changed) await persist();
@@ -5825,6 +5835,7 @@ function newRunRegistry(options) {
5825
5835
  for (const row of merged.values()) {
5826
5836
  if (row.kind !== kind || row.supersededAt !== void 0) continue;
5827
5837
  row.supersededAt = now();
5838
+ if (row.kind === "merge") row.mergeStatus = "superseded";
5828
5839
  marked += 1;
5829
5840
  }
5830
5841
  const kept = capped([...merged.values()]);
@@ -5848,6 +5859,7 @@ function newRunRegistry(options) {
5848
5859
  row.endedAt = now();
5849
5860
  if (outcome.resultRef !== void 0) row.resultRef = outcome.resultRef;
5850
5861
  if (outcome.failure !== void 0) row.failure = outcome.failure;
5862
+ if (outcome.mergeStatus !== void 0) row.mergeStatus = outcome.mergeStatus;
5851
5863
  controllers.delete(id);
5852
5864
  await persist();
5853
5865
  },
@@ -10965,24 +10977,41 @@ var SkillLibrary = class {
10965
10977
  ok: false,
10966
10978
  message: stuck.length === 0 ? `合并失败,已回滚:${reason}` : `合并失败(${reason});已回滚,但以下未回滚:${stuck.join(", ")}:它们仍在 .archive 里,请用 /evolution skill restore 恢复。`
10967
10979
  });
10968
- if (plan.targetExists) {
10969
- for (const source of sources) {
10970
- if (await this.io.readText(join(this.dirOf(source), "SKILL.md")) !== sourceBodies.get(source)) {
10971
- const stuck = await rollbackSources();
10972
- return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
10973
- }
10974
- const result = await this.archive(source, {
10975
- absorbedInto: target,
10980
+ const rollbackTarget = async () => {
10981
+ const stuck = [];
10982
+ const support = plan.supportFiles.map((file) => ({
10983
+ target: join(dir, ...file.path.replace(/\\/g, "/").split("/").filter(Boolean)),
10984
+ content: supportPrevious.get(file.path) ?? null,
10985
+ previous: file.content
10986
+ }));
10987
+ const writes = [{
10988
+ target: targetPath,
10989
+ content: plan.expectedTargetBody ?? "",
10990
+ expected: plan.body.trimEnd() + "\n"
10991
+ }, ...support.map((file) => ({
10992
+ target: file.target,
10993
+ content: file.content ?? "",
10994
+ expected: file.previous
10995
+ }))];
10996
+ try {
10997
+ if (!(await this.applyTreeChange({
10998
+ name: target,
10999
+ origin: plan.origin,
11000
+ protection: "write",
11001
+ writes,
11002
+ auditAction: "merge-rollback",
11003
+ auditSummary: "merge rollback: restore target snapshot",
11004
+ eventAction: "merge-rollback",
10976
11005
  unitId,
10977
- unitHead: { skill: target },
10978
- reason: `Merged into ${target}`
10979
- }, plan.origin);
10980
- if (!result.ok) {
10981
- const stuck = await rollbackSources();
10982
- return rollbackMessage(result.message, stuck);
10983
- }
10984
- archived.push(source);
11006
+ unitHead
11007
+ })).ok) stuck.push(target);
11008
+ } catch {
11009
+ stuck.push(target);
10985
11010
  }
11011
+ return stuck;
11012
+ };
11013
+ if (plan.targetExists) {
11014
+ for (const source of sources) if (await this.io.readText(join(this.dirOf(source), "SKILL.md")) !== sourceBodies.get(source)) return rollbackMessage(`来源技能「${source}」在归档前被改过`, []);
10986
11015
  const writes = [{
10987
11016
  target: targetPath,
10988
11017
  content: plan.body.trimEnd() + "\n",
@@ -11003,9 +11032,25 @@ var SkillLibrary = class {
11003
11032
  unitId,
11004
11033
  unitHead
11005
11034
  });
11006
- if (!result.ok) {
11007
- const stuck = await rollbackSources();
11008
- return rollbackMessage(result.message, stuck);
11035
+ if (!result.ok) return rollbackMessage(result.message, []);
11036
+ for (const source of sources) {
11037
+ if (await this.io.readText(join(this.dirOf(source), "SKILL.md")) !== sourceBodies.get(source)) {
11038
+ const stuck = await rollbackSources();
11039
+ stuck.push(...await rollbackTarget());
11040
+ return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
11041
+ }
11042
+ const archivedResult = await this.archive(source, {
11043
+ absorbedInto: target,
11044
+ unitId,
11045
+ unitHead: { skill: target },
11046
+ reason: `Merged into ${target}`
11047
+ }, plan.origin);
11048
+ if (!archivedResult.ok) {
11049
+ const stuck = await rollbackSources();
11050
+ stuck.push(...await rollbackTarget());
11051
+ return rollbackMessage(archivedResult.message, stuck);
11052
+ }
11053
+ archived.push(source);
11009
11054
  }
11010
11055
  return {
11011
11056
  ok: true,
@@ -56,8 +56,8 @@ export declare const HOST_ROUTES: {
56
56
  readonly merge: {
57
57
  readonly view: "/api/dsh-evolution/merge/view";
58
58
  readonly rows: "/api/dsh-evolution/merge/rows";
59
- readonly run: "/api/dsh-evolution/merge/run";
60
- readonly runs: "/api/dsh-evolution/merge/runs";
59
+ readonly body: "/api/dsh-evolution/merge/body";
60
+ readonly history: "/api/dsh-evolution/merge/history";
61
61
  };
62
62
  readonly diagnostics: {
63
63
  readonly view: "/api/dsh-evolution/diagnostics";
@@ -53,8 +53,8 @@ export const HOST_ROUTES = {
53
53
  merge: {
54
54
  view: '/api/dsh-evolution/merge/view',
55
55
  rows: '/api/dsh-evolution/merge/rows',
56
- run: '/api/dsh-evolution/merge/run',
57
- runs: '/api/dsh-evolution/merge/runs',
56
+ body: '/api/dsh-evolution/merge/body',
57
+ history: '/api/dsh-evolution/merge/history',
58
58
  },
59
59
  diagnostics: {
60
60
  view: '/api/dsh-evolution/diagnostics',
@@ -75,6 +75,11 @@ export interface RunRecord {
75
75
  supersededAt?: number | undefined;
76
76
  /** The run this run was spawned from (approve/resume after an earlier command). */
77
77
  parentRunId?: string | undefined;
78
+ /** Merge-side projection; core lifecycle stays four-state. */
79
+ mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
80
+ stagingRef?: string | undefined;
81
+ pairKey?: string | undefined;
82
+ pairs?: readonly unknown[] | undefined;
78
83
  }
79
84
  /** What a started run gets: its id and the signal its work must honour. */
80
85
  export interface RunHandle {
@@ -85,12 +90,18 @@ export interface RunHandle {
85
90
  readonly startedAt: number;
86
91
  readonly command?: RunCommand | undefined;
87
92
  readonly parentRunId?: string | undefined;
93
+ /** Merge-side projection; core lifecycle stays four-state. */
94
+ readonly mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
95
+ readonly stagingRef?: string | undefined;
96
+ readonly pairKey?: string | undefined;
97
+ readonly pairs?: readonly unknown[] | undefined;
88
98
  }
89
99
  /** How a run ended, as reported by its owner. */
90
100
  export interface RunOutcome {
91
101
  state: 'succeeded' | 'failed' | 'cancelled';
92
102
  resultRef?: string | undefined;
93
103
  failure?: string | undefined;
104
+ mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
94
105
  }
95
106
  /** The registry surface the commands row drives. */
96
107
  export interface RunRegistry {
@@ -114,6 +125,10 @@ export interface RunRegistry {
114
125
  command?: RunCommand;
115
126
  total?: number;
116
127
  parentRunId?: string;
128
+ mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan';
129
+ stagingRef?: string;
130
+ pairKey?: string;
131
+ pairs?: readonly unknown[];
117
132
  }): Promise<RunHandle>;
118
133
  /** Record a run's terminal state (first writer wins). */
119
134
  settle(id: string, outcome: RunOutcome): Promise<void>;
@@ -110,6 +110,10 @@ function parseIndex(raw) {
110
110
  ...(typeof record.failure === 'string' ? { failure: record.failure } : {}),
111
111
  ...(typeof record.supersededAt === 'number' ? { supersededAt: record.supersededAt } : {}),
112
112
  ...(typeof record.parentRunId === 'string' ? { parentRunId: record.parentRunId } : {}),
113
+ ...(record.mergeStatus === 'running' || record.mergeStatus === 'succeeded' || record.mergeStatus === 'failed' || record.mergeStatus === 'cancelled' || record.mergeStatus === 'awaiting-approval' || record.mergeStatus === 'undone' || record.mergeStatus === 'superseded' || record.mergeStatus === 'interrupted' || record.mergeStatus === 'orphan' ? { mergeStatus: record.mergeStatus } : {}),
114
+ ...(typeof record.stagingRef === 'string' ? { stagingRef: record.stagingRef } : {}),
115
+ ...(typeof record.pairKey === 'string' ? { pairKey: record.pairKey } : {}),
116
+ ...(Array.isArray(record.pairs) ? { pairs: record.pairs } : {}),
113
117
  });
114
118
  }
115
119
  return { ok: true, records, foreign, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
@@ -135,7 +139,7 @@ export function newRunRegistry(options) {
135
139
  /** A row whose owner cannot be reached: `pid` absent (an older build) or not alive. */
136
140
  const ownerIsGone = (row) => row.pid === undefined || !isProcessAlive(row.pid);
137
141
  /** The verdict is materialized ONCE, so its `endedAt` (and the duration a reader sees) is stable. */
138
- const orphanOf = (row) => ({ ...row, state: 'failed', endedAt: now(), failure: ORPHAN_FAILURE });
142
+ const orphanOf = (row) => ({ ...row, state: 'failed', mergeStatus: 'orphan', endedAt: now(), failure: ORPHAN_FAILURE });
139
143
  /** Every row this process knows, newest first. ALL reads go through here. */
140
144
  const view = () => [...owned.values(), ...foreign.values()].sort((a, b) => b.startedAt - a.startedAt);
141
145
  /** Retained history: every running row plus the newest terminal rows per kind. */
@@ -301,6 +305,10 @@ export function newRunRegistry(options) {
301
305
  pid: process.pid,
302
306
  ...(seed?.command !== undefined ? { command: seed.command } : {}),
303
307
  ...(seed?.parentRunId !== undefined ? { parentRunId: seed.parentRunId } : {}),
308
+ ...(seed?.mergeStatus !== undefined ? { mergeStatus: seed.mergeStatus } : {}),
309
+ ...(seed?.stagingRef !== undefined ? { stagingRef: seed.stagingRef } : {}),
310
+ ...(seed?.pairKey !== undefined ? { pairKey: seed.pairKey } : {}),
311
+ ...(seed?.pairs !== undefined ? { pairs: seed.pairs } : {}),
304
312
  ...(seed?.total !== undefined ? { progress: { done: 0, total: seed.total } } : {}),
305
313
  });
306
314
  // Write-through (0.19.1, review P2): answering "started" promises a run another
@@ -332,6 +340,8 @@ export function newRunRegistry(options) {
332
340
  if (row === undefined || row.supersededAt !== undefined)
333
341
  continue;
334
342
  row.supersededAt = now();
343
+ if (row.kind === 'merge')
344
+ row.mergeStatus = 'superseded';
335
345
  changed = true;
336
346
  }
337
347
  if (changed)
@@ -350,6 +360,8 @@ export function newRunRegistry(options) {
350
360
  if (row.kind !== kind || row.supersededAt !== undefined)
351
361
  continue;
352
362
  row.supersededAt = now();
363
+ if (row.kind === 'merge')
364
+ row.mergeStatus = 'superseded';
353
365
  marked += 1;
354
366
  }
355
367
  const kept = capped([...merged.values()]);
@@ -371,6 +383,8 @@ export function newRunRegistry(options) {
371
383
  row.resultRef = outcome.resultRef;
372
384
  if (outcome.failure !== undefined)
373
385
  row.failure = outcome.failure;
386
+ if (outcome.mergeStatus !== undefined)
387
+ row.mergeStatus = outcome.mergeStatus;
374
388
  controllers.delete(id);
375
389
  await persist();
376
390
  },
@@ -2036,24 +2036,42 @@ export class SkillLibrary {
2036
2036
  ? `合并失败,已回滚:${reason}`
2037
2037
  : `合并失败(${reason});已回滚,但以下未回滚:${stuck.join(', ')}:它们仍在 .archive 里,请用 /evolution skill restore 恢复。`,
2038
2038
  });
2039
+ const rollbackTarget = async () => {
2040
+ const stuck = [];
2041
+ const support = plan.supportFiles.map(file => ({
2042
+ target: join(dir, ...file.path.replace(/\\/g, '/').split('/').filter(Boolean)),
2043
+ content: supportPrevious.get(file.path) ?? null,
2044
+ previous: file.content,
2045
+ }));
2046
+ const writes = [
2047
+ { target: targetPath, content: plan.expectedTargetBody ?? '', expected: plan.body.trimEnd() + '\n' },
2048
+ ...support.map(file => ({ target: file.target, content: file.content ?? '', expected: file.previous })),
2049
+ ];
2050
+ try {
2051
+ const restored = await this.applyTreeChange({
2052
+ name: target,
2053
+ origin: plan.origin,
2054
+ protection: 'write',
2055
+ writes,
2056
+ auditAction: 'merge-rollback',
2057
+ auditSummary: 'merge rollback: restore target snapshot',
2058
+ eventAction: 'merge-rollback',
2059
+ unitId,
2060
+ unitHead,
2061
+ });
2062
+ if (!restored.ok)
2063
+ stuck.push(target);
2064
+ }
2065
+ catch {
2066
+ stuck.push(target);
2067
+ }
2068
+ return stuck;
2069
+ };
2039
2070
  if (plan.targetExists) {
2040
2071
  for (const source of sources) {
2041
2072
  const fresh = await this.io.readText(join(this.dirOf(source), 'SKILL.md'));
2042
- if (fresh !== sourceBodies.get(source)) {
2043
- const stuck = await rollbackSources();
2044
- return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
2045
- }
2046
- const result = await this.archive(source, {
2047
- absorbedInto: target,
2048
- unitId,
2049
- unitHead: { skill: target },
2050
- reason: `Merged into ${target}`,
2051
- }, plan.origin);
2052
- if (!result.ok) {
2053
- const stuck = await rollbackSources();
2054
- return rollbackMessage(result.message, stuck);
2055
- }
2056
- archived.push(source);
2073
+ if (fresh !== sourceBodies.get(source))
2074
+ return rollbackMessage(`来源技能「${source}」在归档前被改过`, []);
2057
2075
  }
2058
2076
  const writes = [
2059
2077
  { target: targetPath, content: plan.body.trimEnd() + '\n', expected: plan.expectedTargetBody },
@@ -2074,9 +2092,27 @@ export class SkillLibrary {
2074
2092
  unitId,
2075
2093
  unitHead,
2076
2094
  });
2077
- if (!result.ok) {
2078
- const stuck = await rollbackSources();
2079
- return rollbackMessage(result.message, stuck);
2095
+ if (!result.ok)
2096
+ return rollbackMessage(result.message, []);
2097
+ for (const source of sources) {
2098
+ const fresh = await this.io.readText(join(this.dirOf(source), 'SKILL.md'));
2099
+ if (fresh !== sourceBodies.get(source)) {
2100
+ const stuck = await rollbackSources();
2101
+ stuck.push(...await rollbackTarget());
2102
+ return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
2103
+ }
2104
+ const archivedResult = await this.archive(source, {
2105
+ absorbedInto: target,
2106
+ unitId,
2107
+ unitHead: { skill: target },
2108
+ reason: `Merged into ${target}`,
2109
+ }, plan.origin);
2110
+ if (!archivedResult.ok) {
2111
+ const stuck = await rollbackSources();
2112
+ stuck.push(...await rollbackTarget());
2113
+ return rollbackMessage(archivedResult.message, stuck);
2114
+ }
2115
+ archived.push(source);
2080
2116
  }
2081
2117
  return { ok: true, message: `已把 ${sources.join(', ')} 合并进「${target}」。`, unitId, targetPath: dir, archived, mutations: 1 + sources.length };
2082
2118
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.29.0",
4
+ "version": "0.30.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -207,6 +207,15 @@
207
207
  "marker": "transactIo(options.io, path",
208
208
  "state": [],
209
209
  "note": "The release half of the same lock: delete only when the recorded token still matches."
210
+ },
211
+ {
212
+ "id": "merge-pair-index",
213
+ "path": "<evolutionHome>/merge/pair-index.json",
214
+ "writer": "evolution-merge/src/domain/pair-index-store.ts",
215
+ "serializedBy": "transact",
216
+ "marker": "transactIo(io, pairIndexPath(home)",
217
+ "state": [],
218
+ "note": "pairKey → result/runId/target index; RMW under the same IO lock so two merge runs cannot lose each other's entries."
210
219
  }
211
220
  ]
212
221
  }