@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.
- package/README.md +90 -106
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/resources.d.ts +1 -2
- package/dist/resources.js +190 -238
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +16 -11
- package/dist/server.d.ts +23 -0
- package/dist/server.js +152 -7
- package/dist/tools/run-dossier.d.ts +3 -1
- package/dist/tools/run-dossier.js +40 -2
- package/dist/tools/run-loop.d.ts +1 -4
- package/dist/tools/run-loop.js +6 -15
- package/dist/tools/tool-support.d.ts +2 -1
- package/dist/tools/tool-support.js +1 -12
- package/dist/vendor/adapters/claude-cli.js +38 -35
- package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
- package/dist/vendor/adapters/cli-bridge.js +54 -13
- package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
- package/dist/vendor/adapters/codex-launcher.js +118 -19
- package/dist/vendor/adapters/index.d.ts +1 -3
- package/dist/vendor/adapters/index.js +1 -3
- package/dist/vendor/adapters/openai-compatible.js +57 -18
- package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
- package/dist/vendor/contracts/context-handoff.d.ts +100 -0
- package/dist/vendor/contracts/context-handoff.js +11 -0
- package/dist/vendor/contracts/context-manifest.d.ts +195 -0
- package/dist/vendor/contracts/context-manifest.js +17 -0
- package/dist/vendor/contracts/context-shadow.d.ts +75 -0
- package/dist/vendor/contracts/context-shadow.js +8 -0
- package/dist/vendor/contracts/exits.d.ts +126 -0
- package/dist/vendor/contracts/exits.js +32 -0
- package/dist/vendor/contracts/index.d.ts +39 -2
- package/dist/vendor/contracts/index.js +22 -0
- package/dist/vendor/contracts/mission.d.ts +102 -0
- package/dist/vendor/contracts/mission.js +64 -0
- package/dist/vendor/contracts/operator.d.ts +3 -1
- package/dist/vendor/contracts/operator.js +2 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +100 -0
- package/dist/vendor/contracts/verified-handoff.js +35 -0
- package/dist/vendor/core/context-chain-gate.d.ts +89 -0
- package/dist/vendor/core/context-chain-gate.js +173 -0
- package/dist/vendor/core/context-compiler.d.ts +53 -0
- package/dist/vendor/core/context-compiler.js +261 -0
- package/dist/vendor/core/context-handoff.d.ts +42 -0
- package/dist/vendor/core/context-handoff.js +167 -0
- package/dist/vendor/core/context-shadow.d.ts +39 -0
- package/dist/vendor/core/context-shadow.js +200 -0
- package/dist/vendor/core/exit-signal.d.ts +88 -0
- package/dist/vendor/core/exit-signal.js +339 -0
- package/dist/vendor/core/exits.d.ts +41 -0
- package/dist/vendor/core/exits.js +258 -0
- package/dist/vendor/core/index.d.ts +42 -1
- package/dist/vendor/core/index.js +1086 -662
- package/dist/vendor/core/mission/aggregate.d.ts +27 -0
- package/dist/vendor/core/mission/aggregate.js +42 -0
- package/dist/vendor/core/mission/index.d.ts +2 -0
- package/dist/vendor/core/mission/index.js +1 -0
- package/dist/vendor/core/persistence/compiler.d.ts +8 -0
- package/dist/vendor/core/persistence/compiler.js +156 -0
- package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
- package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
- package/dist/vendor/core/persistence/index.d.ts +4 -0
- package/dist/vendor/core/persistence/index.js +2 -0
- package/dist/vendor/core/persistence/ledger.d.ts +1 -1
- package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
- package/dist/vendor/core/persistence/mission-store.js +281 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +5 -2
- package/dist/vendor/core/rollback.js +15 -1
- package/dist/vendor/core/savings.d.ts +13 -0
- package/dist/vendor/core/savings.js +22 -0
- package/dist/vendor/core/termination-store.d.ts +15 -0
- package/dist/vendor/core/termination-store.js +44 -0
- package/dist/vendor/core/verified-handoff.d.ts +67 -0
- package/dist/vendor/core/verified-handoff.js +210 -0
- package/package.json +76 -83
- package/server.json +21 -21
- package/LICENSE +0 -216
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
- package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
- package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
- package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/index.js +0 -8
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
- package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
- package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
- package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
- package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
- package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
- package/dist/vendor/adapters/contracts/src/governance.js +0 -11
- package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
- package/dist/vendor/adapters/contracts/src/identity.js +0 -30
- package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
- package/dist/vendor/adapters/contracts/src/index.js +0 -225
- package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
- package/dist/vendor/adapters/contracts/src/operator.js +0 -11
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
- package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
- package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
- package/dist/vendor/adapters/core/src/compiler.js +0 -52
- package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
- package/dist/vendor/adapters/core/src/context-graph.js +0 -371
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
- package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
- package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
- package/dist/vendor/adapters/core/src/grounding.js +0 -273
- package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
- package/dist/vendor/adapters/core/src/identity.js +0 -86
- package/dist/vendor/adapters/core/src/index.d.ts +0 -186
- package/dist/vendor/adapters/core/src/index.js +0 -1495
- package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
- package/dist/vendor/adapters/core/src/leash.js +0 -497
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
- package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
- package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
- package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
- package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
- package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
- package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
- package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
- package/dist/vendor/adapters/core/src/policy.js +0 -704
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
- package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
- package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
- package/dist/vendor/adapters/core/src/rollback.js +0 -253
- package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
- package/dist/vendor/adapters/core/src/routing.js +0 -336
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
- package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
- package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
- package/dist/vendor/adapters/core/src/trajectory.js +0 -295
- package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
- package/dist/vendor/adapters/stub-agent-cli.js +0 -40
- package/dist/vendor/adapters/verifier-only.d.ts +0 -9
- 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;
|