@lmzhen/dsh-evolution-merge 0.29.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +1125 -568
- package/lib/types/adapters/channel.js +29 -0
- package/lib/types/adapters/engine-screen.d.ts +1 -1
- package/lib/types/adapters/engine-screen.js +45 -0
- package/lib/types/adapters/library.js +14 -0
- package/lib/types/adapters/model-runner.js +34 -0
- package/lib/types/adapters/select.d.ts +3 -0
- package/lib/types/adapters/select.js +35 -0
- package/lib/types/adapters/undo.js +35 -0
- package/lib/types/approve-command.js +42 -0
- package/lib/types/authoring-flow.js +48 -0
- package/lib/types/channel-port.js +74 -0
- package/lib/types/commands.d.ts +9 -0
- package/lib/types/commands.js +79 -0
- package/lib/types/commit-assembly.js +48 -0
- package/lib/types/commit-execute.js +52 -0
- package/lib/types/commit-plan.js +56 -0
- package/lib/types/commit-receipt.d.ts +2 -0
- package/lib/types/commit-receipt.js +85 -0
- package/lib/types/contracts.d.ts +14 -5
- package/lib/types/contracts.js +57 -0
- package/lib/types/coordinator.d.ts +3 -0
- package/lib/types/coordinator.js +131 -0
- package/lib/types/decision.js +61 -0
- package/lib/types/doctor.js +50 -0
- package/lib/types/domain/claim.d.ts +10 -0
- package/lib/types/domain/claim.js +12 -0
- package/lib/types/domain/commit-journal-store.d.ts +6 -0
- package/lib/types/domain/commit-journal-store.js +22 -0
- package/lib/types/domain/commit-journal.d.ts +16 -0
- package/lib/types/domain/commit-journal.js +25 -0
- package/lib/types/domain/errors.d.ts +10 -0
- package/lib/types/domain/errors.js +7 -0
- package/lib/types/domain/failure-route.d.ts +25 -0
- package/lib/types/domain/failure-route.js +22 -0
- package/lib/types/domain/indexes.d.ts +21 -0
- package/lib/types/domain/indexes.js +15 -0
- package/lib/types/domain/ledger.d.ts +17 -0
- package/lib/types/domain/ledger.js +65 -0
- package/lib/types/domain/markdown.d.ts +2 -0
- package/lib/types/domain/markdown.js +57 -0
- package/lib/types/domain/migrate-v1.d.ts +41 -0
- package/lib/types/domain/migrate-v1.js +23 -0
- package/lib/types/domain/pair-index-store.d.ts +6 -0
- package/lib/types/domain/pair-index-store.js +25 -0
- package/lib/types/domain/pair-index.d.ts +9 -0
- package/lib/types/domain/pair-index.js +28 -0
- package/lib/types/domain/resolve-pair.d.ts +11 -0
- package/lib/types/domain/resolve-pair.js +17 -0
- package/lib/types/domain/row.d.ts +113 -0
- package/lib/types/domain/row.js +111 -0
- package/lib/types/domain/run-record.d.ts +45 -0
- package/lib/types/domain/run-record.js +24 -0
- package/lib/types/domain/state.d.ts +57 -0
- package/lib/types/domain/state.js +65 -0
- package/lib/types/gate-cache.js +36 -0
- package/lib/types/index.js +241 -0
- package/lib/types/ladder.js +162 -0
- package/lib/types/merge-command.js +43 -0
- package/lib/types/merge-routes.d.ts +14 -4
- package/lib/types/merge-routes.js +99 -0
- package/lib/types/merge-tool.js +69 -0
- package/lib/types/new-child-authoring.js +83 -0
- package/lib/types/pair-command.js +18 -0
- package/lib/types/params.js +49 -0
- package/lib/types/pending-actions.js +28 -0
- package/lib/types/prompts.js +60 -0
- package/lib/types/read-commands.js +32 -0
- package/lib/types/report.js +70 -0
- package/lib/types/resume-command.js +57 -0
- package/lib/types/round2-outcome.js +50 -0
- package/lib/types/routes.d.ts +5 -2
- package/lib/types/routes.js +75 -0
- package/lib/types/rules.js +55 -0
- package/lib/types/run-shell.d.ts +7 -0
- package/lib/types/run-shell.js +69 -0
- package/lib/types/runtime.d.ts +2 -0
- package/lib/types/runtime.js +269 -0
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +30 -0
- package/lib/types/scan-ports.js +93 -0
- package/lib/types/scan-run.d.ts +2 -0
- package/lib/types/scan-run.js +186 -0
- package/lib/types/skill-card.js +2 -0
- package/lib/types/skipped.js +24 -0
- package/lib/types/stage-tool-registry.js +32 -0
- package/lib/types/stage-tool.js +31 -0
- package/lib/types/staged-tree.js +75 -0
- package/lib/types/staging-files.js +86 -0
- package/lib/types/staging.d.ts +7 -0
- package/lib/types/staging.js +149 -0
- package/lib/types/state.js +103 -0
- package/lib/types/status.d.ts +2 -1
- package/lib/types/status.js +28 -0
- package/lib/types/supersede.js +14 -0
- package/lib/types/undo-merge.js +23 -0
- package/package.json +4 -4
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Pure merge decision state machine. No IO, no host imports. */
|
|
2
|
+
export type MergeState = 'verify' | 'pending' | 'approved' | 'fuse' | 'skipped';
|
|
3
|
+
export type MergeQueue = 'verify' | 'pending' | 'fuse' | 'skipped';
|
|
4
|
+
export type MergeEvent = {
|
|
5
|
+
readonly type: 'candidate';
|
|
6
|
+
} | {
|
|
7
|
+
readonly type: 'round1-start';
|
|
8
|
+
} | {
|
|
9
|
+
readonly type: 'round1-keep';
|
|
10
|
+
} | {
|
|
11
|
+
readonly type: 'round1-merge';
|
|
12
|
+
readonly approval: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
readonly type: 'round1-fail';
|
|
15
|
+
} | {
|
|
16
|
+
readonly type: 'approve';
|
|
17
|
+
} | {
|
|
18
|
+
readonly type: 'reject';
|
|
19
|
+
} | {
|
|
20
|
+
readonly type: 'round2-start';
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: 'round2-fail';
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: 'round2-merged';
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: 'round2-cancel';
|
|
27
|
+
} | {
|
|
28
|
+
readonly type: 'staging-expired';
|
|
29
|
+
} | {
|
|
30
|
+
readonly type: 'source-gone';
|
|
31
|
+
} | {
|
|
32
|
+
readonly type: 'target-taken';
|
|
33
|
+
} | {
|
|
34
|
+
readonly type: 'source-changed';
|
|
35
|
+
} | {
|
|
36
|
+
readonly type: 'cancel';
|
|
37
|
+
} | {
|
|
38
|
+
readonly type: 'timeout';
|
|
39
|
+
};
|
|
40
|
+
export type TransitionResult = {
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly state: MergeState;
|
|
43
|
+
} | {
|
|
44
|
+
readonly ok: true;
|
|
45
|
+
readonly state: null;
|
|
46
|
+
readonly removed: true;
|
|
47
|
+
} | {
|
|
48
|
+
readonly ok: false;
|
|
49
|
+
readonly code: 'invalid-transition';
|
|
50
|
+
readonly message: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function queueOf(state: MergeState): MergeQueue;
|
|
53
|
+
export declare function isTerminal(state: MergeState): boolean;
|
|
54
|
+
/** Single transition point; callers must not mutate fields outside this function. */
|
|
55
|
+
export declare function transition(state: MergeState, event: MergeEvent): TransitionResult;
|
|
56
|
+
export declare function allowedEvents(state: MergeState): readonly MergeEvent['type'][];
|
|
57
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function queueOf(state) {
|
|
2
|
+
switch (state) {
|
|
3
|
+
case 'verify': return 'verify';
|
|
4
|
+
case 'pending': return 'pending';
|
|
5
|
+
case 'approved':
|
|
6
|
+
case 'fuse': return 'fuse';
|
|
7
|
+
case 'skipped': return 'skipped';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function isTerminal(state) {
|
|
11
|
+
return state === 'skipped';
|
|
12
|
+
}
|
|
13
|
+
function fail(state, event) {
|
|
14
|
+
return { ok: false, code: 'invalid-transition', message: 'cannot apply ' + event.type + ' to ' + state };
|
|
15
|
+
}
|
|
16
|
+
/** Single transition point; callers must not mutate fields outside this function. */
|
|
17
|
+
export function transition(state, event) {
|
|
18
|
+
switch (event.type) {
|
|
19
|
+
case 'candidate':
|
|
20
|
+
return state === 'verify' ? { ok: true, state: 'verify' } : fail(state, event);
|
|
21
|
+
case 'round1-start':
|
|
22
|
+
return state === 'verify' ? { ok: true, state: 'verify' } : fail(state, event);
|
|
23
|
+
case 'round1-keep':
|
|
24
|
+
return state === 'verify' ? { ok: true, state: 'skipped' } : fail(state, event);
|
|
25
|
+
case 'round1-merge':
|
|
26
|
+
return state === 'verify'
|
|
27
|
+
? { ok: true, state: event.approval ? 'pending' : 'approved' }
|
|
28
|
+
: fail(state, event);
|
|
29
|
+
case 'round1-fail':
|
|
30
|
+
return state === 'verify' ? { ok: true, state: 'verify' } : fail(state, event);
|
|
31
|
+
case 'approve':
|
|
32
|
+
return state === 'pending' ? { ok: true, state: 'approved' } : fail(state, event);
|
|
33
|
+
case 'reject':
|
|
34
|
+
return state === 'pending' || state === 'approved' || state === 'fuse'
|
|
35
|
+
? { ok: true, state: 'skipped' }
|
|
36
|
+
: fail(state, event);
|
|
37
|
+
case 'round2-start':
|
|
38
|
+
return state === 'approved' ? { ok: true, state: 'fuse' } : fail(state, event);
|
|
39
|
+
case 'round2-fail':
|
|
40
|
+
case 'round2-cancel':
|
|
41
|
+
return state === 'fuse' || state === 'approved' ? { ok: true, state: 'approved' } : fail(state, event);
|
|
42
|
+
case 'round2-merged':
|
|
43
|
+
return state === 'fuse' || state === 'approved' ? { ok: true, state: null, removed: true } : fail(state, event);
|
|
44
|
+
case 'staging-expired':
|
|
45
|
+
return state === 'fuse' ? { ok: true, state: 'approved' } : fail(state, event);
|
|
46
|
+
case 'source-gone':
|
|
47
|
+
return { ok: true, state: null, removed: true };
|
|
48
|
+
case 'target-taken':
|
|
49
|
+
case 'source-changed':
|
|
50
|
+
return state === 'pending' ? { ok: true, state: 'pending' } : fail(state, event);
|
|
51
|
+
case 'cancel':
|
|
52
|
+
case 'timeout':
|
|
53
|
+
return state === 'verify' || state === 'approved' || state === 'fuse' ? { ok: true, state } : fail(state, event);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function allowedEvents(state) {
|
|
57
|
+
switch (state) {
|
|
58
|
+
case 'verify': return ['candidate', 'round1-start', 'round1-keep', 'round1-merge', 'round1-fail', 'source-gone', 'cancel', 'timeout'];
|
|
59
|
+
case 'pending': return ['approve', 'reject', 'target-taken', 'source-changed', 'source-gone'];
|
|
60
|
+
case 'approved': return ['round2-start', 'reject', 'staging-expired', 'source-gone', 'cancel', 'timeout'];
|
|
61
|
+
case 'fuse': return ['round2-fail', 'round2-merged', 'round2-cancel', 'staging-expired', 'reject', 'source-gone', 'cancel', 'timeout'];
|
|
62
|
+
case 'skipped': return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed gate cache: <home>/merge/gate-cache.json.
|
|
3
|
+
* Reads are three-state; a malformed file is reported, not overwritten. Writes are one
|
|
4
|
+
* transact so two runs cannot interleave a partial cache.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { probeText, transactIo } from '@lmzhen/dsh-evolution-core';
|
|
8
|
+
import { GateCache } from "./ladder.js";
|
|
9
|
+
export const GATE_CACHE_VERSION = 1;
|
|
10
|
+
export function gateCachePath(home) {
|
|
11
|
+
return join(home, 'merge', 'gate-cache.json');
|
|
12
|
+
}
|
|
13
|
+
export async function loadGateCache(io, home) {
|
|
14
|
+
const probe = await probeText(io, gateCachePath(home));
|
|
15
|
+
if (probe.kind === 'absent')
|
|
16
|
+
return { cache: new GateCache() };
|
|
17
|
+
if (probe.kind === 'unknown')
|
|
18
|
+
return { cache: new GateCache(), note: probe.reason };
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(probe.value);
|
|
21
|
+
if (parsed.schemaVersion !== GATE_CACHE_VERSION || typeof parsed.entries !== 'object' || parsed.entries === null) {
|
|
22
|
+
return { cache: new GateCache(), note: 'gate cache has an unexpected shape' };
|
|
23
|
+
}
|
|
24
|
+
return { cache: GateCache.fromObject(parsed.entries) };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return { cache: new GateCache(), note: 'gate cache is not JSON: ' + (error instanceof Error ? error.message : String(error)) };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function saveGateCache(io, home, cache) {
|
|
31
|
+
await transactIo(io, gateCachePath(home), () => JSON.stringify({ schemaVersion: GATE_CACHE_VERSION, entries: cache.toObject() }, null, 2) + '\n');
|
|
32
|
+
}
|
|
33
|
+
export function hasGateCache(cache) {
|
|
34
|
+
return cache.size > 0;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=gate-cache.js.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
import { screen as engineScreen } from '@lmzhen/dsh-evolution-merge-engine';
|
|
4
|
+
import { selectWithErrors } from "./adapters/select.js";
|
|
5
|
+
import { createEngineScreening } from "./adapters/engine-screen.js";
|
|
6
|
+
import { createEngineRunner } from "./adapters/model-runner.js";
|
|
7
|
+
import { authorityFrom, createSubagentChannel, DEFAULT_SKILL_LIMITS, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, policyStageLimits } from '@lmzhen/dsh-evolution-core';
|
|
8
|
+
import { registerHostRoutes } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
9
|
+
import { loadMergeState, upsertRow } from "./state.js";
|
|
10
|
+
import { cleanupExpiredStaging } from "./staging.js";
|
|
11
|
+
import { readCommitReceipts } from "./commit-receipt.js";
|
|
12
|
+
import { transition } from "./coordinator.js";
|
|
13
|
+
import { makeMergeRoutes } from "./merge-routes.js";
|
|
14
|
+
import { createMergeCommand, runMergeCommand } from "./merge-command.js";
|
|
15
|
+
import { createMergeRuntime } from "./runtime.js";
|
|
16
|
+
import { resolveMergeParams } from "./params.js";
|
|
17
|
+
import { subagentChannelAdapter } from "./adapters/channel.js";
|
|
18
|
+
import { StageToolRegistry } from "./stage-tool-registry.js";
|
|
19
|
+
import { createMergeStageTool } from "./merge-tool.js";
|
|
20
|
+
import { supersedeMergeRuns } from "./supersede.js";
|
|
21
|
+
/** E3 knobs are volatile (user layer); the E2 pair timeout is deployment-only. */
|
|
22
|
+
export const Config = z.object({
|
|
23
|
+
mergeApproval: z.boolean().default(false).volatile(),
|
|
24
|
+
mergeSkippedTtlDays: z.number().min(0).default(0).volatile(),
|
|
25
|
+
mergeRunTimeoutMinutes: z.number().min(0).default(0).volatile(),
|
|
26
|
+
mergePairTimeoutMinutes: z.number().min(1).default(15),
|
|
27
|
+
mergeModelsRoot: z.string().default(''),
|
|
28
|
+
});
|
|
29
|
+
export function engineConfigFromEnv(configuredRoot) {
|
|
30
|
+
const root = configuredRoot !== undefined && configuredRoot !== '' ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
|
|
31
|
+
if (root === undefined || root === '')
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
models_dir: '',
|
|
35
|
+
offline: true,
|
|
36
|
+
auto_provision: false,
|
|
37
|
+
embed_backend: 'local',
|
|
38
|
+
embedder_model: join(root, 'potion-multilingual-128M-int8'),
|
|
39
|
+
embedder_model_last: join(root, 'potion-multilingual-128M-pca64-int8'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Suppress the platform auto form; the registry renders the merge card instead. */
|
|
43
|
+
export function configureMergePresentation(ctx) {
|
|
44
|
+
ctx.inject(['settings'], (scope) => {
|
|
45
|
+
const settings = scope.settings;
|
|
46
|
+
if (typeof settings?.configure !== 'function')
|
|
47
|
+
return;
|
|
48
|
+
const disposer = settings.configure({ auto: false }, ctx.fiber);
|
|
49
|
+
if (typeof disposer === 'function')
|
|
50
|
+
scope.effect(() => disposer, 'evolution-merge: settings presentation');
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export * from "./contracts.js";
|
|
54
|
+
export * from "./skill-card.js";
|
|
55
|
+
export * from "./state.js";
|
|
56
|
+
export * from "./coordinator.js";
|
|
57
|
+
export * from "./report.js";
|
|
58
|
+
export * from "./staging.js";
|
|
59
|
+
export * from "./staging-files.js";
|
|
60
|
+
export * from "./rules.js";
|
|
61
|
+
export * from "./commit-plan.js";
|
|
62
|
+
export * from "./stage-tool.js";
|
|
63
|
+
export * from "./stage-tool-registry.js";
|
|
64
|
+
export * from "./staged-tree.js";
|
|
65
|
+
export * from "./commit-assembly.js";
|
|
66
|
+
export * from "./commit-receipt.js";
|
|
67
|
+
export * from "./commit-execute.js";
|
|
68
|
+
export * from "./undo-merge.js";
|
|
69
|
+
export * from "./adapters/library.js";
|
|
70
|
+
export * from "./adapters/model-runner.js";
|
|
71
|
+
export * from "./adapters/channel.js";
|
|
72
|
+
export * from "./adapters/engine-screen.js";
|
|
73
|
+
export * from "./adapters/undo.js";
|
|
74
|
+
export * from "./run-shell.js";
|
|
75
|
+
export * from "./scan-run.js";
|
|
76
|
+
export * from "./pending-actions.js";
|
|
77
|
+
export * from "./scan-command.js";
|
|
78
|
+
export * from "./round2-outcome.js";
|
|
79
|
+
export * from "./approve-command.js";
|
|
80
|
+
export * from "./pair-command.js";
|
|
81
|
+
export * from "./resume-command.js";
|
|
82
|
+
export * from "./decision.js";
|
|
83
|
+
export * from "./prompts.js";
|
|
84
|
+
export * from "./channel-port.js";
|
|
85
|
+
export * from "./authoring-flow.js";
|
|
86
|
+
export * from "./merge-tool.js";
|
|
87
|
+
export * from "./commands.js";
|
|
88
|
+
export * from "./scan-ports.js";
|
|
89
|
+
export * from "./new-child-authoring.js";
|
|
90
|
+
export * from "./status.js";
|
|
91
|
+
export * from "./skipped.js";
|
|
92
|
+
export * from "./read-commands.js";
|
|
93
|
+
export * from "./routes.js";
|
|
94
|
+
export * from "./merge-routes.js";
|
|
95
|
+
export * from "./merge-command.js";
|
|
96
|
+
export * from "./params.js";
|
|
97
|
+
export * from "./doctor.js";
|
|
98
|
+
export * from "./supersede.js";
|
|
99
|
+
export * from "./runtime.js";
|
|
100
|
+
export const inject = [];
|
|
101
|
+
export function apply(ctx, rawConfig = {}) {
|
|
102
|
+
configureMergePresentation(ctx);
|
|
103
|
+
const params = resolveMergeParams(rawConfig);
|
|
104
|
+
let mergeRun;
|
|
105
|
+
const mergeService = {
|
|
106
|
+
async run(rawInput, invocation) {
|
|
107
|
+
return mergeRun === undefined ? { ok: false, text: 'merge 未就绪:evolutionIo 尚未挂载' } : mergeRun(rawInput, invocation);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const mergeContext = ctx;
|
|
111
|
+
if (typeof mergeContext.provide === 'function') {
|
|
112
|
+
;
|
|
113
|
+
mergeContext.provide('evolutionMerge', mergeService);
|
|
114
|
+
}
|
|
115
|
+
let source;
|
|
116
|
+
registerHostRoutes(ctx, audit => makeMergeRoutes({ getSource: () => source }, audit));
|
|
117
|
+
ctx.inject(['commands', 'evolutionIo'], (scope) => {
|
|
118
|
+
const registry = scope.get('evolutionIo');
|
|
119
|
+
if (registry === undefined)
|
|
120
|
+
return;
|
|
121
|
+
const io = evolutionIoAdapter(() => registry.provider());
|
|
122
|
+
const home = evolutionHome();
|
|
123
|
+
void loadMergeState(io, home).then(loaded => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map(row => row.runId).filter((id) => id !== undefined)))).catch((error) => { ctx.logger.warn('merge staging cleanup failed: ' + (error instanceof Error ? error.message : String(error))); });
|
|
124
|
+
const runs = newRunRegistry({ io, home, warn: (message) => { ctx.logger.warn(message); } });
|
|
125
|
+
void runs.load();
|
|
126
|
+
ctx.effect(() => {
|
|
127
|
+
const host = ctx;
|
|
128
|
+
if (typeof host.on !== 'function')
|
|
129
|
+
return () => { };
|
|
130
|
+
return host.on('evolution/skill-mutated', (event) => {
|
|
131
|
+
if (event.action === 'restore')
|
|
132
|
+
void supersedeMergeRuns(runs);
|
|
133
|
+
});
|
|
134
|
+
}, 'evolution-merge: restore supersede');
|
|
135
|
+
const policySnapshot = ctx.get('evolutionPolicy');
|
|
136
|
+
const skills = newSkillLibrary({
|
|
137
|
+
io, ctx, authority: authorityFrom({ origin: 'subagent' }),
|
|
138
|
+
limits: { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policySnapshot?.get?.()) },
|
|
139
|
+
});
|
|
140
|
+
const channel = subagentChannelAdapter(createSubagentChannel(ctx));
|
|
141
|
+
const stage = new StageToolRegistry();
|
|
142
|
+
const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
|
|
143
|
+
const llm = ctx.get('llm');
|
|
144
|
+
const defaultModel = ctx.get('agentDefaultModel');
|
|
145
|
+
const runner = llm === undefined || defaultModel === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: createEngineRunner(llm, () => defaultModel.currentSelection());
|
|
148
|
+
const engineScreening = createEngineScreening({
|
|
149
|
+
io,
|
|
150
|
+
home,
|
|
151
|
+
engine: { screen: engineScreen },
|
|
152
|
+
select: selectWithErrors,
|
|
153
|
+
cfg: { ...(engineConfig ?? {}), judge: 'deepseek' },
|
|
154
|
+
...(runner === undefined ? {} : { runner }),
|
|
155
|
+
async onSelection(selection) {
|
|
156
|
+
const loaded = await loadMergeState(io, home);
|
|
157
|
+
const at = new Date().toISOString();
|
|
158
|
+
for (const skipped of selection.skipped) {
|
|
159
|
+
if (loaded.state.rows.some(row => row.pairKey === skipped.pairKey))
|
|
160
|
+
continue;
|
|
161
|
+
const next = transition(undefined, {
|
|
162
|
+
type: 'gate-keep', a: skipped.a, b: skipped.b,
|
|
163
|
+
...(skipped.reason === undefined ? {} : { reason: skipped.reason }), at,
|
|
164
|
+
});
|
|
165
|
+
if (next.ok && next.row !== null)
|
|
166
|
+
await upsertRow(io, home, () => Date.now(), next.row);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
source = {
|
|
171
|
+
rows() { return loadMergeState(io, home).then(loaded => loaded.state.rows); },
|
|
172
|
+
run(id) { return Promise.resolve(runs.runs().find(record => record.id === id)); },
|
|
173
|
+
runs() { return Promise.resolve(runs.runs()); },
|
|
174
|
+
async body(id, side) {
|
|
175
|
+
const row = (await loadMergeState(io, home)).state.rows.find(item => item.id === id);
|
|
176
|
+
if (row === undefined)
|
|
177
|
+
return null;
|
|
178
|
+
return await skills.read(side === 'a' ? row.a : row.b);
|
|
179
|
+
},
|
|
180
|
+
async history() {
|
|
181
|
+
const receipts = await readCommitReceipts(io, home);
|
|
182
|
+
const items = new Map();
|
|
183
|
+
for (const receipt of receipts) {
|
|
184
|
+
for (const source of receipt.sources) {
|
|
185
|
+
const key = source + ':' + receipt.runId;
|
|
186
|
+
if (items.has(key))
|
|
187
|
+
continue;
|
|
188
|
+
items.set(key, { name: source, kind: 'merged', reason: 'Merged into ' + receipt.target, lastVersion: 1, archivedAt: new Date(receipt.at).toISOString(), sourceRunId: receipt.runId });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const archived = await skills.archivedNames?.() ?? [];
|
|
192
|
+
const seen = new Set([...items.values()].map(item => item.name));
|
|
193
|
+
for (const name of archived) {
|
|
194
|
+
if (seen.has(name))
|
|
195
|
+
continue;
|
|
196
|
+
items.set('archived:' + name, { name, kind: 'other', reason: 'archived', lastVersion: 1, archivedAt: new Date(Date.now()).toISOString() });
|
|
197
|
+
}
|
|
198
|
+
return [...items.values()];
|
|
199
|
+
},
|
|
200
|
+
approval() { return params.mergeApproval; },
|
|
201
|
+
commands() {
|
|
202
|
+
return ['merge scan', 'merge doctor', 'merge status', 'merge pending', 'merge skipped', 'merge report', 'merge runs', 'merge cancel'];
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const commands = scope.get('commands');
|
|
206
|
+
if (commands === undefined)
|
|
207
|
+
return;
|
|
208
|
+
const runtime = createMergeRuntime({
|
|
209
|
+
io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite: true,
|
|
210
|
+
channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
|
|
211
|
+
async cards(signal) {
|
|
212
|
+
const listed = await skills.list({ withContent: true });
|
|
213
|
+
if (signal.aborted)
|
|
214
|
+
return [];
|
|
215
|
+
return Promise.all(listed.map(async (item) => {
|
|
216
|
+
const probe = await skills.listSupportFiles(item.name);
|
|
217
|
+
const paths = probe.kind === 'present' ? probe.value : [];
|
|
218
|
+
const files = await Promise.all(paths.map(async (path) => ({ path, content: await skills.readSupportFile(item.name, path) ?? '' })));
|
|
219
|
+
return { name: item.name, description: item.description, body: item.content ?? '', files, protectedBy: item.protectedBy, protectionUnknown: item.protectionUnknown };
|
|
220
|
+
}));
|
|
221
|
+
},
|
|
222
|
+
screeningForRun: engineScreening,
|
|
223
|
+
stage: {
|
|
224
|
+
bind: (context) => { stage.bind(context.childId, context); },
|
|
225
|
+
unbind: (childId) => { stage.unbind(childId); },
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
mergeRun = (rawInput, invocation) => runMergeCommand(runtime, rawInput, invocation);
|
|
229
|
+
const command = createMergeCommand(runtime);
|
|
230
|
+
scope.effect(() => commands.register(command), 'evolution-merge: command');
|
|
231
|
+
ctx.inject(['tools'], (toolScope) => {
|
|
232
|
+
const tools = toolScope.get('tools');
|
|
233
|
+
if (tools === undefined)
|
|
234
|
+
return;
|
|
235
|
+
const tool = createMergeStageTool(io, home, exec => stage.resolve(exec));
|
|
236
|
+
toolScope.effect(() => tools.register(tool), 'evolution-merge: merge_stage');
|
|
237
|
+
});
|
|
238
|
+
scope.effect(() => () => { runs.cancelAll(); }, 'evolution-merge: runs');
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L0–L2 candidate gate. This is a CANDIDATE classifier, not a content quality judge.
|
|
3
|
+
* Routing follows the frozen table: L0 -1 stops; L0 1/0 → L1; L1 -1 stops; L1 0/1 → L2;
|
|
4
|
+
* L2 1 → agent1; L2 0 → agent1 (undecided); L2 -1 → skipped.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
export class GateCache {
|
|
8
|
+
entries = new Map();
|
|
9
|
+
get(key) { return this.entries.get(key); }
|
|
10
|
+
put(key, value) { this.entries.set(key, value); }
|
|
11
|
+
get size() { return this.entries.size; }
|
|
12
|
+
toObject() { return Object.fromEntries(this.entries); }
|
|
13
|
+
static fromObject(value) {
|
|
14
|
+
const cache = new GateCache();
|
|
15
|
+
if (typeof value !== 'object' || value === null)
|
|
16
|
+
return cache;
|
|
17
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
18
|
+
if (typeof entry !== 'object' || entry === null)
|
|
19
|
+
continue;
|
|
20
|
+
const verdict = entry;
|
|
21
|
+
if (verdict.verdict === 1 || verdict.verdict === 0 || verdict.verdict === -1) {
|
|
22
|
+
cache.put(key, { level: verdict.level === 'L0' || verdict.level === 'L1' || verdict.level === 'L2' ? verdict.level : 'L2', verdict: verdict.verdict, reason: typeof verdict.reason === 'string' ? verdict.reason : '', evidence: Array.isArray(verdict.evidence) ? verdict.evidence : [], cached: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return cache;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function gateCacheKey(level, payload, model) {
|
|
29
|
+
return createHash('sha256').update(JSON.stringify({ level, payload, model })).digest('hex');
|
|
30
|
+
}
|
|
31
|
+
function payloadFor(level, candidate, facts) {
|
|
32
|
+
const a = facts.get(candidate.a);
|
|
33
|
+
const b = facts.get(candidate.b);
|
|
34
|
+
if (a === undefined || b === undefined)
|
|
35
|
+
throw new Error('candidate names are not in the fact set');
|
|
36
|
+
const base = {
|
|
37
|
+
level,
|
|
38
|
+
skill_a: { name: a.name, description: a.description },
|
|
39
|
+
skill_b: { name: b.name, description: b.description },
|
|
40
|
+
scores: { jaccard_summary: candidate.score ?? null },
|
|
41
|
+
schema: { verdict: '1 | 0 | -1', reason: '<=80 chars' },
|
|
42
|
+
};
|
|
43
|
+
if (level === 'L0')
|
|
44
|
+
return JSON.stringify(base);
|
|
45
|
+
const withHead = {
|
|
46
|
+
...base,
|
|
47
|
+
skill_a: { ...base.skill_a, head: a.body.slice(0, 2400) },
|
|
48
|
+
skill_b: { ...base.skill_b, head: b.body.slice(0, 2400) },
|
|
49
|
+
};
|
|
50
|
+
if (level === 'L1')
|
|
51
|
+
return JSON.stringify(withHead);
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
...withHead,
|
|
54
|
+
skill_a: { ...withHead.skill_a, body_excerpt: a.body.slice(0, 4000) },
|
|
55
|
+
skill_b: { ...withHead.skill_b, body_excerpt: b.body.slice(0, 4000) },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const SYSTEM = 'Judge whether two skill documents describe the same reusable capability. Shared vocabulary or a shared preamble is not duplication. Return only strict JSON: {"verdict":1|0|-1,"reason":"..."}';
|
|
59
|
+
function extractJson(text) {
|
|
60
|
+
const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
|
|
61
|
+
const body = fenced?.[1] ?? text;
|
|
62
|
+
const start = body.indexOf('{');
|
|
63
|
+
if (start < 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
let depth = 0;
|
|
66
|
+
for (let index = start; index < body.length; index += 1) {
|
|
67
|
+
if (body[index] === '{')
|
|
68
|
+
depth += 1;
|
|
69
|
+
else if (body[index] === '}') {
|
|
70
|
+
depth -= 1;
|
|
71
|
+
if (depth === 0) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(body.slice(start, index + 1));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
export function parseVerdict(level, raw, cached = false) {
|
|
84
|
+
const value = typeof raw === 'string' ? extractJson(raw) : raw;
|
|
85
|
+
if (typeof value !== 'object' || value === null)
|
|
86
|
+
return undefined;
|
|
87
|
+
const record = value;
|
|
88
|
+
const verdict = record.verdict === 1 || record.verdict === '1' ? 1
|
|
89
|
+
: record.verdict === 0 || record.verdict === '0' ? 0
|
|
90
|
+
: record.verdict === -1 || record.verdict === '-1' ? -1
|
|
91
|
+
: undefined;
|
|
92
|
+
if (verdict === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const reason = typeof record.reason === 'string' ? record.reason.slice(0, 80) : '';
|
|
95
|
+
const evidence = Array.isArray(record.evidence)
|
|
96
|
+
? record.evidence
|
|
97
|
+
.filter(item => typeof item === 'object' && item !== null)
|
|
98
|
+
.map((item) => {
|
|
99
|
+
const entry = item;
|
|
100
|
+
return { path: typeof entry.path === 'string' ? entry.path : '', line: typeof entry.line === 'number' ? entry.line : -1, quote: typeof entry.quote === 'string' ? entry.quote.slice(0, 120) : '' };
|
|
101
|
+
})
|
|
102
|
+
: [];
|
|
103
|
+
return { level, verdict, reason, evidence, cached };
|
|
104
|
+
}
|
|
105
|
+
/** Deterministic 10% L0 audit selection from the run seed + pair key. */
|
|
106
|
+
export function selectedForAudit(pairKey, seed, rate = 0.1) {
|
|
107
|
+
const digest = createHash('sha256').update(seed + '\u0000' + pairKey).digest();
|
|
108
|
+
return digest[0] !== undefined && digest[0] < Math.round(Math.max(0, Math.min(1, rate)) * 256);
|
|
109
|
+
}
|
|
110
|
+
export async function runLadder(options) {
|
|
111
|
+
const facts = new Map(options.facts.map(fact => [fact.name, fact]));
|
|
112
|
+
const verdicts = [];
|
|
113
|
+
let calls = 0;
|
|
114
|
+
let level = options.startLevel ?? 'L0';
|
|
115
|
+
for (;;) {
|
|
116
|
+
let payload;
|
|
117
|
+
try {
|
|
118
|
+
payload = payloadFor(level, options.candidate, facts);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { route: 'error', verdicts, calls, error: error instanceof Error ? error.message : String(error) };
|
|
122
|
+
}
|
|
123
|
+
const key = gateCacheKey(level, payload, options.modelName);
|
|
124
|
+
const cached = options.cache?.get(key);
|
|
125
|
+
let verdict;
|
|
126
|
+
if (cached !== undefined)
|
|
127
|
+
verdict = { ...cached, level, cached: true };
|
|
128
|
+
else {
|
|
129
|
+
try {
|
|
130
|
+
calls += 1;
|
|
131
|
+
const raw = await options.model.callJson({ system: SYSTEM, user: payload, maxTokens: 500, signal: options.signal });
|
|
132
|
+
verdict = parseVerdict(level, raw);
|
|
133
|
+
if (verdict !== undefined)
|
|
134
|
+
options.cache?.put(key, verdict);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return { route: 'error', verdicts, calls, error: error instanceof Error ? error.message : String(error) };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (verdict === undefined)
|
|
141
|
+
return { route: 'error', verdicts, calls, error: 'invalid gate JSON' };
|
|
142
|
+
verdicts.push(verdict);
|
|
143
|
+
if (level === 'L0') {
|
|
144
|
+
if (verdict.verdict === -1)
|
|
145
|
+
return { route: 'keep', verdicts, calls, droppedAt: 'L0' };
|
|
146
|
+
level = 'L1';
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (level === 'L1') {
|
|
150
|
+
if (verdict.verdict === -1)
|
|
151
|
+
return { route: 'keep', verdicts, calls, droppedAt: 'L1' };
|
|
152
|
+
level = 'L2';
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (verdict.verdict === 1)
|
|
156
|
+
return { route: 'merge', verdicts, calls };
|
|
157
|
+
if (verdict.verdict === 0)
|
|
158
|
+
return { route: 'undecided', verdicts, calls };
|
|
159
|
+
return { route: 'keep', verdicts, calls, droppedAt: 'L2' };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=ladder.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseMergeCommand, mergeUsage } from "./commands.js";
|
|
2
|
+
import { handleReadOnlyMergeCommand } from "./read-commands.js";
|
|
3
|
+
import { formatMergeError } from "./domain/errors.js";
|
|
4
|
+
export function createReadOnlyMergeService(io, home) {
|
|
5
|
+
return {
|
|
6
|
+
async execute(command) {
|
|
7
|
+
const text = await handleReadOnlyMergeCommand(command, io, home);
|
|
8
|
+
return text === null
|
|
9
|
+
? { ok: false, text: 'merge ' + command.verb + ' 需要运行引擎(当前只挂载了只读命令)' }
|
|
10
|
+
: { ok: true, text };
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function runMergeCommand(service, rawInput, invocation) {
|
|
15
|
+
const tokens = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
16
|
+
if (tokens[0] === 'merge' || tokens[0] === '/merge' || tokens[0] === 'evolution' || tokens[0] === '/evolution')
|
|
17
|
+
tokens.shift();
|
|
18
|
+
if (tokens[0] === 'merge')
|
|
19
|
+
tokens.shift();
|
|
20
|
+
const parsed = parseMergeCommand(tokens);
|
|
21
|
+
if (!parsed.ok)
|
|
22
|
+
return { ok: false, text: parsed.message };
|
|
23
|
+
if (parsed.help === true)
|
|
24
|
+
return { ok: true, text: mergeUsage() };
|
|
25
|
+
try {
|
|
26
|
+
return await service.execute(parsed.command, invocation?.agent === undefined ? {} : { parent: invocation.agent });
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return { ok: false, text: formatMergeError({ kind: 'runtime', code: 'merge-command-failed', message: error instanceof Error ? error.message : String(error) }) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function createMergeCommand(service) {
|
|
33
|
+
return {
|
|
34
|
+
name: 'merge',
|
|
35
|
+
description: '技能自动合并:扫描、待批准、重跑、报告与 undo(兼容别名;请用 /evolution merge)',
|
|
36
|
+
input: { hint: mergeUsage() },
|
|
37
|
+
async handler(invocation) {
|
|
38
|
+
const result = await runMergeCommand(service, invocation.rawInput, invocation);
|
|
39
|
+
return { kind: result.ok ? 'success' : 'error', text: result.text };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=merge-command.js.map
|
|
@@ -6,15 +6,25 @@ import type { RunRecord } from '@lmzhen/dsh-evolution-core';
|
|
|
6
6
|
import { type ConnectionFetchRouteLike, type DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
7
7
|
import type { MergeRow } from './contracts.ts';
|
|
8
8
|
export declare const MERGE_ROUTES: {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
view: `/api/dsh-evolution/${string}`;
|
|
10
|
+
rows: `/api/dsh-evolution/${string}`;
|
|
11
|
+
body: `/api/dsh-evolution/${string}`;
|
|
12
|
+
history: `/api/dsh-evolution/${string}`;
|
|
13
13
|
};
|
|
14
|
+
export interface MergeHistoryItem {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly kind: 'merged' | 'deleted' | 'split' | 'other';
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
readonly lastVersion: number;
|
|
19
|
+
readonly archivedAt: string;
|
|
20
|
+
readonly sourceRunId?: string;
|
|
21
|
+
}
|
|
14
22
|
export interface MergeRouteSource {
|
|
15
23
|
rows(): Promise<readonly MergeRow[]>;
|
|
16
24
|
run(id: string): Promise<RunRecord | undefined>;
|
|
17
25
|
runs(cursor?: string, limit?: number): Promise<readonly RunRecord[]>;
|
|
26
|
+
body(id: string, side: 'a' | 'b'): Promise<string | null>;
|
|
27
|
+
history(): Promise<readonly MergeHistoryItem[]>;
|
|
18
28
|
commands(): readonly string[];
|
|
19
29
|
approval?(): boolean;
|
|
20
30
|
}
|