@lmzhen/dsh-evolution-merge 0.30.1 → 0.30.3
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 +328 -55
- package/lib/types/adapters/library.js +8 -0
- package/lib/types/commit-execute.d.ts +4 -0
- package/lib/types/commit-execute.js +13 -5
- package/lib/types/commit-recovery.d.ts +10 -0
- package/lib/types/commit-recovery.js +84 -0
- package/lib/types/commit-snapshot.d.ts +38 -0
- package/lib/types/commit-snapshot.js +65 -0
- package/lib/types/coordinator.d.ts +8 -0
- package/lib/types/coordinator.js +7 -2
- package/lib/types/decision.js +5 -1
- package/lib/types/index.js +8 -2
- package/lib/types/presentation-budget.d.ts +3 -0
- package/lib/types/presentation-budget.js +11 -0
- package/lib/types/revision.d.ts +4 -0
- package/lib/types/revision.js +12 -0
- package/lib/types/routes.d.ts +3 -0
- package/lib/types/routes.js +6 -3
- package/lib/types/runtime.js +50 -12
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +1 -0
- package/lib/types/scan-ports.js +6 -1
- package/lib/types/scan-run.d.ts +1 -0
- package/lib/types/scan-run.js +4 -2
- package/lib/types/skill-card.d.ts +1 -0
- package/lib/types/state.d.ts +8 -1
- package/lib/types/state.js +12 -5
- package/lib/types/status.js +3 -2
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, resolve, sep } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { pairKeyOf, screen, selectCandidates, toEngineCandidate } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
|
-
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
4
|
+
import { DEFAULT_SKILL_LIMITS, authorityFrom, contentHash, createSubagentChannel, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, scanContentThreats, skillsRoot, supportRefs, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
//#region lib/types/adapters/select.js
|
|
@@ -345,22 +345,37 @@ async function mutateMergeState(io, home, now, fn) {
|
|
|
345
345
|
});
|
|
346
346
|
return result;
|
|
347
347
|
}
|
|
348
|
-
async function upsertRow(io, home, now, row) {
|
|
349
|
-
await mutateMergeState(io, home, now, (rows) => {
|
|
348
|
+
async function upsertRow(io, home, now, row, expect = "any") {
|
|
349
|
+
return await mutateMergeState(io, home, now, (rows) => {
|
|
350
350
|
const index = rows.findIndex((item) => item.pairKey === row.pairKey);
|
|
351
|
+
const current = index < 0 ? void 0 : rows[index];
|
|
352
|
+
if (current !== void 0 && expect !== "any" && (expect === "absent" || queueOf(current.state) !== expect)) return {
|
|
353
|
+
rows,
|
|
354
|
+
result: {
|
|
355
|
+
ok: false,
|
|
356
|
+
current
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
if (current === void 0 && expect !== "any" && expect !== "absent" && expect !== "verify") return {
|
|
360
|
+
rows,
|
|
361
|
+
result: {
|
|
362
|
+
ok: false,
|
|
363
|
+
current: row
|
|
364
|
+
}
|
|
365
|
+
};
|
|
351
366
|
const normalized = {
|
|
352
367
|
...row,
|
|
353
368
|
queue: queueOf(row.state)
|
|
354
369
|
};
|
|
355
370
|
const next = [...rows];
|
|
356
|
-
if (
|
|
371
|
+
if (current === void 0) next.push(normalized);
|
|
357
372
|
else next[index] = {
|
|
358
373
|
...normalized,
|
|
359
|
-
createdAt:
|
|
374
|
+
createdAt: current.createdAt
|
|
360
375
|
};
|
|
361
376
|
return {
|
|
362
377
|
rows: next,
|
|
363
|
-
result:
|
|
378
|
+
result: { ok: true }
|
|
364
379
|
};
|
|
365
380
|
});
|
|
366
381
|
}
|
|
@@ -563,6 +578,152 @@ async function cleanupExpiredStaging(io, home, now, keep = /* @__PURE__ */ new S
|
|
|
563
578
|
};
|
|
564
579
|
}
|
|
565
580
|
//#endregion
|
|
581
|
+
//#region lib/types/domain/commit-journal.js
|
|
582
|
+
/** What a restart may do with the last durable phase. */
|
|
583
|
+
function recoveryFor(journal) {
|
|
584
|
+
switch (journal.phase) {
|
|
585
|
+
case "prepared": return "retry-commit";
|
|
586
|
+
case "target-written": return "rollback-target";
|
|
587
|
+
case "sources-archived": return "rollback-sources";
|
|
588
|
+
case "committed":
|
|
589
|
+
case "rolled-back":
|
|
590
|
+
case "failed": return "none";
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region lib/types/domain/commit-journal-store.js
|
|
595
|
+
/** Durable commit journal: <home>/merge/journals/<run>.json. */
|
|
596
|
+
function journalPath(home, runId) {
|
|
597
|
+
return join(home, "merge", "journals", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
|
|
598
|
+
}
|
|
599
|
+
async function writeJournal(io, home, journal) {
|
|
600
|
+
const path = journalPath(home, journal.runId);
|
|
601
|
+
await io.writeText(path, JSON.stringify(journal, null, 2));
|
|
602
|
+
return path;
|
|
603
|
+
}
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region lib/types/commit-snapshot.js
|
|
606
|
+
/** Pre-commit snapshot: enough to restore the target and archived sources after a crash. */
|
|
607
|
+
function snapshotPath(home, runId) {
|
|
608
|
+
return join(home, "merge", "snapshots", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
|
|
609
|
+
}
|
|
610
|
+
async function writeCommitSnapshot(io, home, runId, plan, now, port) {
|
|
611
|
+
if (!isSafeSkillName(plan.target) || plan.sources.some((name) => !isSafeSkillName(name))) throw new Error("unsafe skill name in commit plan: " + plan.target);
|
|
612
|
+
const readSupport = port?.readSupportFiles === void 0 ? void 0 : (name) => port.readSupportFiles?.(name) ?? Promise.resolve([]);
|
|
613
|
+
const targetSupport = readSupport === void 0 || !plan.targetExists ? [] : await readSupport(plan.target);
|
|
614
|
+
const sourceSupport = [];
|
|
615
|
+
if (readSupport !== void 0) for (const name of plan.sources) for (const file of await readSupport(name)) sourceSupport.push({
|
|
616
|
+
name,
|
|
617
|
+
path: file.path,
|
|
618
|
+
content: file.content
|
|
619
|
+
});
|
|
620
|
+
const snapshot = {
|
|
621
|
+
runId,
|
|
622
|
+
target: plan.target,
|
|
623
|
+
create: !plan.targetExists,
|
|
624
|
+
sources: plan.sources,
|
|
625
|
+
targetBody: plan.expectedTargetBody,
|
|
626
|
+
sourceBodies: plan.expectedSourceBodies.map((item) => ({
|
|
627
|
+
name: item.name,
|
|
628
|
+
body: item.body
|
|
629
|
+
})),
|
|
630
|
+
...targetSupport.length === 0 ? {} : { targetSupport },
|
|
631
|
+
...plan.supportFiles.length === 0 ? {} : { targetSupportAfter: plan.supportFiles },
|
|
632
|
+
...sourceSupport.length === 0 ? {} : { sourceSupport },
|
|
633
|
+
at: new Date(now).toISOString()
|
|
634
|
+
};
|
|
635
|
+
await io.writeText(snapshotPath(home, runId), JSON.stringify(snapshot, null, 2));
|
|
636
|
+
return snapshot;
|
|
637
|
+
}
|
|
638
|
+
async function readCommitSnapshot(io, home, runId) {
|
|
639
|
+
const raw = await io.readText(snapshotPath(home, runId));
|
|
640
|
+
if (raw === null) return null;
|
|
641
|
+
try {
|
|
642
|
+
return JSON.parse(raw);
|
|
643
|
+
} catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function isSafeSkillName(name) {
|
|
648
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== "." && name !== "..";
|
|
649
|
+
}
|
|
650
|
+
function skillFilePath(root, name, path) {
|
|
651
|
+
if (!isSafeSkillName(name) || path.startsWith("/") || path.includes("..")) throw new Error("unsafe skill file: " + name + "/" + path);
|
|
652
|
+
const base = resolve(root, name);
|
|
653
|
+
const file = resolve(base, path);
|
|
654
|
+
if (!file.startsWith(base + sep)) throw new Error("skill file escapes skill: " + name + "/" + path);
|
|
655
|
+
return file;
|
|
656
|
+
}
|
|
657
|
+
function skillBodyPath(root, name) {
|
|
658
|
+
if (!isSafeSkillName(name)) throw new Error("unsafe skill name: " + name);
|
|
659
|
+
const base = resolve(root);
|
|
660
|
+
const path = resolve(base, name, "SKILL.md");
|
|
661
|
+
if (!path.startsWith(base + sep)) throw new Error("skill path escapes root: " + name);
|
|
662
|
+
return path;
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region lib/types/commit-recovery.js
|
|
666
|
+
/** Startup replay for non-terminal commit journals. */
|
|
667
|
+
async function recoverCommitJournals(io, home, now, skillRoot, restoreSource) {
|
|
668
|
+
const dir = join(home, "merge", "journals");
|
|
669
|
+
if (!await io.exists(dir)) return [];
|
|
670
|
+
const files = (await io.list(dir)).filter((name) => name.endsWith(".json"));
|
|
671
|
+
const reports = [];
|
|
672
|
+
for (const file of files) {
|
|
673
|
+
let journal;
|
|
674
|
+
try {
|
|
675
|
+
journal = JSON.parse(await io.readText(join(dir, file)) ?? "");
|
|
676
|
+
} catch {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const recovery = recoveryFor(journal);
|
|
680
|
+
if (recovery === "none") continue;
|
|
681
|
+
const snapshot = await readCommitSnapshot(io, home, journal.runId);
|
|
682
|
+
let action = "report-only";
|
|
683
|
+
let ok = true;
|
|
684
|
+
if (recovery === "retry-commit") action = "no-host-write";
|
|
685
|
+
else if (snapshot === null) {
|
|
686
|
+
action = "snapshot-missing";
|
|
687
|
+
ok = false;
|
|
688
|
+
} else try {
|
|
689
|
+
if (recovery === "rollback-sources") {
|
|
690
|
+
let restoredArchived = restoreSource !== void 0;
|
|
691
|
+
if (restoreSource !== void 0) {
|
|
692
|
+
for (const source of snapshot.sourceBodies) if (!await restoreSource(source.name)) restoredArchived = false;
|
|
693
|
+
}
|
|
694
|
+
if (!restoredArchived) for (const source of snapshot.sourceBodies) await io.writeText(skillBodyPath(skillRoot, source.name), source.body);
|
|
695
|
+
}
|
|
696
|
+
if (snapshot.create && snapshot.targetBody === null) try {
|
|
697
|
+
await io.remove(skillBodyPath(skillRoot, snapshot.target));
|
|
698
|
+
} catch {}
|
|
699
|
+
else if (snapshot.targetBody !== null) await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
|
|
700
|
+
const oldPaths = new Set((snapshot.targetSupport ?? []).map((file) => file.path));
|
|
701
|
+
for (const file of snapshot.targetSupport ?? []) await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
|
|
702
|
+
for (const file of snapshot.targetSupportAfter ?? []) if (!oldPaths.has(file.path)) try {
|
|
703
|
+
await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
|
|
704
|
+
} catch {}
|
|
705
|
+
for (const file of snapshot.sourceSupport ?? []) await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
|
|
706
|
+
action = recovery === "rollback-sources" ? "restored-sources-and-target" : "restored-target";
|
|
707
|
+
} catch (error) {
|
|
708
|
+
action = "restore-failed: " + (error instanceof Error ? error.message : String(error));
|
|
709
|
+
ok = false;
|
|
710
|
+
}
|
|
711
|
+
await writeJournal(io, home, {
|
|
712
|
+
...journal,
|
|
713
|
+
phase: ok ? "rolled-back" : "failed",
|
|
714
|
+
updatedAt: new Date(now).toISOString()
|
|
715
|
+
});
|
|
716
|
+
reports.push({
|
|
717
|
+
runId: journal.runId,
|
|
718
|
+
phase: journal.phase,
|
|
719
|
+
action,
|
|
720
|
+
ok
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
await io.writeText(join(home, "merge", "recovery.json"), JSON.stringify(reports, null, 2));
|
|
724
|
+
return reports;
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
566
727
|
//#region lib/types/commit-receipt.js
|
|
567
728
|
/**
|
|
568
729
|
* Commit receipt: <run>/commit.json, written immediately after a successful commit.
|
|
@@ -743,11 +904,15 @@ function transition(current, event) {
|
|
|
743
904
|
return event.approval === "on" ? next(current, event.at, "pending", {
|
|
744
905
|
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
745
906
|
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
746
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
907
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
908
|
+
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
909
|
+
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
747
910
|
}) : next(current, event.at, "approved", {
|
|
748
911
|
...event.decision === void 0 ? {} : { decision: event.decision },
|
|
749
912
|
...event.stagingRef === void 0 ? {} : { stagingRef: event.stagingRef },
|
|
750
|
-
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt }
|
|
913
|
+
...event.stagingExpiresAt === void 0 ? {} : { stagingExpiresAt: event.stagingExpiresAt },
|
|
914
|
+
...event.sourceRevision === void 0 ? {} : { sourceRevision: event.sourceRevision },
|
|
915
|
+
...event.targetRevision === void 0 ? {} : { targetRevision: event.targetRevision }
|
|
751
916
|
});
|
|
752
917
|
case "round1-fail": return current.state === "verify" ? next(current, event.at, "verify", {
|
|
753
918
|
failure: event.failure,
|
|
@@ -764,10 +929,23 @@ function transition(current, event) {
|
|
|
764
929
|
ok: true,
|
|
765
930
|
row: null
|
|
766
931
|
} : invalid(event, current.state);
|
|
932
|
+
case "target-taken":
|
|
933
|
+
case "source-changed": return current.state === "pending" || current.state === "approved" || current.state === "fuse" ? next(current, event.at, current.state, { expired: event.type }) : invalid(event, current.state);
|
|
767
934
|
case "restore": return current.state === "skipped" ? next(current, event.at, "verify") : invalid(event, current.state);
|
|
768
935
|
}
|
|
769
936
|
}
|
|
770
937
|
//#endregion
|
|
938
|
+
//#region lib/types/presentation-budget.js
|
|
939
|
+
/** Tool-result budget: keep the head and tail, mark what was omitted. */
|
|
940
|
+
function fitToolText(text, max = 8e3) {
|
|
941
|
+
if (text.length <= max) return text;
|
|
942
|
+
const marker = "\n... [truncated " + String(text.length - max) + " chars] ...\n";
|
|
943
|
+
const keep = Math.max(0, max - marker.length);
|
|
944
|
+
const head = Math.ceil(keep * .7);
|
|
945
|
+
const tail = keep - head;
|
|
946
|
+
return text.slice(0, head) + marker + (tail === 0 ? "" : text.slice(-tail));
|
|
947
|
+
}
|
|
948
|
+
//#endregion
|
|
771
949
|
//#region lib/types/status.js
|
|
772
950
|
function countRows(rows) {
|
|
773
951
|
const counts = {
|
|
@@ -788,12 +966,12 @@ function formatStatus(rows) {
|
|
|
788
966
|
const counts = countRows(rows);
|
|
789
967
|
const lines = ["merge: verify " + String(counts.verify) + ", pending " + String(counts.pending) + ", fuse " + String(counts.fuse) + ", skipped " + String(counts.skipped)];
|
|
790
968
|
for (const row of rows) lines.push("- " + describe(row));
|
|
791
|
-
return lines.join("\n");
|
|
969
|
+
return fitToolText(lines.join("\n"));
|
|
792
970
|
}
|
|
793
971
|
function formatQueue(rows, queue) {
|
|
794
972
|
const selected = rows.filter((row) => queueOf(row.state) === queue);
|
|
795
973
|
if (selected.length === 0) return "merge " + queue + ": empty";
|
|
796
|
-
return ["merge " + queue + ": " + String(selected.length)].concat(selected.map((row) => "- " + describe(row))).join("\n");
|
|
974
|
+
return fitToolText(["merge " + queue + ": " + String(selected.length)].concat(selected.map((row) => "- " + describe(row))).join("\n"));
|
|
797
975
|
}
|
|
798
976
|
//#endregion
|
|
799
977
|
//#region lib/types/routes.js
|
|
@@ -811,6 +989,9 @@ function toRowView(row) {
|
|
|
811
989
|
createdAt: row.createdAt,
|
|
812
990
|
...row.decision?.target === void 0 ? {} : { target: row.decision.target },
|
|
813
991
|
...row.decision?.confidence === void 0 ? {} : { confidence: row.decision.confidence },
|
|
992
|
+
...row.decision?.aSummary === void 0 ? {} : { aSummary: row.decision.aSummary },
|
|
993
|
+
...row.decision?.bSummary === void 0 ? {} : { bSummary: row.decision.bSummary },
|
|
994
|
+
...row.decision?.rationale === void 0 ? {} : { rationale: row.decision.rationale },
|
|
814
995
|
...row.failure === void 0 ? {} : { failure: {
|
|
815
996
|
stage: row.failure.stage,
|
|
816
997
|
code: row.failure.code,
|
|
@@ -823,9 +1004,9 @@ function toRowView(row) {
|
|
|
823
1004
|
...row.expired === void 0 ? {} : { expired: row.expired },
|
|
824
1005
|
...row.reason === void 0 ? {} : { reason: row.reason },
|
|
825
1006
|
summary: [
|
|
826
|
-
row.
|
|
827
|
-
row.decision?.
|
|
828
|
-
row.
|
|
1007
|
+
row.decision?.aSummary ?? "未提供技能1摘要",
|
|
1008
|
+
row.decision?.bSummary ?? "未提供技能2摘要",
|
|
1009
|
+
row.decision?.rationale ?? row.reason ?? "未提供融合原因"
|
|
829
1010
|
],
|
|
830
1011
|
actions: row.state === "pending" ? ["/evolution merge approve " + row.id, "/evolution merge reject " + row.id] : row.state === "skipped" ? ["/evolution merge skipped --restore " + row.id] : ["/evolution merge resume " + row.id]
|
|
831
1012
|
};
|
|
@@ -1539,7 +1720,8 @@ async function runScan(options) {
|
|
|
1539
1720
|
});
|
|
1540
1721
|
if (!created.ok || created.row === null) throw new Error(created.ok ? "candidate row missing" : created.message);
|
|
1541
1722
|
row = created.row;
|
|
1542
|
-
await upsertRow(options.io, options.home, options.now, row);
|
|
1723
|
+
const placed = await upsertRow(options.io, options.home, options.now, row, "verify");
|
|
1724
|
+
if (!placed.ok) throw new Error("pair already in " + placed.current.state);
|
|
1543
1725
|
} catch (error) {
|
|
1544
1726
|
totals.failed += 1;
|
|
1545
1727
|
pairs.push({
|
|
@@ -1711,7 +1893,7 @@ async function runScan(options) {
|
|
|
1711
1893
|
},
|
|
1712
1894
|
pairs,
|
|
1713
1895
|
totals,
|
|
1714
|
-
notes: [],
|
|
1896
|
+
notes: [...options.notes ?? []],
|
|
1715
1897
|
...remaining.length === 0 ? {} : { remaining }
|
|
1716
1898
|
};
|
|
1717
1899
|
}
|
|
@@ -1905,7 +2087,8 @@ async function runScanCommand(options) {
|
|
|
1905
2087
|
ports,
|
|
1906
2088
|
stage: ctx.stage,
|
|
1907
2089
|
startedAt: ctx.startedAt,
|
|
1908
|
-
...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes }
|
|
2090
|
+
...options.timeoutMinutes === void 0 ? {} : { timeoutMinutes: options.timeoutMinutes },
|
|
2091
|
+
...options.notes === void 0 ? {} : { notes: options.notes }
|
|
1909
2092
|
});
|
|
1910
2093
|
}
|
|
1911
2094
|
});
|
|
@@ -2372,6 +2555,12 @@ function parseDecision(text, pair) {
|
|
|
2372
2555
|
const target = typeof record.target === "string" ? record.target.trim() : "";
|
|
2373
2556
|
const sources = Array.isArray(record.sources) ? record.sources.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
|
|
2374
2557
|
const rationale = typeof record.rationale === "string" ? record.rationale.trim() : "";
|
|
2558
|
+
const aSummary = typeof record.aSummary === "string" ? record.aSummary.trim() : typeof record.a_summary === "string" ? record.a_summary.trim() : "";
|
|
2559
|
+
const bSummary = typeof record.bSummary === "string" ? record.bSummary.trim() : typeof record.b_summary === "string" ? record.b_summary.trim() : "";
|
|
2560
|
+
const summaries = {
|
|
2561
|
+
...aSummary === "" ? {} : { aSummary },
|
|
2562
|
+
...bSummary === "" ? {} : { bSummary }
|
|
2563
|
+
};
|
|
2375
2564
|
if (!shouldMerge || mode === "keep") return {
|
|
2376
2565
|
ok: true,
|
|
2377
2566
|
decision: {
|
|
@@ -2379,7 +2568,8 @@ function parseDecision(text, pair) {
|
|
|
2379
2568
|
mode: "keep",
|
|
2380
2569
|
target,
|
|
2381
2570
|
sources,
|
|
2382
|
-
rationale
|
|
2571
|
+
rationale,
|
|
2572
|
+
...summaries
|
|
2383
2573
|
}
|
|
2384
2574
|
};
|
|
2385
2575
|
if (target === "") return {
|
|
@@ -2405,6 +2595,7 @@ function parseDecision(text, pair) {
|
|
|
2405
2595
|
target,
|
|
2406
2596
|
sources,
|
|
2407
2597
|
rationale,
|
|
2598
|
+
...summaries,
|
|
2408
2599
|
...typeof record.confidence === "number" ? { confidence: record.confidence } : {}
|
|
2409
2600
|
}
|
|
2410
2601
|
};
|
|
@@ -2890,17 +3081,6 @@ async function assembleStagedCommit(io, home, runId, port, options) {
|
|
|
2890
3081
|
});
|
|
2891
3082
|
}
|
|
2892
3083
|
//#endregion
|
|
2893
|
-
//#region lib/types/domain/commit-journal-store.js
|
|
2894
|
-
/** Durable commit journal: <home>/merge/journals/<run>.json. */
|
|
2895
|
-
function journalPath(home, runId) {
|
|
2896
|
-
return join(home, "merge", "journals", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
|
|
2897
|
-
}
|
|
2898
|
-
async function writeJournal(io, home, journal) {
|
|
2899
|
-
const path = journalPath(home, journal.runId);
|
|
2900
|
-
await io.writeText(path, JSON.stringify(journal, null, 2));
|
|
2901
|
-
return path;
|
|
2902
|
-
}
|
|
2903
|
-
//#endregion
|
|
2904
3084
|
//#region lib/types/commit-execute.js
|
|
2905
3085
|
async function commitStagedRun(io, home, runId, port, options) {
|
|
2906
3086
|
const assembled = await assembleStagedCommit(io, home, runId, port, {
|
|
@@ -2911,12 +3091,21 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
2911
3091
|
ok: false,
|
|
2912
3092
|
message: assembled.message
|
|
2913
3093
|
};
|
|
3094
|
+
try {
|
|
3095
|
+
await writeCommitSnapshot(io, home, runId, assembled.plan, options.now(), port);
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
return {
|
|
3098
|
+
ok: false,
|
|
3099
|
+
message: "commit snapshot failed; refusing to write: " + (error instanceof Error ? error.message : String(error)),
|
|
3100
|
+
plan: assembled.plan
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
2914
3103
|
const journal = {
|
|
2915
3104
|
runId,
|
|
2916
3105
|
target: assembled.plan.target,
|
|
2917
3106
|
create: !assembled.plan.targetExists,
|
|
2918
3107
|
sources: assembled.plan.sources,
|
|
2919
|
-
phase: "
|
|
3108
|
+
phase: "sources-archived",
|
|
2920
3109
|
updatedAt: new Date(options.now()).toISOString()
|
|
2921
3110
|
};
|
|
2922
3111
|
try {
|
|
@@ -2937,6 +3126,14 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
2937
3126
|
plan: assembled.plan
|
|
2938
3127
|
};
|
|
2939
3128
|
}
|
|
3129
|
+
const committed = {
|
|
3130
|
+
...journal,
|
|
3131
|
+
phase: "committed",
|
|
3132
|
+
updatedAt: new Date(options.now()).toISOString()
|
|
3133
|
+
};
|
|
3134
|
+
try {
|
|
3135
|
+
await writeJournal(io, home, committed);
|
|
3136
|
+
} catch {}
|
|
2940
3137
|
if (result.unitId === void 0) return {
|
|
2941
3138
|
ok: true,
|
|
2942
3139
|
plan: assembled.plan,
|
|
@@ -2956,14 +3153,6 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
2956
3153
|
};
|
|
2957
3154
|
try {
|
|
2958
3155
|
await writeCommitReceipt(io, home, receipt);
|
|
2959
|
-
try {
|
|
2960
|
-
await writeJournal(io, home, {
|
|
2961
|
-
...journal,
|
|
2962
|
-
unitId: result.unitId,
|
|
2963
|
-
phase: "committed",
|
|
2964
|
-
updatedAt: new Date(options.now()).toISOString()
|
|
2965
|
-
});
|
|
2966
|
-
} catch {}
|
|
2967
3156
|
return {
|
|
2968
3157
|
ok: true,
|
|
2969
3158
|
plan: assembled.plan,
|
|
@@ -3050,6 +3239,17 @@ async function runAuthoring(options) {
|
|
|
3050
3239
|
};
|
|
3051
3240
|
}
|
|
3052
3241
|
//#endregion
|
|
3242
|
+
//#region lib/types/revision.js
|
|
3243
|
+
/** Stable source revision for a pair: names sorted, body hashed in that order. */
|
|
3244
|
+
function sourceRevisionOf(cards, a, b) {
|
|
3245
|
+
const byName = new Map(cards.map((card) => [card.name, card]));
|
|
3246
|
+
const names = [a, b].sort();
|
|
3247
|
+
const first = byName.get(names[0] ?? "");
|
|
3248
|
+
const second = byName.get(names[1] ?? "");
|
|
3249
|
+
if (first === void 0 || second === void 0) return void 0;
|
|
3250
|
+
return contentHash(first.name + "\0" + first.body + "\0" + second.name + "\0" + second.body);
|
|
3251
|
+
}
|
|
3252
|
+
//#endregion
|
|
3053
3253
|
//#region lib/types/scan-ports.js
|
|
3054
3254
|
function createChannelScanPorts(options) {
|
|
3055
3255
|
const sessions = /* @__PURE__ */ new Map();
|
|
@@ -3107,10 +3307,12 @@ function createChannelScanPorts(options) {
|
|
|
3107
3307
|
phase: "author"
|
|
3108
3308
|
});
|
|
3109
3309
|
}
|
|
3310
|
+
const sourceRevision = result.outcome.kind === "merge" ? sourceRevisionOf(options.cards, candidate.a, candidate.b) : void 0;
|
|
3110
3311
|
return result.outcome.kind === "merge" ? {
|
|
3111
3312
|
...result.outcome,
|
|
3112
3313
|
stagingRef: stageKey,
|
|
3113
|
-
stagingExpiresAt: options.now() + 7 * 864e5
|
|
3314
|
+
stagingExpiresAt: options.now() + 7 * 864e5,
|
|
3315
|
+
...sourceRevision === void 0 ? {} : { sourceRevision }
|
|
3114
3316
|
} : result.outcome;
|
|
3115
3317
|
},
|
|
3116
3318
|
async round2(row, decision, signal) {
|
|
@@ -3346,7 +3548,16 @@ function skillLibraryPort(skills) {
|
|
|
3346
3548
|
const names = await archivedNames.call(skills);
|
|
3347
3549
|
return names === null ? true : names.includes(name);
|
|
3348
3550
|
},
|
|
3349
|
-
commitMerge: (plan) => skills.commitMerge(plan)
|
|
3551
|
+
commitMerge: (plan) => skills.commitMerge(plan),
|
|
3552
|
+
async readSupportFiles(name) {
|
|
3553
|
+
if (typeof skills.listSupportFiles !== "function") return [];
|
|
3554
|
+
const probe = await skills.listSupportFiles(name);
|
|
3555
|
+
if (probe.kind !== "present") return [];
|
|
3556
|
+
return await Promise.all(probe.value.map(async (path) => ({
|
|
3557
|
+
path,
|
|
3558
|
+
content: await skills.readSupportFile(name, path) ?? ""
|
|
3559
|
+
})));
|
|
3560
|
+
}
|
|
3350
3561
|
};
|
|
3351
3562
|
}
|
|
3352
3563
|
//#endregion
|
|
@@ -3356,6 +3567,16 @@ function skillLibraryPort(skills) {
|
|
|
3356
3567
|
* Plugin wiring supplies the channel, model and library; this module keeps the orchestration
|
|
3357
3568
|
* out of apply() and makes the mutating verbs testable with fakes.
|
|
3358
3569
|
*/
|
|
3570
|
+
function forcedDecisionFor(a, b) {
|
|
3571
|
+
const sorted = [a, b].sort();
|
|
3572
|
+
return {
|
|
3573
|
+
shouldMerge: true,
|
|
3574
|
+
mode: "create-umbrella",
|
|
3575
|
+
target: ("merge-" + sorted.join("-")).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64),
|
|
3576
|
+
sources: [a, b],
|
|
3577
|
+
rationale: "forced pair merge of " + sorted.join(" and ")
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3359
3580
|
function createMergeRuntime(deps) {
|
|
3360
3581
|
const readOnly = createReadOnlyMergeService(deps.io, deps.home);
|
|
3361
3582
|
const timeout = deps.timeoutMinutes > 0 ? deps.timeoutMinutes : void 0;
|
|
@@ -3382,20 +3603,27 @@ function createMergeRuntime(deps) {
|
|
|
3382
3603
|
if (command.verb === "pair") {
|
|
3383
3604
|
const signal = new AbortController().signal;
|
|
3384
3605
|
const cards = await deps.cards(signal);
|
|
3606
|
+
const cardA = cards.find((card) => card.name === command.a);
|
|
3607
|
+
const cardB = cards.find((card) => card.name === command.b);
|
|
3608
|
+
if (cardA === void 0 || cardB === void 0 || cardA.managed !== true || cardB.managed !== true) return {
|
|
3609
|
+
ok: false,
|
|
3610
|
+
text: "merge pair 仅支持 family-managed 技能: " + command.a + " / " + command.b
|
|
3611
|
+
};
|
|
3385
3612
|
const pair = [{
|
|
3386
3613
|
a: command.a,
|
|
3387
3614
|
b: command.b
|
|
3388
3615
|
}];
|
|
3389
|
-
|
|
3390
|
-
if (
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
}
|
|
3398
|
-
const
|
|
3616
|
+
const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === pairKey(command.a, command.b));
|
|
3617
|
+
if (existing?.state === "pending") return {
|
|
3618
|
+
ok: false,
|
|
3619
|
+
text: "该 pair 已在待批准队列,先 approve/reject/discard: " + existing.id
|
|
3620
|
+
};
|
|
3621
|
+
if (existing?.state === "approved" || existing?.state === "fuse") return {
|
|
3622
|
+
ok: false,
|
|
3623
|
+
text: "该 pair 已在待合并队列,用 merge resume --retry 复用: " + (existing.runId ?? existing.id)
|
|
3624
|
+
};
|
|
3625
|
+
const forced = command.force === true ? forcedDecisionFor(command.a, command.b) : void 0;
|
|
3626
|
+
const approval = deps.params.mergeApproval;
|
|
3399
3627
|
const result = await runScanCommand({
|
|
3400
3628
|
...common,
|
|
3401
3629
|
approval,
|
|
@@ -3421,7 +3649,7 @@ function createMergeRuntime(deps) {
|
|
|
3421
3649
|
...ports,
|
|
3422
3650
|
round1: () => Promise.resolve({
|
|
3423
3651
|
kind: "merge",
|
|
3424
|
-
decision: forced
|
|
3652
|
+
decision: forced
|
|
3425
3653
|
})
|
|
3426
3654
|
};
|
|
3427
3655
|
}
|
|
@@ -3446,12 +3674,15 @@ function createMergeRuntime(deps) {
|
|
|
3446
3674
|
if (command.verb === "scan") {
|
|
3447
3675
|
const signal = new AbortController().signal;
|
|
3448
3676
|
const cards = await deps.cards(signal);
|
|
3677
|
+
const managed = new Set(cards.filter((card) => card.managed === true).map((card) => card.name));
|
|
3449
3678
|
const loadedState = await loadMergeState(deps.io, deps.home);
|
|
3450
3679
|
const skipped = skippedPairKeys(loadedState.state.rows, deps.params.mergeSkippedTtlDays, deps.now());
|
|
3451
|
-
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || !canRetry(row)).map((row) => row.pairKey));
|
|
3680
|
+
const held = new Set(loadedState.state.rows.filter((row) => row.queue === "pending" || row.queue === "fuse" || !canRetry(row)).map((row) => row.pairKey));
|
|
3681
|
+
const conflictNotes = loadedState.state.rows.filter((row) => row.state === "pending" || row.state === "approved" || row.state === "fuse" || row.state === "skipped").map((row) => "conflict:" + row.state + ":" + row.pairKey + ":" + row.id);
|
|
3452
3682
|
const result = await runScanCommand({
|
|
3453
3683
|
...common,
|
|
3454
3684
|
approval: deps.params.mergeApproval,
|
|
3685
|
+
notes: conflictNotes,
|
|
3455
3686
|
...detached ? { detached: true } : {},
|
|
3456
3687
|
makePorts: (runId) => createChannelScanPorts({
|
|
3457
3688
|
io: deps.io,
|
|
@@ -3462,7 +3693,7 @@ function createMergeRuntime(deps) {
|
|
|
3462
3693
|
const raw = await deps.screeningForRun(cards, runId, turnSignal);
|
|
3463
3694
|
const filtered = raw.filter((candidate) => {
|
|
3464
3695
|
const key = pairKey(candidate.a, candidate.b);
|
|
3465
|
-
return !skipped.has(key) && !held.has(key);
|
|
3696
|
+
return managed.has(candidate.a) && managed.has(candidate.b) && !skipped.has(key) && !held.has(key);
|
|
3466
3697
|
});
|
|
3467
3698
|
const candidatePairs = raw.candidatePairs;
|
|
3468
3699
|
return candidatePairs === void 0 ? filtered : Object.assign(filtered, { candidatePairs });
|
|
@@ -3561,7 +3792,7 @@ function createMergeRuntime(deps) {
|
|
|
3561
3792
|
text: "report is missing: " + run.resultRef
|
|
3562
3793
|
} : {
|
|
3563
3794
|
ok: true,
|
|
3564
|
-
text: report.value
|
|
3795
|
+
text: "report: " + run.resultRef + "\n" + fitToolText(report.value, 7600)
|
|
3565
3796
|
};
|
|
3566
3797
|
}
|
|
3567
3798
|
if (command.verb === "discard") {
|
|
@@ -3707,6 +3938,40 @@ function createMergeRuntime(deps) {
|
|
|
3707
3938
|
if (command.verb === "approve") {
|
|
3708
3939
|
const signal = new AbortController().signal;
|
|
3709
3940
|
const cards = await deps.cards(signal);
|
|
3941
|
+
const candidate = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.id === command.id);
|
|
3942
|
+
if (candidate !== void 0 && candidate.state === "pending" && candidate.decision !== void 0) {
|
|
3943
|
+
const missingSource = [candidate.a, candidate.b].find((name) => !cards.some((card) => card.name === name && card.managed === true));
|
|
3944
|
+
if (missingSource !== void 0) {
|
|
3945
|
+
await dropRow(deps.io, deps.home, deps.now, candidate.id);
|
|
3946
|
+
return {
|
|
3947
|
+
ok: false,
|
|
3948
|
+
text: "source-gone:" + missingSource + " 已不在 family-managed 技能库,已从 active 队列清除;可 merge skipped --discard 清理"
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
const currentRevision = sourceRevisionOf(cards, candidate.a, candidate.b);
|
|
3952
|
+
if (candidate.sourceRevision !== void 0 && currentRevision !== void 0 && currentRevision !== candidate.sourceRevision) {
|
|
3953
|
+
const changed = transition(candidate, {
|
|
3954
|
+
type: "source-changed",
|
|
3955
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3956
|
+
});
|
|
3957
|
+
if (changed.ok && changed.row !== null) await upsertRow(deps.io, deps.home, deps.now, changed.row);
|
|
3958
|
+
return {
|
|
3959
|
+
ok: false,
|
|
3960
|
+
text: "source-changed:源技能内容在决策后变化,拒绝照跑;出口:重新 merge pair,或 merge skipped --discard"
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
if (candidate.decision.mode === "merge-into-existing" && !cards.some((card) => card.name === candidate.decision?.target)) {
|
|
3964
|
+
const expired = transition(candidate, {
|
|
3965
|
+
type: "target-taken",
|
|
3966
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3967
|
+
});
|
|
3968
|
+
if (expired.ok && expired.row !== null) await upsertRow(deps.io, deps.home, deps.now, expired.row);
|
|
3969
|
+
return {
|
|
3970
|
+
ok: false,
|
|
3971
|
+
text: "target-taken:目标 " + candidate.decision.target + " 已不存在,行保留 pending --expired;出口:重新 merge pair,或 merge skipped --discard"
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3710
3975
|
const result = await runApproveCommand({
|
|
3711
3976
|
...common,
|
|
3712
3977
|
rowId: command.id,
|
|
@@ -4217,6 +4482,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4217
4482
|
...policyStageLimits(policySnapshot?.get?.())
|
|
4218
4483
|
}
|
|
4219
4484
|
});
|
|
4485
|
+
recoverCommitJournals(io, home, Date.now(), skillsRoot(), async (name) => {
|
|
4486
|
+
return (await skills.restoreFromArchive(name)).ok;
|
|
4487
|
+
}).then((reports) => {
|
|
4488
|
+
if (reports.length > 0) ctx.logger.info("merge commit recovery: " + reports.map((report) => report.runId + "=" + report.action).join(", "));
|
|
4489
|
+
}).catch((error) => {
|
|
4490
|
+
ctx.logger.warn("merge commit recovery failed: " + (error instanceof Error ? error.message : String(error)));
|
|
4491
|
+
});
|
|
4220
4492
|
const channel = subagentChannelAdapter(createSubagentChannel(ctx));
|
|
4221
4493
|
const stage = new StageToolRegistry();
|
|
4222
4494
|
const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
|
|
@@ -4339,7 +4611,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
4339
4611
|
body: item.content ?? "",
|
|
4340
4612
|
files,
|
|
4341
4613
|
protectedBy: item.protectedBy,
|
|
4342
|
-
protectionUnknown: item.protectionUnknown
|
|
4614
|
+
protectionUnknown: item.protectionUnknown,
|
|
4615
|
+
managed: item.managed
|
|
4343
4616
|
};
|
|
4344
4617
|
}));
|
|
4345
4618
|
},
|
|
@@ -9,6 +9,14 @@ export function skillLibraryPort(skills) {
|
|
|
9
9
|
return names === null ? true : names.includes(name);
|
|
10
10
|
},
|
|
11
11
|
commitMerge: plan => skills.commitMerge(plan),
|
|
12
|
+
async readSupportFiles(name) {
|
|
13
|
+
if (typeof skills.listSupportFiles !== 'function')
|
|
14
|
+
return [];
|
|
15
|
+
const probe = await skills.listSupportFiles(name);
|
|
16
|
+
if (probe.kind !== 'present')
|
|
17
|
+
return [];
|
|
18
|
+
return await Promise.all(probe.value.map(async (path) => ({ path, content: await skills.readSupportFile(name, path) ?? '' })));
|
|
19
|
+
},
|
|
12
20
|
};
|
|
13
21
|
}
|
|
14
22
|
//# sourceMappingURL=library.js.map
|
|
@@ -8,6 +8,10 @@ import { type CommitAssemblyPort } from './commit-assembly.ts';
|
|
|
8
8
|
import { type CommitReceipt } from './commit-receipt.ts';
|
|
9
9
|
export interface CommitPort extends CommitAssemblyPort {
|
|
10
10
|
commitMerge(plan: CommitMergePlan): Promise<CommitMergeResult>;
|
|
11
|
+
readSupportFiles?(name: string): Promise<readonly {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly content: string;
|
|
14
|
+
}[]>;
|
|
11
15
|
}
|
|
12
16
|
export interface CommitStagedOptions {
|
|
13
17
|
readonly origin: CommitMergePlan['origin'];
|