@lmzhen/dsh-evolution-merge 0.32.2 → 0.32.4
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 +157 -42
- package/lib/types/adapters/engine-screen.d.ts +2 -2
- package/lib/types/adapters/engine-screen.js +2 -1
- package/lib/types/adapters/model-runner.d.ts +1 -1
- package/lib/types/adapters/model-runner.js +7 -2
- package/lib/types/approve-command.js +2 -0
- package/lib/types/authoring-flow.js +4 -0
- package/lib/types/commit-execute.js +3 -6
- package/lib/types/commit-recovery.d.ts +0 -1
- package/lib/types/commit-recovery.js +1 -2
- package/lib/types/doctor.js +6 -9
- package/lib/types/domain/authoring-manifest.d.ts +10 -4
- package/lib/types/domain/authoring-manifest.js +87 -13
- package/lib/types/domain/row-run.d.ts +4 -0
- package/lib/types/domain/row-run.js +14 -0
- package/lib/types/domain/row-v4.d.ts +3 -0
- package/lib/types/domain/row-v4.js +4 -0
- package/lib/types/domain/stage-aggregate.js +3 -1
- package/lib/types/domain/validation-plan.d.ts +1 -0
- package/lib/types/domain/validation-plan.js +7 -2
- package/lib/types/recovery-coordinator.js +6 -0
- package/lib/types/resume-command.js +2 -0
- package/lib/types/run-shell.js +16 -0
- package/lib/types/surface/merge-view.js +3 -1
- package/package.json +4 -4
- package/lib/types/commit-snapshot.d.ts +0 -38
- package/lib/types/commit-snapshot.js +0 -65
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { pairKeyOf, screen, selectCandidates } from "@lmzhen/dsh-evolution-merge-engine";
|
|
4
|
-
import { DEFAULT_SKILL_LIMITS, SkillLibrary, UnitTransactions, authorityFrom, contentHash, createSubagentChannel, errorText, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, isProcessAlive, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, readTreeLock, readUnitEvidence, reportsSweepLockTarget, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runsFile, scanContentThreats, skillLibraryUnitBackend, supportRefs, sweepReports, transactIo, validateFrontmatter, validateSupportPath } from "@lmzhen/dsh-evolution-core";
|
|
4
|
+
import { DEFAULT_SKILL_LIMITS, SkillLibrary, UnitTransactions, authorityFrom, contentHash, createSubagentChannel, errorText, evolutionHome, evolutionIoAdapter, executeUnitUndo, isPresent, isProcessAlive, newRunRegistry, newSkillLibrary, parseFrontmatter, planUnitUndoAcross, policyStageLimits, probeAbsent, probeList, probePresent, probeText, probeUnknown, readTreeLock, readUnitEvidence, readUnitJournal, reportsSweepLockTarget, revisionAnchorOf, revisionFingerprint, revisionSupportDigest, runsFile, scanContentThreats, skillLibraryUnitBackend, supportRefs, sweepReports, 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, randomUUID } from "node:crypto";
|
|
7
7
|
import { chmod, mkdir, open, readFile, stat, unlink } from "node:fs/promises";
|
|
@@ -70,11 +70,12 @@ function createEngineScreening(deps) {
|
|
|
70
70
|
return async (cards, runId, signal) => {
|
|
71
71
|
const corpusDir = await snapshotCards(deps.io, deps.home, runId, cards);
|
|
72
72
|
const outDir = join(deps.home, "merge", "runs", runId, "engine");
|
|
73
|
+
const runner = deps.runner === void 0 ? void 0 : (prompt, cfg) => deps.runner(prompt, cfg, signal);
|
|
73
74
|
const result = await deps.engine.screen({
|
|
74
75
|
corpusDir,
|
|
75
76
|
outDir,
|
|
76
77
|
...deps.cfg === void 0 ? {} : { cfg: deps.cfg },
|
|
77
|
-
...
|
|
78
|
+
...runner === void 0 ? {} : { runner },
|
|
78
79
|
signal
|
|
79
80
|
});
|
|
80
81
|
if (!result.ok) throw new Error("engine screen failed: " + result.error.code + (result.error.stage === void 0 ? "" : " @" + result.error.stage) + (result.error.detail === void 0 ? "" : ": " + result.error.detail));
|
|
@@ -110,8 +111,11 @@ function append(chunk, text) {
|
|
|
110
111
|
return text;
|
|
111
112
|
}
|
|
112
113
|
function createEngineRunner(llm, route) {
|
|
113
|
-
return async (prompt) => {
|
|
114
|
+
return async (prompt, cfg, runSignal) => {
|
|
114
115
|
const selection = route();
|
|
116
|
+
const timeout = Number(cfg?.timeoutMs);
|
|
117
|
+
const timeoutSignal = Number.isFinite(timeout) && timeout > 0 ? AbortSignal.timeout(timeout) : void 0;
|
|
118
|
+
const signal = runSignal === void 0 ? timeoutSignal : timeoutSignal === void 0 ? runSignal : typeof AbortSignal.any === "function" ? AbortSignal.any([runSignal, timeoutSignal]) : runSignal;
|
|
115
119
|
let text = "";
|
|
116
120
|
if (llm.stream !== void 0) {
|
|
117
121
|
for await (const chunk of llm.stream({
|
|
@@ -124,7 +128,8 @@ function createEngineRunner(llm, route) {
|
|
|
124
128
|
text: prompt
|
|
125
129
|
}]
|
|
126
130
|
}],
|
|
127
|
-
temperature: 0
|
|
131
|
+
temperature: 0,
|
|
132
|
+
...signal === void 0 ? {} : { signal }
|
|
128
133
|
})) text = append(chunk, text);
|
|
129
134
|
return text;
|
|
130
135
|
}
|
|
@@ -133,7 +138,7 @@ function createEngineRunner(llm, route) {
|
|
|
133
138
|
provider: normalizeProviderKey(selection.provider),
|
|
134
139
|
model: selection.model,
|
|
135
140
|
temperature: 0
|
|
136
|
-
});
|
|
141
|
+
}, signal);
|
|
137
142
|
for await (const chunk of call.stream({
|
|
138
143
|
...call.config,
|
|
139
144
|
messages: [{
|
|
@@ -142,7 +147,8 @@ function createEngineRunner(llm, route) {
|
|
|
142
147
|
type: "text",
|
|
143
148
|
text: prompt
|
|
144
149
|
}]
|
|
145
|
-
}]
|
|
150
|
+
}],
|
|
151
|
+
...signal === void 0 ? {} : { signal }
|
|
146
152
|
})) text = append(chunk, text);
|
|
147
153
|
return text;
|
|
148
154
|
};
|
|
@@ -633,6 +639,11 @@ function transitionV4(row, event, expectedVersion, at = (/* @__PURE__ */ new Dat
|
|
|
633
639
|
};
|
|
634
640
|
return illegal("pending/" + event.type);
|
|
635
641
|
case "approved":
|
|
642
|
+
if (event.type === "run-attached") return {
|
|
643
|
+
ok: true,
|
|
644
|
+
row: advance(row, at, { runId: event.runId }),
|
|
645
|
+
version: expectedVersion + 1
|
|
646
|
+
};
|
|
636
647
|
if (event.type === "staged") return {
|
|
637
648
|
ok: true,
|
|
638
649
|
row: advance(row, at, {
|
|
@@ -672,6 +683,11 @@ function transitionV4(row, event, expectedVersion, at = (/* @__PURE__ */ new Dat
|
|
|
672
683
|
};
|
|
673
684
|
return illegal("approved/" + event.type);
|
|
674
685
|
case "fuse":
|
|
686
|
+
if (event.type === "run-attached") return {
|
|
687
|
+
ok: true,
|
|
688
|
+
row: advance(row, at, { runId: event.runId }),
|
|
689
|
+
version: expectedVersion + 1
|
|
690
|
+
};
|
|
675
691
|
if (event.type === "committed") return {
|
|
676
692
|
ok: true,
|
|
677
693
|
row: null,
|
|
@@ -2334,7 +2350,7 @@ async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
|
2334
2350
|
const refs = /* @__PURE__ */ new Set();
|
|
2335
2351
|
const indexRoot = join(home, "merge", "staging-index");
|
|
2336
2352
|
const names = await io.exists(indexRoot) ? await listOrEmpty(io, indexRoot) : [];
|
|
2337
|
-
for (const name of names
|
|
2353
|
+
for (const name of names) {
|
|
2338
2354
|
if (!name.endsWith(".json")) continue;
|
|
2339
2355
|
const raw = await readTextOrNull(io, join(indexRoot, name));
|
|
2340
2356
|
if (raw === null) continue;
|
|
@@ -3769,7 +3785,7 @@ function parseCursor(cursor, snapshot) {
|
|
|
3769
3785
|
}
|
|
3770
3786
|
}
|
|
3771
3787
|
function buildQueuePage(snapshot, queue, expiredOnly, cursor, limit = 50) {
|
|
3772
|
-
const selected = snapshot.rows.filter((row) => row.queue === queue && (
|
|
3788
|
+
const selected = snapshot.rows.filter((row) => row.queue === queue && (expiredOnly ? row.expired : !row.expired));
|
|
3773
3789
|
const offset = cursor === void 0 ? 0 : parseCursor(cursor, snapshot);
|
|
3774
3790
|
const page = selected.slice(offset, offset + Math.max(0, Math.min(200, limit)));
|
|
3775
3791
|
const next = offset + page.length < selected.length ? encodeCursor(snapshot, offset + page.length) : void 0;
|
|
@@ -4168,9 +4184,6 @@ async function mergeDoctor(io, home, configuredRoot) {
|
|
|
4168
4184
|
for (const report of JSON.parse(raw)) if (report.ok === true && report.runId !== void 0) recoveryRuns.add(report.runId);
|
|
4169
4185
|
}
|
|
4170
4186
|
} catch {}
|
|
4171
|
-
const receiptRuns = /* @__PURE__ */ new Set();
|
|
4172
|
-
const receiptDir = await probeList(io, join(home, "merge", "receipts"));
|
|
4173
|
-
if (receiptDir.kind === "present") for (const name of receiptDir.value) receiptRuns.add(name);
|
|
4174
4187
|
const invalid = [];
|
|
4175
4188
|
if (snapshot.blocked !== void 0) invalid.push({
|
|
4176
4189
|
id: "state",
|
|
@@ -4182,13 +4195,14 @@ async function mergeDoctor(io, home, configuredRoot) {
|
|
|
4182
4195
|
const artifactRow = row.state === "approved" ? "approved" : row.state === "fuse" ? "fuse" : row.state;
|
|
4183
4196
|
const run = runId === void 0 ? "none" : row.state === "pending" ? "awaiting-approval" : "running";
|
|
4184
4197
|
const staging = item.stage === null ? row.state === "fuse" ? "missing" : "none" : item.stage.state === "present" || item.stage.state === "reserved" ? "kept" : row.state === "fuse" ? "missing" : "none";
|
|
4185
|
-
const
|
|
4198
|
+
const receipt = runId === void 0 ? null : await readCommitReceipt(io, home, runId);
|
|
4199
|
+
const phase = receipt !== null ? await readUnitJournal(io, home, receipt.unitId) : runId === void 0 ? null : await readJournal(io, home, runId);
|
|
4186
4200
|
const result = validateArtifacts({
|
|
4187
4201
|
row: artifactRow,
|
|
4188
4202
|
run,
|
|
4189
4203
|
staging,
|
|
4190
|
-
journal: phase === null ? "none" : phase.phase === "prepared" ? "prepared" : phase.phase === "target-written" || phase.phase === "sources-archived" ? "committing" : phase.phase === "committed" ? "committed" : phase.phase === "rolled-back" ? "rolled-back" : "failed",
|
|
4191
|
-
evidence:
|
|
4204
|
+
journal: phase === null ? "none" : phase.phase === "prepared" || phase.phase === "target-pending" ? "prepared" : phase.phase === "target-written" || phase.phase === "sources-archived" || phase.phase === "support-pending" || phase.phase === "source-archiving" ? "committing" : phase.phase === "committed" ? "committed" : phase.phase === "rolled-back" ? "rolled-back" : "failed",
|
|
4205
|
+
evidence: receipt === null ? "none" : "receipt",
|
|
4192
4206
|
activeRuns: run === "running" || run === "awaiting-approval" ? 1 : 0,
|
|
4193
4207
|
...runId !== void 0 && recoveryRuns.has(runId) ? { recovery: true } : {}
|
|
4194
4208
|
});
|
|
@@ -4628,6 +4642,18 @@ async function recoverCommitUnits(options) {
|
|
|
4628
4642
|
journal = null;
|
|
4629
4643
|
}
|
|
4630
4644
|
if (journal === null || typeof journal.unitId !== "string") continue;
|
|
4645
|
+
if (journal.kind === "tree-change" || journal.kind === void 0 && journal.runId === journal.unitId) {
|
|
4646
|
+
reports.push({
|
|
4647
|
+
unitId: journal.unitId,
|
|
4648
|
+
runId: journal.runId,
|
|
4649
|
+
phase: journal.phase,
|
|
4650
|
+
branch: branchOf(journal.phase),
|
|
4651
|
+
action: "none",
|
|
4652
|
+
ok: true,
|
|
4653
|
+
code: "skill-unit-skipped"
|
|
4654
|
+
});
|
|
4655
|
+
continue;
|
|
4656
|
+
}
|
|
4631
4657
|
if (options.isActive?.(journal.runId) === true) {
|
|
4632
4658
|
reports.push({
|
|
4633
4659
|
unitId: journal.unitId,
|
|
@@ -6213,6 +6239,24 @@ async function withMergeRun(options) {
|
|
|
6213
6239
|
createdAt: new Date(startedAt).toISOString(),
|
|
6214
6240
|
updatedAt: new Date(options.now()).toISOString()
|
|
6215
6241
|
});
|
|
6242
|
+
const writeFailureMeta = async (stopReason) => {
|
|
6243
|
+
try {
|
|
6244
|
+
await writeRunMeta(options.io, options.home, {
|
|
6245
|
+
version: 1,
|
|
6246
|
+
runId: handle.id,
|
|
6247
|
+
pid: process.pid,
|
|
6248
|
+
...options.parentRunId === void 0 ? {} : { parentRunId: options.parentRunId },
|
|
6249
|
+
...options.pairKey === void 0 ? {} : { pairKey: options.pairKey },
|
|
6250
|
+
...options.stagingRef === void 0 ? {} : { stagingRef: options.stagingRef },
|
|
6251
|
+
pairCount: options.pairCount ?? (options.pairKey === void 0 ? 0 : 1),
|
|
6252
|
+
phase: "settled",
|
|
6253
|
+
approval: "approved",
|
|
6254
|
+
stopReason,
|
|
6255
|
+
createdAt: new Date(startedAt).toISOString(),
|
|
6256
|
+
updatedAt: new Date(options.now()).toISOString()
|
|
6257
|
+
});
|
|
6258
|
+
} catch {}
|
|
6259
|
+
};
|
|
6216
6260
|
const finish = async () => {
|
|
6217
6261
|
let draft;
|
|
6218
6262
|
const runController = new AbortController();
|
|
@@ -6304,6 +6348,7 @@ async function withMergeRun(options) {
|
|
|
6304
6348
|
state: "failed",
|
|
6305
6349
|
failure: "report write failed: " + (error instanceof Error ? error.message : String(error))
|
|
6306
6350
|
});
|
|
6351
|
+
await writeFailureMeta("failed");
|
|
6307
6352
|
return {
|
|
6308
6353
|
ok: false,
|
|
6309
6354
|
runId: handle.id,
|
|
@@ -6318,6 +6363,7 @@ async function withMergeRun(options) {
|
|
|
6318
6363
|
resultRef: reportPath,
|
|
6319
6364
|
failure: "post-report finalize failed: " + (error instanceof Error ? error.message : String(error))
|
|
6320
6365
|
});
|
|
6366
|
+
await writeFailureMeta("failed");
|
|
6321
6367
|
return {
|
|
6322
6368
|
ok: false,
|
|
6323
6369
|
runId: handle.id,
|
|
@@ -6467,6 +6513,19 @@ async function rejectPending(io, home, now, id) {
|
|
|
6467
6513
|
return await act(io, home, now, id, "reject");
|
|
6468
6514
|
}
|
|
6469
6515
|
//#endregion
|
|
6516
|
+
//#region lib/types/domain/row-run.js
|
|
6517
|
+
/** Attach the in-flight child run to a row before its round2 starts (P1-8). */
|
|
6518
|
+
async function attachRunToRow(io, home, rowId, runId) {
|
|
6519
|
+
if (!await pairLedger(io, home).transact((tx) => {
|
|
6520
|
+
const current = tx.rows().find((row) => row.id === rowId);
|
|
6521
|
+
if (current === void 0) return false;
|
|
6522
|
+
return tx.transition(rowId, {
|
|
6523
|
+
type: "run-attached",
|
|
6524
|
+
runId
|
|
6525
|
+
}, current.version).ok;
|
|
6526
|
+
})) throw new Error("row is not attachable to run: " + rowId);
|
|
6527
|
+
}
|
|
6528
|
+
//#endregion
|
|
6470
6529
|
//#region lib/types/domain/failure-route.js
|
|
6471
6530
|
function casExpiredHook(code) {
|
|
6472
6531
|
if (code === "target-changed" || code === "target-taken") return {
|
|
@@ -6749,6 +6808,7 @@ async function runApproveCommand(options) {
|
|
|
6749
6808
|
now: options.now,
|
|
6750
6809
|
...options.detached === true ? { detached: true } : {},
|
|
6751
6810
|
async body(ctx) {
|
|
6811
|
+
await attachRunToRow(options.io, options.home, row.id, ctx.runId);
|
|
6752
6812
|
const stage = (name) => ctx.stage(name, {
|
|
6753
6813
|
done: 0,
|
|
6754
6814
|
total: 1
|
|
@@ -6882,6 +6942,7 @@ async function runResumeCommand(options) {
|
|
|
6882
6942
|
now: options.now,
|
|
6883
6943
|
...options.detached === true ? { detached: true } : {},
|
|
6884
6944
|
async body(ctx) {
|
|
6945
|
+
await attachRunToRow(options.io, options.home, row.id, ctx.runId);
|
|
6885
6946
|
await ctx.stage("author", {
|
|
6886
6947
|
done: 0,
|
|
6887
6948
|
total: 1
|
|
@@ -7927,12 +7988,6 @@ async function assembleStagedCommit(io, home, runId, port, options) {
|
|
|
7927
7988
|
});
|
|
7928
7989
|
}
|
|
7929
7990
|
//#endregion
|
|
7930
|
-
//#region lib/types/commit-snapshot.js
|
|
7931
|
-
/** Pre-commit snapshot: enough to restore the target and archived sources after a crash. */
|
|
7932
|
-
function snapshotPath(home, runId) {
|
|
7933
|
-
return join(home, "merge", "snapshots", runId.replace(/[^A-Za-z0-9_-]+/g, "_") + ".json");
|
|
7934
|
-
}
|
|
7935
|
-
//#endregion
|
|
7936
7991
|
//#region lib/types/commit-execute.js
|
|
7937
7992
|
async function commitStagedRun(io, home, runId, port, options) {
|
|
7938
7993
|
const assembled = await assembleStagedCommit(io, home, runId, port, {
|
|
@@ -7973,7 +8028,7 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
7973
8028
|
{
|
|
7974
8029
|
const first = assembled.plan.sources[0] ?? assembled.plan.target;
|
|
7975
8030
|
const unitPlan = {
|
|
7976
|
-
runId,
|
|
8031
|
+
runId: options.parentRunId ?? runId,
|
|
7977
8032
|
pairKey: pairKey(assembled.plan.target, first),
|
|
7978
8033
|
target: assembled.plan.target,
|
|
7979
8034
|
targetExists: assembled.plan.targetExists,
|
|
@@ -8079,9 +8134,6 @@ async function commitStagedRun(io, home, runId, port, options) {
|
|
|
8079
8134
|
};
|
|
8080
8135
|
try {
|
|
8081
8136
|
await writeCommitReceipt(io, home, receipt);
|
|
8082
|
-
try {
|
|
8083
|
-
await io.remove(snapshotPath(home, runId));
|
|
8084
|
-
} catch {}
|
|
8085
8137
|
return {
|
|
8086
8138
|
ok: true,
|
|
8087
8139
|
plan: assembled.plan,
|
|
@@ -8218,7 +8270,9 @@ function buildValidationPlan(input) {
|
|
|
8218
8270
|
const manifest = new Set(manifestFiles);
|
|
8219
8271
|
const forwardRefs = [...refs].filter((ref) => !staged.has(ref)).sort();
|
|
8220
8272
|
const reverseRefs = files.map((file) => file.path).filter((path) => path !== "SKILL.md" && !refs.has(path) && manifest.has(path)).sort();
|
|
8221
|
-
const
|
|
8273
|
+
const existing = new Set(input.existingSupport ?? []);
|
|
8274
|
+
const orphanFiles = [...existing].filter((path) => !staged.has(path)).sort();
|
|
8275
|
+
const undeclaredFiles = [...staged].filter((path) => path !== "SKILL.md" && !manifest.has(path) && !existing.has(path)).sort();
|
|
8222
8276
|
const byPath = /* @__PURE__ */ new Map();
|
|
8223
8277
|
const supportSources = input.supportSources ?? [];
|
|
8224
8278
|
for (const file of supportSources) {
|
|
@@ -8249,7 +8303,8 @@ function buildValidationPlan(input) {
|
|
|
8249
8303
|
q09: {
|
|
8250
8304
|
forwardRefs,
|
|
8251
8305
|
reverseRefs,
|
|
8252
|
-
orphanFiles
|
|
8306
|
+
orphanFiles,
|
|
8307
|
+
undeclaredFiles
|
|
8253
8308
|
},
|
|
8254
8309
|
supportConflicts,
|
|
8255
8310
|
threat,
|
|
@@ -8276,6 +8331,12 @@ function buildValidationPlan(input) {
|
|
|
8276
8331
|
severity: "block",
|
|
8277
8332
|
message: "target tree support is not declared by plan"
|
|
8278
8333
|
});
|
|
8334
|
+
if (undeclaredFiles.length > 0) findings.push({
|
|
8335
|
+
rule: "q09-undeclared",
|
|
8336
|
+
path: undeclaredFiles.join(","),
|
|
8337
|
+
severity: "block",
|
|
8338
|
+
message: "staged support file is not declared by the authoring manifest"
|
|
8339
|
+
});
|
|
8279
8340
|
if (supportConflicts.length > 0) findings.push({
|
|
8280
8341
|
rule: "support-conflict",
|
|
8281
8342
|
path: supportConflicts.map((item) => item.path).join(","),
|
|
@@ -8317,7 +8378,8 @@ function buildValidationPlan(input) {
|
|
|
8317
8378
|
q09: {
|
|
8318
8379
|
forwardRefs,
|
|
8319
8380
|
reverseRefs,
|
|
8320
|
-
orphanFiles
|
|
8381
|
+
orphanFiles,
|
|
8382
|
+
undeclaredFiles
|
|
8321
8383
|
},
|
|
8322
8384
|
supportConflicts,
|
|
8323
8385
|
threat,
|
|
@@ -8332,28 +8394,80 @@ function buildValidationPlan(input) {
|
|
|
8332
8394
|
}
|
|
8333
8395
|
//#endregion
|
|
8334
8396
|
//#region lib/types/domain/authoring-manifest.js
|
|
8397
|
+
function asManifest(value) {
|
|
8398
|
+
if (value === null || typeof value !== "object") return null;
|
|
8399
|
+
const record = value;
|
|
8400
|
+
if (!Array.isArray(record.files) || record.files.some((file) => typeof file !== "string")) return null;
|
|
8401
|
+
if (record.target !== void 0 && typeof record.target !== "string") return null;
|
|
8402
|
+
return {
|
|
8403
|
+
...typeof record.target === "string" ? { target: record.target } : {},
|
|
8404
|
+
files: record.files
|
|
8405
|
+
};
|
|
8406
|
+
}
|
|
8407
|
+
/** Every balanced object literal in a prose/fenced answer (string-aware). */
|
|
8408
|
+
function balancedObjects(text) {
|
|
8409
|
+
const found = [];
|
|
8410
|
+
let depth = 0;
|
|
8411
|
+
let start = -1;
|
|
8412
|
+
let quoted = false;
|
|
8413
|
+
let escaped = false;
|
|
8414
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
8415
|
+
const char = text[index];
|
|
8416
|
+
if (quoted) {
|
|
8417
|
+
if (escaped) escaped = false;
|
|
8418
|
+
else if (char === "\\") escaped = true;
|
|
8419
|
+
else if (char === "\"") quoted = false;
|
|
8420
|
+
continue;
|
|
8421
|
+
}
|
|
8422
|
+
if (char === "\"") {
|
|
8423
|
+
quoted = true;
|
|
8424
|
+
continue;
|
|
8425
|
+
}
|
|
8426
|
+
if (char === "{") {
|
|
8427
|
+
if (depth === 0) start = index;
|
|
8428
|
+
depth += 1;
|
|
8429
|
+
continue;
|
|
8430
|
+
}
|
|
8431
|
+
if (char !== "}" || depth === 0) continue;
|
|
8432
|
+
depth -= 1;
|
|
8433
|
+
if (depth === 0 && start >= 0) {
|
|
8434
|
+
found.push(text.slice(start, index + 1));
|
|
8435
|
+
start = -1;
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
8438
|
+
return found;
|
|
8439
|
+
}
|
|
8335
8440
|
/**
|
|
8336
|
-
* Parse
|
|
8337
|
-
*
|
|
8338
|
-
*
|
|
8441
|
+
* Parse the authoring manifest from a child's final turn.
|
|
8442
|
+
*
|
|
8443
|
+
* P2-20: a child that wraps the envelope in a fenced code block or in one prose
|
|
8444
|
+
* sentence is the normal shape, not a malformed answer. A strict whole-text
|
|
8445
|
+
* JSON.parse returned null there, and null used to mean "every staged file was
|
|
8446
|
+
* declared" - the exact files the check exists to catch. Try the whole answer,
|
|
8447
|
+
* then every fenced body, then every balanced object literal.
|
|
8448
|
+
* @param text - the child's answer.
|
|
8449
|
+
* @returns the first valid envelope, or null when the turn really declared none.
|
|
8339
8450
|
*/
|
|
8340
8451
|
function parseAuthoringManifest(text) {
|
|
8452
|
+
if (typeof text !== "string") return null;
|
|
8341
8453
|
try {
|
|
8342
|
-
const
|
|
8343
|
-
if (
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8454
|
+
const direct = asManifest(JSON.parse(text));
|
|
8455
|
+
if (direct !== null) return direct;
|
|
8456
|
+
} catch {}
|
|
8457
|
+
for (const match of text.matchAll(/\x60\x60\x60(?:json)?\s*([\s\S]*?)\x60\x60\x60/g)) try {
|
|
8458
|
+
const parsed = asManifest(JSON.parse(match[1] ?? ""));
|
|
8459
|
+
if (parsed !== null) return parsed;
|
|
8460
|
+
} catch {}
|
|
8461
|
+
for (const candidate of balancedObjects(text)) try {
|
|
8462
|
+
const parsed = asManifest(JSON.parse(candidate));
|
|
8463
|
+
if (parsed !== null) return parsed;
|
|
8464
|
+
} catch {}
|
|
8465
|
+
return null;
|
|
8352
8466
|
}
|
|
8353
|
-
/** Files a manifest promises for a staged tree
|
|
8467
|
+
/** Files a manifest promises for a staged tree. */
|
|
8354
8468
|
function manifestFilesOf(manifest, stagedPaths) {
|
|
8355
8469
|
if (manifest !== null) return manifest.files;
|
|
8356
|
-
return ["SKILL.md"
|
|
8470
|
+
return ["SKILL.md"];
|
|
8357
8471
|
}
|
|
8358
8472
|
/**
|
|
8359
8473
|
* The first file the manifest promises but the staged tree does not contain.
|
|
@@ -8492,6 +8606,7 @@ async function runAuthoring(options) {
|
|
|
8492
8606
|
lengths
|
|
8493
8607
|
};
|
|
8494
8608
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
8609
|
+
parentRunId: options.runId,
|
|
8495
8610
|
origin: "foreground",
|
|
8496
8611
|
summary: options.summary,
|
|
8497
8612
|
now: options.now,
|
|
@@ -23,7 +23,7 @@ export interface EngineScreenPort {
|
|
|
23
23
|
done?: number;
|
|
24
24
|
total?: number;
|
|
25
25
|
}) => void;
|
|
26
|
-
readonly runner?: (prompt: string, cfg?: Record<string, unknown
|
|
26
|
+
readonly runner?: (prompt: string, cfg?: Record<string, unknown>, signal?: AbortSignal | undefined) => Promise<string>;
|
|
27
27
|
}): Promise<EngineScreenResult>;
|
|
28
28
|
}
|
|
29
29
|
export interface CandidateSelection {
|
|
@@ -46,7 +46,7 @@ export interface EngineScreeningDeps {
|
|
|
46
46
|
readonly engine: EngineScreenPort;
|
|
47
47
|
readonly select: (report: Record<string, unknown>) => CandidateSelection;
|
|
48
48
|
readonly cfg?: Record<string, unknown>;
|
|
49
|
-
readonly runner?: (prompt: string, cfg?: Record<string, unknown
|
|
49
|
+
readonly runner?: (prompt: string, cfg?: Record<string, unknown>, signal?: AbortSignal | undefined) => Promise<string>;
|
|
50
50
|
readonly onSelection?: (selection: CandidateSelection, runId: string, cards: readonly SkillCard[]) => Promise<void>;
|
|
51
51
|
}
|
|
52
52
|
/** Write the exact card bodies as a support-free corpus snapshot and return its root. */
|
|
@@ -26,11 +26,12 @@ export function createEngineScreening(deps) {
|
|
|
26
26
|
return async (cards, runId, signal) => {
|
|
27
27
|
const corpusDir = await snapshotCards(deps.io, deps.home, runId, cards);
|
|
28
28
|
const outDir = join(deps.home, 'merge', 'runs', runId, 'engine');
|
|
29
|
+
const runner = deps.runner === undefined ? undefined : (prompt, cfg) => deps.runner(prompt, cfg, signal);
|
|
29
30
|
const result = await deps.engine.screen({
|
|
30
31
|
corpusDir,
|
|
31
32
|
outDir,
|
|
32
33
|
...(deps.cfg === undefined ? {} : { cfg: deps.cfg }),
|
|
33
|
-
...(
|
|
34
|
+
...(runner === undefined ? {} : { runner }),
|
|
34
35
|
signal,
|
|
35
36
|
});
|
|
36
37
|
if (!result.ok) {
|
|
@@ -19,5 +19,5 @@ export interface LlmServiceLike {
|
|
|
19
19
|
stream(input: Record<string, unknown>): AsyncIterable<LlmChunkLike>;
|
|
20
20
|
}>;
|
|
21
21
|
}
|
|
22
|
-
export declare function createEngineRunner(llm: LlmServiceLike, route: () => ModelRoute): (prompt: string) => Promise<string>;
|
|
22
|
+
export declare function createEngineRunner(llm: LlmServiceLike, route: () => ModelRoute): (prompt: string, cfg?: Record<string, unknown>, signal?: AbortSignal | undefined) => Promise<string>;
|
|
23
23
|
//# sourceMappingURL=model-runner.d.ts.map
|
|
@@ -13,8 +13,11 @@ function append(chunk, text) {
|
|
|
13
13
|
return text;
|
|
14
14
|
}
|
|
15
15
|
export function createEngineRunner(llm, route) {
|
|
16
|
-
return async (prompt) => {
|
|
16
|
+
return async (prompt, cfg, runSignal) => {
|
|
17
17
|
const selection = route();
|
|
18
|
+
const timeout = Number(cfg?.timeoutMs);
|
|
19
|
+
const timeoutSignal = Number.isFinite(timeout) && timeout > 0 ? AbortSignal.timeout(timeout) : undefined;
|
|
20
|
+
const signal = runSignal === undefined ? timeoutSignal : timeoutSignal === undefined ? runSignal : (typeof AbortSignal.any === 'function' ? AbortSignal.any([runSignal, timeoutSignal]) : runSignal);
|
|
18
21
|
let text = '';
|
|
19
22
|
if (llm.stream !== undefined) {
|
|
20
23
|
for await (const chunk of llm.stream({
|
|
@@ -22,16 +25,18 @@ export function createEngineRunner(llm, route) {
|
|
|
22
25
|
model: selection.model,
|
|
23
26
|
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
|
24
27
|
temperature: 0,
|
|
28
|
+
...(signal === undefined ? {} : { signal }),
|
|
25
29
|
}))
|
|
26
30
|
text = append(chunk, text);
|
|
27
31
|
return text;
|
|
28
32
|
}
|
|
29
33
|
if (llm.prepareCall === undefined)
|
|
30
34
|
throw new Error('llm service has neither stream nor prepareCall');
|
|
31
|
-
const call = await llm.prepareCall({ provider: normalizeProviderKey(selection.provider), model: selection.model, temperature: 0 });
|
|
35
|
+
const call = await llm.prepareCall({ provider: normalizeProviderKey(selection.provider), model: selection.model, temperature: 0 }, signal);
|
|
32
36
|
for await (const chunk of call.stream({
|
|
33
37
|
...call.config,
|
|
34
38
|
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
|
39
|
+
...(signal === undefined ? {} : { signal }),
|
|
35
40
|
}))
|
|
36
41
|
text = append(chunk, text);
|
|
37
42
|
return text;
|
|
@@ -9,6 +9,7 @@ import { presentStage } from "./domain/stage-aggregate.js";
|
|
|
9
9
|
import { v4ToLegacy } from "./domain/row-v4.js";
|
|
10
10
|
import { loadMergeState } from "./state.js";
|
|
11
11
|
import { withMergeRun } from "./run-shell.js";
|
|
12
|
+
import { attachRunToRow } from "./domain/row-run.js";
|
|
12
13
|
import { recordRound2Outcome } from "./round2-outcome.js";
|
|
13
14
|
import { failure } from "./domain/errors.js";
|
|
14
15
|
export async function runApproveCommand(options) {
|
|
@@ -58,6 +59,7 @@ export async function runApproveCommand(options) {
|
|
|
58
59
|
now: options.now,
|
|
59
60
|
...(options.detached === true ? { detached: true } : {}),
|
|
60
61
|
async body(ctx) {
|
|
62
|
+
await attachRunToRow(options.io, options.home, row.id, ctx.runId);
|
|
61
63
|
const stage = (name) => ctx.stage(name, { done: 0, total: 1 });
|
|
62
64
|
await stage('author');
|
|
63
65
|
let outcome;
|
|
@@ -71,6 +71,10 @@ export async function runAuthoring(options) {
|
|
|
71
71
|
if (hasBlockingFinding(findings))
|
|
72
72
|
return { ok: false, code: findings.some(finding => finding.rule === 'limit') ? 'merge-over-limit' : 'merge-validation', message: 'staged tree failed the rule gate', findings, lengths };
|
|
73
73
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
74
|
+
// P1-6: the staged key is `run-1/pairs/abc`; the ROW's run id is the parent.
|
|
75
|
+
// Without it the scan path writes its unit journal/receipt under the staged
|
|
76
|
+
// address, so crash recovery looks for a run that was never registered.
|
|
77
|
+
parentRunId: options.runId,
|
|
74
78
|
origin: 'foreground',
|
|
75
79
|
summary: options.summary,
|
|
76
80
|
now: options.now,
|
|
@@ -3,7 +3,6 @@ import { writeCommitReceipt } from "./commit-receipt.js";
|
|
|
3
3
|
import { pairKey } from "./contracts.js";
|
|
4
4
|
import { writeJournal } from "./domain/commit-journal-store.js";
|
|
5
5
|
import { evidenceSha256 as digestEvidence } from "./domain/evidence-hash.js";
|
|
6
|
-
import { snapshotPath } from "./commit-snapshot.js";
|
|
7
6
|
export async function commitStagedRun(io, home, runId, port, options) {
|
|
8
7
|
const assembled = await assembleStagedCommit(io, home, runId, port, {
|
|
9
8
|
origin: options.origin,
|
|
@@ -27,7 +26,9 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
27
26
|
return { ok: false, message: 'commit port missing: UnitTransaction or commitMerge required', plan: assembled.plan };
|
|
28
27
|
{
|
|
29
28
|
const first = assembled.plan.sources[0] ?? assembled.plan.target;
|
|
30
|
-
|
|
29
|
+
// P1-6: the unit's run identity is the canonical parent run, not the stage key.
|
|
30
|
+
const unitRunId = options.parentRunId ?? runId;
|
|
31
|
+
const unitPlan = { runId: unitRunId, pairKey: pairKey(assembled.plan.target, first), target: assembled.plan.target, targetExists: assembled.plan.targetExists, sources: assembled.plan.sources, body: assembled.plan.body, supportFiles: assembled.plan.supportFiles, expectedTargetBody: assembled.plan.expectedTargetBody, expectedSourceBodies: assembled.plan.expectedSourceBodies, origin: options.origin, summary: options.summary, ...(options.limits === undefined ? {} : { skillLimits: options.limits }), ...(options.validationPlan === undefined ? {} : { validationPlan: options.validationPlan }), ...(options.targetRevision === undefined ? {} : { targetRevision: options.targetRevision }), ...(options.sourceRevisions === undefined ? {} : { sourceRevisions: options.sourceRevisions }), ...(options.supportDigest === undefined ? {} : { supportDigest: options.supportDigest }) };
|
|
31
32
|
try {
|
|
32
33
|
await writeJournal(io, home, journal);
|
|
33
34
|
}
|
|
@@ -87,10 +88,6 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
87
88
|
};
|
|
88
89
|
try {
|
|
89
90
|
await writeCommitReceipt(io, home, receipt);
|
|
90
|
-
try {
|
|
91
|
-
await io.remove(snapshotPath(home, runId));
|
|
92
|
-
}
|
|
93
|
-
catch { /* committed journal is the record */ }
|
|
94
91
|
return { ok: true, plan: assembled.plan, result, receipt, ...(warning === undefined ? {} : { warning }) };
|
|
95
92
|
}
|
|
96
93
|
catch (error) {
|
|
@@ -8,7 +8,6 @@ export interface CommitRecoveryReport {
|
|
|
8
8
|
readonly target?: string;
|
|
9
9
|
readonly sources?: readonly string[];
|
|
10
10
|
readonly journalPath?: string;
|
|
11
|
-
readonly snapshotPath?: string;
|
|
12
11
|
readonly unitId?: string;
|
|
13
12
|
}
|
|
14
13
|
export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number, isActive?: (runId: string) => boolean): Promise<readonly CommitRecoveryReport[]>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** Startup replay for non-terminal commit journals: report only, never write skills. */
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { writeJournal } from "./domain/commit-journal-store.js";
|
|
4
|
-
import { snapshotPath } from "./commit-snapshot.js";
|
|
5
4
|
import { readCommitReceipt } from "./commit-receipt.js";
|
|
6
5
|
import { readUnitEvidence } from '@lmzhen/dsh-evolution-core';
|
|
7
6
|
export async function recoverCommitJournals(io, home, now, isActive) {
|
|
@@ -52,7 +51,7 @@ export async function recoverCommitJournals(io, home, now, isActive) {
|
|
|
52
51
|
continue;
|
|
53
52
|
reports.push({
|
|
54
53
|
runId: journal.runId, phase: journal.phase, action: 'manual-required', ok: false,
|
|
55
|
-
target: journal.target, sources: journal.sources, journalPath: path,
|
|
54
|
+
target: journal.target, sources: journal.sources, journalPath: path,
|
|
56
55
|
});
|
|
57
56
|
}
|
|
58
57
|
catch (error) {
|
package/lib/types/doctor.js
CHANGED
|
@@ -7,11 +7,12 @@ import { readTextOrNull } from "./domain/read-probe.js";
|
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import { stat } from 'node:fs/promises';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { probeList, readTreeLock, runsFile } from '@lmzhen/dsh-evolution-core';
|
|
10
|
+
import { probeList, readTreeLock, readUnitJournal, runsFile } from '@lmzhen/dsh-evolution-core';
|
|
11
11
|
import { migrationJournalPath, migrationReportPath, v2BackupPath } from "./domain/state-bootstrap.js";
|
|
12
12
|
import { readMergeSnapshot } from "./domain/merge-snapshot.js";
|
|
13
13
|
import { validateArtifacts } from "./domain/consistency.js";
|
|
14
14
|
import { readJournal } from "./domain/commit-journal-store.js";
|
|
15
|
+
import { readCommitReceipt } from "./commit-receipt.js";
|
|
15
16
|
import { stagingRoot } from "./staging.js";
|
|
16
17
|
import { readReconcileReport } from "./domain/reconcile.js";
|
|
17
18
|
export const MERGE_DOCTOR_SCHEMA_VERSION = 3;
|
|
@@ -72,11 +73,6 @@ export async function mergeDoctor(io, home, configuredRoot) {
|
|
|
72
73
|
recoveryRuns.add(report.runId);
|
|
73
74
|
}
|
|
74
75
|
catch { }
|
|
75
|
-
const receiptRuns = new Set();
|
|
76
|
-
const receiptDir = await probeList(io, join(home, 'merge', 'receipts'));
|
|
77
|
-
if (receiptDir.kind === 'present')
|
|
78
|
-
for (const name of receiptDir.value)
|
|
79
|
-
receiptRuns.add(name);
|
|
80
76
|
const invalid = [];
|
|
81
77
|
if (snapshot.blocked !== undefined)
|
|
82
78
|
invalid.push({ id: 'state', reason: snapshot.blocked.code });
|
|
@@ -86,9 +82,10 @@ export async function mergeDoctor(io, home, configuredRoot) {
|
|
|
86
82
|
const artifactRow = row.state === 'approved' ? 'approved' : row.state === 'fuse' ? 'fuse' : row.state;
|
|
87
83
|
const run = runId === undefined ? 'none' : row.state === 'pending' ? 'awaiting-approval' : 'running';
|
|
88
84
|
const staging = item.stage === null ? (row.state === 'fuse' ? 'missing' : 'none') : item.stage.state === 'present' || item.stage.state === 'reserved' ? 'kept' : row.state === 'fuse' ? 'missing' : 'none';
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
85
|
+
const receipt = runId === undefined ? null : await readCommitReceipt(io, home, runId);
|
|
86
|
+
const phase = receipt !== null ? await readUnitJournal(io, home, receipt.unitId) : runId === undefined ? null : await readJournal(io, home, runId);
|
|
87
|
+
const journal = phase === null ? 'none' : phase.phase === 'prepared' || phase.phase === 'target-pending' ? 'prepared' : phase.phase === 'target-written' || phase.phase === 'sources-archived' || phase.phase === 'support-pending' || phase.phase === 'source-archiving' ? 'committing' : phase.phase === 'committed' ? 'committed' : phase.phase === 'rolled-back' ? 'rolled-back' : 'failed';
|
|
88
|
+
const evidence = receipt === null ? 'none' : 'receipt';
|
|
92
89
|
const result = validateArtifacts({ row: artifactRow, run, staging, journal, evidence, activeRuns: run === 'running' || run === 'awaiting-approval' ? 1 : 0, ...(runId !== undefined && recoveryRuns.has(runId) ? { recovery: true } : {}) });
|
|
93
90
|
if (!result.ok)
|
|
94
91
|
invalid.push({ id: row.id, reason: result.reason ?? 'not-allowed' });
|
|
@@ -8,12 +8,18 @@ export interface AuthoringManifest {
|
|
|
8
8
|
readonly files: readonly string[];
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Parse
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Parse the authoring manifest from a child's final turn.
|
|
12
|
+
*
|
|
13
|
+
* P2-20: a child that wraps the envelope in a fenced code block or in one prose
|
|
14
|
+
* sentence is the normal shape, not a malformed answer. A strict whole-text
|
|
15
|
+
* JSON.parse returned null there, and null used to mean "every staged file was
|
|
16
|
+
* declared" - the exact files the check exists to catch. Try the whole answer,
|
|
17
|
+
* then every fenced body, then every balanced object literal.
|
|
18
|
+
* @param text - the child's answer.
|
|
19
|
+
* @returns the first valid envelope, or null when the turn really declared none.
|
|
14
20
|
*/
|
|
15
21
|
export declare function parseAuthoringManifest(text: string): AuthoringManifest | null;
|
|
16
|
-
/** Files a manifest promises for a staged tree
|
|
22
|
+
/** Files a manifest promises for a staged tree. */
|
|
17
23
|
export declare function manifestFilesOf(manifest: AuthoringManifest | null, stagedPaths: readonly string[]): readonly string[];
|
|
18
24
|
/**
|
|
19
25
|
* The first file the manifest promises but the staged tree does not contain.
|
|
@@ -1,26 +1,100 @@
|
|
|
1
|
+
function asManifest(value) {
|
|
2
|
+
if (value === null || typeof value !== 'object')
|
|
3
|
+
return null;
|
|
4
|
+
const record = value;
|
|
5
|
+
if (!Array.isArray(record.files) || record.files.some(file => typeof file !== 'string'))
|
|
6
|
+
return null;
|
|
7
|
+
if (record.target !== undefined && typeof record.target !== 'string')
|
|
8
|
+
return null;
|
|
9
|
+
return { ...(typeof record.target === 'string' ? { target: record.target } : {}), files: record.files };
|
|
10
|
+
}
|
|
11
|
+
/** Every balanced object literal in a prose/fenced answer (string-aware). */
|
|
12
|
+
function balancedObjects(text) {
|
|
13
|
+
const found = [];
|
|
14
|
+
let depth = 0;
|
|
15
|
+
let start = -1;
|
|
16
|
+
let quoted = false;
|
|
17
|
+
let escaped = false;
|
|
18
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
19
|
+
const char = text[index];
|
|
20
|
+
if (quoted) {
|
|
21
|
+
if (escaped)
|
|
22
|
+
escaped = false;
|
|
23
|
+
else if (char === '\\')
|
|
24
|
+
escaped = true;
|
|
25
|
+
else if (char === '"')
|
|
26
|
+
quoted = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === '"') {
|
|
30
|
+
quoted = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (char === '{') {
|
|
34
|
+
if (depth === 0)
|
|
35
|
+
start = index;
|
|
36
|
+
depth += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (char !== '}' || depth === 0)
|
|
40
|
+
continue;
|
|
41
|
+
depth -= 1;
|
|
42
|
+
if (depth === 0 && start >= 0) {
|
|
43
|
+
found.push(text.slice(start, index + 1));
|
|
44
|
+
start = -1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
1
49
|
/**
|
|
2
|
-
* Parse
|
|
3
|
-
*
|
|
4
|
-
*
|
|
50
|
+
* Parse the authoring manifest from a child's final turn.
|
|
51
|
+
*
|
|
52
|
+
* P2-20: a child that wraps the envelope in a fenced code block or in one prose
|
|
53
|
+
* sentence is the normal shape, not a malformed answer. A strict whole-text
|
|
54
|
+
* JSON.parse returned null there, and null used to mean "every staged file was
|
|
55
|
+
* declared" - the exact files the check exists to catch. Try the whole answer,
|
|
56
|
+
* then every fenced body, then every balanced object literal.
|
|
57
|
+
* @param text - the child's answer.
|
|
58
|
+
* @returns the first valid envelope, or null when the turn really declared none.
|
|
5
59
|
*/
|
|
6
60
|
export function parseAuthoringManifest(text) {
|
|
61
|
+
// Author callbacks in tests (and a bare void answer) can hand undefined;
|
|
62
|
+
// "no answer" is the same as "no envelope", not a crash.
|
|
63
|
+
if (typeof text !== 'string')
|
|
64
|
+
return null;
|
|
7
65
|
try {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
10
|
-
return
|
|
11
|
-
if (value.target !== undefined && typeof value.target !== 'string')
|
|
12
|
-
return null;
|
|
13
|
-
return { ...(typeof value.target === 'string' ? { target: value.target } : {}), files: value.files };
|
|
66
|
+
const direct = asManifest(JSON.parse(text));
|
|
67
|
+
if (direct !== null)
|
|
68
|
+
return direct;
|
|
14
69
|
}
|
|
15
|
-
catch {
|
|
16
|
-
|
|
70
|
+
catch { /* not the whole answer */ }
|
|
71
|
+
for (const match of text.matchAll(/\x60\x60\x60(?:json)?\s*([\s\S]*?)\x60\x60\x60/g)) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = asManifest(JSON.parse(match[1] ?? ''));
|
|
74
|
+
if (parsed !== null)
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
catch { /* try the next fence/object */ }
|
|
78
|
+
}
|
|
79
|
+
for (const candidate of balancedObjects(text)) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = asManifest(JSON.parse(candidate));
|
|
82
|
+
if (parsed !== null)
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
catch { /* try the next candidate */ }
|
|
17
86
|
}
|
|
87
|
+
return null;
|
|
18
88
|
}
|
|
19
|
-
/** Files a manifest promises for a staged tree
|
|
89
|
+
/** Files a manifest promises for a staged tree. */
|
|
20
90
|
export function manifestFilesOf(manifest, stagedPaths) {
|
|
91
|
+
// P2-20: an answer that declared nothing is NOT a declaration of every staged
|
|
92
|
+
// file. Only SKILL.md is inherently required; any other staged file must be
|
|
93
|
+
// named (or be an existing target support file) or q09-undeclared blocks it.
|
|
21
94
|
if (manifest !== null)
|
|
22
95
|
return manifest.files;
|
|
23
|
-
|
|
96
|
+
void stagedPaths;
|
|
97
|
+
return ['SKILL.md'];
|
|
24
98
|
}
|
|
25
99
|
/**
|
|
26
100
|
* The first file the manifest promises but the staged tree does not contain.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
/** Attach the in-flight child run to a row before its round2 starts (P1-8). */
|
|
3
|
+
export declare function attachRunToRow(io: EvolutionIoLike, home: string, rowId: string, runId: string): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=row-run.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { pairLedger } from "./pair-ledger.js";
|
|
2
|
+
/** Attach the in-flight child run to a row before its round2 starts (P1-8). */
|
|
3
|
+
export async function attachRunToRow(io, home, rowId, runId) {
|
|
4
|
+
const attached = await pairLedger(io, home).transact(tx => {
|
|
5
|
+
const current = tx.rows().find(row => row.id === rowId);
|
|
6
|
+
if (current === undefined)
|
|
7
|
+
return false;
|
|
8
|
+
const moved = tx.transition(rowId, { type: 'run-attached', runId }, current.version);
|
|
9
|
+
return moved.ok;
|
|
10
|
+
});
|
|
11
|
+
if (!attached)
|
|
12
|
+
throw new Error('row is not attachable to run: ' + rowId);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=row-run.js.map
|
|
@@ -80,6 +80,9 @@ export type V4Event = {
|
|
|
80
80
|
readonly type: 'staged';
|
|
81
81
|
readonly stagingRef?: string;
|
|
82
82
|
readonly stagingExpiresAt?: number;
|
|
83
|
+
} | {
|
|
84
|
+
readonly type: 'run-attached';
|
|
85
|
+
readonly runId: string;
|
|
83
86
|
} | {
|
|
84
87
|
readonly type: 'committed';
|
|
85
88
|
readonly unitId: CommitUnitRef;
|
|
@@ -35,6 +35,8 @@ export function transitionV4(row, event, expectedVersion, at = new Date().toISOS
|
|
|
35
35
|
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
36
36
|
return illegal('pending/' + event.type);
|
|
37
37
|
case 'approved':
|
|
38
|
+
if (event.type === 'run-attached')
|
|
39
|
+
return { ok: true, row: advance(row, at, { runId: event.runId }), version: expectedVersion + 1 };
|
|
38
40
|
if (event.type === 'staged')
|
|
39
41
|
return { ok: true, row: advance(row, at, { state: 'fuse', ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) }), version: expectedVersion + 1 };
|
|
40
42
|
if (event.type === 'committed')
|
|
@@ -49,6 +51,8 @@ export function transitionV4(row, event, expectedVersion, at = new Date().toISOS
|
|
|
49
51
|
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
50
52
|
return illegal('approved/' + event.type);
|
|
51
53
|
case 'fuse':
|
|
54
|
+
if (event.type === 'run-attached')
|
|
55
|
+
return { ok: true, row: advance(row, at, { runId: event.runId }), version: expectedVersion + 1 };
|
|
52
56
|
if (event.type === 'committed')
|
|
53
57
|
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
54
58
|
if (event.type === 'skipped')
|
|
@@ -224,7 +224,9 @@ export async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
|
224
224
|
const refs = new Set();
|
|
225
225
|
const indexRoot = join(home, 'merge', 'staging-index');
|
|
226
226
|
const names = await io.exists(indexRoot) ? await listOrEmpty(io, indexRoot) : [];
|
|
227
|
-
|
|
227
|
+
// P1-7: refs must be collected from EVERY index before deleting run trees; the
|
|
228
|
+
// limit only bounds how many orphan run dirs one sweep removes, never the ref set.
|
|
229
|
+
for (const name of names) {
|
|
228
230
|
if (!name.endsWith('.json'))
|
|
229
231
|
continue;
|
|
230
232
|
const raw = await readTextOrNull(io, join(indexRoot, name));
|
|
@@ -23,6 +23,7 @@ export interface ValidationPlan {
|
|
|
23
23
|
readonly forwardRefs: readonly string[];
|
|
24
24
|
readonly reverseRefs: readonly string[];
|
|
25
25
|
readonly orphanFiles: readonly string[];
|
|
26
|
+
readonly undeclaredFiles: readonly string[];
|
|
26
27
|
};
|
|
27
28
|
readonly supportConflicts: readonly {
|
|
28
29
|
readonly path: string;
|
|
@@ -15,6 +15,9 @@ export function buildValidationPlan(input) {
|
|
|
15
15
|
const reverseRefs = files.map(file => file.path).filter(path => path !== 'SKILL.md' && !refs.has(path) && manifest.has(path)).sort();
|
|
16
16
|
const existing = new Set(input.existingSupport ?? []);
|
|
17
17
|
const orphanFiles = [...existing].filter(path => !staged.has(path)).sort();
|
|
18
|
+
// P2-20: a clean JSON subset must not exempt the staged files it omitted.
|
|
19
|
+
// Existing target support files stay exempt (round 2 legitimately updates them).
|
|
20
|
+
const undeclaredFiles = [...staged].filter(path => path !== 'SKILL.md' && !manifest.has(path) && !existing.has(path)).sort();
|
|
18
21
|
const byPath = new Map();
|
|
19
22
|
// Conflict means TWO SKILLS contributing different bytes at one path. Staged edits and
|
|
20
23
|
// the target's previous file are not conflicts: the target file is a CAS baseline that
|
|
@@ -32,7 +35,7 @@ export function buildValidationPlan(input) {
|
|
|
32
35
|
const sourceThreatFiles = participantCards.flatMap(card => (card.files ?? []).filter(file => scanContentThreats(file.content) !== null).map(file => card.name + '/' + file.path));
|
|
33
36
|
const threat = { body: scanContentThreats(input.tree.body) !== null || participantCards.some(card => scanContentThreats(card.body) !== null), files: [...files.filter(file => scanContentThreats(file.content) !== null).map(file => file.path), ...sourceThreatFiles] };
|
|
34
37
|
const sourceRevisions = input.sourceRevisions ?? Object.fromEntries(participantCards.map(card => [card.name, digest(card.body)]));
|
|
35
|
-
const revision = digest(JSON.stringify({ target: input.target, body: digest(input.tree.body), files: files.map(file => [file.path, file.digest]), q09: { forwardRefs, reverseRefs, orphanFiles }, supportConflicts, threat, sourceRevisions, targetRevision: input.targetRevision ?? null }));
|
|
38
|
+
const revision = digest(JSON.stringify({ target: input.target, body: digest(input.tree.body), files: files.map(file => [file.path, file.digest]), q09: { forwardRefs, reverseRefs, orphanFiles, undeclaredFiles }, supportConflicts, threat, sourceRevisions, targetRevision: input.targetRevision ?? null }));
|
|
36
39
|
const findings = [];
|
|
37
40
|
const stageDigest = digest(JSON.stringify(files.map(file => [file.path, file.digest])));
|
|
38
41
|
if (forwardRefs.length > 0)
|
|
@@ -41,6 +44,8 @@ export function buildValidationPlan(input) {
|
|
|
41
44
|
findings.push({ rule: 'q09-reverse', path: reverseRefs.join(','), severity: 'block', message: 'staged manifest file is not referenced by body' });
|
|
42
45
|
if (orphanFiles.length > 0)
|
|
43
46
|
findings.push({ rule: 'q09-orphan', path: orphanFiles.join(','), severity: 'block', message: 'target tree support is not declared by plan' });
|
|
47
|
+
if (undeclaredFiles.length > 0)
|
|
48
|
+
findings.push({ rule: 'q09-undeclared', path: undeclaredFiles.join(','), severity: 'block', message: 'staged support file is not declared by the authoring manifest' });
|
|
44
49
|
if (supportConflicts.length > 0)
|
|
45
50
|
findings.push({ rule: 'support-conflict', path: supportConflicts.map(item => item.path).join(','), severity: 'block', message: 'same support path has different contents' });
|
|
46
51
|
if (threat.body)
|
|
@@ -52,6 +57,6 @@ export function buildValidationPlan(input) {
|
|
|
52
57
|
for (const file of files)
|
|
53
58
|
if (file.bytes > input.limits.skillLimits.maxSkillFileBytes)
|
|
54
59
|
findings.push({ rule: 'limit', path: file.path, severity: 'block', message: 'file exceeds maxSkillFileBytes' });
|
|
55
|
-
return { planVersion: 1, stageDigest, target: input.target, body: input.tree.body, files, supportFiles, manifestFiles, q09: { forwardRefs, reverseRefs, orphanFiles }, supportConflicts, threat, sourceRevisions, targetRevision: input.targetRevision ?? null, limitsDigest: input.limits.limitsDigest, ...(input.promptVersion === undefined ? {} : { promptVersion: input.promptVersion }), ...(input.promptHash === undefined ? {} : { promptHash: input.promptHash }), revision, findings };
|
|
60
|
+
return { planVersion: 1, stageDigest, target: input.target, body: input.tree.body, files, supportFiles, manifestFiles, q09: { forwardRefs, reverseRefs, orphanFiles, undeclaredFiles }, supportConflicts, threat, sourceRevisions, targetRevision: input.targetRevision ?? null, limitsDigest: input.limits.limitsDigest, ...(input.promptVersion === undefined ? {} : { promptVersion: input.promptVersion }), ...(input.promptHash === undefined ? {} : { promptHash: input.promptHash }), revision, findings };
|
|
56
61
|
}
|
|
57
62
|
//# sourceMappingURL=validation-plan.js.map
|
|
@@ -33,6 +33,12 @@ export async function recoverCommitUnits(options) {
|
|
|
33
33
|
}
|
|
34
34
|
if (journal === null || typeof journal.unitId !== 'string')
|
|
35
35
|
continue;
|
|
36
|
+
// P1-4: legacy tree-change journals may still sit in the merge directory; they are ordinary
|
|
37
|
+
// skill units, never merge runs. Never finalize/rollback/project them as a merge.
|
|
38
|
+
if (journal.kind === 'tree-change' || (journal.kind === undefined && journal.runId === journal.unitId)) {
|
|
39
|
+
reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch: branchOf(journal.phase), action: 'none', ok: true, code: 'skill-unit-skipped' });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
36
42
|
if (options.isActive?.(journal.runId) === true) {
|
|
37
43
|
reports.push({ unitId: journal.unitId, runId: journal.runId, phase: journal.phase, branch: branchOf(journal.phase), action: 'none', ok: true, code: 'skipped-active-run' });
|
|
38
44
|
continue;
|
|
@@ -6,6 +6,7 @@ import { errorText } from '@lmzhen/dsh-evolution-core';
|
|
|
6
6
|
import { loadMergeState } from "./state.js";
|
|
7
7
|
import { runScanCommand } from "./scan-command.js";
|
|
8
8
|
import { withMergeRun } from "./run-shell.js";
|
|
9
|
+
import { attachRunToRow } from "./domain/row-run.js";
|
|
9
10
|
import { recordRound2Outcome } from "./round2-outcome.js";
|
|
10
11
|
import { pairLedger } from "./domain/pair-ledger.js";
|
|
11
12
|
import { presentStage } from "./domain/stage-aggregate.js";
|
|
@@ -75,6 +76,7 @@ export async function runResumeCommand(options) {
|
|
|
75
76
|
parentRunId, now: options.now,
|
|
76
77
|
...(options.detached === true ? { detached: true } : {}),
|
|
77
78
|
async body(ctx) {
|
|
79
|
+
await attachRunToRow(options.io, options.home, row.id, ctx.runId);
|
|
78
80
|
await ctx.stage('author', { done: 0, total: 1 });
|
|
79
81
|
let outcome;
|
|
80
82
|
try {
|
package/lib/types/run-shell.js
CHANGED
|
@@ -33,6 +33,20 @@ export async function withMergeRun(options) {
|
|
|
33
33
|
phase: 'running', approval: options.approval.mergeApproval ? 'pending' : 'approved',
|
|
34
34
|
createdAt: new Date(startedAt).toISOString(), updatedAt: new Date(options.now()).toISOString(),
|
|
35
35
|
});
|
|
36
|
+
const writeFailureMeta = async (stopReason) => {
|
|
37
|
+
try {
|
|
38
|
+
await writeRunMeta(options.io, options.home, {
|
|
39
|
+
version: 1, runId: handle.id, pid: process.pid,
|
|
40
|
+
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
41
|
+
...(options.pairKey === undefined ? {} : { pairKey: options.pairKey }),
|
|
42
|
+
...(options.stagingRef === undefined ? {} : { stagingRef: options.stagingRef }),
|
|
43
|
+
pairCount: options.pairCount ?? (options.pairKey === undefined ? 0 : 1),
|
|
44
|
+
phase: 'settled', approval: 'approved', stopReason,
|
|
45
|
+
createdAt: new Date(startedAt).toISOString(), updatedAt: new Date(options.now()).toISOString(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch { /* best-effort: the registry settle below remains authoritative */ }
|
|
49
|
+
};
|
|
36
50
|
const finish = async () => {
|
|
37
51
|
let draft;
|
|
38
52
|
const runController = new AbortController();
|
|
@@ -115,6 +129,7 @@ export async function withMergeRun(options) {
|
|
|
115
129
|
}
|
|
116
130
|
catch (error) {
|
|
117
131
|
await options.registry.settle(handle.id, { state: 'failed', failure: 'report write failed: ' + (error instanceof Error ? error.message : String(error)) });
|
|
132
|
+
await writeFailureMeta('failed');
|
|
118
133
|
return { ok: false, runId: handle.id, message: 'report write failed' };
|
|
119
134
|
}
|
|
120
135
|
try {
|
|
@@ -122,6 +137,7 @@ export async function withMergeRun(options) {
|
|
|
122
137
|
}
|
|
123
138
|
catch (error) {
|
|
124
139
|
await options.registry.settle(handle.id, { state: 'failed', resultRef: reportPath, failure: 'post-report finalize failed: ' + (error instanceof Error ? error.message : String(error)) });
|
|
140
|
+
await writeFailureMeta('failed');
|
|
125
141
|
return { ok: false, runId: handle.id, message: 'post-report finalize failed', reportPath };
|
|
126
142
|
}
|
|
127
143
|
const runState = report.stopReason === 'completed'
|
|
@@ -107,7 +107,9 @@ export function parseCursor(cursor, snapshot) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
export function buildQueuePage(snapshot, queue, expiredOnly, cursor, limit = 50) {
|
|
110
|
-
|
|
110
|
+
// P2-17: skipped rows are a normal queue. Expired ones belong to the expired bucket
|
|
111
|
+
// (counts.expired); the default list must not show them beside an active badge.
|
|
112
|
+
const selected = snapshot.rows.filter(row => row.queue === queue && (expiredOnly ? row.expired : !row.expired));
|
|
111
113
|
const offset = cursor === undefined ? 0 : parseCursor(cursor, snapshot);
|
|
112
114
|
const page = selected.slice(offset, offset + Math.max(0, Math.min(200, limit)));
|
|
113
115
|
const next = offset + page.length < selected.length ? encodeCursor(snapshot, offset + page.length) : undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-merge",
|
|
3
3
|
"description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
|
|
4
|
-
"version": "0.32.
|
|
4
|
+
"version": "0.32.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@deepseek-ai/schemastery": "^3.18.5",
|
|
35
|
-
"@lmzhen/dsh-evolution-core": "^0.32.
|
|
36
|
-
"@lmzhen/dsh-evolution-commands": "^0.32.
|
|
37
|
-
"@lmzhen/dsh-evolution-merge-engine": "0.32.
|
|
35
|
+
"@lmzhen/dsh-evolution-core": "^0.32.4",
|
|
36
|
+
"@lmzhen/dsh-evolution-commands": "^0.32.4",
|
|
37
|
+
"@lmzhen/dsh-evolution-merge-engine": "0.32.4"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.5"
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { CommitMergePlan, EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
-
export interface CommitSnapshot {
|
|
3
|
-
readonly runId: string;
|
|
4
|
-
readonly target: string;
|
|
5
|
-
readonly create: boolean;
|
|
6
|
-
readonly sources: readonly string[];
|
|
7
|
-
readonly targetBody: string | null;
|
|
8
|
-
readonly sourceBodies: readonly {
|
|
9
|
-
readonly name: string;
|
|
10
|
-
readonly body: string;
|
|
11
|
-
}[];
|
|
12
|
-
readonly targetSupport?: readonly {
|
|
13
|
-
readonly path: string;
|
|
14
|
-
readonly content: string;
|
|
15
|
-
}[];
|
|
16
|
-
readonly targetSupportAfter?: readonly {
|
|
17
|
-
readonly path: string;
|
|
18
|
-
readonly content: string;
|
|
19
|
-
}[];
|
|
20
|
-
readonly sourceSupport?: readonly {
|
|
21
|
-
readonly name: string;
|
|
22
|
-
readonly path: string;
|
|
23
|
-
readonly content: string;
|
|
24
|
-
}[];
|
|
25
|
-
readonly at: string;
|
|
26
|
-
}
|
|
27
|
-
export declare function snapshotPath(home: string, runId: string): string;
|
|
28
|
-
export declare function writeCommitSnapshot(io: EvolutionIoLike, home: string, runId: string, plan: CommitMergePlan, now: number, port?: {
|
|
29
|
-
readSupportFiles?(name: string): Promise<readonly {
|
|
30
|
-
readonly path: string;
|
|
31
|
-
readonly content: string;
|
|
32
|
-
}[]>;
|
|
33
|
-
}): Promise<CommitSnapshot>;
|
|
34
|
-
export declare function readCommitSnapshot(io: EvolutionIoLike, home: string, runId: string): Promise<CommitSnapshot | null>;
|
|
35
|
-
export declare function isSafeSkillName(name: string): boolean;
|
|
36
|
-
export declare function skillFilePath(root: string, name: string, path: string): string;
|
|
37
|
-
export declare function skillBodyPath(root: string, name: string): string;
|
|
38
|
-
//# sourceMappingURL=commit-snapshot.d.ts.map
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/** Pre-commit snapshot: enough to restore the target and archived sources after a crash. */
|
|
2
|
-
import { join, resolve, sep } from 'node:path';
|
|
3
|
-
export function snapshotPath(home, runId) {
|
|
4
|
-
return join(home, 'merge', 'snapshots', runId.replace(/[^A-Za-z0-9_-]+/g, '_') + '.json');
|
|
5
|
-
}
|
|
6
|
-
export async function writeCommitSnapshot(io, home, runId, plan, now, port) {
|
|
7
|
-
if (!isSafeSkillName(plan.target) || plan.sources.some(name => !isSafeSkillName(name))) {
|
|
8
|
-
throw new Error('unsafe skill name in commit plan: ' + plan.target);
|
|
9
|
-
}
|
|
10
|
-
const readSupport = port?.readSupportFiles === undefined
|
|
11
|
-
? undefined
|
|
12
|
-
: (name) => port.readSupportFiles?.(name) ?? Promise.resolve([]);
|
|
13
|
-
const targetSupport = readSupport === undefined || !plan.targetExists ? [] : await readSupport(plan.target);
|
|
14
|
-
const sourceSupport = [];
|
|
15
|
-
if (readSupport !== undefined) {
|
|
16
|
-
for (const name of plan.sources) {
|
|
17
|
-
for (const file of await readSupport(name))
|
|
18
|
-
sourceSupport.push({ name, path: file.path, content: file.content });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
const snapshot = {
|
|
22
|
-
runId, target: plan.target, create: !plan.targetExists, sources: plan.sources,
|
|
23
|
-
targetBody: plan.expectedTargetBody,
|
|
24
|
-
sourceBodies: plan.expectedSourceBodies.map(item => ({ name: item.name, body: item.body })),
|
|
25
|
-
...(targetSupport.length === 0 ? {} : { targetSupport }),
|
|
26
|
-
...(plan.supportFiles.length === 0 ? {} : { targetSupportAfter: plan.supportFiles }),
|
|
27
|
-
...(sourceSupport.length === 0 ? {} : { sourceSupport }),
|
|
28
|
-
at: new Date(now).toISOString(),
|
|
29
|
-
};
|
|
30
|
-
await io.writeText(snapshotPath(home, runId), JSON.stringify(snapshot, null, 2));
|
|
31
|
-
return snapshot;
|
|
32
|
-
}
|
|
33
|
-
export async function readCommitSnapshot(io, home, runId) {
|
|
34
|
-
const raw = await io.readText(snapshotPath(home, runId));
|
|
35
|
-
if (raw === null)
|
|
36
|
-
return null;
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(raw);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export function isSafeSkillName(name) {
|
|
45
|
-
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== '.' && name !== '..';
|
|
46
|
-
}
|
|
47
|
-
export function skillFilePath(root, name, path) {
|
|
48
|
-
if (!isSafeSkillName(name) || path.startsWith('/') || path.split('/').includes('..'))
|
|
49
|
-
throw new Error('unsafe skill file: ' + name + '/' + path);
|
|
50
|
-
const base = resolve(root, name);
|
|
51
|
-
const file = resolve(base, path);
|
|
52
|
-
if (!file.startsWith(base + sep))
|
|
53
|
-
throw new Error('skill file escapes skill: ' + name + '/' + path);
|
|
54
|
-
return file;
|
|
55
|
-
}
|
|
56
|
-
export function skillBodyPath(root, name) {
|
|
57
|
-
if (!isSafeSkillName(name))
|
|
58
|
-
throw new Error('unsafe skill name: ' + name);
|
|
59
|
-
const base = resolve(root);
|
|
60
|
-
const path = resolve(base, name, 'SKILL.md');
|
|
61
|
-
if (!path.startsWith(base + sep))
|
|
62
|
-
throw new Error('skill path escapes root: ' + name);
|
|
63
|
-
return path;
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=commit-snapshot.js.map
|