@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,29 @@
|
|
|
1
|
+
function convert(outcome) {
|
|
2
|
+
if (outcome.kind !== 'ok')
|
|
3
|
+
return outcome;
|
|
4
|
+
return {
|
|
5
|
+
kind: 'ok',
|
|
6
|
+
childId: String(outcome.childId),
|
|
7
|
+
turn: { text: outcome.turn.text, stopReason: outcome.turn.stopReason },
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function subagentChannelAdapter(channel) {
|
|
11
|
+
return {
|
|
12
|
+
async open(spec) {
|
|
13
|
+
if (channel.open === undefined)
|
|
14
|
+
return { kind: 'unavailable', code: 'E-323', reason: 'subagent channel has no open()' };
|
|
15
|
+
return convert(await channel.open(spec));
|
|
16
|
+
},
|
|
17
|
+
async continueTurn(childId, promptText, options) {
|
|
18
|
+
if (channel.continueTurn === undefined)
|
|
19
|
+
return { kind: 'unavailable', code: 'E-323', reason: 'subagent channel has no continueTurn()' };
|
|
20
|
+
return convert(await channel.continueTurn(childId, promptText, options));
|
|
21
|
+
},
|
|
22
|
+
async dispose(childId) {
|
|
23
|
+
if (channel.dispose === undefined)
|
|
24
|
+
return;
|
|
25
|
+
await channel.dispose(childId);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -34,7 +34,7 @@ export interface CandidateSelection {
|
|
|
34
34
|
readonly b: string;
|
|
35
35
|
readonly reason?: string;
|
|
36
36
|
}[];
|
|
37
|
-
readonly reportOnly:
|
|
37
|
+
readonly reportOnly: readonly unknown[];
|
|
38
38
|
readonly candidatePairs?: number;
|
|
39
39
|
}
|
|
40
40
|
export type ScreeningCandidates = readonly MergeCandidate[] & {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G3 screen adapter: cards -> corpus snapshot -> EnginePort.screen.
|
|
3
|
+
*
|
|
4
|
+
* The vendor engine reads a corpus from disk; host cards are content. This module
|
|
5
|
+
* writes only SKILL.md support-free snapshots under the run directory. It does not
|
|
6
|
+
* decide candidates: the Q02 selector is injected so engine/report-map stays the
|
|
7
|
+
* single source of that policy.
|
|
8
|
+
*/
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
function safeName(name, index) {
|
|
11
|
+
const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
12
|
+
return (cleaned === '' ? 'skill-' + String(index) : cleaned) + '__' + String(index);
|
|
13
|
+
}
|
|
14
|
+
/** Write the exact card bodies as a support-free corpus snapshot and return its root. */
|
|
15
|
+
export async function snapshotCards(io, home, runId, cards) {
|
|
16
|
+
const root = join(home, 'merge', 'runs', runId, 'corpus');
|
|
17
|
+
for (let index = 0; index < cards.length; index += 1) {
|
|
18
|
+
const card = cards[index];
|
|
19
|
+
if (card === undefined)
|
|
20
|
+
continue;
|
|
21
|
+
await io.writeText(join(root, safeName(card.name, index), 'SKILL.md'), card.body);
|
|
22
|
+
}
|
|
23
|
+
return root;
|
|
24
|
+
}
|
|
25
|
+
export function createEngineScreening(deps) {
|
|
26
|
+
return async (cards, runId, signal) => {
|
|
27
|
+
const corpusDir = await snapshotCards(deps.io, deps.home, runId, cards);
|
|
28
|
+
const outDir = join(deps.home, 'merge', 'runs', runId, 'engine');
|
|
29
|
+
const result = await deps.engine.screen({
|
|
30
|
+
corpusDir,
|
|
31
|
+
outDir,
|
|
32
|
+
...(deps.cfg === undefined ? {} : { cfg: deps.cfg }),
|
|
33
|
+
...(deps.runner === undefined ? {} : { runner: deps.runner }),
|
|
34
|
+
signal,
|
|
35
|
+
});
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
throw new Error('engine screen failed: ' + result.error.code + (result.error.stage === undefined ? '' : ' @' + result.error.stage) + (result.error.detail === undefined ? '' : ': ' + result.error.detail));
|
|
38
|
+
}
|
|
39
|
+
const selection = deps.select(result.report);
|
|
40
|
+
if (deps.onSelection !== undefined)
|
|
41
|
+
await deps.onSelection(selection, runId);
|
|
42
|
+
return Object.assign(selection.candidates.slice(), selection.candidatePairs === undefined ? {} : { candidatePairs: selection.candidatePairs });
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=engine-screen.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function skillLibraryPort(skills) {
|
|
2
|
+
const archivedNames = skills.archivedNames;
|
|
3
|
+
return {
|
|
4
|
+
readSkill: name => skills.read(name),
|
|
5
|
+
isArchived: async (name) => {
|
|
6
|
+
if (typeof archivedNames !== 'function')
|
|
7
|
+
return false;
|
|
8
|
+
const names = await archivedNames.call(skills);
|
|
9
|
+
return names === null ? true : names.includes(name);
|
|
10
|
+
},
|
|
11
|
+
commitMerge: plan => skills.commitMerge(plan),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=library.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function append(chunk, text) {
|
|
2
|
+
if (chunk.type === 'text-delta' || chunk.type === 'text')
|
|
3
|
+
return text + (chunk.text ?? '');
|
|
4
|
+
if (chunk.type === 'finish' && chunk.reason?.kind === 'error') {
|
|
5
|
+
throw new Error('model call failed: ' + (chunk.reason.failure?.message ?? 'unknown'));
|
|
6
|
+
}
|
|
7
|
+
return text;
|
|
8
|
+
}
|
|
9
|
+
export function createEngineRunner(llm, route) {
|
|
10
|
+
return async (prompt) => {
|
|
11
|
+
const selection = route();
|
|
12
|
+
let text = '';
|
|
13
|
+
if (llm.stream !== undefined) {
|
|
14
|
+
for await (const chunk of llm.stream({
|
|
15
|
+
provider: selection.provider,
|
|
16
|
+
model: selection.model,
|
|
17
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
|
18
|
+
temperature: 0,
|
|
19
|
+
}))
|
|
20
|
+
text = append(chunk, text);
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
if (llm.prepareCall === undefined)
|
|
24
|
+
throw new Error('llm service has neither stream nor prepareCall');
|
|
25
|
+
const call = await llm.prepareCall({ provider: selection.provider, model: selection.model, temperature: 0 });
|
|
26
|
+
for await (const chunk of call.stream({
|
|
27
|
+
...call.config,
|
|
28
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
|
29
|
+
}))
|
|
30
|
+
text = append(chunk, text);
|
|
31
|
+
return text;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=model-runner.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Host selector correction: engine selector does not route judge errors to candidates. */
|
|
2
|
+
import { pairKeyOf, selectCandidates, toEngineCandidate } from '@lmzhen/dsh-evolution-merge-engine';
|
|
3
|
+
export function selectWithErrors(report) {
|
|
4
|
+
const base = selectCandidates(report);
|
|
5
|
+
const doc = (report.reportDoc ?? report);
|
|
6
|
+
const rows = Array.isArray(doc.pairs_final) ? doc.pairs_final : [];
|
|
7
|
+
const errors = new Set();
|
|
8
|
+
for (const row of rows) {
|
|
9
|
+
if (row.error !== true)
|
|
10
|
+
continue;
|
|
11
|
+
const a = typeof row.a === 'string' ? row.a : '';
|
|
12
|
+
const b = typeof row.b === 'string' ? row.b : '';
|
|
13
|
+
if (a !== '' && b !== '')
|
|
14
|
+
errors.add(pairKeyOf(a, b));
|
|
15
|
+
}
|
|
16
|
+
const candidates = base.candidates.filter(candidate => !errors.has(pairKeyOf(candidate.a, candidate.b)));
|
|
17
|
+
const skipped = base.skipped.filter(item => !errors.has(item.pairKey));
|
|
18
|
+
const reportOnly = base.reportOnly.filter((row) => {
|
|
19
|
+
const value = row;
|
|
20
|
+
if (typeof value.a !== 'string' || typeof value.b !== 'string')
|
|
21
|
+
return true;
|
|
22
|
+
return !errors.has(pairKeyOf(value.a, value.b));
|
|
23
|
+
});
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
if (row.error !== true)
|
|
26
|
+
continue;
|
|
27
|
+
const a = typeof row.a === 'string' ? row.a : '';
|
|
28
|
+
const b = typeof row.b === 'string' ? row.b : '';
|
|
29
|
+
if (a === '' || b === '')
|
|
30
|
+
continue;
|
|
31
|
+
candidates.push(toEngineCandidate(row, typeof row.reason === 'string' ? row.reason : 'judge-error'));
|
|
32
|
+
}
|
|
33
|
+
return { ...base, candidates, skipped, reportOnly };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillLibrary-backed undo sink. Blobs are content-addressed but the library exposes them by
|
|
3
|
+
* version, so the first readBlob call indexes all version bodies once; an undo is rare enough
|
|
4
|
+
* that this is preferable to adding a second hash index.
|
|
5
|
+
*/
|
|
6
|
+
import { contentHash } from '@lmzhen/dsh-evolution-core';
|
|
7
|
+
export function skillLibraryUndoPort(skills) {
|
|
8
|
+
let blobs;
|
|
9
|
+
const readBlob = async (hash) => {
|
|
10
|
+
if (blobs === undefined) {
|
|
11
|
+
blobs = new Map();
|
|
12
|
+
for (const summary of await skills.list()) {
|
|
13
|
+
for (const version of await skills.listVersions(summary.name)) {
|
|
14
|
+
const body = await skills.readVersion(summary.name, version.v);
|
|
15
|
+
if (body !== null)
|
|
16
|
+
blobs.set(contentHash(body), body);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return blobs.get(hash) ?? null;
|
|
21
|
+
};
|
|
22
|
+
const sink = {
|
|
23
|
+
readSkill: name => skills.read(name),
|
|
24
|
+
readSupport: (name, path) => skills.readSupportFile(name, path),
|
|
25
|
+
readBlob,
|
|
26
|
+
async hasArchived(name) { return (await skills.read(name)) === null; },
|
|
27
|
+
restoreFromArchive: name => skills.restoreFromArchive(name),
|
|
28
|
+
archive: (name, reason) => skills.archive(name, { reason }),
|
|
29
|
+
updateSkill: (name, body, anchor) => skills.update(name, body, undefined, anchor),
|
|
30
|
+
writeSupportFile: (name, path, content, anchor) => skills.writeSupportFile(name, path, content, undefined, anchor),
|
|
31
|
+
removeSupportFile: (name, path, anchor) => skills.removeSupportFile(name, path, undefined, anchor),
|
|
32
|
+
};
|
|
33
|
+
return { ...sink, listVersions: name => skills.listVersions(name) };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=undo.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { approvePending } from "./pending-actions.js";
|
|
2
|
+
import { withMergeRun } from "./run-shell.js";
|
|
3
|
+
import { recordRound2Outcome } from "./round2-outcome.js";
|
|
4
|
+
export async function runApproveCommand(options) {
|
|
5
|
+
const taken = await approvePending(options.io, options.home, options.now, options.rowId);
|
|
6
|
+
if (!taken.ok)
|
|
7
|
+
return { ok: false, message: taken.message };
|
|
8
|
+
const row = taken.row;
|
|
9
|
+
const decision = row.decision;
|
|
10
|
+
if (decision === undefined)
|
|
11
|
+
return { ok: false, message: 'pending row has no decision' };
|
|
12
|
+
return await withMergeRun({
|
|
13
|
+
io: options.io,
|
|
14
|
+
home: options.home,
|
|
15
|
+
registry: options.registry,
|
|
16
|
+
command: 'approve',
|
|
17
|
+
approval: { platformWrite: options.platformWrite, mergeApproval: true },
|
|
18
|
+
now: options.now,
|
|
19
|
+
async body(ctx) {
|
|
20
|
+
const stage = (name) => ctx.stage(name, { done: 0, total: 1 });
|
|
21
|
+
await stage('author');
|
|
22
|
+
let outcome;
|
|
23
|
+
try {
|
|
24
|
+
outcome = await options.ports.round2(row, decision, ctx.signal, ctx.runId);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
|
|
28
|
+
}
|
|
29
|
+
const base = { scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } }, notes: [] };
|
|
30
|
+
const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
|
|
31
|
+
return {
|
|
32
|
+
...base,
|
|
33
|
+
stopReason: record.merged ? 'completed' : 'failed',
|
|
34
|
+
pairs: [record.pair],
|
|
35
|
+
totals: record.merged
|
|
36
|
+
? { merged: 1, kept: 0, pending: 0, failed: 0 }
|
|
37
|
+
: { merged: 0, kept: 0, pending: 0, failed: 1 },
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=approve-command.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createStaging, setStagingTarget } from "./staging.js";
|
|
2
|
+
import { readStagedTree } from "./staged-tree.js";
|
|
3
|
+
import { validateStagedTree, hasBlockingFinding } from "./rules.js";
|
|
4
|
+
import { commitStagedRun } from "./commit-execute.js";
|
|
5
|
+
export async function runAuthoring(options) {
|
|
6
|
+
const targetCard = options.cards.find(card => card.name === options.target);
|
|
7
|
+
if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === 'pinned')
|
|
8
|
+
return { ok: false, message: 'target is pinned: ' + options.target };
|
|
9
|
+
const stageKey = options.stageKey ?? options.runId;
|
|
10
|
+
if (options.reuseStaging === true) {
|
|
11
|
+
if (!await setStagingTarget(options.io, options.home, stageKey, options.target, options.mode))
|
|
12
|
+
return { ok: false, message: 'staging missing or unreadable: ' + stageKey };
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const created = await createStaging(options.io, options.home, {
|
|
16
|
+
runId: stageKey,
|
|
17
|
+
target: options.target,
|
|
18
|
+
...(options.mode === undefined ? {} : { mode: options.mode }),
|
|
19
|
+
sources: options.cards.map(card => ({ name: card.name, body: card.body, files: card.files ?? [] })),
|
|
20
|
+
promptVersion: 'merge-b/v1',
|
|
21
|
+
ttlDays: 7,
|
|
22
|
+
now: options.now,
|
|
23
|
+
});
|
|
24
|
+
if (!created.ok)
|
|
25
|
+
return { ok: false, message: created.message };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await options.author({ home: options.home, runId: stageKey });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return { ok: false, message: 'author failed: ' + (error instanceof Error ? error.message : String(error)) };
|
|
32
|
+
}
|
|
33
|
+
const tree = await readStagedTree(options.io, options.home, stageKey);
|
|
34
|
+
if (tree.kind !== 'present')
|
|
35
|
+
return { ok: false, message: 'staged tree is not readable: ' + (tree.kind === 'unknown' ? tree.reason : 'absent') };
|
|
36
|
+
const findings = validateStagedTree(tree.value);
|
|
37
|
+
if (hasBlockingFinding(findings))
|
|
38
|
+
return { ok: false, message: 'staged tree failed the rule gate', findings };
|
|
39
|
+
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
40
|
+
origin: 'subagent',
|
|
41
|
+
summary: options.summary,
|
|
42
|
+
now: options.now,
|
|
43
|
+
});
|
|
44
|
+
if (!commit.ok)
|
|
45
|
+
return { ok: false, message: commit.message };
|
|
46
|
+
return { ok: true, tree: tree.value, commit };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=authoring-flow.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { parseDecision } from "./decision.js";
|
|
2
|
+
import { buildRound1Prompt, buildRound2Prompt, STANDBY_PROMPT } from "./prompts.js";
|
|
3
|
+
export async function runRound1OverChannel(options, candidate, signal) {
|
|
4
|
+
let childId;
|
|
5
|
+
const dispose = async () => {
|
|
6
|
+
if (childId !== undefined) {
|
|
7
|
+
const id = childId;
|
|
8
|
+
childId = undefined;
|
|
9
|
+
try {
|
|
10
|
+
await options.channel.dispose(id);
|
|
11
|
+
}
|
|
12
|
+
catch { /* teardown must not replace the outcome */ }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const opened = await options.channel.open({
|
|
16
|
+
parent: options.parent,
|
|
17
|
+
label: 'merge-round1',
|
|
18
|
+
promptText: STANDBY_PROMPT,
|
|
19
|
+
kind: 'reviewer',
|
|
20
|
+
allow: options.allow,
|
|
21
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
22
|
+
timeoutMs: options.timeoutMs,
|
|
23
|
+
signal,
|
|
24
|
+
});
|
|
25
|
+
if (opened.kind === 'unavailable')
|
|
26
|
+
return { outcome: { kind: 'failed', failure: { stage: 'decide', code: opened.code, message: opened.reason, retryable: true, retryFrom: 'agent1' } }, dispose };
|
|
27
|
+
if (opened.kind === 'aborted')
|
|
28
|
+
return { outcome: { kind: 'failed', failure: { stage: 'decide', code: 'aborted', message: 'round1 aborted', retryable: false, retryFrom: 'none' } }, dispose };
|
|
29
|
+
childId = opened.childId;
|
|
30
|
+
if (opened.turn.stopReason !== 'completed') {
|
|
31
|
+
await dispose();
|
|
32
|
+
return { outcome: { kind: 'failed', failure: { stage: 'decide', code: opened.turn.stopReason, message: 'standby did not complete', retryable: true, retryFrom: 'agent1' } }, dispose };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
options.onChildOpened?.(opened.childId);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
await dispose();
|
|
39
|
+
return { outcome: { kind: 'failed', failure: { stage: 'decide', code: 'child-bind', message: String(error), retryable: true, retryFrom: 'agent1' } }, dispose };
|
|
40
|
+
}
|
|
41
|
+
const round1 = await options.channel.continueTurn(opened.childId, buildRound1Prompt(candidate, options.cards), { timeoutMs: options.timeoutMs, signal });
|
|
42
|
+
if (round1.kind !== 'ok') {
|
|
43
|
+
await dispose();
|
|
44
|
+
return { outcome: { kind: 'failed', failure: { stage: 'decide', code: round1.kind, message: round1.kind === 'unavailable' ? round1.reason : 'round1 aborted', retryable: round1.kind !== 'aborted', retryFrom: round1.kind === 'aborted' ? 'none' : 'agent1' } }, dispose };
|
|
45
|
+
}
|
|
46
|
+
if (round1.turn.stopReason !== 'completed') {
|
|
47
|
+
await dispose();
|
|
48
|
+
return { outcome: { kind: 'failed', failure: { stage: 'decide', code: round1.turn.stopReason, message: 'round1 did not complete', retryable: true, retryFrom: 'agent1' } }, dispose };
|
|
49
|
+
}
|
|
50
|
+
const parsed = parseDecision(round1.turn.text, candidate);
|
|
51
|
+
if (!parsed.ok) {
|
|
52
|
+
await dispose();
|
|
53
|
+
return { outcome: { kind: 'failed', failure: { stage: 'decide', code: parsed.code, message: parsed.message, retryable: true, retryFrom: 'agent1' } }, dispose };
|
|
54
|
+
}
|
|
55
|
+
if (!parsed.decision.shouldMerge || parsed.decision.mode === 'keep') {
|
|
56
|
+
await dispose();
|
|
57
|
+
return { outcome: { kind: 'keep', decision: parsed.decision }, dispose };
|
|
58
|
+
}
|
|
59
|
+
const child = opened.childId;
|
|
60
|
+
return {
|
|
61
|
+
outcome: { kind: 'merge', decision: parsed.decision },
|
|
62
|
+
childId: child,
|
|
63
|
+
dispose,
|
|
64
|
+
async author(row, decision, turnSignal) {
|
|
65
|
+
const turn = await options.channel.continueTurn(child, buildRound2Prompt(row, decision), { timeoutMs: options.timeoutMs, signal: turnSignal });
|
|
66
|
+
if (turn.kind !== 'ok')
|
|
67
|
+
throw new Error(turn.kind === 'unavailable' ? turn.reason : 'round2 aborted');
|
|
68
|
+
if (turn.turn.stopReason !== 'completed')
|
|
69
|
+
throw new Error('round2 did not complete: ' + turn.turn.stopReason);
|
|
70
|
+
return turn.turn.text;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=channel-port.js.map
|
package/lib/types/commands.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type MergeCliCommand = {
|
|
|
8
8
|
readonly verb: 'pair';
|
|
9
9
|
readonly a: string;
|
|
10
10
|
readonly b: string;
|
|
11
|
+
readonly force?: true;
|
|
11
12
|
} | {
|
|
12
13
|
readonly verb: 'status';
|
|
13
14
|
readonly id?: string;
|
|
@@ -23,6 +24,10 @@ export type MergeCliCommand = {
|
|
|
23
24
|
readonly verb: 'doctor';
|
|
24
25
|
} | {
|
|
25
26
|
readonly verb: 'pending';
|
|
27
|
+
readonly expired?: true;
|
|
28
|
+
} | {
|
|
29
|
+
readonly verb: 'candidates';
|
|
30
|
+
readonly retry?: true;
|
|
26
31
|
} | {
|
|
27
32
|
readonly verb: 'approve';
|
|
28
33
|
readonly id: string;
|
|
@@ -33,9 +38,13 @@ export type MergeCliCommand = {
|
|
|
33
38
|
readonly verb: 'resume';
|
|
34
39
|
readonly id?: string;
|
|
35
40
|
readonly clear?: string;
|
|
41
|
+
readonly retry?: true;
|
|
36
42
|
} | {
|
|
37
43
|
readonly verb: 'skipped';
|
|
38
44
|
readonly restore?: string;
|
|
45
|
+
} | {
|
|
46
|
+
readonly verb: 'discard';
|
|
47
|
+
readonly id: string;
|
|
39
48
|
} | {
|
|
40
49
|
readonly verb: 'undo';
|
|
41
50
|
readonly target: string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const USAGE = 'usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>';
|
|
2
|
+
function flags(args, allowed) {
|
|
3
|
+
const values = new Map();
|
|
4
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5
|
+
const token = args[index];
|
|
6
|
+
if (!token.startsWith('--') || token === '--force' || token === '--expired' || token === '--retry')
|
|
7
|
+
continue;
|
|
8
|
+
const name = token.slice(2);
|
|
9
|
+
if (!allowed.includes(name))
|
|
10
|
+
return { values, error: 'unknown flag: --' + name };
|
|
11
|
+
const value = args[index + 1];
|
|
12
|
+
if (value === undefined || value.startsWith('--'))
|
|
13
|
+
return { values, error: 'missing value for --' + name };
|
|
14
|
+
values.set(name, value);
|
|
15
|
+
index += 1;
|
|
16
|
+
}
|
|
17
|
+
return { values };
|
|
18
|
+
}
|
|
19
|
+
function switches(args) {
|
|
20
|
+
return new Set(args.filter(token => token === '--force' || token === '--expired' || token === '--retry').map(token => token.slice(2)));
|
|
21
|
+
}
|
|
22
|
+
function positional(args) {
|
|
23
|
+
const filtered = args.filter(token => token !== '--force' && token !== '--expired' && token !== '--retry');
|
|
24
|
+
return filtered.filter((token, index) => !token.startsWith('--') && !(index > 0 && filtered[index - 1].startsWith('--')));
|
|
25
|
+
}
|
|
26
|
+
export function parseMergeCommand(argv) {
|
|
27
|
+
const [verb, ...rest] = argv;
|
|
28
|
+
if (verb === undefined || verb === 'help' || verb === '--help' || verb === '-h')
|
|
29
|
+
return { ok: true, command: { verb: 'scan' }, help: true };
|
|
30
|
+
const pos = positional(rest);
|
|
31
|
+
const marked = switches(rest);
|
|
32
|
+
switch (verb) {
|
|
33
|
+
case 'scan': return pos.length === 0 ? { ok: true, command: { verb: 'scan' } } : { ok: false, code: 'usage', message: USAGE };
|
|
34
|
+
case 'pair': return pos.length === 2 ? { ok: true, command: { verb: 'pair', a: pos[0], b: pos[1], ...(marked.has('force') ? { force: true } : {}) } } : { ok: false, code: 'usage', message: USAGE };
|
|
35
|
+
case 'status': return pos.length <= 1 ? { ok: true, command: { verb: 'status', ...(pos[0] === undefined ? {} : { id: pos[0] }) } } : { ok: false, code: 'usage', message: USAGE };
|
|
36
|
+
case 'report': return pos.length <= 1 ? { ok: true, command: { verb: 'report', ...(pos[0] === undefined ? {} : { id: pos[0] }) } } : { ok: false, code: 'usage', message: USAGE };
|
|
37
|
+
case 'cancel': return pos.length <= 1 ? { ok: true, command: { verb: 'cancel', ...(pos[0] === undefined ? {} : { id: pos[0] }) } } : { ok: false, code: 'usage', message: USAGE };
|
|
38
|
+
case 'runs': return pos.length === 0 ? { ok: true, command: { verb: 'runs' } } : { ok: false, code: 'usage', message: USAGE };
|
|
39
|
+
case 'doctor': return pos.length === 0 ? { ok: true, command: { verb: 'doctor' } } : { ok: false, code: 'usage', message: USAGE };
|
|
40
|
+
case 'pending': return pos.length === 0 ? { ok: true, command: { verb: 'pending', ...(marked.has('expired') ? { expired: true } : {}) } } : { ok: false, code: 'usage', message: USAGE };
|
|
41
|
+
case 'candidates': return pos.length === 0 ? { ok: true, command: { verb: 'candidates', ...(marked.has('retry') ? { retry: true } : {}) } } : { ok: false, code: 'usage', message: USAGE };
|
|
42
|
+
case 'approve': return pos.length === 1 ? { ok: true, command: { verb: 'approve', id: pos[0] } } : { ok: false, code: 'usage', message: USAGE };
|
|
43
|
+
case 'reject': return pos.length === 1 ? { ok: true, command: { verb: 'reject', id: pos[0] } } : { ok: false, code: 'usage', message: USAGE };
|
|
44
|
+
case 'resume': {
|
|
45
|
+
const parsed = flags(rest, ['clear']);
|
|
46
|
+
if (parsed.error !== undefined)
|
|
47
|
+
return { ok: false, code: 'usage', message: parsed.error };
|
|
48
|
+
const clear = parsed.values.get('clear');
|
|
49
|
+
if (pos.length > 1)
|
|
50
|
+
return { ok: false, code: 'usage', message: USAGE };
|
|
51
|
+
return { ok: true, command: { verb: 'resume', ...(pos[0] === undefined ? {} : { id: pos[0] }), ...(clear === undefined ? {} : { clear }), ...(marked.has('retry') ? { retry: true } : {}) } };
|
|
52
|
+
}
|
|
53
|
+
case 'skipped': {
|
|
54
|
+
const parsed = flags(rest, ['restore']);
|
|
55
|
+
if (parsed.error !== undefined)
|
|
56
|
+
return { ok: false, code: 'usage', message: parsed.error };
|
|
57
|
+
if (pos.length > 0)
|
|
58
|
+
return { ok: false, code: 'usage', message: USAGE };
|
|
59
|
+
const restore = parsed.values.get('restore');
|
|
60
|
+
return { ok: true, command: { verb: 'skipped', ...(restore === undefined ? {} : { restore }) } };
|
|
61
|
+
}
|
|
62
|
+
case 'discard': return pos.length === 1 ? { ok: true, command: { verb: 'discard', id: pos[0] } } : { ok: false, code: 'usage', message: USAGE };
|
|
63
|
+
case 'undo': {
|
|
64
|
+
const parsed = flags(rest, ['run']);
|
|
65
|
+
if (parsed.error !== undefined)
|
|
66
|
+
return { ok: false, code: 'usage', message: parsed.error };
|
|
67
|
+
const target = pos[0];
|
|
68
|
+
const run = parsed.values.get('run');
|
|
69
|
+
if (target === undefined || run === undefined || pos.length !== 1)
|
|
70
|
+
return { ok: false, code: 'usage', message: USAGE };
|
|
71
|
+
return { ok: true, command: { verb: 'undo', target, run } };
|
|
72
|
+
}
|
|
73
|
+
default: return { ok: false, code: 'usage', message: 'unknown merge command: ' + verb + '\n' + USAGE };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function mergeUsage() {
|
|
77
|
+
return USAGE;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isPresent } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
import { readStaging } from "./staging.js";
|
|
3
|
+
import { readStagedTree } from "./staged-tree.js";
|
|
4
|
+
import { buildCommitPlan } from "./commit-plan.js";
|
|
5
|
+
export async function assembleStagedCommit(io, home, runId, port, options) {
|
|
6
|
+
const staging = await readStaging(io, home, runId);
|
|
7
|
+
if (!isPresent(staging))
|
|
8
|
+
return { ok: false, message: 'staging is not present for run ' + runId };
|
|
9
|
+
const tree = await readStagedTree(io, home, runId);
|
|
10
|
+
if (!isPresent(tree))
|
|
11
|
+
return { ok: false, message: 'merged tree is not readable for run ' + runId };
|
|
12
|
+
const requested = staging.value.manifest.target;
|
|
13
|
+
let target = requested;
|
|
14
|
+
let targetCurrent = await port.readSkill(requested);
|
|
15
|
+
const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
|
|
16
|
+
if (staging.value.manifest.mode === 'create-umbrella' && (targetCurrent !== null || requestedArchived)) {
|
|
17
|
+
target = requested;
|
|
18
|
+
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
19
|
+
const candidate = requested + '-v' + String(suffix);
|
|
20
|
+
if (await port.readSkill(candidate) === null
|
|
21
|
+
&& await port.isArchived?.(candidate) !== true) {
|
|
22
|
+
target = candidate;
|
|
23
|
+
targetCurrent = null;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (target === requested)
|
|
28
|
+
return { ok: false, message: 'no free umbrella target for ' + requested };
|
|
29
|
+
}
|
|
30
|
+
const sources = [];
|
|
31
|
+
for (const name of staging.value.manifest.sources) {
|
|
32
|
+
const body = await port.readSkill(name);
|
|
33
|
+
if (body === null)
|
|
34
|
+
return { ok: false, message: 'source skill is not live: ' + name };
|
|
35
|
+
sources.push({ name, body });
|
|
36
|
+
}
|
|
37
|
+
return buildCommitPlan({
|
|
38
|
+
target,
|
|
39
|
+
create: targetCurrent === null,
|
|
40
|
+
mergedBody: tree.value.body,
|
|
41
|
+
mergedFiles: tree.value.files,
|
|
42
|
+
targetCurrent,
|
|
43
|
+
sourceBodies: sources,
|
|
44
|
+
origin: options.origin,
|
|
45
|
+
summary: options.summary,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=commit-assembly.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { assembleStagedCommit } from "./commit-assembly.js";
|
|
2
|
+
import { writeCommitReceipt } from "./commit-receipt.js";
|
|
3
|
+
import { pairKey } from "./contracts.js";
|
|
4
|
+
import { writeJournal } from "./domain/commit-journal-store.js";
|
|
5
|
+
export async function commitStagedRun(io, home, runId, port, options) {
|
|
6
|
+
const assembled = await assembleStagedCommit(io, home, runId, port, { origin: options.origin, summary: options.summary });
|
|
7
|
+
if (!assembled.ok)
|
|
8
|
+
return { ok: false, message: assembled.message };
|
|
9
|
+
const journal = { runId, target: assembled.plan.target, create: !assembled.plan.targetExists, sources: assembled.plan.sources, phase: 'prepared', updatedAt: new Date(options.now()).toISOString() };
|
|
10
|
+
try {
|
|
11
|
+
await writeJournal(io, home, journal);
|
|
12
|
+
}
|
|
13
|
+
catch { /* journal is best-effort; receipt remains the commit record */ }
|
|
14
|
+
const result = await port.commitMerge(assembled.plan);
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
try {
|
|
17
|
+
await writeJournal(io, home, { ...journal, phase: 'failed', updatedAt: new Date(options.now()).toISOString() });
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return { ok: false, message: result.message, plan: assembled.plan };
|
|
21
|
+
}
|
|
22
|
+
if (result.unitId === undefined)
|
|
23
|
+
return { ok: true, plan: assembled.plan, result, warning: 'commit landed without a unitId; receipt not written' };
|
|
24
|
+
const receipt = {
|
|
25
|
+
runId,
|
|
26
|
+
parentRunId: runId.split('/pairs/')[0] ?? runId,
|
|
27
|
+
unitId: result.unitId,
|
|
28
|
+
target: assembled.plan.target,
|
|
29
|
+
create: !assembled.plan.targetExists,
|
|
30
|
+
sources: assembled.plan.sources,
|
|
31
|
+
...(assembled.plan.sources[0] === undefined ? {} : { pairKey: pairKey(assembled.plan.target, assembled.plan.sources[0]) }),
|
|
32
|
+
archived: result.archived ?? [],
|
|
33
|
+
at: options.now(),
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
await writeCommitReceipt(io, home, receipt);
|
|
37
|
+
try {
|
|
38
|
+
await writeJournal(io, home, { ...journal, unitId: result.unitId, phase: 'committed', updatedAt: new Date(options.now()).toISOString() });
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
return { ok: true, plan: assembled.plan, result, receipt };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
plan: assembled.plan,
|
|
47
|
+
result,
|
|
48
|
+
warning: 'commit landed but receipt write failed: ' + (error instanceof Error ? error.message : String(error)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=commit-execute.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const MAX_SKILL_NAME_LENGTH = 64;
|
|
2
|
+
/** Pick the first free target: requested, then -v2, -v3, ... (never truncate a name). */
|
|
3
|
+
export function resolveTargetName(requested, exists) {
|
|
4
|
+
if (requested.length === 0 || requested.length > MAX_SKILL_NAME_LENGTH) {
|
|
5
|
+
return { ok: false, message: 'target name must be 1..' + String(MAX_SKILL_NAME_LENGTH) + ' characters' };
|
|
6
|
+
}
|
|
7
|
+
if (!exists(requested))
|
|
8
|
+
return { ok: true, name: requested, suffix: 1 };
|
|
9
|
+
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
10
|
+
const candidate = requested + '-v' + String(suffix);
|
|
11
|
+
if (candidate.length > MAX_SKILL_NAME_LENGTH)
|
|
12
|
+
break;
|
|
13
|
+
if (!exists(candidate))
|
|
14
|
+
return { ok: true, name: candidate, suffix };
|
|
15
|
+
}
|
|
16
|
+
return { ok: false, message: 'no free target name for ' + requested + ' (tried up to -v99)' };
|
|
17
|
+
}
|
|
18
|
+
/** Rewrite only the frontmatter name; body bytes stay otherwise intact. */
|
|
19
|
+
export function rewriteFrontmatterName(body, name) {
|
|
20
|
+
const lines = body.split('\n');
|
|
21
|
+
if (lines[0]?.trim() !== '---')
|
|
22
|
+
return body;
|
|
23
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
24
|
+
if (end < 0)
|
|
25
|
+
return body;
|
|
26
|
+
for (let index = 1; index < end; index += 1) {
|
|
27
|
+
if (/^name\s*:/.test(lines[index] ?? '')) {
|
|
28
|
+
lines[index] = 'name: ' + name;
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
34
|
+
export function buildCommitPlan(input) {
|
|
35
|
+
if (input.create !== (input.targetCurrent === null))
|
|
36
|
+
return { ok: false, message: 'create flag disagrees with the live target' };
|
|
37
|
+
const sources = input.sourceBodies.filter(source => source.name !== input.target);
|
|
38
|
+
if (sources.length === 0)
|
|
39
|
+
return { ok: false, message: 'a merge needs at least one source different from the target' };
|
|
40
|
+
const body = rewriteFrontmatterName(input.mergedBody, input.target);
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
plan: {
|
|
44
|
+
target: input.target,
|
|
45
|
+
targetExists: !input.create,
|
|
46
|
+
sources: sources.map(source => source.name),
|
|
47
|
+
body,
|
|
48
|
+
supportFiles: input.mergedFiles,
|
|
49
|
+
expectedTargetBody: input.targetCurrent,
|
|
50
|
+
expectedSourceBodies: sources,
|
|
51
|
+
origin: input.origin,
|
|
52
|
+
summary: input.summary,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=commit-plan.js.map
|