@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.
- package/lib/composition/index.js +2 -2
- package/lib/{composition-D8Y3-ROd.js → composition-DSesmTvf.js} +3 -3
- package/lib/{http-routes-ifJKGnu6.js → http-routes-D1Ty6nnh.js} +2 -0
- package/lib/http-routes.js +1 -1
- package/lib/index.js +3205 -2080
- package/lib/subagent/index.js +1 -1
- package/lib/types/composition/composer.js +1 -1
- package/lib/types/frontmatter.js +1 -1
- package/lib/types/http-routes.d.ts +2 -0
- package/lib/types/http-routes.js +2 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/index.js +5 -0
- package/lib/types/params.js +1 -2
- package/lib/types/platform-services.js +1 -1
- package/lib/types/redact.js +2 -2
- package/lib/types/run-registry.d.ts +27 -6
- package/lib/types/run-registry.js +58 -15
- package/lib/types/skill-history.d.ts +4 -0
- package/lib/types/skill-revision.d.ts +37 -0
- package/lib/types/skill-revision.js +45 -0
- package/lib/types/skill-store.d.ts +43 -1
- package/lib/types/skill-store.js +235 -9
- package/lib/types/tree-lock.d.ts +3 -0
- package/lib/types/tree-lock.js +13 -5
- package/lib/types/unit-backend.d.ts +5 -0
- package/lib/types/unit-backend.js +55 -0
- package/lib/types/unit-journal.d.ts +29 -0
- package/lib/types/unit-journal.js +51 -0
- package/lib/types/unit-recovery.d.ts +5 -0
- package/lib/types/unit-recovery.js +12 -0
- package/lib/types/unit-storage.d.ts +95 -0
- package/lib/types/unit-storage.js +124 -0
- package/lib/types/unit-transaction.d.ts +220 -0
- package/lib/types/unit-transaction.js +487 -0
- package/package.json +1 -1
- package/persisted-write-inventory.json +91 -1
- package/lib/{ensure-MEYMj9jn.js → ensure-BuWsjPGD.js} +1 -1
package/lib/subagent/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-
|
|
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 =
|
|
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';
|
package/lib/types/frontmatter.js
CHANGED
|
@@ -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 (/^[-?:,[\]{}#&*!|>'
|
|
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
|
};
|
package/lib/types/http-routes.js
CHANGED
|
@@ -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
|
},
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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';
|
package/lib/types/index.js
CHANGED
|
@@ -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";
|
package/lib/types/params.js
CHANGED
|
@@ -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: '
|
|
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' },
|
package/lib/types/redact.js
CHANGED
|
@@ -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._
|
|
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-]*[_
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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',
|
|
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.
|