opencode-swarm 7.122.0 → 7.123.0
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/.opencode/skills/issue-tracer/SKILL.md +341 -0
- package/.opencode/skills/issue-tracer/assets/pr-template.md +59 -0
- package/.opencode/skills/issue-tracer/references/critic-gate.md +245 -0
- package/.opencode/skills/issue-tracer/references/evidence-artifacts.md +267 -0
- package/.opencode/skills/issue-tracer/references/install.md +63 -0
- package/.opencode/skills/issue-tracer/references/localization-playbook.md +95 -0
- package/.opencode/skills/issue-tracer/references/method-provenance.md +13 -0
- package/.opencode/skills/issue-tracer/references/untrusted-content.md +22 -0
- package/.opencode/skills/issue-tracer/scripts/scan-deferred.sh +103 -0
- package/.opencode/skills/issue-tracer/scripts/trace-init.sh +119 -0
- package/dist/cli/{config-doctor-404c8mf8.js → config-doctor-3ksh1534.js} +2 -2
- package/dist/cli/{core-xnwasfpc.js → core-cwgb7ev8.js} +1 -1
- package/dist/cli/{curation-policy-cj1tpzr5.js → curation-policy-4j4qce5a.js} +5 -5
- package/dist/cli/{curator-llm-factory-8rg0f77t.js → curator-llm-factory-mfvd4sqx.js} +22 -20
- package/dist/cli/{curator-3rze3pd4.js → curator-tkyn9bzz.js} +22 -20
- package/dist/cli/{evidence-summary-service-k14ykm03.js → evidence-summary-service-96ndczbk.js} +7 -5
- package/dist/cli/{gate-evidence-tbpezk28.js → gate-evidence-xqks3kk2.js} +3 -3
- package/dist/cli/{guardrail-explain-hdsxa2c5.js → guardrail-explain-4vytbchb.js} +23 -21
- package/dist/cli/{guardrail-log-dhpp0kmn.js → guardrail-log-z291m4gw.js} +3 -3
- package/dist/cli/{hive-promoter-dh38qd9m.js → hive-promoter-y5wsztkc.js} +22 -20
- package/dist/cli/{index-zvae4m8g.js → index-17eybc3z.js} +1 -1
- package/dist/cli/{index-kyvg2cmp.js → index-19zn67q6.js} +1 -1
- package/dist/cli/{index-rbfq77sz.js → index-2j25br0p.js} +12 -10
- package/dist/cli/{index-9xpgytj9.js → index-3rr0ese8.js} +1 -1
- package/dist/cli/{index-68aggz0v.js → index-6sgqnvpw.js} +2 -2
- package/dist/cli/{index-qg11yf8q.js → index-6xnxz6z0.js} +1 -1
- package/dist/cli/{index-emkb0bbe.js → index-7cspygmb.js} +277 -673
- package/dist/cli/{index-jzpv74mq.js → index-7g91saha.js} +24 -22
- package/dist/cli/{index-3r1w7xky.js → index-cqsak7bf.js} +2 -2
- package/dist/cli/index-ctpgsj84.js +547 -0
- package/dist/cli/index-d6h5hq91.js +564 -0
- package/dist/cli/{index-vqcgmy4y.js → index-det5jwfk.js} +1 -1
- package/dist/cli/{index-3sx27bxt.js → index-dywekeen.js} +3 -3
- package/dist/cli/{index-wtn8wf7h.js → index-gw3m5fw0.js} +1 -1
- package/dist/cli/{index-rkd6qwjh.js → index-hns7xjm7.js} +2 -2
- package/dist/cli/{index-xdsj4qtd.js → index-j9kxwshw.js} +5 -5
- package/dist/cli/{index-ahxrne50.js → index-jyrhy4ww.js} +161 -58
- package/dist/cli/{index-j33zb3cx.js → index-m0bmh3w0.js} +4 -4
- package/dist/cli/index-m1abzg93.js +68 -0
- package/dist/cli/{index-ny13nr0x.js → index-n2712byd.js} +6 -6
- package/dist/cli/{index-gr7ssnj9.js → index-q00e4s1s.js} +5 -4
- package/dist/cli/{index-3jvqw5hy.js → index-q27bajqb.js} +1 -1
- package/dist/cli/{index-qt2yaghg.js → index-vcgfqd8f.js} +1 -63
- package/dist/cli/{index-tftv9xh9.js → index-wr8qgpz5.js} +1 -1
- package/dist/cli/index.js +22 -20
- package/dist/cli/{knowledge-escalator-sya06p12.js → knowledge-escalator-dyxsm78k.js} +6 -6
- package/dist/cli/{knowledge-events-xzps61a9.js → knowledge-events-36q6nvae.js} +4 -4
- package/dist/cli/{knowledge-link-sy85fw3w.js → knowledge-link-4yd2z8ev.js} +3 -3
- package/dist/cli/{knowledge-store-qg66a93n.js → knowledge-store-e07b4s54.js} +4 -4
- package/dist/cli/{knowledge-validator-gar6wv4f.js → knowledge-validator-sfyf7wn2.js} +7 -7
- package/dist/cli/pr-subscriptions-x653151h.js +1 -1
- package/dist/cli/{scan-cursor-nx5y5k7r.js → scan-cursor-hm0aqqae.js} +5 -5
- package/dist/cli/{schema-vwxpsk6j.js → schema-13bnng2n.js} +1 -1
- package/dist/cli/scope-persistence-d9erwnvb.js +36 -0
- package/dist/cli/{skill-generator-nt5qxafq.js → skill-generator-63r20a9k.js} +8 -8
- package/dist/config/bundled-skills.d.ts +1 -1
- package/dist/config/constants.d.ts +3 -2
- package/dist/hooks/delegation-gate.d.ts +19 -1
- package/dist/hooks/guardrails/nontransient-circuit.d.ts +47 -0
- package/dist/hooks/scope-guard.d.ts +9 -2
- package/dist/hooks/write-target-resolver.d.ts +30 -0
- package/dist/index.js +406 -370
- package/dist/sandbox/win32/restricted-environment-executor.d.ts +3 -2
- package/dist/scope/scope-binding.d.ts +89 -0
- package/dist/scope/scope-persistence.d.ts +53 -12
- package/dist/state.d.ts +25 -1
- package/dist/tools/declare-scope.d.ts +5 -1
- package/dist/tools/file-extractor-planner.d.ts +31 -0
- package/dist/tools/file-extractor.d.ts +2 -7
- package/package.json +2 -1
- package/dist/cli/index-51gg9947.js +0 -254
- package/dist/cli/scope-persistence-mfr36yc9.js +0 -20
- package/dist/cli/{index-h07wn84h.js → index-7mkpsw6g.js} +3 -3
- package/dist/cli/{pending-delegations-0z3znsec.js → pending-delegations-68cbaqv2.js} +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
handleGuardrailExplain
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-6sgqnvpw.js";
|
|
5
5
|
import {
|
|
6
6
|
handleGuardrailLog
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-17eybc3z.js";
|
|
8
8
|
import {
|
|
9
9
|
COMMAND_REGISTRY,
|
|
10
10
|
SWARM_COMMAND_TOOL_ALLOWLIST,
|
|
@@ -83,49 +83,51 @@ import {
|
|
|
83
83
|
handleWriteRetroCommand,
|
|
84
84
|
normalizeSwarmCommandInput,
|
|
85
85
|
resolveCommand
|
|
86
|
-
} from "./index-
|
|
87
|
-
import"./index-
|
|
88
|
-
import"./index-
|
|
89
|
-
import"./index-
|
|
86
|
+
} from "./index-jyrhy4ww.js";
|
|
87
|
+
import"./index-19zn67q6.js";
|
|
88
|
+
import"./index-det5jwfk.js";
|
|
89
|
+
import"./index-7cspygmb.js";
|
|
90
90
|
import"./index-4905hd2m.js";
|
|
91
91
|
import"./index-nprawcc0.js";
|
|
92
|
-
import"./index-
|
|
93
|
-
import"./index-
|
|
94
|
-
import"./index-
|
|
92
|
+
import"./index-q00e4s1s.js";
|
|
93
|
+
import"./index-j9kxwshw.js";
|
|
94
|
+
import"./index-hns7xjm7.js";
|
|
95
95
|
import"./index-vxv732ex.js";
|
|
96
96
|
import"./index-c8s9a3zh.js";
|
|
97
97
|
import"./index-816rkp4y.js";
|
|
98
98
|
import"./index-22e127jw.js";
|
|
99
|
-
import"./index-
|
|
100
|
-
import"./index-
|
|
99
|
+
import"./index-m0bmh3w0.js";
|
|
100
|
+
import"./index-cqsak7bf.js";
|
|
101
101
|
import"./index-rtry5xyf.js";
|
|
102
|
-
import"./index-
|
|
103
|
-
import"./index-
|
|
102
|
+
import"./index-n2712byd.js";
|
|
103
|
+
import"./index-dywekeen.js";
|
|
104
104
|
import"./index-ae75rja9.js";
|
|
105
|
-
import"./index-
|
|
106
|
-
import"./index-
|
|
107
|
-
import"./index-
|
|
105
|
+
import"./index-q27bajqb.js";
|
|
106
|
+
import"./index-3rr0ese8.js";
|
|
107
|
+
import"./index-vcgfqd8f.js";
|
|
108
108
|
import"./index-gnp9x6se.js";
|
|
109
109
|
import {
|
|
110
110
|
AGENT_TOOL_MAP,
|
|
111
111
|
ORCHESTRATOR_NAME,
|
|
112
112
|
stripKnownSwarmPrefix
|
|
113
|
-
} from "./index-
|
|
113
|
+
} from "./index-wr8qgpz5.js";
|
|
114
114
|
import"./index-4rhcth7a.js";
|
|
115
115
|
import"./index-8fwhhayc.js";
|
|
116
116
|
import"./index-q1exe2b3.js";
|
|
117
|
-
import"./index-
|
|
118
|
-
import"./index-
|
|
117
|
+
import"./index-d6h5hq91.js";
|
|
118
|
+
import"./index-ctpgsj84.js";
|
|
119
|
+
import"./index-m1abzg93.js";
|
|
120
|
+
import"./index-gw3m5fw0.js";
|
|
119
121
|
import"./index-ye3fndsw.js";
|
|
120
122
|
import"./index-kzj3fp7c.js";
|
|
121
123
|
import"./index-5e4e2hvv.js";
|
|
122
124
|
import"./index-p0arc26j.js";
|
|
123
125
|
import"./index-zgwm4ryv.js";
|
|
124
126
|
import"./index-dqh3zhhc.js";
|
|
125
|
-
import"./index-
|
|
126
|
-
import"./index-
|
|
127
|
-
import"./index-76kvm9z5.js";
|
|
127
|
+
import"./index-6xnxz6z0.js";
|
|
128
|
+
import"./index-7mkpsw6g.js";
|
|
128
129
|
import"./index-fsrp8wp3.js";
|
|
130
|
+
import"./index-76kvm9z5.js";
|
|
129
131
|
import"./index-q2rrv0cy.js";
|
|
130
132
|
import"./index-293f68mj.js";
|
|
131
133
|
import"./index-7g4c7s5r.js";
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
} from "./index-rtry5xyf.js";
|
|
5
5
|
import {
|
|
6
6
|
readKnowledgeEvents
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-dywekeen.js";
|
|
8
8
|
import {
|
|
9
9
|
isLinked,
|
|
10
10
|
readLinkPointer,
|
|
11
11
|
resolveKnowledgeStoreDir,
|
|
12
12
|
resolveLinkDir
|
|
13
|
-
} from "./index-
|
|
13
|
+
} from "./index-q27bajqb.js";
|
|
14
14
|
|
|
15
15
|
// src/knowledge/curation-policy.ts
|
|
16
16
|
import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ExecutionProfileSchema,
|
|
4
|
+
PlanSchema,
|
|
5
|
+
TaskStatusSchema
|
|
6
|
+
} from "./index-m1abzg93.js";
|
|
7
|
+
import {
|
|
8
|
+
criticalWarn,
|
|
9
|
+
init_logger,
|
|
10
|
+
log
|
|
11
|
+
} from "./index-zgwm4ryv.js";
|
|
12
|
+
import {
|
|
13
|
+
withEvidenceLock
|
|
14
|
+
} from "./index-76kvm9z5.js";
|
|
15
|
+
import {
|
|
16
|
+
emit
|
|
17
|
+
} from "./index-q2rrv0cy.js";
|
|
18
|
+
|
|
19
|
+
// src/plan/utils.ts
|
|
20
|
+
function derivePlanId(plan) {
|
|
21
|
+
return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/plan/ledger.ts
|
|
25
|
+
import * as crypto from "crypto";
|
|
26
|
+
import * as fs from "fs";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
init_logger();
|
|
29
|
+
var LEDGER_SCHEMA_VERSION = "1.1.0";
|
|
30
|
+
var LEDGER_FILENAME = "plan-ledger.jsonl";
|
|
31
|
+
var LEDGER_LOCK_PATH = path.join(".swarm", LEDGER_FILENAME);
|
|
32
|
+
var PLAN_JSON_FILENAME = "plan.json";
|
|
33
|
+
|
|
34
|
+
class LedgerStaleWriterError extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "LedgerStaleWriterError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getLedgerPath(directory) {
|
|
41
|
+
return path.join(directory, ".swarm", LEDGER_FILENAME);
|
|
42
|
+
}
|
|
43
|
+
function getPlanJsonPath(directory) {
|
|
44
|
+
return path.join(directory, ".swarm", PLAN_JSON_FILENAME);
|
|
45
|
+
}
|
|
46
|
+
function writeFileFsyncedThenRename(tempPath, targetPath, data) {
|
|
47
|
+
const fd = fs.openSync(tempPath, "w");
|
|
48
|
+
try {
|
|
49
|
+
fs.writeFileSync(fd, data, "utf8");
|
|
50
|
+
fs.fsyncSync(fd);
|
|
51
|
+
} finally {
|
|
52
|
+
fs.closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
fs.renameSync(tempPath, targetPath);
|
|
55
|
+
}
|
|
56
|
+
function computePlanHash(plan) {
|
|
57
|
+
const normalized = {
|
|
58
|
+
schema_version: plan.schema_version,
|
|
59
|
+
title: plan.title,
|
|
60
|
+
swarm: plan.swarm,
|
|
61
|
+
current_phase: plan.current_phase,
|
|
62
|
+
migration_status: plan.migration_status,
|
|
63
|
+
execution_profile: plan.execution_profile,
|
|
64
|
+
phases: plan.phases.map((phase) => ({
|
|
65
|
+
id: phase.id,
|
|
66
|
+
name: phase.name,
|
|
67
|
+
status: phase.status,
|
|
68
|
+
required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
|
|
69
|
+
tasks: phase.tasks.map((task) => ({
|
|
70
|
+
id: task.id,
|
|
71
|
+
phase: task.phase,
|
|
72
|
+
status: task.status,
|
|
73
|
+
size: task.size,
|
|
74
|
+
description: task.description,
|
|
75
|
+
depends: [...task.depends].sort(),
|
|
76
|
+
acceptance: task.acceptance,
|
|
77
|
+
files_touched: [...task.files_touched].sort(),
|
|
78
|
+
evidence_path: task.evidence_path,
|
|
79
|
+
blocked_reason: task.blocked_reason
|
|
80
|
+
}))
|
|
81
|
+
}))
|
|
82
|
+
};
|
|
83
|
+
const jsonString = JSON.stringify(normalized);
|
|
84
|
+
return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
|
|
85
|
+
}
|
|
86
|
+
function computePlanStructureHash(plan) {
|
|
87
|
+
const normalized = {
|
|
88
|
+
schema_version: plan.schema_version,
|
|
89
|
+
title: plan.title,
|
|
90
|
+
swarm: plan.swarm,
|
|
91
|
+
current_phase: plan.current_phase,
|
|
92
|
+
migration_status: plan.migration_status,
|
|
93
|
+
execution_profile: plan.execution_profile,
|
|
94
|
+
phases: plan.phases.map((phase) => ({
|
|
95
|
+
id: phase.id,
|
|
96
|
+
name: phase.name,
|
|
97
|
+
required_agents: phase.required_agents ? [...phase.required_agents].sort() : undefined,
|
|
98
|
+
tasks: phase.tasks.map((task) => ({
|
|
99
|
+
id: task.id,
|
|
100
|
+
phase: task.phase,
|
|
101
|
+
size: task.size,
|
|
102
|
+
description: task.description,
|
|
103
|
+
depends: [...task.depends].sort(),
|
|
104
|
+
acceptance: task.acceptance,
|
|
105
|
+
files_touched: [...task.files_touched].sort(),
|
|
106
|
+
evidence_path: task.evidence_path,
|
|
107
|
+
blocked_reason: task.blocked_reason
|
|
108
|
+
}))
|
|
109
|
+
}))
|
|
110
|
+
};
|
|
111
|
+
const jsonString = JSON.stringify(normalized);
|
|
112
|
+
return crypto.createHash("sha256").update(jsonString, "utf8").digest("hex");
|
|
113
|
+
}
|
|
114
|
+
function computeCurrentPlanHash(directory) {
|
|
115
|
+
const planPath = getPlanJsonPath(directory);
|
|
116
|
+
try {
|
|
117
|
+
const content = fs.readFileSync(planPath, "utf8");
|
|
118
|
+
const plan = JSON.parse(content);
|
|
119
|
+
return computePlanHash(plan);
|
|
120
|
+
} catch {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function ledgerExists(directory) {
|
|
125
|
+
const ledgerPath = getLedgerPath(directory);
|
|
126
|
+
return fs.existsSync(ledgerPath);
|
|
127
|
+
}
|
|
128
|
+
async function getLatestLedgerSeq(directory) {
|
|
129
|
+
const ledgerPath = getLedgerPath(directory);
|
|
130
|
+
if (!fs.existsSync(ledgerPath)) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const content = fs.readFileSync(ledgerPath, "utf8");
|
|
135
|
+
const lines = content.trim().split(`
|
|
136
|
+
`).filter((line) => line.trim() !== "");
|
|
137
|
+
if (lines.length === 0) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
let maxSeq = 0;
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
try {
|
|
143
|
+
const event = JSON.parse(line);
|
|
144
|
+
if (event.seq > maxSeq) {
|
|
145
|
+
maxSeq = event.seq;
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
return maxSeq;
|
|
150
|
+
} catch {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function readLedgerEvents(directory) {
|
|
155
|
+
const ledgerPath = getLedgerPath(directory);
|
|
156
|
+
if (!fs.existsSync(ledgerPath)) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const content = fs.readFileSync(ledgerPath, "utf8");
|
|
161
|
+
const lines = content.trim().split(`
|
|
162
|
+
`).filter((line) => line.trim() !== "");
|
|
163
|
+
const events = [];
|
|
164
|
+
let skippedCount = 0;
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
try {
|
|
167
|
+
const event = JSON.parse(line);
|
|
168
|
+
events.push(event);
|
|
169
|
+
} catch {
|
|
170
|
+
skippedCount++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (skippedCount > 0) {
|
|
174
|
+
log(`[ledger] Skipped ${skippedCount} malformed line(s) in plan-ledger.jsonl`);
|
|
175
|
+
}
|
|
176
|
+
events.sort((a, b) => a.seq - b.seq);
|
|
177
|
+
return events;
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function initLedger(directory, planId, initialPlanHash, initialPlan) {
|
|
183
|
+
const ledgerPath = getLedgerPath(directory);
|
|
184
|
+
const planJsonPath = getPlanJsonPath(directory);
|
|
185
|
+
if (fs.existsSync(ledgerPath)) {
|
|
186
|
+
throw new Error("Ledger already initialized. Use appendLedgerEvent to add events.");
|
|
187
|
+
}
|
|
188
|
+
let planHashAfter = initialPlanHash ?? "";
|
|
189
|
+
let embeddedPlan = initialPlan;
|
|
190
|
+
if (!initialPlanHash) {
|
|
191
|
+
try {
|
|
192
|
+
if (fs.existsSync(planJsonPath)) {
|
|
193
|
+
const content = fs.readFileSync(planJsonPath, "utf8");
|
|
194
|
+
const plan = JSON.parse(content);
|
|
195
|
+
planHashAfter = computePlanHash(plan);
|
|
196
|
+
if (!embeddedPlan)
|
|
197
|
+
embeddedPlan = plan;
|
|
198
|
+
}
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
const payload = embeddedPlan ? { plan: embeddedPlan, payload_hash: planHashAfter } : undefined;
|
|
202
|
+
const event = {
|
|
203
|
+
seq: 1,
|
|
204
|
+
timestamp: new Date().toISOString(),
|
|
205
|
+
plan_id: planId,
|
|
206
|
+
event_type: "plan_created",
|
|
207
|
+
source: "initLedger",
|
|
208
|
+
plan_hash_before: "",
|
|
209
|
+
plan_hash_after: planHashAfter,
|
|
210
|
+
schema_version: LEDGER_SCHEMA_VERSION,
|
|
211
|
+
...payload ? { payload } : {}
|
|
212
|
+
};
|
|
213
|
+
fs.mkdirSync(path.join(directory, ".swarm"), { recursive: true });
|
|
214
|
+
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
215
|
+
const line = `${JSON.stringify(event)}
|
|
216
|
+
`;
|
|
217
|
+
writeFileFsyncedThenRename(tempPath, ledgerPath, line);
|
|
218
|
+
}
|
|
219
|
+
async function appendLedgerEvent(directory, eventInput, options) {
|
|
220
|
+
return withEvidenceLock(directory, LEDGER_LOCK_PATH, "plan-ledger", "append-ledger-event", async () => {
|
|
221
|
+
const ledgerPath = getLedgerPath(directory);
|
|
222
|
+
const latestSeq = await getLatestLedgerSeq(directory);
|
|
223
|
+
const nextSeq = latestSeq + 1;
|
|
224
|
+
const planHashBefore = computeCurrentPlanHash(directory);
|
|
225
|
+
if (options?.expectedSeq !== undefined && options.expectedSeq !== latestSeq) {
|
|
226
|
+
throw new LedgerStaleWriterError(`Stale writer: expected seq ${options.expectedSeq} but found ${latestSeq}`);
|
|
227
|
+
}
|
|
228
|
+
if (options?.expectedHash !== undefined && options.expectedHash !== planHashBefore) {
|
|
229
|
+
throw new LedgerStaleWriterError(`Stale writer: expected hash ${options.expectedHash} but found ${planHashBefore}`);
|
|
230
|
+
}
|
|
231
|
+
const planHashAfter = options?.planHashAfter ?? planHashBefore;
|
|
232
|
+
const event = {
|
|
233
|
+
...eventInput,
|
|
234
|
+
seq: nextSeq,
|
|
235
|
+
timestamp: new Date().toISOString(),
|
|
236
|
+
plan_hash_before: planHashBefore,
|
|
237
|
+
plan_hash_after: planHashAfter,
|
|
238
|
+
schema_version: LEDGER_SCHEMA_VERSION
|
|
239
|
+
};
|
|
240
|
+
fs.mkdirSync(path.join(directory, ".swarm"), { recursive: true });
|
|
241
|
+
const tempPath = `${ledgerPath}.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
|
|
242
|
+
const line = `${JSON.stringify(event)}
|
|
243
|
+
`;
|
|
244
|
+
if (!fs.existsSync(ledgerPath)) {
|
|
245
|
+
throw new Error("Ledger not initialized. Call initLedger() first.");
|
|
246
|
+
}
|
|
247
|
+
const existingContent = fs.readFileSync(ledgerPath, "utf8");
|
|
248
|
+
writeFileFsyncedThenRename(tempPath, ledgerPath, existingContent + line);
|
|
249
|
+
return event;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async function takeSnapshotWithRetry(directory, plan, options) {
|
|
253
|
+
const MAX_RETRIES = 3;
|
|
254
|
+
const TOTAL_ATTEMPTS = 1 + MAX_RETRIES;
|
|
255
|
+
const telemetrySource = options?.source ?? "save_plan_tool";
|
|
256
|
+
const snapshotOptions = { planHashAfter: options?.planHashAfter };
|
|
257
|
+
let lastError;
|
|
258
|
+
for (let attempt = 1;attempt <= TOTAL_ATTEMPTS; attempt++) {
|
|
259
|
+
try {
|
|
260
|
+
await takeSnapshotEvent(directory, plan, snapshotOptions);
|
|
261
|
+
return;
|
|
262
|
+
} catch (err) {
|
|
263
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
264
|
+
if (attempt < TOTAL_ATTEMPTS) {
|
|
265
|
+
await new Promise((r) => setTimeout(r, 10 * 2 ** (attempt - 1)));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
criticalWarn(`[takeSnapshotWithRetry] Snapshot failed after ${MAX_RETRIES} retries (${TOTAL_ATTEMPTS} attempts): ${lastError.message}`);
|
|
270
|
+
try {
|
|
271
|
+
emit("snapshot_failed", {
|
|
272
|
+
error: lastError.message,
|
|
273
|
+
retries: MAX_RETRIES,
|
|
274
|
+
source: telemetrySource
|
|
275
|
+
});
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
278
|
+
async function takeSnapshotEvent(directory, plan, options) {
|
|
279
|
+
const payloadHash = options?.payloadHashOverride ?? computePlanHash(plan);
|
|
280
|
+
const snapshotPayload = {
|
|
281
|
+
plan,
|
|
282
|
+
payload_hash: payloadHash
|
|
283
|
+
};
|
|
284
|
+
if (options?.approvalMetadata) {
|
|
285
|
+
snapshotPayload.approval = options.approvalMetadata;
|
|
286
|
+
}
|
|
287
|
+
const planId = derivePlanId(plan);
|
|
288
|
+
return appendLedgerEvent(directory, {
|
|
289
|
+
event_type: "snapshot",
|
|
290
|
+
source: options?.source ?? "takeSnapshotEvent",
|
|
291
|
+
plan_id: planId,
|
|
292
|
+
payload: snapshotPayload
|
|
293
|
+
}, { planHashAfter: options?.planHashAfter });
|
|
294
|
+
}
|
|
295
|
+
async function replayFromLedger(directory, options) {
|
|
296
|
+
const { plan } = await replayFromLedgerWithStatus(directory, options);
|
|
297
|
+
return plan;
|
|
298
|
+
}
|
|
299
|
+
async function replayFromLedgerWithStatus(directory, _options) {
|
|
300
|
+
const { events, truncated, badSuffix } = await readLedgerEventsWithIntegrity(directory);
|
|
301
|
+
if (events.length === 0) {
|
|
302
|
+
return { plan: null, truncated, badSuffix };
|
|
303
|
+
}
|
|
304
|
+
if (truncated && badSuffix !== null) {
|
|
305
|
+
await quarantineLedgerSuffix(directory, badSuffix);
|
|
306
|
+
}
|
|
307
|
+
const plan = reconstructPlanFromEvents(directory, events);
|
|
308
|
+
return { plan, truncated, badSuffix };
|
|
309
|
+
}
|
|
310
|
+
function reconstructPlanFromEvents(directory, events) {
|
|
311
|
+
const targetPlanId = events[0].plan_id;
|
|
312
|
+
const relevantEvents = events.filter((e) => e.plan_id === targetPlanId);
|
|
313
|
+
{
|
|
314
|
+
const snapshotEvents = relevantEvents.filter((e) => e.event_type === "snapshot");
|
|
315
|
+
if (snapshotEvents.length > 0) {
|
|
316
|
+
const latestSnapshotEvent = snapshotEvents[snapshotEvents.length - 1];
|
|
317
|
+
const snapshotPayload = latestSnapshotEvent.payload;
|
|
318
|
+
let plan2 = snapshotPayload.plan;
|
|
319
|
+
const eventsAfterSnapshot = relevantEvents.filter((e) => e.seq > latestSnapshotEvent.seq);
|
|
320
|
+
for (const event of eventsAfterSnapshot) {
|
|
321
|
+
plan2 = applyEventToPlan(plan2, event);
|
|
322
|
+
if (plan2 === null) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return plan2;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const createdEvent = relevantEvents.find((e) => e.event_type === "plan_created");
|
|
330
|
+
if (createdEvent?.payload && typeof createdEvent.payload === "object" && "plan" in createdEvent.payload) {
|
|
331
|
+
const parseResult = PlanSchema.safeParse(createdEvent.payload.plan);
|
|
332
|
+
if (parseResult.success) {
|
|
333
|
+
let plan2 = parseResult.data;
|
|
334
|
+
const eventsAfterCreated = relevantEvents.filter((e) => e.seq > createdEvent.seq);
|
|
335
|
+
for (const event of eventsAfterCreated) {
|
|
336
|
+
if (plan2 === null)
|
|
337
|
+
return null;
|
|
338
|
+
plan2 = applyEventToPlan(plan2, event);
|
|
339
|
+
}
|
|
340
|
+
return plan2;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const planJsonPath = getPlanJsonPath(directory);
|
|
344
|
+
if (!fs.existsSync(planJsonPath)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
let plan;
|
|
348
|
+
try {
|
|
349
|
+
const content = fs.readFileSync(planJsonPath, "utf8");
|
|
350
|
+
plan = JSON.parse(content);
|
|
351
|
+
} catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
for (const event of relevantEvents) {
|
|
355
|
+
if (plan === null) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
plan = applyEventToPlan(plan, event);
|
|
359
|
+
}
|
|
360
|
+
return plan;
|
|
361
|
+
}
|
|
362
|
+
function applyEventToPlan(plan, event) {
|
|
363
|
+
switch (event.event_type) {
|
|
364
|
+
case "plan_created":
|
|
365
|
+
if (event.payload && typeof event.payload === "object" && "plan" in event.payload) {
|
|
366
|
+
const parsed = PlanSchema.safeParse(event.payload.plan);
|
|
367
|
+
if (parsed.success)
|
|
368
|
+
return parsed.data;
|
|
369
|
+
}
|
|
370
|
+
return plan;
|
|
371
|
+
case "task_status_changed":
|
|
372
|
+
if (event.task_id && event.to_status) {
|
|
373
|
+
const parseResult = TaskStatusSchema.safeParse(event.to_status);
|
|
374
|
+
if (!parseResult.success) {
|
|
375
|
+
return plan;
|
|
376
|
+
}
|
|
377
|
+
for (const phase of plan.phases) {
|
|
378
|
+
const task = phase.tasks.find((t) => t.id === event.task_id);
|
|
379
|
+
if (task) {
|
|
380
|
+
task.status = parseResult.data;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return plan;
|
|
386
|
+
case "phase_completed":
|
|
387
|
+
if (event.phase_id) {
|
|
388
|
+
const phase = plan.phases.find((p) => p.id === event.phase_id);
|
|
389
|
+
if (phase) {
|
|
390
|
+
phase.status = "complete";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return plan;
|
|
394
|
+
case "plan_exported":
|
|
395
|
+
return plan;
|
|
396
|
+
case "task_added":
|
|
397
|
+
return plan;
|
|
398
|
+
case "task_removed":
|
|
399
|
+
if (event.task_id) {
|
|
400
|
+
for (const phase of plan.phases) {
|
|
401
|
+
const idx = phase.tasks.findIndex((t) => t.id === event.task_id);
|
|
402
|
+
if (idx !== -1) {
|
|
403
|
+
phase.tasks.splice(idx, 1);
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return plan;
|
|
409
|
+
case "task_updated":
|
|
410
|
+
return plan;
|
|
411
|
+
case "plan_rebuilt":
|
|
412
|
+
return plan;
|
|
413
|
+
case "task_reordered":
|
|
414
|
+
return plan;
|
|
415
|
+
case "snapshot":
|
|
416
|
+
return plan;
|
|
417
|
+
case "plan_reset":
|
|
418
|
+
return null;
|
|
419
|
+
case "execution_profile_set": {
|
|
420
|
+
const rawProfile = event.payload?.execution_profile;
|
|
421
|
+
if (rawProfile !== undefined) {
|
|
422
|
+
const parsed = ExecutionProfileSchema.safeParse(rawProfile);
|
|
423
|
+
if (parsed.success) {
|
|
424
|
+
return { ...plan, execution_profile: parsed.data };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return plan;
|
|
428
|
+
}
|
|
429
|
+
case "execution_profile_locked": {
|
|
430
|
+
if (plan.execution_profile) {
|
|
431
|
+
return {
|
|
432
|
+
...plan,
|
|
433
|
+
execution_profile: { ...plan.execution_profile, locked: true }
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return plan;
|
|
437
|
+
}
|
|
438
|
+
default:
|
|
439
|
+
throw new Error(`applyEventToPlan: unhandled event type "${event.event_type}" at seq ${event.seq}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function readLedgerEventsWithIntegrity(directory) {
|
|
443
|
+
const ledgerPath = getLedgerPath(directory);
|
|
444
|
+
if (!fs.existsSync(ledgerPath)) {
|
|
445
|
+
return { events: [], truncated: false, badSuffix: null };
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const content = fs.readFileSync(ledgerPath, "utf8");
|
|
449
|
+
const lines = content.split(`
|
|
450
|
+
`);
|
|
451
|
+
const events = [];
|
|
452
|
+
let truncated = false;
|
|
453
|
+
let badSuffix = null;
|
|
454
|
+
for (let i = 0;i < lines.length; i++) {
|
|
455
|
+
const line = lines[i];
|
|
456
|
+
if (line.trim() === "") {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const event = JSON.parse(line);
|
|
461
|
+
events.push(event);
|
|
462
|
+
} catch {
|
|
463
|
+
truncated = true;
|
|
464
|
+
badSuffix = lines.slice(i).join(`
|
|
465
|
+
`);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
events.sort((a, b) => a.seq - b.seq);
|
|
470
|
+
return { events, truncated, badSuffix };
|
|
471
|
+
} catch {
|
|
472
|
+
return { events: [], truncated: false, badSuffix: null };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function quarantineLedgerSuffix(directory, badSuffix) {
|
|
476
|
+
let salvagedCount = 0;
|
|
477
|
+
for (const line of badSuffix.split(`
|
|
478
|
+
`)) {
|
|
479
|
+
if (line.trim() === "")
|
|
480
|
+
continue;
|
|
481
|
+
try {
|
|
482
|
+
JSON.parse(line);
|
|
483
|
+
salvagedCount++;
|
|
484
|
+
} catch {}
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const hash = crypto.createHash("sha256").update(badSuffix, "utf8").digest("hex").slice(0, 12);
|
|
488
|
+
const swarmDir = path.join(directory, ".swarm");
|
|
489
|
+
try {
|
|
490
|
+
const existing = fs.readdirSync(swarmDir).filter((name) => name.startsWith("plan-ledger.quarantine.") && name.endsWith(`.${hash}`));
|
|
491
|
+
for (const name of existing) {
|
|
492
|
+
const existingPath = path.join(swarmDir, name);
|
|
493
|
+
if (fs.readFileSync(existingPath, "utf8") === badSuffix) {
|
|
494
|
+
return { path: existingPath, salvagedCount };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} catch {}
|
|
498
|
+
const quarantinePath = path.join(swarmDir, `plan-ledger.quarantine.${Date.now()}.${hash}`);
|
|
499
|
+
fs.writeFileSync(quarantinePath, badSuffix, "utf8");
|
|
500
|
+
log(`[ledger] Corrupted suffix quarantined to ${path.relative(directory, quarantinePath)} (salvageable events: ${salvagedCount})`);
|
|
501
|
+
return { path: quarantinePath, salvagedCount };
|
|
502
|
+
} catch {
|
|
503
|
+
return { path: null, salvagedCount };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function loadLastApprovedPlan(directory, expectedPlanId) {
|
|
507
|
+
const events = await readLedgerEvents(directory);
|
|
508
|
+
return findLastApprovedSnapshot(events, expectedPlanId);
|
|
509
|
+
}
|
|
510
|
+
function findLastApprovedSnapshot(events, expectedPlanId, extraFilter) {
|
|
511
|
+
if (events.length === 0) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
for (let i = events.length - 1;i >= 0; i--) {
|
|
515
|
+
const event = events[i];
|
|
516
|
+
if (event.event_type !== "snapshot")
|
|
517
|
+
continue;
|
|
518
|
+
if (event.source !== "critic_approved")
|
|
519
|
+
continue;
|
|
520
|
+
if (expectedPlanId !== undefined && event.plan_id !== expectedPlanId) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const payload = event.payload;
|
|
524
|
+
if (!payload || typeof payload !== "object" || !payload.plan) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (expectedPlanId !== undefined) {
|
|
528
|
+
const payloadPlanId = derivePlanId(payload.plan);
|
|
529
|
+
if (payloadPlanId !== expectedPlanId) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (extraFilter && !extraFilter(payload)) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
plan: payload.plan,
|
|
538
|
+
seq: event.seq,
|
|
539
|
+
timestamp: event.timestamp,
|
|
540
|
+
approval: payload.approval,
|
|
541
|
+
payloadHash: payload.payload_hash
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export { derivePlanId, LedgerStaleWriterError, computePlanHash, computePlanStructureHash, computeCurrentPlanHash, ledgerExists, getLatestLedgerSeq, readLedgerEvents, initLedger, appendLedgerEvent, takeSnapshotWithRetry, takeSnapshotEvent, replayFromLedger, replayFromLedgerWithStatus, loadLastApprovedPlan };
|