@lmzhen/dsh-evolution-core 0.30.17 → 0.30.19

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.
Files changed (37) hide show
  1. package/lib/composition/index.js +2 -2
  2. package/lib/{composition-D8Y3-ROd.js → composition-DSesmTvf.js} +3 -3
  3. package/lib/{http-routes-ifJKGnu6.js → http-routes-D1Ty6nnh.js} +2 -0
  4. package/lib/http-routes.js +1 -1
  5. package/lib/index.js +3205 -2080
  6. package/lib/subagent/index.js +1 -1
  7. package/lib/types/composition/composer.js +1 -1
  8. package/lib/types/frontmatter.js +1 -1
  9. package/lib/types/http-routes.d.ts +2 -0
  10. package/lib/types/http-routes.js +2 -0
  11. package/lib/types/index.d.ts +5 -0
  12. package/lib/types/index.js +5 -0
  13. package/lib/types/params.js +1 -2
  14. package/lib/types/platform-services.js +1 -1
  15. package/lib/types/redact.js +2 -2
  16. package/lib/types/run-registry.d.ts +27 -6
  17. package/lib/types/run-registry.js +58 -15
  18. package/lib/types/skill-history.d.ts +4 -0
  19. package/lib/types/skill-revision.d.ts +37 -0
  20. package/lib/types/skill-revision.js +45 -0
  21. package/lib/types/skill-store.d.ts +43 -1
  22. package/lib/types/skill-store.js +235 -9
  23. package/lib/types/tree-lock.d.ts +3 -0
  24. package/lib/types/tree-lock.js +13 -5
  25. package/lib/types/unit-backend.d.ts +5 -0
  26. package/lib/types/unit-backend.js +55 -0
  27. package/lib/types/unit-journal.d.ts +29 -0
  28. package/lib/types/unit-journal.js +51 -0
  29. package/lib/types/unit-recovery.d.ts +5 -0
  30. package/lib/types/unit-recovery.js +12 -0
  31. package/lib/types/unit-storage.d.ts +95 -0
  32. package/lib/types/unit-storage.js +124 -0
  33. package/lib/types/unit-transaction.d.ts +220 -0
  34. package/lib/types/unit-transaction.js +487 -0
  35. package/package.json +1 -1
  36. package/persisted-write-inventory.json +91 -1
  37. package/lib/{ensure-MEYMj9jn.js → ensure-BuWsjPGD.js} +1 -1
@@ -1,4 +1,4 @@
1
- import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-MEYMj9jn.js";
1
+ import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-BuWsjPGD.js";
2
2
  //#region lib/types/subagent/readiness.js
