@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,99 @@
|
|
|
1
|
+
import { HOST_ROUTES, json, methodNotAllowed } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
2
|
+
import { buildMergeView, pageQueue, toRunFact } from "./routes.js";
|
|
3
|
+
import { renderMarkdown } from "./domain/markdown.js";
|
|
4
|
+
const MERGE_PATHS = HOST_ROUTES.merge;
|
|
5
|
+
const MERGE_BASE = MERGE_PATHS.view.slice(0, -'view'.length);
|
|
6
|
+
export const MERGE_ROUTES = {
|
|
7
|
+
view: MERGE_PATHS.view,
|
|
8
|
+
rows: MERGE_PATHS.rows,
|
|
9
|
+
body: (MERGE_PATHS.body ?? MERGE_BASE + 'body'),
|
|
10
|
+
history: (MERGE_PATHS.history ?? MERGE_BASE + 'history'),
|
|
11
|
+
};
|
|
12
|
+
export function makeMergeRoutes(deps, _audit) {
|
|
13
|
+
const absent = () => json(503, { ok: false, error: { code: 'merge-service-absent', message: 'merge service is not mounted' } });
|
|
14
|
+
const get = (request) => (request.method === 'GET' ? null : methodNotAllowed(['GET']));
|
|
15
|
+
const url = (request) => new URL(request.url, 'http://local');
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
path: MERGE_ROUTES.view,
|
|
19
|
+
methods: ['GET'],
|
|
20
|
+
requestBody: 'buffered',
|
|
21
|
+
fetch: async (request) => {
|
|
22
|
+
const denied = get(request);
|
|
23
|
+
if (denied !== null)
|
|
24
|
+
return denied;
|
|
25
|
+
const source = deps.getSource();
|
|
26
|
+
if (source === undefined)
|
|
27
|
+
return absent();
|
|
28
|
+
const runs = await source.runs(undefined, 20);
|
|
29
|
+
const latestMerge = runs.find(run => run.kind === 'merge');
|
|
30
|
+
return json(200, {
|
|
31
|
+
ok: true,
|
|
32
|
+
data: buildMergeView(await source.rows(), source.commands(), latestMerge === undefined ? undefined : toRunFact(latestMerge), source.approval?.()),
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: MERGE_ROUTES.rows,
|
|
38
|
+
methods: ['GET'],
|
|
39
|
+
requestBody: 'buffered',
|
|
40
|
+
fetch: async (request) => {
|
|
41
|
+
const denied = get(request);
|
|
42
|
+
if (denied !== null)
|
|
43
|
+
return denied;
|
|
44
|
+
const source = deps.getSource();
|
|
45
|
+
if (source === undefined)
|
|
46
|
+
return absent();
|
|
47
|
+
const query = url(request).searchParams;
|
|
48
|
+
const queue = query.get('queue') ?? 'verify';
|
|
49
|
+
if (queue !== 'verify' && queue !== 'pending' && queue !== 'fuse' && queue !== 'skipped') {
|
|
50
|
+
return json(400, { ok: false, error: { code: 'bad-queue', message: 'queue must be verify/pending/fuse/skipped' } });
|
|
51
|
+
}
|
|
52
|
+
const cursor = Number.parseInt(query.get('cursor') ?? '0', 10);
|
|
53
|
+
const limit = Number.parseInt(query.get('limit') ?? '50', 10);
|
|
54
|
+
return json(200, {
|
|
55
|
+
ok: true,
|
|
56
|
+
data: pageQueue(await source.rows(), queue, Number.isFinite(cursor) ? cursor : 0, Number.isFinite(limit) ? limit : 50),
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
path: MERGE_ROUTES.body,
|
|
62
|
+
methods: ['GET'],
|
|
63
|
+
requestBody: 'buffered',
|
|
64
|
+
fetch: async (request) => {
|
|
65
|
+
const denied = get(request);
|
|
66
|
+
if (denied !== null)
|
|
67
|
+
return denied;
|
|
68
|
+
const source = deps.getSource();
|
|
69
|
+
if (source === undefined)
|
|
70
|
+
return absent();
|
|
71
|
+
const query = url(request).searchParams;
|
|
72
|
+
const id = query.get('id');
|
|
73
|
+
const side = query.get('side');
|
|
74
|
+
if (id === null || id === '' || (side !== 'a' && side !== 'b')) {
|
|
75
|
+
return json(400, { ok: false, error: { code: 'bad-body-query', message: 'id and side=a|b are required' } });
|
|
76
|
+
}
|
|
77
|
+
const content = await source.body(id, side);
|
|
78
|
+
return content === null
|
|
79
|
+
? json(404, { ok: false, error: { code: 'body-not-found', message: 'merge body not found: ' + id } })
|
|
80
|
+
: json(200, { ok: true, data: { content, markdown: true, html: renderMarkdown(content) } });
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
path: MERGE_ROUTES.history,
|
|
85
|
+
methods: ['GET'],
|
|
86
|
+
requestBody: 'buffered',
|
|
87
|
+
fetch: async (request) => {
|
|
88
|
+
const denied = get(request);
|
|
89
|
+
if (denied !== null)
|
|
90
|
+
return denied;
|
|
91
|
+
const source = deps.getSource();
|
|
92
|
+
if (source === undefined)
|
|
93
|
+
return absent();
|
|
94
|
+
return json(200, { ok: true, data: { items: await source.history() } });
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=merge-routes.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { applyStageTool } from "./stage-tool.js";
|
|
2
|
+
export const MERGE_STAGE_TOOL_NAME = 'merge_stage';
|
|
3
|
+
function shapeError(message) {
|
|
4
|
+
return { ok: false, code: 'bad-path', message };
|
|
5
|
+
}
|
|
6
|
+
function operation(args) {
|
|
7
|
+
const op = args.op;
|
|
8
|
+
if (op === 'list') {
|
|
9
|
+
const area = args.area;
|
|
10
|
+
if (area !== 'sources' && area !== 'merged')
|
|
11
|
+
return shapeError('list.area must be sources or merged');
|
|
12
|
+
return { op, area, ...(typeof args.path === 'string' ? { path: args.path } : {}), ...(typeof args.source === 'string' ? { source: args.source } : {}) };
|
|
13
|
+
}
|
|
14
|
+
if (op === 'read') {
|
|
15
|
+
const area = args.area;
|
|
16
|
+
if (area !== 'sources' && area !== 'merged')
|
|
17
|
+
return shapeError('read.area must be sources or merged');
|
|
18
|
+
if (typeof args.path !== 'string' || args.path === '')
|
|
19
|
+
return shapeError('read.path is required');
|
|
20
|
+
return { op, area, path: args.path, ...(typeof args.source === 'string' ? { source: args.source } : {}) };
|
|
21
|
+
}
|
|
22
|
+
if (op === 'write') {
|
|
23
|
+
if (typeof args.path !== 'string' || args.path === '')
|
|
24
|
+
return shapeError('write.path is required');
|
|
25
|
+
if (typeof args.content !== 'string')
|
|
26
|
+
return shapeError('write.content must be a string');
|
|
27
|
+
return { op, path: args.path, content: args.content };
|
|
28
|
+
}
|
|
29
|
+
if (op === 'copy') {
|
|
30
|
+
if (typeof args.source !== 'string' || typeof args.from !== 'string' || typeof args.to !== 'string')
|
|
31
|
+
return shapeError('copy requires source/from/to');
|
|
32
|
+
return { op, source: args.source, from: args.from, to: args.to };
|
|
33
|
+
}
|
|
34
|
+
return shapeError('op must be list/read/write/copy');
|
|
35
|
+
}
|
|
36
|
+
export function createMergeStageTool(io, home, context) {
|
|
37
|
+
return {
|
|
38
|
+
name: MERGE_STAGE_TOOL_NAME,
|
|
39
|
+
description: 'Read source copies and write the merged skill into the staging tree. list/read use area=sources|merged plus source (for sources); write/copy paths are relative to merged/<target> (for example path=SKILL.md, from=SKILL.md, to=references/x.md). Write/copy are only allowed in the author phase.',
|
|
40
|
+
parameters: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
op: { type: 'string', enum: ['list', 'read', 'write', 'copy'] },
|
|
44
|
+
area: { type: 'string', enum: ['sources', 'merged'] },
|
|
45
|
+
path: { type: 'string' },
|
|
46
|
+
source: { type: 'string' },
|
|
47
|
+
content: { type: 'string' },
|
|
48
|
+
from: { type: 'string' },
|
|
49
|
+
to: { type: 'string' },
|
|
50
|
+
},
|
|
51
|
+
required: ['op'],
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
},
|
|
54
|
+
output: {
|
|
55
|
+
schema: { type: 'string' },
|
|
56
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
57
|
+
},
|
|
58
|
+
async execute(args, exec) {
|
|
59
|
+
const op = operation(args);
|
|
60
|
+
if ('ok' in op)
|
|
61
|
+
return JSON.stringify(op);
|
|
62
|
+
const ctx = context(exec);
|
|
63
|
+
if (ctx === undefined)
|
|
64
|
+
return JSON.stringify({ ok: false, code: 'not-merge-child', message: 'merge_stage is only available to an active merge child' });
|
|
65
|
+
return JSON.stringify(await applyStageTool(io, home, ctx, op));
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=merge-tool.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { buildRound2Prompt, STANDBY_PROMPT } from "./prompts.js";
|
|
2
|
+
import { createStaging, readStaging, setStagingTarget, stageKeyOf, stagingRoot } from "./staging.js";
|
|
3
|
+
import { pairKey } from "./contracts.js";
|
|
4
|
+
import { readStagedTree } from "./staged-tree.js";
|
|
5
|
+
import { validateStagedTree, hasBlockingFinding } from "./rules.js";
|
|
6
|
+
import { commitStagedRun } from "./commit-execute.js";
|
|
7
|
+
export async function runNewChildAuthoring(options, row, decision, signal) {
|
|
8
|
+
const targetCard = options.cards.find(card => card.name === decision.target);
|
|
9
|
+
if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === 'pinned')
|
|
10
|
+
return { kind: 'failed', failure: { stage: 'author', code: 'target-pinned', message: 'target is pinned: ' + decision.target, retryable: false, retryFrom: 'none' } };
|
|
11
|
+
const stageKey = row.stagingRef ?? stageKeyOf(options.runId, pairKey(row.a, row.b));
|
|
12
|
+
const existing = await readStaging(options.io, options.home, stageKey);
|
|
13
|
+
if (existing.kind === 'present') {
|
|
14
|
+
await setStagingTarget(options.io, options.home, stageKey, decision.target, decision.mode);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const staging = await createStaging(options.io, options.home, {
|
|
18
|
+
runId: stageKey,
|
|
19
|
+
target: decision.target,
|
|
20
|
+
mode: decision.mode,
|
|
21
|
+
sources: options.cards
|
|
22
|
+
.filter(card => card.name === row.a || card.name === row.b)
|
|
23
|
+
.map(card => ({ name: card.name, body: card.body, files: card.files ?? [] })),
|
|
24
|
+
promptVersion: 'merge-b/v1',
|
|
25
|
+
ttlDays: 7,
|
|
26
|
+
now: options.now,
|
|
27
|
+
});
|
|
28
|
+
if (!staging.ok)
|
|
29
|
+
return { kind: 'failed', failure: { stage: 'author', code: 'staging-create', message: staging.message, retryable: true, retryFrom: 'agent2' } };
|
|
30
|
+
}
|
|
31
|
+
const opened = await options.channel.open({
|
|
32
|
+
parent: options.parent,
|
|
33
|
+
label: 'merge-round2',
|
|
34
|
+
promptText: STANDBY_PROMPT,
|
|
35
|
+
kind: 'maintainer',
|
|
36
|
+
allow: options.allow,
|
|
37
|
+
cwd: stagingRoot(options.home, stageKey),
|
|
38
|
+
timeoutMs: options.timeoutMs,
|
|
39
|
+
signal,
|
|
40
|
+
});
|
|
41
|
+
if (opened.kind !== 'ok') {
|
|
42
|
+
return { kind: 'failed', failure: { stage: 'author', code: opened.kind, message: opened.kind === 'unavailable' ? opened.reason : 'round2 aborted', retryable: opened.kind !== 'aborted', retryFrom: opened.kind === 'aborted' ? 'none' : 'agent2' } };
|
|
43
|
+
}
|
|
44
|
+
options.onChild?.({ runId: stageKey, childId: opened.childId, expectedChildId: opened.childId, phase: 'author' });
|
|
45
|
+
try {
|
|
46
|
+
if (opened.turn.stopReason !== 'completed') {
|
|
47
|
+
return { kind: 'failed', failure: { stage: 'author', code: opened.turn.stopReason, message: 'standby did not complete', retryable: true, retryFrom: 'agent2' } };
|
|
48
|
+
}
|
|
49
|
+
const turn = await options.channel.continueTurn(opened.childId, buildRound2Prompt(row, decision, 'fresh'), { timeoutMs: options.timeoutMs, signal });
|
|
50
|
+
if (turn.kind !== 'ok')
|
|
51
|
+
return { kind: 'failed', failure: { stage: 'author', code: turn.kind, message: turn.kind === 'unavailable' ? turn.reason : 'round2 aborted', retryable: turn.kind !== 'aborted', retryFrom: turn.kind === 'aborted' ? 'none' : 'agent2' } };
|
|
52
|
+
if (turn.turn.stopReason !== 'completed') {
|
|
53
|
+
return { kind: 'failed', failure: { stage: 'author', code: turn.turn.stopReason, message: 'round2 did not complete', retryable: true, retryFrom: 'agent2' } };
|
|
54
|
+
}
|
|
55
|
+
const tree = await readStagedTree(options.io, options.home, stageKey);
|
|
56
|
+
if (tree.kind !== 'present')
|
|
57
|
+
return { kind: 'failed', failure: { stage: 'author', code: 'staging-missing', message: tree.kind === 'unknown' ? tree.reason : 'staged tree absent', retryable: true, retryFrom: 'agent2' } };
|
|
58
|
+
const findings = validateStagedTree(tree.value);
|
|
59
|
+
if (hasBlockingFinding(findings))
|
|
60
|
+
return { kind: 'failed', failure: { stage: 'validate', code: 'rule-gate', message: 'staged tree failed the rule gate', retryable: true, retryFrom: 'agent2' } };
|
|
61
|
+
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, { origin: 'subagent', summary: 'merge ' + row.a + ' + ' + row.b, now: options.now });
|
|
62
|
+
if (!commit.ok)
|
|
63
|
+
return { kind: 'failed', failure: { stage: 'commit', code: 'commit-failed', message: commit.message, retryable: true, retryFrom: 'agent2' } };
|
|
64
|
+
const receipt = commit.receipt;
|
|
65
|
+
return {
|
|
66
|
+
kind: 'merged',
|
|
67
|
+
...(receipt?.unitId === undefined ? {} : { unitId: receipt.unitId }),
|
|
68
|
+
archived: receipt?.archived ?? [],
|
|
69
|
+
target: { requested: decision.target, actual: commit.plan.target, created: receipt?.create ?? false },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
try {
|
|
74
|
+
await options.channel.dispose(opened.childId);
|
|
75
|
+
}
|
|
76
|
+
catch { /* teardown must not replace the outcome */ }
|
|
77
|
+
options.onChildClosed?.(opened.childId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function createNewChildRound2(options) {
|
|
81
|
+
return (row, decision, signal, runId) => runNewChildAuthoring({ ...options, runId }, row, decision, signal);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=new-child-authoring.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runScanCommand } from "./scan-command.js";
|
|
2
|
+
export async function runPairCommand(options) {
|
|
3
|
+
const scanOptions = {
|
|
4
|
+
io: options.io,
|
|
5
|
+
home: options.home,
|
|
6
|
+
registry: options.registry,
|
|
7
|
+
approval: options.approval,
|
|
8
|
+
platformWrite: options.platformWrite,
|
|
9
|
+
now: options.now,
|
|
10
|
+
ports: {
|
|
11
|
+
...options.ports,
|
|
12
|
+
screening: () => Promise.resolve([{ a: options.a, b: options.b }]),
|
|
13
|
+
},
|
|
14
|
+
...(options.timeoutMinutes === undefined ? {} : { timeoutMinutes: options.timeoutMinutes }),
|
|
15
|
+
};
|
|
16
|
+
return await runScanCommand(scanOptions);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=pair-command.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const MERGE_PARAM_DEFAULTS = {
|
|
2
|
+
mergeApproval: false,
|
|
3
|
+
mergeSkippedTtlDays: 0,
|
|
4
|
+
mergeRunTimeoutMinutes: 0,
|
|
5
|
+
mergePairTimeoutMinutes: 15,
|
|
6
|
+
mergeModelsRoot: '',
|
|
7
|
+
};
|
|
8
|
+
const KEYS = new Set(Object.keys(MERGE_PARAM_DEFAULTS));
|
|
9
|
+
function materialize(value) {
|
|
10
|
+
if (typeof value === 'object' && value !== null && typeof value.get === 'function') {
|
|
11
|
+
return value.get();
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function nonNegative(name, value) {
|
|
16
|
+
const resolved = materialize(value);
|
|
17
|
+
if (typeof resolved !== 'number' || !Number.isInteger(resolved) || resolved < 0)
|
|
18
|
+
throw new Error(name + ' must be a non-negative integer');
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
export function resolveMergeParams(input = {}) {
|
|
22
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input))
|
|
23
|
+
throw new Error('merge params must be an object');
|
|
24
|
+
for (const key of Object.keys(input))
|
|
25
|
+
if (!KEYS.has(key))
|
|
26
|
+
throw new Error('unknown merge parameter: ' + key);
|
|
27
|
+
const record = input;
|
|
28
|
+
const approval = record.mergeApproval === undefined ? MERGE_PARAM_DEFAULTS.mergeApproval : materialize(record.mergeApproval);
|
|
29
|
+
if (typeof approval !== 'boolean')
|
|
30
|
+
throw new Error('mergeApproval must be a boolean');
|
|
31
|
+
return {
|
|
32
|
+
mergeApproval: approval,
|
|
33
|
+
mergeSkippedTtlDays: record.mergeSkippedTtlDays === undefined ? MERGE_PARAM_DEFAULTS.mergeSkippedTtlDays : nonNegative('mergeSkippedTtlDays', record.mergeSkippedTtlDays),
|
|
34
|
+
mergeRunTimeoutMinutes: record.mergeRunTimeoutMinutes === undefined ? MERGE_PARAM_DEFAULTS.mergeRunTimeoutMinutes : nonNegative('mergeRunTimeoutMinutes', record.mergeRunTimeoutMinutes),
|
|
35
|
+
mergeModelsRoot: record.mergeModelsRoot === undefined ? MERGE_PARAM_DEFAULTS.mergeModelsRoot : (() => {
|
|
36
|
+
const value = materialize(record.mergeModelsRoot);
|
|
37
|
+
if (typeof value !== 'string')
|
|
38
|
+
throw new Error('mergeModelsRoot must be a string');
|
|
39
|
+
return value;
|
|
40
|
+
})(),
|
|
41
|
+
mergePairTimeoutMinutes: record.mergePairTimeoutMinutes === undefined ? MERGE_PARAM_DEFAULTS.mergePairTimeoutMinutes : (() => {
|
|
42
|
+
const value = nonNegative('mergePairTimeoutMinutes', record.mergePairTimeoutMinutes);
|
|
43
|
+
if (value === 0)
|
|
44
|
+
throw new Error('mergePairTimeoutMinutes must be > 0');
|
|
45
|
+
return value;
|
|
46
|
+
})(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=params.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { transition } from "./coordinator.js";
|
|
2
|
+
import { mutateMergeState } from "./state.js";
|
|
3
|
+
async function act(io, home, now, id, type) {
|
|
4
|
+
return await mutateMergeState(io, home, now, (rows) => {
|
|
5
|
+
const index = rows.findIndex(row => row.id === id);
|
|
6
|
+
if (index < 0)
|
|
7
|
+
return { rows, result: { ok: false, reason: 'not-found', message: 'pending row not found: ' + id } };
|
|
8
|
+
const current = rows[index];
|
|
9
|
+
if (current.state !== 'pending')
|
|
10
|
+
return { rows, result: { ok: false, reason: 'not-pending', message: 'row is not pending: ' + current.state } };
|
|
11
|
+
const event = type === 'approve'
|
|
12
|
+
? { type: 'approve', at: new Date(now()).toISOString() }
|
|
13
|
+
: { type: 'reject', at: new Date(now()).toISOString() };
|
|
14
|
+
const next = transition(current, event);
|
|
15
|
+
if (!next.ok || next.row === null)
|
|
16
|
+
return { rows, result: { ok: false, reason: 'invalid', message: next.ok ? 'transition produced no row' : next.message } };
|
|
17
|
+
const copy = [...rows];
|
|
18
|
+
copy[index] = next.row;
|
|
19
|
+
return { rows: copy, result: { ok: true, row: next.row } };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function approvePending(io, home, now, id) {
|
|
23
|
+
return await act(io, home, now, id, 'approve');
|
|
24
|
+
}
|
|
25
|
+
export async function rejectPending(io, home, now, id) {
|
|
26
|
+
return await act(io, home, now, id, 'reject');
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=pending-actions.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const MERGE_PROMPT_VERSION = 'merge-b/v1';
|
|
2
|
+
export const STANDBY_PROMPT = 'You are a merge worker. Wait for instructions.';
|
|
3
|
+
function sourcePath(name) { return 'sources/' + name; }
|
|
4
|
+
function mergedPath(target) { return 'merged/' + target; }
|
|
5
|
+
function wireDecision(decision) {
|
|
6
|
+
return {
|
|
7
|
+
shouldMerge: decision.shouldMerge,
|
|
8
|
+
mode: decision.mode,
|
|
9
|
+
target: decision.target,
|
|
10
|
+
sources: decision.sources,
|
|
11
|
+
rationale: decision.rationale,
|
|
12
|
+
confidence: decision.confidence,
|
|
13
|
+
a_summary: decision.aSummary,
|
|
14
|
+
b_summary: decision.bSummary,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function buildRound1Prompt(candidate, cards) {
|
|
18
|
+
void cards;
|
|
19
|
+
return [
|
|
20
|
+
'你是技能合并决策 agent。第 1 轮只做判断,不要写文件、不要修改任何源目录。',
|
|
21
|
+
'',
|
|
22
|
+
'在当前工作目录下用 merge_stage 的 list/read 完整阅读两个源技能:',
|
|
23
|
+
'- A: ' + sourcePath(candidate.a) + '(name=' + candidate.a + ')',
|
|
24
|
+
'- B: ' + sourcePath(candidate.b) + '(name=' + candidate.b + ')',
|
|
25
|
+
'包含 SKILL.md 与 references/templates/scripts 等支撑文件。',
|
|
26
|
+
'',
|
|
27
|
+
'判断规则:',
|
|
28
|
+
'1) 两者是否语义等价/互补到值得合并,而不是词面相似;',
|
|
29
|
+
'2) 合并后是否更容易发现和使用,而不是变成更大的巨技能;',
|
|
30
|
+
'3) 若合并会破坏触发面/生命周期/可发现性,应输出 keep。',
|
|
31
|
+
'',
|
|
32
|
+
'只输出一个 JSON(不要 markdown 围栏):',
|
|
33
|
+
'{"should_merge":true|false,"mode":"merge-into-existing|create-umbrella|keep","target":"kebab-case","sources":["源技能名"],"rationale":"<=150字","confidence":0.0,"a_summary":"A 的一句话摘要","b_summary":"B 的一句话摘要"}',
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
export function buildRound2Prompt(row, decision, mode = 'continue') {
|
|
37
|
+
const decisionJson = JSON.stringify(wireDecision(decision));
|
|
38
|
+
const head = [
|
|
39
|
+
'现在执行第 2 轮:把两个技能融合成一个完整、可用的技能,直接在当前工作目录的 merged/ 下写文件。',
|
|
40
|
+
'不要修改 ' + sourcePath(row.a) + ' 或 ' + sourcePath(row.b) + '(只读源副本);不要访问工作目录以外的路径。',
|
|
41
|
+
'',
|
|
42
|
+
];
|
|
43
|
+
const body = [
|
|
44
|
+
'决策结果:' + decisionJson,
|
|
45
|
+
'目标技能名:' + decision.target,
|
|
46
|
+
'',
|
|
47
|
+
'工作方式:',
|
|
48
|
+
'- 用 merge_stage 的 list/read/write/copy 创建 ' + mergedPath(decision.target) + '/SKILL.md 与需要的 references/templates/scripts;',
|
|
49
|
+
'- 可以复制源文件作为草稿基础,然后逐段修改;可以多轮读写;',
|
|
50
|
+
'- 保留两源独特内容,不编造事实,不留下 TODO/占位符;',
|
|
51
|
+
'- SKILL.md 以合法 YAML frontmatter 开头,name 等于目标名,description 不超过 60 字符;',
|
|
52
|
+
'- 正文与支撑文件的引用必须成对,路径只允许 references/templates/scripts/assets 下的相对路径。',
|
|
53
|
+
'',
|
|
54
|
+
'完成后只输出一个 JSON(不要 markdown 围栏):',
|
|
55
|
+
'{"target":"' + decision.target + '","files":["SKILL.md","references/..."],"changelog":["一条变更"],"notes":"可选的一句话"}',
|
|
56
|
+
];
|
|
57
|
+
return (mode === 'fresh' ? ['基于已持久化的 decision。', ...head, ...body] : [...head, ...body]).join('\n');
|
|
58
|
+
}
|
|
59
|
+
export function buildStandbyPrompt() { return STANDBY_PROMPT; }
|
|
60
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { loadMergeState } from "./state.js";
|
|
2
|
+
import { formatQueue, formatStatus } from "./status.js";
|
|
3
|
+
import { mergeUsage } from "./commands.js";
|
|
4
|
+
export async function handleReadOnlyMergeCommand(command, io, home) {
|
|
5
|
+
if (command.verb === 'status') {
|
|
6
|
+
const loaded = await loadMergeState(io, home);
|
|
7
|
+
if (command.id !== undefined) {
|
|
8
|
+
const row = loaded.state.rows.find(item => item.id === command.id);
|
|
9
|
+
return row === undefined ? 'merge row not found: ' + command.id : formatStatus([row]);
|
|
10
|
+
}
|
|
11
|
+
return formatStatus(loaded.state.rows);
|
|
12
|
+
}
|
|
13
|
+
if (command.verb === 'pending') {
|
|
14
|
+
const loaded = await loadMergeState(io, home);
|
|
15
|
+
const rows = command.expired === true ? loaded.state.rows.filter(row => row.expired !== undefined) : loaded.state.rows;
|
|
16
|
+
return command.expired === true ? formatStatus(rows) : formatQueue(rows, 'pending');
|
|
17
|
+
}
|
|
18
|
+
if (command.verb === 'candidates') {
|
|
19
|
+
const loaded = await loadMergeState(io, home);
|
|
20
|
+
const rows = command.retry === true ? loaded.state.rows.filter(row => row.state === 'approved') : loaded.state.rows;
|
|
21
|
+
return formatQueue(rows, command.retry === true ? 'fuse' : 'verify');
|
|
22
|
+
}
|
|
23
|
+
if (command.verb === 'skipped') {
|
|
24
|
+
const loaded = await loadMergeState(io, home);
|
|
25
|
+
return formatQueue(loaded.state.rows, 'skipped');
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function mergeCommandHelp() {
|
|
30
|
+
return mergeUsage();
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=read-commands.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge run report: JSON is the machine source, Markdown is the human digest.
|
|
3
|
+
*
|
|
4
|
+
* Reports are written before registry.settle, so a settled run's resultRef is readable.
|
|
5
|
+
* readMergeReport uses the three-state probe: absent is not "no pairs", unknown is not empty.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { isPresent, probePresent, probeText, probeUnknown, } from '@lmzhen/dsh-evolution-core';
|
|
9
|
+
export function mergeReportPath(home, runId) {
|
|
10
|
+
return join(home, 'reports', 'merge-' + runId + '.json');
|
|
11
|
+
}
|
|
12
|
+
export function mergeReportDigestPath(home, runId) {
|
|
13
|
+
return join(home, 'reports', 'merge-' + runId + '.md');
|
|
14
|
+
}
|
|
15
|
+
function isReport(value) {
|
|
16
|
+
if (typeof value !== 'object' || value === null)
|
|
17
|
+
return false;
|
|
18
|
+
const report = value;
|
|
19
|
+
return report.schemaVersion === 2
|
|
20
|
+
&& report.kind === 'merge'
|
|
21
|
+
&& typeof report.runId === 'string'
|
|
22
|
+
&& typeof report.command === 'string'
|
|
23
|
+
&& typeof report.startedAt === 'number'
|
|
24
|
+
&& typeof report.endedAt === 'number'
|
|
25
|
+
&& Array.isArray(report.pairs)
|
|
26
|
+
&& typeof report.totals === 'object'
|
|
27
|
+
&& Array.isArray(report.notes);
|
|
28
|
+
}
|
|
29
|
+
function digest(report) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
lines.push('# merge ' + report.runId);
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('- command: ' + report.command);
|
|
34
|
+
lines.push('- stop: ' + report.stopReason);
|
|
35
|
+
lines.push('- pairs: ' + String(report.pairs.length));
|
|
36
|
+
lines.push('- totals: merged ' + String(report.totals.merged) + ', kept ' + String(report.totals.kept) + ', pending ' + String(report.totals.pending) + ', failed ' + String(report.totals.failed));
|
|
37
|
+
for (const pair of report.pairs) {
|
|
38
|
+
lines.push('- ' + pair.a + ' + ' + pair.b + ' -> ' + pair.outcome + (pair.target === undefined ? '' : ' (' + pair.target.actual + ')'));
|
|
39
|
+
}
|
|
40
|
+
if (report.remaining !== undefined && report.remaining.length > 0) {
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push('- remaining: ' + report.remaining.map(item => item.pairKey).join(', '));
|
|
43
|
+
}
|
|
44
|
+
return lines.join('\n') + '\n';
|
|
45
|
+
}
|
|
46
|
+
/** Write JSON first, then the digest; returns the JSON path (the registry resultRef). */
|
|
47
|
+
export async function writeMergeReport(io, home, report) {
|
|
48
|
+
const path = mergeReportPath(home, report.runId);
|
|
49
|
+
await io.writeText(path, JSON.stringify(report, null, 2) + '\n');
|
|
50
|
+
await io.writeText(mergeReportDigestPath(home, report.runId), digest(report));
|
|
51
|
+
return path;
|
|
52
|
+
}
|
|
53
|
+
export async function readMergeReport(io, home, runId) {
|
|
54
|
+
const probe = await probeText(io, mergeReportPath(home, runId));
|
|
55
|
+
if (!isPresent(probe))
|
|
56
|
+
return probe;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(probe.value);
|
|
59
|
+
return isReport(parsed) ? probePresent(parsed) : probeUnknown('merge report has an unexpected shape');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return probeUnknown('merge report is not JSON: ' + (error instanceof Error ? error.message : String(error)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Pairs this run actually merged (for merge undo / reports). */
|
|
66
|
+
export async function committedPairs(io, home, runId) {
|
|
67
|
+
const probe = await readMergeReport(io, home, runId);
|
|
68
|
+
return isPresent(probe) ? probe.value.pairs.filter(pair => pair.outcome === 'merged') : [];
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { loadMergeState } from "./state.js";
|
|
2
|
+
import { runScanCommand } from "./scan-command.js";
|
|
3
|
+
import { withMergeRun } from "./run-shell.js";
|
|
4
|
+
import { recordRound2Outcome } from "./round2-outcome.js";
|
|
5
|
+
export async function runResumeCommand(options) {
|
|
6
|
+
const loaded = await loadMergeState(options.io, options.home);
|
|
7
|
+
const row = loaded.state.rows.find(item => item.id === options.rowId);
|
|
8
|
+
if (row === undefined)
|
|
9
|
+
return { ok: false, message: 'merge row not found: ' + options.rowId };
|
|
10
|
+
if (row.state === 'verify') {
|
|
11
|
+
return await runScanCommand({
|
|
12
|
+
io: options.io, home: options.home, registry: options.registry,
|
|
13
|
+
approval: true, platformWrite: options.platformWrite, now: options.now,
|
|
14
|
+
...(options.makePorts === undefined
|
|
15
|
+
? { ports: { ...options.ports, screening: () => Promise.resolve([{ a: row.a, b: row.b }]) } }
|
|
16
|
+
: {
|
|
17
|
+
makePorts: (runId) => {
|
|
18
|
+
const makePorts = options.makePorts;
|
|
19
|
+
if (makePorts === undefined)
|
|
20
|
+
throw new Error('resume makePorts disappeared');
|
|
21
|
+
return { ...makePorts(runId), screening: () => Promise.resolve([{ a: row.a, b: row.b }]) };
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
...(options.timeoutMinutes === undefined ? {} : { timeoutMinutes: options.timeoutMinutes }),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (row.state !== 'fuse' && row.state !== 'approved')
|
|
28
|
+
return { ok: false, message: 'row is not resumable from state ' + row.state };
|
|
29
|
+
const decision = row.decision;
|
|
30
|
+
if (decision === undefined)
|
|
31
|
+
return { ok: false, message: 'fuse row has no decision' };
|
|
32
|
+
return await withMergeRun({
|
|
33
|
+
io: options.io, home: options.home, registry: options.registry,
|
|
34
|
+
command: 'resume', approval: { platformWrite: options.platformWrite, mergeApproval: true }, now: options.now,
|
|
35
|
+
async body(ctx) {
|
|
36
|
+
await ctx.stage('author', { done: 0, total: 1 });
|
|
37
|
+
let outcome;
|
|
38
|
+
try {
|
|
39
|
+
outcome = options.round2WithRun === undefined
|
|
40
|
+
? await options.ports.round2(row, decision, ctx.signal)
|
|
41
|
+
: await options.round2WithRun(row, decision, ctx.signal, ctx.runId);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
|
|
45
|
+
}
|
|
46
|
+
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
47
|
+
return {
|
|
48
|
+
stopReason: record.merged ? 'completed' : 'failed',
|
|
49
|
+
scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } },
|
|
50
|
+
pairs: [record.pair],
|
|
51
|
+
totals: record.merged ? { merged: 1, kept: 0, pending: 0, failed: 0 } : { merged: 0, kept: 0, pending: 0, failed: 1 },
|
|
52
|
+
notes: [],
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=resume-command.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { transition } from "./coordinator.js";
|
|
2
|
+
import { dropRow, upsertRow } from "./state.js";
|
|
3
|
+
import { recordPair } from "./domain/pair-index-store.js";
|
|
4
|
+
export async function recordRound2Outcome(io, home, now, row, outcome) {
|
|
5
|
+
const at = new Date(now()).toISOString();
|
|
6
|
+
if (outcome.kind === 'merged') {
|
|
7
|
+
const success = transition(row, { type: 'round2-success', at });
|
|
8
|
+
if (success.ok) {
|
|
9
|
+
await dropRow(io, home, now, row.id);
|
|
10
|
+
const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
|
|
11
|
+
await recordPair(io, home, {
|
|
12
|
+
pairKey: row.pairKey,
|
|
13
|
+
runId: row.runId ?? 'merge',
|
|
14
|
+
result: 'merged',
|
|
15
|
+
...(target === undefined ? {} : { target }),
|
|
16
|
+
...(outcome.unitId === undefined ? {} : { reportRef: outcome.unitId }),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
merged: true,
|
|
21
|
+
pair: {
|
|
22
|
+
id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
|
|
23
|
+
...(row.decision === undefined ? {} : { decision: row.decision }),
|
|
24
|
+
outcome: 'merged', archived: outcome.archived ?? [],
|
|
25
|
+
...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }),
|
|
26
|
+
...(outcome.target === undefined ? {} : { target: outcome.target }),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const failure = outcome.failure ?? { stage: 'author', code: 'merge-failed', message: 'round2 failed', retryable: true, retryFrom: 'agent2' };
|
|
31
|
+
const next = transition(row, { type: 'round2-fail', failure, at });
|
|
32
|
+
if (next.ok && next.row !== null) {
|
|
33
|
+
await upsertRow(io, home, now, next.row);
|
|
34
|
+
await recordPair(io, home, {
|
|
35
|
+
pairKey: row.pairKey,
|
|
36
|
+
runId: row.runId ?? 'merge',
|
|
37
|
+
result: 'failed',
|
|
38
|
+
...(row.decision?.target === undefined ? {} : { target: row.decision.target }),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
merged: false,
|
|
43
|
+
pair: {
|
|
44
|
+
id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
|
|
45
|
+
...(row.decision === undefined ? {} : { decision: row.decision }),
|
|
46
|
+
outcome: 'failed', archived: [], failure,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=round2-outcome.js.map
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -25,13 +25,16 @@ export interface MergeRowView {
|
|
|
25
25
|
readonly retryable: boolean;
|
|
26
26
|
};
|
|
27
27
|
readonly attempts?: number;
|
|
28
|
-
readonly origin?: 'agent' | 'human';
|
|
28
|
+
readonly origin?: 'agent' | 'human' | 'system';
|
|
29
29
|
readonly stagingExpiresAt?: number;
|
|
30
|
+
readonly expired?: 'target-taken' | 'source-changed';
|
|
31
|
+
readonly reason?: string;
|
|
32
|
+
readonly summary?: readonly string[];
|
|
30
33
|
readonly actions: readonly string[];
|
|
31
34
|
}
|
|
32
35
|
export declare function toRowView(row: MergeRow): MergeRowView;
|
|
33
36
|
export interface MergeView {
|
|
34
|
-
readonly schemaVersion:
|
|
37
|
+
readonly schemaVersion: 2;
|
|
35
38
|
readonly counts: MergeCounts;
|
|
36
39
|
readonly tabs: readonly {
|
|
37
40
|
readonly queue: MergeQueue;
|