@martinloop/mcp 0.3.8 → 0.5.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.
Files changed (161) hide show
  1. package/README.md +90 -106
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +1 -1
  4. package/dist/resources.d.ts +1 -2
  5. package/dist/resources.js +190 -238
  6. package/dist/server-validation.d.ts +1 -1
  7. package/dist/server-validation.js +16 -11
  8. package/dist/server.d.ts +23 -0
  9. package/dist/server.js +152 -7
  10. package/dist/tools/run-dossier.d.ts +3 -1
  11. package/dist/tools/run-dossier.js +40 -2
  12. package/dist/tools/run-loop.d.ts +1 -4
  13. package/dist/tools/run-loop.js +6 -15
  14. package/dist/tools/tool-support.d.ts +2 -1
  15. package/dist/tools/tool-support.js +1 -12
  16. package/dist/vendor/adapters/claude-cli.js +38 -35
  17. package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
  18. package/dist/vendor/adapters/cli-bridge.js +54 -13
  19. package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
  20. package/dist/vendor/adapters/codex-launcher.js +118 -19
  21. package/dist/vendor/adapters/index.d.ts +1 -3
  22. package/dist/vendor/adapters/index.js +1 -3
  23. package/dist/vendor/adapters/openai-compatible.js +57 -18
  24. package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
  25. package/dist/vendor/contracts/context-handoff.d.ts +100 -0
  26. package/dist/vendor/contracts/context-handoff.js +11 -0
  27. package/dist/vendor/contracts/context-manifest.d.ts +195 -0
  28. package/dist/vendor/contracts/context-manifest.js +17 -0
  29. package/dist/vendor/contracts/context-shadow.d.ts +75 -0
  30. package/dist/vendor/contracts/context-shadow.js +8 -0
  31. package/dist/vendor/contracts/exits.d.ts +126 -0
  32. package/dist/vendor/contracts/exits.js +32 -0
  33. package/dist/vendor/contracts/index.d.ts +39 -2
  34. package/dist/vendor/contracts/index.js +22 -0
  35. package/dist/vendor/contracts/mission.d.ts +102 -0
  36. package/dist/vendor/contracts/mission.js +64 -0
  37. package/dist/vendor/contracts/operator.d.ts +3 -1
  38. package/dist/vendor/contracts/operator.js +2 -1
  39. package/dist/vendor/contracts/verified-handoff.d.ts +100 -0
  40. package/dist/vendor/contracts/verified-handoff.js +35 -0
  41. package/dist/vendor/core/context-chain-gate.d.ts +89 -0
  42. package/dist/vendor/core/context-chain-gate.js +173 -0
  43. package/dist/vendor/core/context-compiler.d.ts +53 -0
  44. package/dist/vendor/core/context-compiler.js +261 -0
  45. package/dist/vendor/core/context-handoff.d.ts +42 -0
  46. package/dist/vendor/core/context-handoff.js +167 -0
  47. package/dist/vendor/core/context-shadow.d.ts +39 -0
  48. package/dist/vendor/core/context-shadow.js +200 -0
  49. package/dist/vendor/core/exit-signal.d.ts +88 -0
  50. package/dist/vendor/core/exit-signal.js +339 -0
  51. package/dist/vendor/core/exits.d.ts +41 -0
  52. package/dist/vendor/core/exits.js +258 -0
  53. package/dist/vendor/core/index.d.ts +42 -1
  54. package/dist/vendor/core/index.js +1086 -662
  55. package/dist/vendor/core/mission/aggregate.d.ts +27 -0
  56. package/dist/vendor/core/mission/aggregate.js +42 -0
  57. package/dist/vendor/core/mission/index.d.ts +2 -0
  58. package/dist/vendor/core/mission/index.js +1 -0
  59. package/dist/vendor/core/persistence/compiler.d.ts +8 -0
  60. package/dist/vendor/core/persistence/compiler.js +156 -0
  61. package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
  62. package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
  63. package/dist/vendor/core/persistence/index.d.ts +4 -0
  64. package/dist/vendor/core/persistence/index.js +2 -0
  65. package/dist/vendor/core/persistence/ledger.d.ts +1 -1
  66. package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
  67. package/dist/vendor/core/persistence/mission-store.js +281 -0
  68. package/dist/vendor/core/policy.d.ts +2 -0
  69. package/dist/vendor/core/policy.js +5 -2
  70. package/dist/vendor/core/rollback.js +15 -1
  71. package/dist/vendor/core/savings.d.ts +13 -0
  72. package/dist/vendor/core/savings.js +22 -0
  73. package/dist/vendor/core/termination-store.d.ts +15 -0
  74. package/dist/vendor/core/termination-store.js +44 -0
  75. package/dist/vendor/core/verified-handoff.d.ts +67 -0
  76. package/dist/vendor/core/verified-handoff.js +210 -0
  77. package/package.json +76 -83
  78. package/server.json +21 -21
  79. package/LICENSE +0 -216
  80. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
  81. package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
  82. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
  83. package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
  84. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
  85. package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
  86. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
  87. package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
  88. package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
  89. package/dist/vendor/adapters/adapters/src/index.js +0 -8
  90. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
  91. package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
  92. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
  93. package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
  94. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
  95. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
  96. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
  97. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
  98. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
  99. package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
  100. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
  101. package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
  102. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
  103. package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
  104. package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
  105. package/dist/vendor/adapters/contracts/src/governance.js +0 -11
  106. package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
  107. package/dist/vendor/adapters/contracts/src/identity.js +0 -30
  108. package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
  109. package/dist/vendor/adapters/contracts/src/index.js +0 -225
  110. package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
  111. package/dist/vendor/adapters/contracts/src/operator.js +0 -11
  112. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
  113. package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
  114. package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
  115. package/dist/vendor/adapters/core/src/compiler.js +0 -52
  116. package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
  117. package/dist/vendor/adapters/core/src/context-graph.js +0 -371
  118. package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
  119. package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
  120. package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
  121. package/dist/vendor/adapters/core/src/grounding.js +0 -273
  122. package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
  123. package/dist/vendor/adapters/core/src/identity.js +0 -86
  124. package/dist/vendor/adapters/core/src/index.d.ts +0 -186
  125. package/dist/vendor/adapters/core/src/index.js +0 -1495
  126. package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
  127. package/dist/vendor/adapters/core/src/leash.js +0 -497
  128. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
  129. package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
  130. package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
  131. package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
  132. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
  133. package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
  134. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
  135. package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
  136. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
  137. package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
  138. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
  139. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
  140. package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
  141. package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
  142. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
  143. package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
  144. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
  145. package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
  146. package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
  147. package/dist/vendor/adapters/core/src/policy.js +0 -704
  148. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
  149. package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
  150. package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
  151. package/dist/vendor/adapters/core/src/rollback.js +0 -253
  152. package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
  153. package/dist/vendor/adapters/core/src/routing.js +0 -336
  154. package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
  155. package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
  156. package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
  157. package/dist/vendor/adapters/core/src/trajectory.js +0 -295
  158. package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
  159. package/dist/vendor/adapters/stub-agent-cli.js +0 -40
  160. package/dist/vendor/adapters/verifier-only.d.ts +0 -9
  161. package/dist/vendor/adapters/verifier-only.js +0 -63
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Mission Store — C2 durable persistence.
3
+ *
4
+ * Layout under <runsRoot>/missions/<missionId>/:
5
+ * mission.json — rebuildable snapshot/cache (NOT the authority)
6
+ * ledger.jsonl — append-only, SHA-256 hash-chained event log (authority)
7
+ * ledger-chain.json — chain head hash for tamper detection
8
+ * .lock — cross-process exclusive lock (created with O_EXCL)
9
+ *
10
+ * Rules:
11
+ * - Ledger is always written before mission.json is updated.
12
+ * - mission.json is written atomically (temp file → rename).
13
+ * - Cross-process lock is held during every write; stale locks (>8s) are removed.
14
+ * - CAS revision must match before any write is accepted.
15
+ * - Corrupt or unverifiable ledger data fails closed.
16
+ * - Workspace isolation: all paths are under caller-supplied runsRoot.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { constants, open } from "node:fs/promises";
20
+ import { appendFile, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
21
+ import { join } from "node:path";
22
+ import { MISSION_SCHEMA_VERSION, isMissionTransitionAllowed } from "../../contracts/index.js";
23
+ // ─── Paths ────────────────────────────────────────────────────────────────────
24
+ export function missionDir(runsRoot, missionId) {
25
+ return join(runsRoot, "missions", missionId);
26
+ }
27
+ function missionJsonPath(runsRoot, missionId) {
28
+ return join(missionDir(runsRoot, missionId), "mission.json");
29
+ }
30
+ function ledgerPath(runsRoot, missionId) {
31
+ return join(missionDir(runsRoot, missionId), "ledger.jsonl");
32
+ }
33
+ function chainPath(runsRoot, missionId) {
34
+ return join(missionDir(runsRoot, missionId), "ledger-chain.json");
35
+ }
36
+ function lockPath(runsRoot, missionId) {
37
+ return join(missionDir(runsRoot, missionId), ".lock");
38
+ }
39
+ // ─── Hash chain ───────────────────────────────────────────────────────────────
40
+ function sha256(value) {
41
+ return createHash("sha256").update(value, "utf8").digest("hex");
42
+ }
43
+ async function readChainHead(runsRoot, missionId) {
44
+ const raw = await readFile(chainPath(runsRoot, missionId), "utf8").catch(() => null);
45
+ if (raw === null)
46
+ return { headHash: "root", entryCount: 0 };
47
+ return JSON.parse(raw);
48
+ }
49
+ async function appendLedgerEntry(runsRoot, missionId, event) {
50
+ const line = JSON.stringify(event);
51
+ const head = await readChainHead(runsRoot, missionId);
52
+ const newHash = sha256(`${head.headHash}\n${line}`);
53
+ const newHead = { headHash: newHash, entryCount: head.entryCount + 1 };
54
+ // Append event first, then update chain head
55
+ await appendFile(ledgerPath(runsRoot, missionId), `${line}\n`, "utf8");
56
+ await atomicWrite(chainPath(runsRoot, missionId), JSON.stringify(newHead, null, 2));
57
+ }
58
+ // ─── Atomic write ─────────────────────────────────────────────────────────────
59
+ async function atomicWrite(filePath, content) {
60
+ const tmp = `${filePath}.tmp`;
61
+ await writeFile(tmp, content, "utf8");
62
+ await rename(tmp, filePath);
63
+ }
64
+ // ─── Cross-process lock ───────────────────────────────────────────────────────
65
+ const LOCK_STALE_MS = 8_000;
66
+ const LOCK_RETRY_INTERVAL_MS = 50;
67
+ const LOCK_MAX_RETRIES = 60; // 3s total
68
+ async function acquireLock(runsRoot, missionId) {
69
+ const lp = lockPath(runsRoot, missionId);
70
+ for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
71
+ try {
72
+ // O_EXCL — fails if file exists (atomic on all POSIX and Windows NTFS)
73
+ const fh = await open(lp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
74
+ await fh.write(String(Date.now()));
75
+ await fh.close();
76
+ return;
77
+ }
78
+ catch {
79
+ // Lock exists — check if stale
80
+ try {
81
+ const st = await stat(lp);
82
+ const ageMs = Date.now() - st.mtimeMs;
83
+ if (ageMs > LOCK_STALE_MS) {
84
+ await rm(lp, { force: true });
85
+ continue; // retry immediately after removing stale lock
86
+ }
87
+ }
88
+ catch {
89
+ // Lock disappeared between check and stat — retry
90
+ continue;
91
+ }
92
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_INTERVAL_MS));
93
+ }
94
+ }
95
+ throw new Error(`mission-store: could not acquire lock for ${missionId} after ${LOCK_MAX_RETRIES} retries`);
96
+ }
97
+ async function releaseLock(runsRoot, missionId) {
98
+ await rm(lockPath(runsRoot, missionId), { force: true });
99
+ }
100
+ async function withLock(runsRoot, missionId, fn) {
101
+ await acquireLock(runsRoot, missionId);
102
+ try {
103
+ return await fn();
104
+ }
105
+ finally {
106
+ await releaseLock(runsRoot, missionId);
107
+ }
108
+ }
109
+ // ─── Event ID factory ─────────────────────────────────────────────────────────
110
+ let _seq = 0;
111
+ function makeEventId(missionId) {
112
+ return `evt_${missionId.slice(0, 8)}_${Date.now()}_${(_seq++).toString().padStart(4, "0")}`;
113
+ }
114
+ // ─── Read mission ─────────────────────────────────────────────────────────────
115
+ /**
116
+ * Read the current mission snapshot.
117
+ * Returns null when no mission exists at this path.
118
+ * Fails closed when the snapshot schema version is not recognised.
119
+ */
120
+ export async function readMission(runsRoot, missionId) {
121
+ const raw = await readFile(missionJsonPath(runsRoot, missionId), "utf8").catch(() => null);
122
+ if (raw === null)
123
+ return null;
124
+ const record = JSON.parse(raw);
125
+ if (record.schemaVersion !== MISSION_SCHEMA_VERSION) {
126
+ throw new Error(`mission-store: unsupported schema "${record.schemaVersion}" for mission ${missionId}`);
127
+ }
128
+ return record;
129
+ }
130
+ // ─── Read ledger ──────────────────────────────────────────────────────────────
131
+ export async function readMissionLedger(runsRoot, missionId) {
132
+ const raw = await readFile(ledgerPath(runsRoot, missionId), "utf8").catch(() => "");
133
+ return raw
134
+ .split(/\r?\n/u)
135
+ .map((l) => l.trim())
136
+ .filter(Boolean)
137
+ .map((l) => JSON.parse(l));
138
+ }
139
+ /**
140
+ * Verify the stored chain head matches a full replay of the ledger.
141
+ * Returns ok=false when the ledger has been tampered with or entries are missing.
142
+ */
143
+ export async function verifyMissionLedger(runsRoot, missionId) {
144
+ const events = await readMissionLedger(runsRoot, missionId);
145
+ const stored = await readChainHead(runsRoot, missionId);
146
+ let hash = "root";
147
+ for (const event of events) {
148
+ hash = sha256(`${hash}\n${JSON.stringify(event)}`);
149
+ }
150
+ if (hash !== stored.headHash) {
151
+ return {
152
+ ok: false,
153
+ reason: `chain_mismatch: expected ${stored.headHash}, replayed ${hash}`,
154
+ entryCount: events.length
155
+ };
156
+ }
157
+ if (events.length !== stored.entryCount) {
158
+ return {
159
+ ok: false,
160
+ reason: `count_mismatch: stored ${stored.entryCount}, actual ${events.length}`,
161
+ entryCount: events.length
162
+ };
163
+ }
164
+ return { ok: true, entryCount: events.length };
165
+ }
166
+ // ─── Create mission ───────────────────────────────────────────────────────────
167
+ export async function createMission(runsRoot, mission) {
168
+ const dir = missionDir(runsRoot, mission.missionId);
169
+ await mkdir(dir, { recursive: true });
170
+ await withLock(runsRoot, mission.missionId, async () => {
171
+ // Fail if already exists
172
+ const existing = await readMission(runsRoot, mission.missionId);
173
+ if (existing !== null) {
174
+ throw new Error(`mission-store: mission ${mission.missionId} already exists`);
175
+ }
176
+ const event = {
177
+ eventId: makeEventId(mission.missionId),
178
+ kind: "mission.created",
179
+ missionId: mission.missionId,
180
+ timestamp: mission.createdAt,
181
+ payload: { status: mission.status, ownerId: mission.ownerId }
182
+ };
183
+ await appendLedgerEntry(runsRoot, mission.missionId, event);
184
+ await atomicWrite(missionJsonPath(runsRoot, mission.missionId), JSON.stringify(mission, null, 2));
185
+ });
186
+ }
187
+ export async function attachRun(runsRoot, missionId, options) {
188
+ return withLock(runsRoot, missionId, async () => {
189
+ const mission = await readMission(runsRoot, missionId);
190
+ if (mission === null)
191
+ throw new Error(`mission-store: mission ${missionId} not found`);
192
+ if (mission.revision !== options.expectedRevision) {
193
+ throw new Error(`mission-store: CAS revision mismatch for ${missionId}: ` +
194
+ `expected ${options.expectedRevision}, found ${mission.revision}`);
195
+ }
196
+ const ts = options.now ? options.now() : new Date().toISOString();
197
+ const link = {
198
+ loopId: options.loopId,
199
+ role: options.role,
200
+ attachedAt: ts,
201
+ ...(options.verifiedOutcome !== undefined ? { verifiedOutcome: options.verifiedOutcome } : {}),
202
+ ...(options.actualUsd !== undefined ? { actualUsd: options.actualUsd } : {})
203
+ };
204
+ const newCost = {
205
+ totalActualUsd: mission.cost.totalActualUsd + (options.actualUsd ?? 0),
206
+ verifiedOutcomeCount: mission.cost.verifiedOutcomeCount + (options.verifiedOutcome === true ? 1 : 0),
207
+ totalRunCount: mission.cost.totalRunCount + 1
208
+ };
209
+ const updated = {
210
+ ...mission,
211
+ revision: mission.revision + 1,
212
+ runLinks: [...mission.runLinks, link],
213
+ cost: newCost,
214
+ updatedAt: ts
215
+ };
216
+ const event = {
217
+ eventId: makeEventId(missionId),
218
+ kind: "mission.run_attached",
219
+ missionId,
220
+ timestamp: ts,
221
+ payload: {
222
+ loopId: options.loopId,
223
+ role: options.role,
224
+ verifiedOutcome: options.verifiedOutcome,
225
+ actualUsd: options.actualUsd
226
+ }
227
+ };
228
+ await appendLedgerEntry(runsRoot, missionId, event);
229
+ await atomicWrite(missionJsonPath(runsRoot, missionId), JSON.stringify(updated, null, 2));
230
+ return updated;
231
+ });
232
+ }
233
+ export async function changeMissionStatus(runsRoot, missionId, options) {
234
+ return withLock(runsRoot, missionId, async () => {
235
+ const mission = await readMission(runsRoot, missionId);
236
+ if (mission === null)
237
+ throw new Error(`mission-store: mission ${missionId} not found`);
238
+ if (mission.revision !== options.expectedRevision) {
239
+ throw new Error(`mission-store: CAS revision mismatch for ${missionId}: ` +
240
+ `expected ${options.expectedRevision}, found ${mission.revision}`);
241
+ }
242
+ if (!isMissionTransitionAllowed(mission.status, options.toStatus)) {
243
+ throw new Error(`mission-store: transition ${mission.status} → ${options.toStatus} is not allowed`);
244
+ }
245
+ const ts = options.now ? options.now() : new Date().toISOString();
246
+ const updated = {
247
+ ...mission,
248
+ revision: mission.revision + 1,
249
+ status: options.toStatus,
250
+ updatedAt: ts,
251
+ ...(options.decision && options.decidedBy
252
+ ? {
253
+ outcome: {
254
+ decision: options.decision,
255
+ decidedAt: ts,
256
+ decidedBy: options.decidedBy,
257
+ ...(options.note ? { note: options.note } : {})
258
+ }
259
+ }
260
+ : {})
261
+ };
262
+ const eventKind = options.toStatus === "shipped" || options.toStatus === "killed" || options.toStatus === "rolled_back"
263
+ ? "mission.closed"
264
+ : "mission.status_changed";
265
+ const event = {
266
+ eventId: makeEventId(missionId),
267
+ kind: eventKind,
268
+ missionId,
269
+ timestamp: ts,
270
+ payload: {
271
+ from: mission.status,
272
+ to: options.toStatus,
273
+ decision: options.decision,
274
+ decidedBy: options.decidedBy
275
+ }
276
+ };
277
+ await appendLedgerEntry(runsRoot, missionId, event);
278
+ await atomicWrite(missionJsonPath(runsRoot, missionId), JSON.stringify(updated, null, 2));
279
+ return updated;
280
+ });
281
+ }
@@ -138,6 +138,8 @@ export interface PatchDecisionInput {
138
138
  safetyViolationCount?: number;
139
139
  scopeViolationCount?: number;
140
140
  changedFileCount?: number;
141
+ mutationRequired?: boolean;
142
+ definitionOfDonePreSatisfied?: boolean;
141
143
  diffNovelty?: number;
142
144
  diffStats?: {
143
145
  filesChanged: number;
@@ -514,6 +514,9 @@ export function scorePatchDecision(input) {
514
514
  const safetyViolationCount = input.safetyViolationCount ?? 0;
515
515
  const changedFileEvidenceAvailable = input.changedFileCount !== undefined;
516
516
  const changedFileCount = input.changedFileCount ?? 0;
517
+ const auditablePreSatisfiedNoChange = input.verificationPassed &&
518
+ input.mutationRequired === true &&
519
+ input.definitionOfDonePreSatisfied === true;
517
520
  const noveltyScore = input.diffNovelty ?? (changedFileCount > 0 ? 1 : 0);
518
521
  const diffRiskScore = computeDiffRiskScore(input.diffStats);
519
522
  const costUsd = roundUsd(input.costUsd ?? 0);
@@ -527,7 +530,7 @@ export function scorePatchDecision(input) {
527
530
  if (scopeViolationCount > 0) {
528
531
  reasonCodes.push("scope_violation");
529
532
  }
530
- if (changedFileEvidenceAvailable && changedFileCount === 0) {
533
+ if (changedFileEvidenceAvailable && changedFileCount === 0 && !auditablePreSatisfiedNoChange) {
531
534
  reasonCodes.push("no_code_change");
532
535
  }
533
536
  if (input.humanApprovalRequired) {
@@ -560,7 +563,7 @@ export function scorePatchDecision(input) {
560
563
  if (input.humanApprovalRequired) {
561
564
  score -= 0.25;
562
565
  }
563
- if (changedFileEvidenceAvailable && changedFileCount === 0) {
566
+ if (changedFileEvidenceAvailable && changedFileCount === 0 && !auditablePreSatisfiedNoChange) {
564
567
  score -= 0.35;
565
568
  }
566
569
  if (!input.verificationPassed && noveltyScore < 0.2 && verifierDelta <= 0) {
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
2
3
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { dirname, relative, resolve } from "node:path";
4
5
  export function listAttemptChangedFilesSinceBoundary(input) {
@@ -11,11 +12,24 @@ export function listAttemptChangedFilesSinceBoundary(input) {
11
12
  }
12
13
  const baselineTracked = new Set(input.boundary.trackedDirtyFiles);
13
14
  const baselineUntracked = new Set(input.boundary.untrackedFiles);
15
+ const baselineContentChanges = input.boundary.snapshots
16
+ .filter((snapshot) => snapshot.existed && repoFileDiffersFromSnapshot(input.repoRoot, snapshot))
17
+ .map((snapshot) => snapshot.path);
14
18
  return uniqueSorted([
15
19
  ...repoState.trackedDirtyFiles.filter((filePath) => !baselineTracked.has(filePath)),
16
- ...repoState.untrackedFiles.filter((filePath) => !baselineUntracked.has(filePath))
20
+ ...repoState.untrackedFiles.filter((filePath) => !baselineUntracked.has(filePath)),
21
+ ...baselineContentChanges
17
22
  ]);
18
23
  }
24
+ function repoFileDiffersFromSnapshot(repoRoot, snapshot) {
25
+ try {
26
+ const current = readFileSync(resolveRepoPath(repoRoot, snapshot.path));
27
+ return current.toString("base64") !== snapshot.contentBase64;
28
+ }
29
+ catch {
30
+ return true;
31
+ }
32
+ }
19
33
  export async function captureRollbackBoundary(input) {
20
34
  if (!input.repoRoot) {
21
35
  return undefined;
@@ -0,0 +1,13 @@
1
+ import type { LoopRecord } from "../contracts/index.js";
2
+ import type { ExitDecision } from "./policy.js";
3
+ export interface AvoidedUsdInput {
4
+ lifecycleState: LoopRecord["lifecycleState"];
5
+ actualUsd: number;
6
+ uncontrolledBaselineUsd?: number;
7
+ }
8
+ export declare function calculateAvoidedUsd(input: AvoidedUsdInput): number;
9
+ export declare function calculateLoopAvoidedUsd(input: {
10
+ loop: LoopRecord;
11
+ decision: ExitDecision;
12
+ uncontrolledBaselineUsd?: number;
13
+ }): number;
@@ -0,0 +1,22 @@
1
+ export function calculateAvoidedUsd(input) {
2
+ if (input.lifecycleState !== "completed")
3
+ return 0;
4
+ if (!Number.isFinite(input.actualUsd) || input.actualUsd < 0)
5
+ return 0;
6
+ if (input.uncontrolledBaselineUsd === undefined ||
7
+ !Number.isFinite(input.uncontrolledBaselineUsd) ||
8
+ input.uncontrolledBaselineUsd <= input.actualUsd) {
9
+ return 0;
10
+ }
11
+ return roundUsd(input.uncontrolledBaselineUsd - input.actualUsd);
12
+ }
13
+ export function calculateLoopAvoidedUsd(input) {
14
+ return calculateAvoidedUsd({
15
+ lifecycleState: input.decision.lifecycleState,
16
+ actualUsd: input.loop.cost.actualUsd,
17
+ uncontrolledBaselineUsd: input.uncontrolledBaselineUsd
18
+ });
19
+ }
20
+ function roundUsd(value) {
21
+ return Math.round(value * 100) / 100;
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { TerminationEnvelopeV1 } from "../contracts/index.js";
2
+ /**
3
+ * Atomically persist the termination envelope for a governed run.
4
+ *
5
+ * Crash-safe write sequence (mirrors exit-signal.ts atomic publication):
6
+ * 1. Write complete JSON to a unique tmp file (wx, 0o600) in runDirectory
7
+ * 2. Sync and close the tmp file
8
+ * 3. Hard-link tmp → termination.json (EEXIST → slot already occupied)
9
+ * 4. On EEXIST, read and return the existing envelope
10
+ * 5. Unlink the tmp file in finally (best-effort)
11
+ *
12
+ * First terminal exit path wins — competing callers that hit EEXIST get back
13
+ * the envelope that won the race. This is the A1 idempotency guarantee.
14
+ */
15
+ export declare function persistTerminationEnvelope(runDirectory: string, envelope: TerminationEnvelopeV1): Promise<TerminationEnvelopeV1>;
@@ -0,0 +1,44 @@
1
+ import { link, open, readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ /**
5
+ * Atomically persist the termination envelope for a governed run.
6
+ *
7
+ * Crash-safe write sequence (mirrors exit-signal.ts atomic publication):
8
+ * 1. Write complete JSON to a unique tmp file (wx, 0o600) in runDirectory
9
+ * 2. Sync and close the tmp file
10
+ * 3. Hard-link tmp → termination.json (EEXIST → slot already occupied)
11
+ * 4. On EEXIST, read and return the existing envelope
12
+ * 5. Unlink the tmp file in finally (best-effort)
13
+ *
14
+ * First terminal exit path wins — competing callers that hit EEXIST get back
15
+ * the envelope that won the race. This is the A1 idempotency guarantee.
16
+ */
17
+ export async function persistTerminationEnvelope(runDirectory, envelope) {
18
+ const finalPath = join(runDirectory, "termination.json");
19
+ const tmpPath = join(runDirectory, `.termination-tmp-${randomUUID()}.json`);
20
+ const json = `${JSON.stringify(envelope, null, 2)}\n`;
21
+ let handle;
22
+ try {
23
+ handle = await open(tmpPath, "wx", 0o600);
24
+ await handle.writeFile(json, "utf8");
25
+ await handle.sync();
26
+ await handle.close();
27
+ handle = undefined;
28
+ try {
29
+ await link(tmpPath, finalPath);
30
+ return envelope;
31
+ }
32
+ catch (linkErr) {
33
+ if (linkErr.code !== "EEXIST")
34
+ throw linkErr;
35
+ // Another path won the race — read what's on disk and return it
36
+ const existing = await readFile(finalPath, "utf8");
37
+ return JSON.parse(existing);
38
+ }
39
+ }
40
+ finally {
41
+ await handle?.close().catch(() => undefined);
42
+ await unlink(tmpPath).catch(() => undefined);
43
+ }
44
+ }
@@ -0,0 +1,67 @@
1
+ import type { LoopRecord, ReceiptIntegritySummary, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1 } from "../contracts/index.js";
2
+ export declare function toTestIntegrityVerdict(status: TestIntegrityStatus): TestIntegrityVerdict;
3
+ export interface VerifierExecutionBinding {
4
+ runId: string;
5
+ workspaceId: string;
6
+ cwd: string;
7
+ commands: string[];
8
+ }
9
+ export interface BoundVerifierEvidence {
10
+ passed: boolean;
11
+ binding?: VerifierExecutionBinding;
12
+ steps?: Array<{
13
+ command: string;
14
+ launched: boolean;
15
+ completed?: boolean;
16
+ crashed?: boolean;
17
+ exitCode?: number;
18
+ timedOut?: boolean;
19
+ }>;
20
+ }
21
+ export declare function verifierActuallyPassed(evidence: BoundVerifierEvidence | null | undefined, expected: VerifierExecutionBinding): boolean;
22
+ export declare function resolveVerifiedHandoffOutcome(input: {
23
+ lifecycleState: LoopRecord["lifecycleState"];
24
+ executionStatus?: "completed" | "write_blocked" | "sandbox_blocked" | "policy_rejected" | "failed";
25
+ verificationStatus: BuildVerifiedHandoffInput["verification"]["status"];
26
+ receiptIntegrity: ReceiptIntegritySummary["state"];
27
+ scopeStatus?: VerifiedHandoffScopeV1["status"];
28
+ testIntegrityStatus?: TestIntegrityStatus;
29
+ mutationRequired?: boolean;
30
+ changedFileCount?: number;
31
+ definitionOfDonePreSatisfied?: boolean;
32
+ evidenceContradicted?: boolean;
33
+ unresolvedWorkCount: number;
34
+ }): VerifiedHandoffOutcome;
35
+ export interface BuildVerifiedHandoffInput {
36
+ loop: LoopRecord;
37
+ generatedAt?: string;
38
+ receiptIntegrity: ReceiptIntegritySummary;
39
+ verification: {
40
+ status: "passed" | "failed" | "contradicted" | "not_run";
41
+ summary: string;
42
+ steps: Array<{
43
+ command: string;
44
+ launched: boolean;
45
+ completed?: boolean;
46
+ crashed?: boolean;
47
+ exitCode?: number;
48
+ timedOut?: boolean;
49
+ detail?: string;
50
+ }>;
51
+ warnings: string[];
52
+ binding?: VerifierExecutionBinding;
53
+ };
54
+ executionStatus?: "completed" | "write_blocked" | "sandbox_blocked" | "policy_rejected" | "failed";
55
+ mutationRequired?: boolean;
56
+ definitionOfDonePreSatisfied?: boolean;
57
+ evidenceContradicted?: boolean;
58
+ changedFiles?: string[];
59
+ scope?: Partial<VerifiedHandoffScopeV1>;
60
+ testIntegrity?: Partial<VerifiedHandoffTestIntegrityV1>;
61
+ requirements?: VerifiedHandoffRequirementV1[];
62
+ unresolvedWork?: string[];
63
+ stopReason?: string;
64
+ recovery?: Partial<VerifiedHandoffRecoveryV1>;
65
+ nextAction: string;
66
+ }
67
+ export declare function buildVerifiedHandoff(input: BuildVerifiedHandoffInput): VerifiedHandoffV1;