3
3
  function serviceOf$1(ctx, name) {
4
4
  return ctx.get(name);
@@ -372,7 +372,7 @@ export function presetRowBlock(patchText, rowId) {
372
372
  const span = rowBlockSpan(lines, rowId);
373
373
  if (span === null)
374
374
  return null;
375
- const block = [...lines.slice(span.start, span.end)];
375
+ const block = lines.slice(span.start, span.end);
376
376
  while (block.length > 0 && (block[block.length - 1] ?? '').trim() === '')
377
377
  block.pop();
378
378
  return block.join('\n') + '\n';
@@ -385,7 +385,7 @@ export function yamlPlainScalarNeedsQuotes(value) {
385
385
  return true;
386
386
  if (/^(?:[-+]?\.inf|\.nan)$/i.test(value))
387
387
  return true;
388
- if (/^[-?:,[\]{}#&*!|>'\"%@`\s]/.test(value))
388
+ if (/^[-?:,[\]{}#&*!|>'"%@`\s]/.test(value))
389
389
  return true;
390
390
  return false;
391
391
  }
@@ -56,6 +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
61
  readonly body: "/api/dsh-evolution/merge/body";
60
62
  readonly history: "/api/dsh-evolution/merge/history";
61
63
  };
@@ -53,6 +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
58
  body: '/api/dsh-evolution/merge/body',
57
59
  history: '/api/dsh-evolution/merge/history',
58
60
  },
@@ -33,6 +33,10 @@
33
33
  */
34
34
  export * from './citations.ts';
35
35
  export * from './tree-write.ts';
36
+ export * from './unit-storage.ts';
37
+ export * from './unit-journal.ts';
38
+ export * from './unit-transaction.ts';
39
+ export * from './unit-backend.ts';
36
40
  export * from './package-identity.ts';
37
41
  export * from './write-authority.ts';
38
42
  export * from './write-gates.ts';
@@ -80,6 +84,7 @@ export * from './drift-signals.ts';
80
84
  export * from './skill-history.ts';
81
85
  export * from './unit-members.ts';
82
86
  export * from './skill-store.ts';
87
+ export * from './skill-revision.ts';
83
88
  export * from './store-ledger.ts';
84
89
  export * from './reachability.ts';
85
90
  export * from './state-store.ts';
@@ -34,6 +34,10 @@
34
34
  export * from "./citations.js";
35
35
  // G1/1.1: the ONE authority behind a write (its type and the authorityFrom builder).
36
36
  export * from "./tree-write.js";
37
+ export * from "./unit-storage.js";
38
+ export * from "./unit-journal.js";
39
+ export * from "./unit-transaction.js";
40
+ export * from "./unit-backend.js";
37
41
  export * from "./package-identity.js";
38
42
  export * from "./write-authority.js";
39
43
  export * from "./write-gates.js";
@@ -81,6 +85,7 @@ export * from "./drift-signals.js";
81
85
  export * from "./skill-history.js";
82
86
  export * from "./unit-members.js";
83
87
  export * from "./skill-store.js";
88
+ export * from "./skill-revision.js";
84
89
  export * from "./store-ledger.js";
85
90
  export * from "./reachability.js";
86
91
  export * from "./state-store.js";
@@ -186,7 +186,7 @@ export const PARAM_EXPOSURE = Object.freeze([
186
186
  { id: 'threatExemptLabels', group: 'deployment', tier: 'E1', authority: 'cordis', owner: 'evolution-threat', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Threat labels the deployment declares benign (safety surface, never user-writable).', why: 'security' },
187
187
  { id: 'evolution-threat.enabled', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-threat', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Mount the write-time threat guard at all.', why: 'security' },
188
188
  { id: 'evolution-threat.maxScanChars', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-threat', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Scan window size of the threat guard.', why: 'security' },
189
- { id: 'evolution-approval.enabled', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-approval', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Require approval before a staged write executes.', why: 'security' },
189
+ { id: 'evolution-approval.enabled', group: 'deployment', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#deployment', summary: 'Master approval switch: run the approval pipeline for foreground writes.', label: '启用批准流水线', hint: '关闭后前台写入不再经批准。', control: 'switch', unit: '', values: '', card: 'evolution-approval' },
190
190
  { id: 'evolution-approval.stageForeground', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-approval', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Stage foreground agent writes for approval too.', why: 'security' },
191
191
  { id: 'evolution-approval.pendingTtlMs', group: 'deployment', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#deployment', summary: 'How long a staged write may wait before it expires: expired writes are closed as rejected and are NEVER executed; 0 (the default) means the window never expires.', label: '待批过期时间', hint: '一条待批写入最多等多久:超时就按「拒绝」关掉(不会执行,写效果不会落地);0 = 永不过期(默认)。', control: 'number', unit: '毫秒', values: '', card: 'evolution-approval' },
192
192
  { id: 'approvalConfirm', group: 'write-caps', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#write-caps', summary: 'What the confirmation before an APPROVE does: write straight through, ask first, or treat an unanswered prompt as a cancel. The same three words skillWriteConfirm uses for a skill write.', label: '批准前先确认', hint: '点「批准」时要不要再确认一次:直接执行 / 先确认 / 等待超时即取消。默认直接执行(不打断批量批准)。', control: 'select', unit: '', values: 'auto|ask|timeout', valueLabels: '直接执行|先确认|等待超时即取消', card: 'evolution-approval' },
@@ -208,7 +208,6 @@ export const PARAM_EXPOSURE = Object.freeze([
208
208
  { id: 'mergeSkippedTtlDays', group: 'merge', tier: 'E3', authority: 'cordis', owner: 'evolution-merge', applies: 'live', docAnchor: 'PARAMETERS.md#merge', summary: 'Days a skipped pair stays out of screening; 0 means it never expires.', label: '无需合并保留天数', hint: '判定无需合并的对,多少天内不再参与筛选;0 表示一直保留。', control: 'number', unit: '天', values: '', card: 'evolution-merge' },
209
209
  { id: 'mergeRunTimeoutMinutes', group: 'merge', tier: 'E3', authority: 'cordis', owner: 'evolution-merge', applies: 'live', docAnchor: 'PARAMETERS.md#merge', summary: 'Wall-clock budget of one merge run; 0 means no limit.', label: '单次运行上限', hint: '一次合并运行最多持续多少分钟;0 表示不设上限。', control: 'number', unit: '分钟', values: '', card: 'evolution-merge' },
210
210
  { id: 'mergePairTimeoutMinutes', group: 'merge', tier: 'E2', authority: 'cordis', owner: 'evolution-merge', applies: 'none', docAnchor: 'PARAMETERS.md#merge', summary: 'Wall-clock budget of one pair round (a stuck child guard).', why: 'internal' },
211
- { id: 'mergeModelsRoot', group: 'merge', tier: 'E2', authority: 'cordis', owner: 'evolution-merge', applies: 'none', docAnchor: 'PARAMETERS.md#merge', summary: 'Local model root containing the int8 and pca64 model directories; empty falls back to SKILL_MERGE_MODELS_ROOT. No auto provisioning.', why: 'path' },
212
211
  ]);
213
212
  /** The settings namespace each OWNER PACKAGE registers (design §7.2).
214
213
  *
@@ -20,9 +20,9 @@ export const PLATFORM_SERVICE_PROBES = Object.freeze([
20
20
  { service: 'evolutionApproval', feature: 'pending writes: list, approve and reject from the command surface', side: 'host' },
21
21
  { service: 'evolutionCurator', feature: '/evolution curator subcommands, skill undo and history', side: 'host' },
22
22
  { service: 'evolutionIo', feature: 'every durable read and write (the IO seam)', side: 'host' },
23
+ { service: 'evolutionMerge', feature: 'merge command delegation and the merge doctor fact', side: 'host' },
23
24
  { service: 'hmr', feature: 'the preset install read-merge-write commit (the platform HMR lock serializes it)', side: 'host' },
24
25
  { service: 'evolutionMemoryBudget', feature: 'the memory-budget divergence section of this report', side: 'host' },
25
- { service: 'evolutionMerge', feature: 'the merge command and its read-only panel routes', side: 'host' },
26
26
  { service: 'evolutionPolicy', feature: 'the policy snapshot that shadows row knobs', side: 'host' },
27
27
  { service: 'evolutionReplay', feature: '/evolution replay', side: 'host' },
28
28
  { service: 'evolutionState', feature: 'the state facade behind activity and replay', side: 'host' },
@@ -30,7 +30,7 @@ const SECRET_PATTERNS = [
30
30
  ['google api key', /AIza[0-9A-Za-z_-]{30,}/g],
31
31
  // F-335 (0.3.23): case-insensitive (`bearer`), and `\s+` so a tab or run of
32
32
  // spaces between `Bearer` and the token is still masked (`Bearer\t...`).
33
- ['bearer credential', /Bearer[\s]+[a-z0-9._~+/=\-]{16,}/gi],
33
+ ['bearer credential', /Bearer[\s]+[a-z0-9._~+/=-]{16,}/gi],
34
34
  // R3 (round-2 audit): HTTP Basic — `Authorization: Basic <base64>` crossed
35
35
  // every model boundary: `Basic` was in no value-shape row and
36
36
  // `authorization` in no keyword set, so the base64 user:password blob
@@ -117,7 +117,7 @@ const PEM_PRIVATE_KEY_PATTERN = new RegExp(`${PEM_HEAD}[\\s\\S]*?${PEM_TAIL}`, '
117
117
  // secret crossed verbatim. Both anchors now tolerate an optional `\r` so the
118
118
  // line-paired pass behaves identically on LF and CRLF input (Windows-authored
119
119
  // .env / compose / kubectl YAML is the realistic carrier).
120
- const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]*)?)\s*:(?:\r)?$/i;
120
+ const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_-][\w-]*)?)\s*:(?:\r)?$/i;
121
121
  // S0.3 (v37 P0-2): the two patterns above cannot express a camelCase hump —
122
122
  // they are case-INSENSITIVE, so [A-Z] would match lowercase too. This pair
123
123
  // decides by a case-insensitive KEYWORD test that rejects a lowercase
@@ -75,10 +75,13 @@ 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. */
78
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
79
79
  mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
80
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
80
81
  stagingRef?: string | undefined;
82
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
81
83
  pairKey?: string | undefined;
84
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
82
85
  pairs?: readonly unknown[] | undefined;
83
86
  }
84
87
  /** What a started run gets: its id and the signal its work must honour. */
@@ -90,10 +93,13 @@ export interface RunHandle {
90
93
  readonly startedAt: number;
91
94
  readonly command?: RunCommand | undefined;
92
95
  readonly parentRunId?: string | undefined;
93
- /** Merge-side projection; core lifecycle stays four-state. */
96
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
94
97
  readonly mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
98
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
95
99
  readonly stagingRef?: string | undefined;
100
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
96
101
  readonly pairKey?: string | undefined;
102
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
97
103
  readonly pairs?: readonly unknown[] | undefined;
98
104
  }
99
105
  /** How a run ended, as reported by its owner. */
@@ -101,6 +107,7 @@ export interface RunOutcome {
101
107
  state: 'succeeded' | 'failed' | 'cancelled';
102
108
  resultRef?: string | undefined;
103
109
  failure?: string | undefined;
110
+ /** @deprecated Legacy merge projection; read-only compatibility, never written. */
104
111
  mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
105
112
  }
106
113
  /** The registry surface the commands row drives. */
@@ -114,6 +121,12 @@ export interface RunRegistry {
114
121
  ok: boolean;
115
122
  note?: string;
116
123
  }>;
124
+ /** Load and throw on unreadable/invalid index. */
125
+ ensureLoaded(): Promise<void>;
126
+ /** The active run after ensureLoaded; explicit read entry. */
127
+ findActive(kind?: RunKind): Promise<RunRecord | undefined>;
128
+ /** Whether this run id is currently owned by a live running process (RG0b). */
129
+ hasOwner(id: string): Promise<boolean>;
117
130
  /**
118
131
  * Register a new run, arm its cancellation, and return its handle.
119
132
  *
@@ -125,13 +138,19 @@ export interface RunRegistry {
125
138
  command?: RunCommand;
126
139
  total?: number;
127
140
  parentRunId?: string;
128
- mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan';
129
- stagingRef?: string;
130
- pairKey?: string;
131
- pairs?: readonly unknown[];
132
141
  }): Promise<RunHandle>;
133
142
  /** Record a run's terminal state (first writer wins). */
134
143
  settle(id: string, outcome: RunOutcome): Promise<void>;
144
+ /**
145
+ * Settle a run this process does not own: the controlled orphan upgrade.
146
+ *
147
+ * Startup recovery runs AFTER `load()` (which materializes the orphan verdict for a
148
+ * dead owner). A committed unit then proves the run really succeeded, so the verdict
149
+ * must be upgradeable — but ONLY from an explicit orphan stamp (or a still-running
150
+ * row whose owner is gone). A run that is genuinely in flight elsewhere is refused.
151
+ * @returns whether the index now carries the requested terminal state.
152
+ */
153
+ settleRecovered(id: string, outcome: RunOutcome): Promise<boolean>;
135
154
  /** Record the current stage/progress of a running run. */
136
155
  stage(id: string, stage: RunStage, progress?: RunProgress): Promise<void>;
137
156
  /** Mark runs as superseded by a tree restore; idempotent. */
@@ -146,6 +165,8 @@ export interface RunRegistry {
146
165
  runs(kind?: RunKind): readonly RunRecord[];
147
166
  /** One run by id, converged view included. */
148
167
  find(id: string): RunRecord | undefined;
168
+ /** Settle a running run whose owner is gone as failed(orphan). @returns whether a run was running. */
169
+ settleOrphan(id: string): Promise<boolean>;
149
170
  /** Ask a running run to stop; it settles as `cancelled`. @returns whether it was running. */
150
171
  cancel(id: string): boolean;
151
172
  /** Ask every running run to stop (fiber dispose). @returns how many were running. */
@@ -110,10 +110,6 @@ 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 } : {}),
117
113
  });
118
114
  }
119
115
  return { ok: true, records, foreign, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
@@ -139,7 +135,7 @@ export function newRunRegistry(options) {
139
135
  /** A row whose owner cannot be reached: `pid` absent (an older build) or not alive. */
140
136
  const ownerIsGone = (row) => row.pid === undefined || !isProcessAlive(row.pid);
141
137
  /** The verdict is materialized ONCE, so its `endedAt` (and the duration a reader sees) is stable. */
142
- const orphanOf = (row) => ({ ...row, state: 'failed', mergeStatus: 'orphan', endedAt: now(), failure: ORPHAN_FAILURE });
138
+ const orphanOf = (row) => ({ ...row, state: 'failed', endedAt: now(), failure: ORPHAN_FAILURE });
143
139
  /** Every row this process knows, newest first. ALL reads go through here. */
144
140
  const view = () => [...owned.values(), ...foreign.values()].sort((a, b) => b.startedAt - a.startedAt);
145
141
  /** Retained history: every running row plus the newest terminal rows per kind. */
@@ -291,6 +287,26 @@ export function newRunRegistry(options) {
291
287
  ].filter((note) => note !== undefined);
292
288
  return { ok: true, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
293
289
  },
290
+ async ensureLoaded() {
291
+ const loaded = await this.load();
292
+ if (!loaded.ok)
293
+ throw new Error(loaded.note ?? 'runs-unreadable');
294
+ },
295
+ async findActive(kind) {
296
+ await this.ensureLoaded();
297
+ return this.inFlight(kind);
298
+ },
299
+ async hasOwner(id) {
300
+ await this.ensureLoaded();
301
+ return this.find(id)?.state === 'running';
302
+ },
303
+ async settleOrphan(id) {
304
+ const run = this.find(id);
305
+ if (run?.state !== 'running')
306
+ return false;
307
+ await this.settle(id, { state: 'failed', failure: 'orphan' });
308
+ return true;
309
+ },
294
310
  async begin(kind, seed) {
295
311
  const id = randomUUID();
296
312
  const startedAt = now();
@@ -305,10 +321,6 @@ export function newRunRegistry(options) {
305
321
  pid: process.pid,
306
322
  ...(seed?.command !== undefined ? { command: seed.command } : {}),
307
323
  ...(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 } : {}),
312
324
  ...(seed?.total !== undefined ? { progress: { done: 0, total: seed.total } } : {}),
313
325
  });
314
326
  // Write-through (0.19.1, review P2): answering "started" promises a run another
@@ -340,8 +352,6 @@ export function newRunRegistry(options) {
340
352
  if (row === undefined || row.supersededAt !== undefined)
341
353
  continue;
342
354
  row.supersededAt = now();
343
- if (row.kind === 'merge')
344
- row.mergeStatus = 'superseded';
345
355
  changed = true;
346
356
  }
347
357
  if (changed)
@@ -360,8 +370,6 @@ export function newRunRegistry(options) {
360
370
  if (row.kind !== kind || row.supersededAt !== undefined)
361
371
  continue;
362
372
  row.supersededAt = now();
363
- if (row.kind === 'merge')
364
- row.mergeStatus = 'superseded';
365
373
  marked += 1;
366
374
  }
367
375
  const kept = capped([...merged.values()]);
@@ -383,11 +391,46 @@ export function newRunRegistry(options) {
383
391
  row.resultRef = outcome.resultRef;
384
392
  if (outcome.failure !== undefined)
385
393
  row.failure = outcome.failure;
386
- if (outcome.mergeStatus !== undefined)
387
- row.mergeStatus = outcome.mergeStatus;
388
394
  controllers.delete(id);
389
395
  await persist();
390
396
  },
397
+ async settleRecovered(id, outcome) {
398
+ const local = owned.get(id);
399
+ if (local !== undefined) {
400
+ await this.settle(id, outcome);
401
+ return owned.get(id)?.state === outcome.state;
402
+ }
403
+ let changed = false;
404
+ await transactIo(options.io, path, (current) => {
405
+ const onDisk = parseIndex(current);
406
+ const row = onDisk.records.find(item => item.id === id);
407
+ if (row === undefined)
408
+ return current;
409
+ // The two admissible sources: an unresolved running row with a dead owner, or
410
+ // the orphan verdict load() already materialized. Anything else is a real
411
+ // terminal answer (someone's cancel/success) and stays put.
412
+ const recoverable = (row.state === 'running' && ownerIsGone(row))
413
+ || (row.state === 'failed' && row.failure === ORPHAN_FAILURE);
414
+ if (!recoverable)
415
+ return current;
416
+ changed = true;
417
+ const next = {
418
+ ...row,
419
+ state: outcome.state,
420
+ endedAt: now(),
421
+ ...(outcome.resultRef === undefined ? {} : { resultRef: outcome.resultRef }),
422
+ ...(outcome.failure === undefined ? {} : { failure: outcome.failure }),
423
+ };
424
+ const kept = capped(onDisk.records.map(item => item.id === id ? next : item));
425
+ const live = kept.filter(item => item.state === 'running');
426
+ const done = kept.filter(item => TERMINAL.has(item.state));
427
+ return JSON.stringify({ schemaVersion: RUNS_SCHEMA_VERSION, runs: [...live, ...done, ...onDisk.foreign] }, null, 2);
428
+ });
429
+ // Rebuild the foreign view so this process answers the upgraded verdict at once.
430
+ if (changed)
431
+ await this.load();
432
+ return changed;
433
+ },
391
434
  inFlight(kind) {
392
435
  return view().find(row => row.state === 'running' && (kind === undefined || row.kind === kind));
393
436
  },
@@ -36,6 +36,10 @@ export declare const HISTORY_INDEX_VERSION = 1;
36
36
  export declare const BASELINE_ACTION = "baseline";
37
37
  /** G3b: one unit's head. Exactly one of the two forms is present (they never coexist). */
38
38
  export interface UnitHead {
39
+ /** The merge run this unit belongs to, when the writer recorded one. */
40
+ runId?: string;
41
+ /** This unit starts a fresh target baseline; older versions remain listable but outside it. */
42
+ baseline?: true;
39
43
  /** Representative shard: the unit's WHOLE manifest of moves (one entry per written path). */
40
44
  moves?: readonly Readonly<Record<string, unknown>>[];
41
45
  /** Representative shard: the skills this unit wrote, so members resolve without a directory scan. */
@@ -0,0 +1,37 @@
1
+ /** One support file as read through the store (path is normalized `references/x.md`). */
2
+ export interface RevisionFile {
3
+ readonly path: string;
4
+ readonly content: string;
5
+ }
6
+ /** The two-state content anchor: absent (no live skill) or a content hash. */
7
+ export type RevisionAnchor = {
8
+ readonly kind: 'absent';
9
+ } | {
10
+ readonly kind: 'present';
11
+ readonly sha256: string;
12
+ };
13
+ /**
14
+ * Canonical fingerprint of one skill's content.
15
+ *
16
+ * Field order and separators are a wire format: decisions persist anchors derived from
17
+ * this string, and a later commit compares against it. Do not reorder.
18
+ * @param name - the (trimmed) skill name.
19
+ * @param body - the SKILL.md text.
20
+ * @param files - every support file under SUPPORT_DIRS.
21
+ * @returns the fingerprint string to hash.
22
+ */
23
+ export declare function revisionFingerprint(name: string, body: string, files: readonly RevisionFile[]): string;
24
+ /** Hash one fingerprint into its anchor (CRLF-normalized by contentHash). */
25
+ export declare function revisionAnchor(name: string, body: string, files: readonly RevisionFile[]): RevisionAnchor;
26
+ /** Anchor for a skill whose body may be absent. */
27
+ export declare function revisionAnchorOf(name: string, body: string | null, files: readonly RevisionFile[]): RevisionAnchor;
28
+ /**
29
+ * Support digest over several skills' fingerprints: the union of their full content.
30
+ *
31
+ * Sort is by fingerprint (name-prefixed), so name order and fingerprint order agree and
32
+ * a decision-time digest can be recomputed by a committer that only has hashes.
33
+ * @param fingerprints - one fingerprint per involved skill (missing skills simply absent).
34
+ * @returns the digest recorded on a decision and re-checked at commit.
35
+ */
36
+ export declare function revisionSupportDigest(fingerprints: readonly string[]): string;
37
+ //# sourceMappingURL=skill-revision.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Content-revision identity shared by every planner that records a decision and every
3
+ * committer that re-checks it.
4
+ *
5
+ * A skill's revision covers the trimmed NAME, the SKILL.md body and every support file
6
+ * (`references/…`, `templates/…`, `scripts/…`, `assets/…`), each as read through the
7
+ * store. The merge domain records a decision-time anchor and the store compares the live
8
+ * anchor before writing; both sides must hash the same bytes or the compare is meaningless,
9
+ * so the hash lives HERE and nowhere else.
10
+ */
11
+ import { contentHash } from "./mutations.js";
12
+ /**
13
+ * Canonical fingerprint of one skill's content.
14
+ *
15
+ * Field order and separators are a wire format: decisions persist anchors derived from
16
+ * this string, and a later commit compares against it. Do not reorder.
17
+ * @param name - the (trimmed) skill name.
18
+ * @param body - the SKILL.md text.
19
+ * @param files - every support file under SUPPORT_DIRS.
20
+ * @returns the fingerprint string to hash.
21
+ */
22
+ export function revisionFingerprint(name, body, files) {
23
+ const ordered = [...files].sort((left, right) => left.path.localeCompare(right.path));
24
+ return name + '\u0000' + body + '\u0000' + ordered.map(file => file.path + '\u0000' + file.content).join('\u0000');
25
+ }
26
+ /** Hash one fingerprint into its anchor (CRLF-normalized by contentHash). */
27
+ export function revisionAnchor(name, body, files) {
28
+ return { kind: 'present', sha256: contentHash(revisionFingerprint(name, body, files)) };
29
+ }
30
+ /** Anchor for a skill whose body may be absent. */
31
+ export function revisionAnchorOf(name, body, files) {
32
+ return body === null ? { kind: 'absent' } : revisionAnchor(name, body, files);
33
+ }
34
+ /**
35
+ * Support digest over several skills' fingerprints: the union of their full content.
36
+ *
37
+ * Sort is by fingerprint (name-prefixed), so name order and fingerprint order agree and
38
+ * a decision-time digest can be recomputed by a committer that only has hashes.
39
+ * @param fingerprints - one fingerprint per involved skill (missing skills simply absent).
40
+ * @returns the digest recorded on a decision and re-checked at commit.
41
+ */
42
+ export function revisionSupportDigest(fingerprints) {
43
+ return contentHash([...fingerprints].sort().join('\u0000'));
44
+ }
45
+ //# sourceMappingURL=skill-revision.js.map
@@ -37,6 +37,7 @@ import { transactIo, type EvolutionIoLike } from './io.ts';
37
37
  import { type Probe } from './probe.ts';
38
38
  import type { SkillLimits } from './limits.ts';
39
39
  import { type MutationRecord } from './mutations.ts';
40
+ import { type RevisionAnchor, type RevisionFile } from './skill-revision.ts';
40
41
  import { type UnitHead, type SkillVersion } from './skill-history.ts';
41
42
  import { type SkillHealthAssessment, type SkillHealthThresholds } from './skill-health.ts';
42
43
  import type { EvolutionSkillMutatedEvent } from './events.ts';
@@ -181,6 +182,9 @@ export interface ArchiveOptions {
181
182
  }
182
183
  /** One multi-skill merge commit: target body + support files, with the source names it absorbs. */
183
184
  export interface CommitMergePlan {
185
+ readonly runId?: string;
186
+ /** The unit transaction id; history must record this exact id for undo. */
187
+ readonly unitId?: string;
184
188
  readonly target: string;
185
189
  /** Whether the target already exists. The caller planned this; commit re-checks it. */
186
190
  readonly targetExists: boolean;
@@ -196,12 +200,27 @@ export interface CommitMergePlan {
196
200
  readonly name: string;
197
201
  readonly body: string;
198
202
  }[];
203
+ /** Stage limits snapshot; absent uses the deployed store limits. */
204
+ readonly limits?: import('./limits.ts').SkillLimits;
205
+ readonly targetRevision?: RevisionAnchor;
206
+ readonly sourceRevisions?: Readonly<Record<string, RevisionAnchor>>;
207
+ readonly supportDigest?: string;
199
208
  readonly origin: WriteOrigin;
200
209
  readonly summary: string;
210
+ /** Real write-phase callback used by UnitTransaction; not persisted. */
211
+ readonly onPhase?: (phase: 'target-pending' | 'target-applied' | 'target-written' | 'source-archiving' | 'sources-archived' | 'support-pending' | 'unit-written', detail?: {
212
+ source?: string;
213
+ }) => void | Promise<void>;
214
+ /** Phase-boundary cancellation; checked before target/source writes. */
215
+ readonly signal?: AbortSignal;
216
+ /** Tree-fence guard; checked before every tree/source write. */
217
+ readonly assertFence?: () => Promise<boolean>;
201
218
  }
202
219
  export interface CommitMergeResult {
203
220
  readonly ok: boolean;
204
221
  readonly message: string;
222
+ /** Typed refusal kind for the revision CAS (target/source/support changed after the plan). */
223
+ readonly code?: 'target-changed' | 'source-changed' | 'support-changed';
205
224
  readonly unitId?: string;
206
225
  readonly targetPath?: string;
207
226
  readonly archived?: readonly string[];
@@ -414,7 +433,20 @@ export declare class SkillLibrary {
414
433
  }): Promise<SkillSummary[]>;
415
434
  read(rawName: string): Promise<string | null>;
416
435
  /**
417
-
436
+ * Content anchor of one live skill: name + body + every support file under SUPPORT_DIRS.
437
+ *
438
+ * This is the commit-side counterpart of the decision-time anchor recorded by the merge
439
+ * domain (see skill-revision.ts): both sides call the same fingerprint, so a changed
440
+ * source, target or support file cannot slip past the commit CAS.
441
+ * @param rawName - caller-supplied skill name (trimmed here).
442
+ * @returns present anchor, or absent when the skill has no SKILL.md.
443
+ */
444
+ revisionFingerprint(rawName: string): Promise<string | null>;
445
+ /** Content anchor of one live skill; the hash a decision records and a commit re-checks. */
446
+ revisionAnchor(rawName: string): Promise<RevisionAnchor>;
447
+ /** Every support file of one live skill, in listing order; unreadable/missing files fail loud. */
448
+ liveSupportFiles(rawName: string): Promise<readonly RevisionFile[]>;
449
+ /**
418
450
  * Single path-building choke point (rc.42 audit P2-5): every directory path
419
451
 
420
452
  * is built from the TRIMMED name, so a name that passes `badName` (which
@@ -444,6 +476,8 @@ export declare class SkillLibrary {
444
476
  deleteProtection(rawName: string, options?: {
445
477
  allowBundled?: boolean | undefined;
446
478
  }): Promise<string | null>;
479
+ /** Whether the target directory exists at all (a SKILL.md-less ghost still occupies the name). */
480
+ skillDirExists(rawName: string): Promise<boolean>;
447
481
  isManaged(rawName: string): Promise<boolean>;
448
482
  /** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
449
483
  isBundled(rawName: string): Promise<boolean>;
@@ -517,6 +551,8 @@ export declare class SkillLibrary {
517
551
  * @returns the versions, oldest first.
518
552
  */
519
553
  listVersions(rawName: string): Promise<SkillVersion[]>;
554
+ /** Versions from the latest merge baseline onward; old versions stay listable via listVersions. */
555
+ listBaselineVersions(rawName: string): Promise<SkillVersion[]>;
520
556
  /**
521
557
  * One version's content, read back from the content-addressed blob store.
522
558
  * @param rawName - the skill's name.
@@ -525,6 +561,10 @@ export declare class SkillLibrary {
525
561
  * blob cannot be read — never an empty string.
526
562
  */
527
563
  readVersion(rawName: string, v: number): Promise<string | null>;
564
+ /** Read an archived skill's human-readable reason, exact or stamped. */
565
+ readArchiveReason(rawName: string): Promise<string | null>;
566
+ /** Read an archived skill body (SKILL.md) by its original name, exact or stamped. */
567
+ readArchived(rawName: string): Promise<string | null>;
528
568
  /**
529
569
  * Pin or unpin a skill (`.pinned` marker). Pinned skills are protected from
530
570
  * deletion, from background-review writes, and from the lifecycle — a
@@ -567,6 +607,8 @@ export declare class SkillLibrary {
567
607
  * P2-4 (v19): a non-string entry (`skills: [123]`) is refused structurally
568
608
  * instead of throwing `name.includes is not a function`. */
569
609
  private safeSnapshotEntryName;
610
+ /** Mark merge-absorbed sources archived in the usage sidecar (FR-DATA-07). */
611
+ private foldArchivedUsage;
570
612
  archive(rawName: string, options?: ArchiveOptions, origin?: WriteOrigin): Promise<SkillActionResult>;
571
613
  /**
572
614
  * Commit one merge plan as a single multi-skill transaction.