@martinloop/mcp 0.3.6 → 0.3.8
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/LICENSE +216 -0
- package/README.md +105 -97
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +1 -1
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/resources.d.ts +2 -1
- package/dist/resources.js +251 -197
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +11 -16
- package/dist/server.d.ts +0 -23
- package/dist/server.js +7 -152
- package/dist/tools/preflight.d.ts +2 -0
- package/dist/tools/preflight.js +42 -2
- package/dist/tools/run-loop.d.ts +2 -1
- package/dist/tools/run-loop.js +5 -1
- package/dist/tools/tool-support.js +12 -1
- package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +146 -0
- package/dist/vendor/adapters/adapters/src/claude-cli.js +1100 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +103 -0
- package/dist/vendor/adapters/adapters/src/cli-bridge.js +569 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +78 -0
- package/dist/vendor/adapters/adapters/src/codex-launcher.js +663 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +10 -0
- package/dist/vendor/adapters/adapters/src/direct-provider.js +40 -0
- package/dist/vendor/adapters/adapters/src/index.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/index.js +8 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +62 -0
- package/dist/vendor/adapters/adapters/src/openai-compatible.js +306 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +17 -0
- package/dist/vendor/adapters/adapters/src/runtime-support.js +59 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +40 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +8 -0
- package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +12 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +9 -0
- package/dist/vendor/adapters/adapters/src/verifier-only.js +63 -0
- package/dist/vendor/adapters/claude-cli.js +130 -68
- package/dist/vendor/adapters/codex-launcher.js +10 -1
- package/dist/vendor/adapters/contracts/src/context-graph.d.ts +48 -0
- package/dist/vendor/adapters/contracts/src/context-graph.js +26 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +78 -0
- package/dist/vendor/adapters/contracts/src/execution-policy.js +43 -0
- package/dist/vendor/adapters/contracts/src/governance.d.ts +24 -0
- package/dist/vendor/adapters/contracts/src/governance.js +11 -0
- package/dist/vendor/adapters/contracts/src/identity.d.ts +29 -0
- package/dist/vendor/adapters/contracts/src/identity.js +30 -0
- package/dist/vendor/adapters/contracts/src/index.d.ts +412 -0
- package/dist/vendor/adapters/contracts/src/index.js +225 -0
- package/dist/vendor/adapters/contracts/src/operator.d.ts +19 -0
- package/dist/vendor/adapters/contracts/src/operator.js +11 -0
- package/dist/vendor/adapters/contracts/src/trajectory.d.ts +26 -0
- package/dist/vendor/adapters/contracts/src/trajectory.js +27 -0
- package/dist/vendor/adapters/core/src/compiler.d.ts +52 -0
- package/dist/vendor/adapters/core/src/compiler.js +52 -0
- package/dist/vendor/adapters/core/src/context-graph.d.ts +3 -0
- package/dist/vendor/adapters/core/src/context-graph.js +371 -0
- package/dist/vendor/adapters/core/src/context-integrity.d.ts +26 -0
- package/dist/vendor/adapters/core/src/context-integrity.js +80 -0
- package/dist/vendor/adapters/core/src/grounding.d.ts +38 -0
- package/dist/vendor/adapters/core/src/grounding.js +273 -0
- package/dist/vendor/adapters/core/src/identity.d.ts +20 -0
- package/dist/vendor/adapters/core/src/identity.js +86 -0
- package/dist/vendor/adapters/core/src/index.d.ts +186 -0
- package/dist/vendor/adapters/core/src/index.js +1495 -0
- package/dist/vendor/adapters/core/src/leash.d.ts +48 -0
- package/dist/vendor/adapters/core/src/leash.js +497 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +18 -0
- package/dist/vendor/adapters/core/src/persistence/compiler.js +34 -0
- package/dist/vendor/adapters/core/src/persistence/index.d.ts +10 -0
- package/dist/vendor/adapters/core/src/persistence/index.js +5 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +39 -0
- package/dist/vendor/adapters/core/src/persistence/integrity.js +252 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +23 -0
- package/dist/vendor/adapters/core/src/persistence/ledger.js +9 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/adapters/core/src/persistence/memory-store.js +115 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +66 -0
- package/dist/vendor/adapters/core/src/persistence/runs-reader.js +140 -0
- package/dist/vendor/adapters/core/src/persistence/store.d.ts +89 -0
- package/dist/vendor/adapters/core/src/persistence/store.js +125 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/adapters/core/src/persistence/trace-store.js +125 -0
- package/dist/vendor/adapters/core/src/policy-compiler.d.ts +2 -0
- package/dist/vendor/adapters/core/src/policy-compiler.js +195 -0
- package/dist/vendor/adapters/core/src/policy.d.ts +156 -0
- package/dist/vendor/adapters/core/src/policy.js +704 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +53 -0
- package/dist/vendor/adapters/core/src/repo-analyzer.js +263 -0
- package/dist/vendor/adapters/core/src/rollback.d.ts +15 -0
- package/dist/vendor/adapters/core/src/rollback.js +253 -0
- package/dist/vendor/adapters/core/src/routing.d.ts +82 -0
- package/dist/vendor/adapters/core/src/routing.js +336 -0
- package/dist/vendor/adapters/core/src/test-integrity.d.ts +64 -0
- package/dist/vendor/adapters/core/src/test-integrity.js +197 -0
- package/dist/vendor/adapters/core/src/trajectory.d.ts +18 -0
- package/dist/vendor/adapters/core/src/trajectory.js +295 -0
- package/dist/vendor/adapters/openai-compatible.js +6 -6
- package/dist/vendor/adapters/verifier-only.js +2 -2
- package/dist/vendor/contracts/delivery.d.ts +47 -0
- package/dist/vendor/contracts/delivery.js +18 -0
- package/dist/vendor/contracts/index.d.ts +3 -1
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/core/compiler.js +4 -9
- package/dist/vendor/core/delivery/index.d.ts +6 -0
- package/dist/vendor/core/delivery/index.js +4 -0
- package/dist/vendor/core/delivery/message-client.d.ts +18 -0
- package/dist/vendor/core/delivery/message-client.js +38 -0
- package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
- package/dist/vendor/core/delivery/message-ledger.js +97 -0
- package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
- package/dist/vendor/core/delivery/message-schema.js +111 -0
- package/dist/vendor/core/delivery/update-check.d.ts +19 -0
- package/dist/vendor/core/delivery/update-check.js +107 -0
- package/dist/vendor/core/index.d.ts +2 -0
- package/dist/vendor/core/index.js +3 -88
- package/dist/vendor/core/rollback.js +2 -3
- package/package.json +83 -76
- package/server.json +21 -21
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { cloneCircuitBreakDecision, cloneTrajectoryAssessment } from "../../../contracts/index.js";
|
|
2
|
+
const STOP_WORDS = new Set([
|
|
3
|
+
"about",
|
|
4
|
+
"after",
|
|
5
|
+
"again",
|
|
6
|
+
"along",
|
|
7
|
+
"also",
|
|
8
|
+
"been",
|
|
9
|
+
"before",
|
|
10
|
+
"being",
|
|
11
|
+
"between",
|
|
12
|
+
"could",
|
|
13
|
+
"failed",
|
|
14
|
+
"failure",
|
|
15
|
+
"from",
|
|
16
|
+
"have",
|
|
17
|
+
"into",
|
|
18
|
+
"just",
|
|
19
|
+
"keep",
|
|
20
|
+
"made",
|
|
21
|
+
"make",
|
|
22
|
+
"need",
|
|
23
|
+
"only",
|
|
24
|
+
"over",
|
|
25
|
+
"same",
|
|
26
|
+
"should",
|
|
27
|
+
"still",
|
|
28
|
+
"than",
|
|
29
|
+
"that",
|
|
30
|
+
"their",
|
|
31
|
+
"them",
|
|
32
|
+
"then",
|
|
33
|
+
"there",
|
|
34
|
+
"these",
|
|
35
|
+
"they",
|
|
36
|
+
"this",
|
|
37
|
+
"those",
|
|
38
|
+
"touching",
|
|
39
|
+
"without",
|
|
40
|
+
"would"
|
|
41
|
+
]);
|
|
42
|
+
const STALL_FAILURE_CLASSES = new Set([
|
|
43
|
+
"logic_error",
|
|
44
|
+
"verification_failure",
|
|
45
|
+
"repo_grounding_failure",
|
|
46
|
+
"scope_creep",
|
|
47
|
+
"no_progress"
|
|
48
|
+
]);
|
|
49
|
+
export function assessTrajectory(input) {
|
|
50
|
+
const attempts = input.attempts.filter((attempt) => attempt.index > 0);
|
|
51
|
+
if (attempts.length === 0) {
|
|
52
|
+
return cloneTrajectoryAssessment({
|
|
53
|
+
status: "healthy",
|
|
54
|
+
confidence: "low",
|
|
55
|
+
summary: "No prior attempts exist, so Martin has no drift evidence yet.",
|
|
56
|
+
signals: []
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const signals = [];
|
|
60
|
+
const recentAttempts = attempts.slice(-4);
|
|
61
|
+
const lastThree = recentAttempts.slice(-3);
|
|
62
|
+
const lastTwo = recentAttempts.slice(-2);
|
|
63
|
+
const oscillation = detectFailureOscillation(lastThree);
|
|
64
|
+
if (oscillation) {
|
|
65
|
+
signals.push(oscillation);
|
|
66
|
+
}
|
|
67
|
+
const repetition = detectAttemptRepetition(lastThree);
|
|
68
|
+
if (repetition) {
|
|
69
|
+
signals.push(repetition);
|
|
70
|
+
}
|
|
71
|
+
const verificationStall = detectVerificationStall(lastTwo);
|
|
72
|
+
if (verificationStall) {
|
|
73
|
+
signals.push(verificationStall);
|
|
74
|
+
}
|
|
75
|
+
const objectiveDrift = detectObjectiveDrift({
|
|
76
|
+
objective: input.objective,
|
|
77
|
+
verificationPlan: input.verificationPlan,
|
|
78
|
+
attempts: lastTwo
|
|
79
|
+
});
|
|
80
|
+
if (objectiveDrift) {
|
|
81
|
+
signals.push(objectiveDrift);
|
|
82
|
+
}
|
|
83
|
+
const status = deriveTrajectoryStatus(signals);
|
|
84
|
+
const confidence = deriveTrajectoryConfidence(signals);
|
|
85
|
+
const recommendedIntervention = deriveRecommendedIntervention(signals);
|
|
86
|
+
if (signals.length === 0) {
|
|
87
|
+
return cloneTrajectoryAssessment({
|
|
88
|
+
status: "healthy",
|
|
89
|
+
confidence: "medium",
|
|
90
|
+
summary: "Recent attempts stay aligned enough that Martin does not need a circuit break yet.",
|
|
91
|
+
signals: []
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const summary = summarizeTrajectory(status, signals);
|
|
95
|
+
return cloneTrajectoryAssessment({
|
|
96
|
+
status,
|
|
97
|
+
confidence,
|
|
98
|
+
summary,
|
|
99
|
+
signals,
|
|
100
|
+
...(recommendedIntervention ? { recommendedIntervention } : {})
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function decideCircuitBreak(input) {
|
|
104
|
+
const assessment = assessTrajectory(input);
|
|
105
|
+
const remainingIterations = input.remainingIterations ?? Number.POSITIVE_INFINITY;
|
|
106
|
+
const stopForHighSignal = assessment.signals.some((signal) => signal.severity === "high");
|
|
107
|
+
const stopForLateDrift = assessment.status === "drifting" &&
|
|
108
|
+
remainingIterations <= 1 &&
|
|
109
|
+
assessment.signals.some((signal) => signal.code === "objective_drift");
|
|
110
|
+
const stopForLateStall = assessment.status === "watch" &&
|
|
111
|
+
remainingIterations <= 1 &&
|
|
112
|
+
assessment.signals.some((signal) => signal.code === "verification_stall");
|
|
113
|
+
const shouldStop = stopForHighSignal || stopForLateDrift || stopForLateStall;
|
|
114
|
+
return cloneCircuitBreakDecision({
|
|
115
|
+
shouldStop,
|
|
116
|
+
reason: shouldStop
|
|
117
|
+
? assessment.summary
|
|
118
|
+
: "Trajectory signals do not justify a hard circuit break yet.",
|
|
119
|
+
assessment,
|
|
120
|
+
...(assessment.recommendedIntervention
|
|
121
|
+
? { recommendedIntervention: assessment.recommendedIntervention }
|
|
122
|
+
: {})
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function detectFailureOscillation(attempts) {
|
|
126
|
+
if (attempts.length < 3) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const [first, second, third] = attempts;
|
|
130
|
+
if (!first?.failureClass ||
|
|
131
|
+
!second?.failureClass ||
|
|
132
|
+
!third?.failureClass ||
|
|
133
|
+
first.failureClass === second.failureClass ||
|
|
134
|
+
first.failureClass !== third.failureClass) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
code: "failure_oscillation",
|
|
139
|
+
severity: "high",
|
|
140
|
+
summary: `Attempts ${first.index}, ${second.index}, and ${third.index} oscillate between failure classes instead of converging.`,
|
|
141
|
+
attemptIndexes: [first.index, second.index, third.index]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function detectAttemptRepetition(attempts) {
|
|
145
|
+
if (attempts.length < 3) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const tokenSets = attempts.map((attempt) => toTokenSet(attempt.summary));
|
|
149
|
+
if (tokenSets.some((tokens) => tokens.size < 4)) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const pairScores = [
|
|
153
|
+
jaccardSimilarity(tokenSets[0], tokenSets[1]),
|
|
154
|
+
jaccardSimilarity(tokenSets[1], tokenSets[2]),
|
|
155
|
+
jaccardSimilarity(tokenSets[0], tokenSets[2])
|
|
156
|
+
];
|
|
157
|
+
if (pairScores.every((score) => score >= 0.55)) {
|
|
158
|
+
const matchedTerms = intersectSets(tokenSets[0], new Set(intersectSets(tokenSets[1], tokenSets[2])));
|
|
159
|
+
return {
|
|
160
|
+
code: "attempt_repetition",
|
|
161
|
+
severity: "high",
|
|
162
|
+
summary: `Attempts ${attempts.map((attempt) => attempt.index).join(", ")} are materially repetitive and are not exploring a new path.`,
|
|
163
|
+
attemptIndexes: attempts.map((attempt) => attempt.index),
|
|
164
|
+
...(matchedTerms.length > 0 ? { matchedTerms: matchedTerms.slice(0, 6) } : {})
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
function detectVerificationStall(attempts) {
|
|
170
|
+
if (attempts.length < 2) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const relevant = attempts.filter((attempt) => {
|
|
174
|
+
const failureClass = attempt.failureClass;
|
|
175
|
+
return failureClass !== undefined && STALL_FAILURE_CLASSES.has(failureClass);
|
|
176
|
+
});
|
|
177
|
+
if (relevant.length < 2) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const tokenSets = relevant.map((attempt) => toTokenSet(attempt.summary));
|
|
181
|
+
const overlap = tokenSets
|
|
182
|
+
.slice(1)
|
|
183
|
+
.map((tokens, index) => jaccardSimilarity(tokenSets[index], tokens))
|
|
184
|
+
.reduce((highest, score) => Math.max(highest, score), 0);
|
|
185
|
+
if (overlap < 0.35) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const latest = relevant.at(-1);
|
|
189
|
+
return {
|
|
190
|
+
code: "verification_stall",
|
|
191
|
+
severity: "medium",
|
|
192
|
+
summary: `Recent attempts keep landing on the same verifier-facing failure surface (${latest.failureClass}).`,
|
|
193
|
+
attemptIndexes: relevant.map((attempt) => attempt.index)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function detectObjectiveDrift(input) {
|
|
197
|
+
if (input.attempts.length < 2) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const objectiveTerms = toTokenSet(`${input.objective} ${(input.verificationPlan ?? []).join(" ")}`);
|
|
201
|
+
if (objectiveTerms.size < 4) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const summaryTerms = input.attempts.map((attempt) => toTokenSet(attempt.summary));
|
|
205
|
+
const overlaps = summaryTerms.map((terms) => jaccardSimilarity(terms, objectiveTerms));
|
|
206
|
+
if (overlaps.some((score) => score >= 0.18)) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
const sharedDriftTerms = intersectSets(summaryTerms[0], summaryTerms[1]);
|
|
210
|
+
if (sharedDriftTerms.length < 3) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
code: "objective_drift",
|
|
215
|
+
severity: "medium",
|
|
216
|
+
summary: "Recent attempt summaries are converging on work that does not overlap with the original objective or verifier plan.",
|
|
217
|
+
attemptIndexes: input.attempts.map((attempt) => attempt.index),
|
|
218
|
+
matchedTerms: sharedDriftTerms.slice(0, 6)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function deriveTrajectoryStatus(signals) {
|
|
222
|
+
if (signals.some((signal) => signal.severity === "high")) {
|
|
223
|
+
return "stalled";
|
|
224
|
+
}
|
|
225
|
+
if (signals.some((signal) => signal.code === "objective_drift")) {
|
|
226
|
+
return "drifting";
|
|
227
|
+
}
|
|
228
|
+
if (signals.some((signal) => signal.severity === "medium")) {
|
|
229
|
+
return "watch";
|
|
230
|
+
}
|
|
231
|
+
return "healthy";
|
|
232
|
+
}
|
|
233
|
+
function deriveTrajectoryConfidence(signals) {
|
|
234
|
+
if (signals.some((signal) => signal.severity === "high")) {
|
|
235
|
+
return "high";
|
|
236
|
+
}
|
|
237
|
+
if (signals.some((signal) => signal.severity === "medium")) {
|
|
238
|
+
return "medium";
|
|
239
|
+
}
|
|
240
|
+
return "low";
|
|
241
|
+
}
|
|
242
|
+
function deriveRecommendedIntervention(signals) {
|
|
243
|
+
if (signals.some((signal) => signal.code === "failure_oscillation" || signal.code === "attempt_repetition")) {
|
|
244
|
+
return "escalate_human";
|
|
245
|
+
}
|
|
246
|
+
if (signals.some((signal) => signal.code === "objective_drift")) {
|
|
247
|
+
return "tighten_task";
|
|
248
|
+
}
|
|
249
|
+
if (signals.some((signal) => signal.code === "verification_stall")) {
|
|
250
|
+
return "run_verifier";
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
function summarizeTrajectory(status, signals) {
|
|
255
|
+
const lead = signals[0];
|
|
256
|
+
if (!lead) {
|
|
257
|
+
return "Trajectory is healthy.";
|
|
258
|
+
}
|
|
259
|
+
switch (status) {
|
|
260
|
+
case "stalled":
|
|
261
|
+
return `Trajectory stalled: ${lead.summary}`;
|
|
262
|
+
case "drifting":
|
|
263
|
+
return `Trajectory drift detected: ${lead.summary}`;
|
|
264
|
+
case "watch":
|
|
265
|
+
return `Trajectory warning: ${lead.summary}`;
|
|
266
|
+
default:
|
|
267
|
+
return lead.summary;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function toTokenSet(text) {
|
|
271
|
+
return new Set((text ?? "")
|
|
272
|
+
.toLowerCase()
|
|
273
|
+
.match(/[a-z][a-z0-9_/-]{2,}/g)
|
|
274
|
+
?.flatMap((token) => splitCompositeToken(token))
|
|
275
|
+
.map((token) => token.trim())
|
|
276
|
+
.filter((token) => token.length >= 3 && !STOP_WORDS.has(token)) ?? []);
|
|
277
|
+
}
|
|
278
|
+
function splitCompositeToken(token) {
|
|
279
|
+
return token
|
|
280
|
+
.split(/[-_/]/g)
|
|
281
|
+
.flatMap((part) => part.split(/(?=[A-Z])/g))
|
|
282
|
+
.map((part) => part.toLowerCase())
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
}
|
|
285
|
+
function jaccardSimilarity(left, right) {
|
|
286
|
+
if (left.size === 0 || right.size === 0) {
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
const intersection = intersectSets(left, right).length;
|
|
290
|
+
const union = new Set([...left, ...right]).size;
|
|
291
|
+
return union === 0 ? 0 : intersection / union;
|
|
292
|
+
}
|
|
293
|
+
function intersectSets(left, right) {
|
|
294
|
+
return [...left].filter((term) => right.has(term));
|
|
295
|
+
}
|
|
@@ -72,12 +72,12 @@ function estimateCost(model, inputChars, outputChars) {
|
|
|
72
72
|
// ---------------------------------------------------------------------------
|
|
73
73
|
// Prompt builder
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
|
-
const DEFAULT_SYSTEM_PROMPT = `You are an expert software engineer executing a governed coding task.
|
|
76
|
-
Follow these rules exactly:
|
|
77
|
-
- Read the task description carefully and implement only what is asked.
|
|
78
|
-
- Do not add features, refactors, or improvements beyond the stated task.
|
|
79
|
-
- If the task asks you to write or modify code, output the complete file content with changes applied.
|
|
80
|
-
- Be precise, minimal, and test-backed in all changes.
|
|
75
|
+
const DEFAULT_SYSTEM_PROMPT = `You are an expert software engineer executing a governed coding task.
|
|
76
|
+
Follow these rules exactly:
|
|
77
|
+
- Read the task description carefully and implement only what is asked.
|
|
78
|
+
- Do not add features, refactors, or improvements beyond the stated task.
|
|
79
|
+
- If the task asks you to write or modify code, output the complete file content with changes applied.
|
|
80
|
+
- Be precise, minimal, and test-backed in all changes.
|
|
81
81
|
- State what you changed and why at the end of your response.`;
|
|
82
82
|
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
|
|
83
83
|
export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
|
|
@@ -20,11 +20,11 @@ export function createVerifierOnlyAdapter(options = {}) {
|
|
|
20
20
|
const shouldTrackVerifierWrites = request.context.verificationPlan.length > 0 ||
|
|
21
21
|
(request.context.verificationStack?.length ?? 0) > 0;
|
|
22
22
|
const baselineChangedFiles = shouldTrackVerifierWrites
|
|
23
|
-
? new Set(await readGitChangedFiles(workingDirectory, 5_000))
|
|
23
|
+
? new Set(await readGitChangedFiles(workingDirectory, 5_000, options.spawnImpl))
|
|
24
24
|
: new Set();
|
|
25
25
|
const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl);
|
|
26
26
|
const changedFiles = shouldTrackVerifierWrites
|
|
27
|
-
? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
|
|
27
|
+
? (await readGitChangedFiles(workingDirectory, 5_000, options.spawnImpl)).filter((file) => !baselineChangedFiles.has(file))
|
|
28
28
|
: [];
|
|
29
29
|
const execution = { changedFiles };
|
|
30
30
|
if (verification.passed) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R4 Delivery — M1 Contract
|
|
3
|
+
*
|
|
4
|
+
* Shared with Lane B (Control Plane). Do not modify unilaterally.
|
|
5
|
+
* The server selects one message; the client never downloads the catalog.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DELIVERY_MESSAGE_SCHEMA_VERSION: "delivery-message/1";
|
|
8
|
+
export declare const DELIVERY_RECORD_SCHEMA_VERSION: "delivery-record/1";
|
|
9
|
+
export declare const MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION: "martin-message-selection/1";
|
|
10
|
+
export declare const ALLOWED_ACTION_TYPES: readonly ["upgrade_cli", "upgrade_mcp", "submit_feedback", "open_release_notes", "dismiss", "view_spend_report", "view_run_explain"];
|
|
11
|
+
export type ActionType = (typeof ALLOWED_ACTION_TYPES)[number];
|
|
12
|
+
export type MessageKind = "update" | "feedback_request" | "milestone";
|
|
13
|
+
export interface DeliveryMessage {
|
|
14
|
+
schemaVersion: typeof DELIVERY_MESSAGE_SCHEMA_VERSION;
|
|
15
|
+
id: string;
|
|
16
|
+
revision: number;
|
|
17
|
+
kind: MessageKind;
|
|
18
|
+
title: string;
|
|
19
|
+
body: string;
|
|
20
|
+
action: {
|
|
21
|
+
type: ActionType;
|
|
22
|
+
url?: string;
|
|
23
|
+
targetVersion?: string;
|
|
24
|
+
};
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
cooldownHours: number;
|
|
27
|
+
}
|
|
28
|
+
export interface MessageSelectionResponse {
|
|
29
|
+
schemaVersion: typeof MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION;
|
|
30
|
+
message?: DeliveryMessage;
|
|
31
|
+
}
|
|
32
|
+
/** Local cooldown/dismissal state — persisted in .martin/delivery-record.json */
|
|
33
|
+
export interface DeliveryRecord {
|
|
34
|
+
schemaVersion: typeof DELIVERY_RECORD_SCHEMA_VERSION;
|
|
35
|
+
lastMessageId?: string;
|
|
36
|
+
lastShownAtEpochMs?: number;
|
|
37
|
+
dismissedIds: string[];
|
|
38
|
+
cooldownUntilEpochMs?: number;
|
|
39
|
+
cachedMessage?: DeliveryMessage;
|
|
40
|
+
cachedAtEpochMs?: number;
|
|
41
|
+
}
|
|
42
|
+
/** Structured update field for --json and MCP responses */
|
|
43
|
+
export interface UpdateAvailableField {
|
|
44
|
+
targetVersion: string;
|
|
45
|
+
kind: "cli" | "mcp";
|
|
46
|
+
message?: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R4 Delivery — M1 Contract
|
|
3
|
+
*
|
|
4
|
+
* Shared with Lane B (Control Plane). Do not modify unilaterally.
|
|
5
|
+
* The server selects one message; the client never downloads the catalog.
|
|
6
|
+
*/
|
|
7
|
+
export const DELIVERY_MESSAGE_SCHEMA_VERSION = "delivery-message/1";
|
|
8
|
+
export const DELIVERY_RECORD_SCHEMA_VERSION = "delivery-record/1";
|
|
9
|
+
export const MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION = "martin-message-selection/1";
|
|
10
|
+
export const ALLOWED_ACTION_TYPES = [
|
|
11
|
+
"upgrade_cli",
|
|
12
|
+
"upgrade_mcp",
|
|
13
|
+
"submit_feedback",
|
|
14
|
+
"open_release_notes",
|
|
15
|
+
"dismiss",
|
|
16
|
+
"view_spend_report",
|
|
17
|
+
"view_run_explain",
|
|
18
|
+
];
|
|
@@ -23,7 +23,7 @@ export interface LoopTask {
|
|
|
23
23
|
acceptanceCriteria?: string[];
|
|
24
24
|
}
|
|
25
25
|
export type ExecutionProfile = "strict_local" | "ci_safe" | "staging_controlled" | "research_untrusted";
|
|
26
|
-
export type MutationMode = "edit"
|
|
26
|
+
export type MutationMode = "edit";
|
|
27
27
|
export interface ApprovalPolicy {
|
|
28
28
|
dependencyAdds?: boolean;
|
|
29
29
|
migrations?: boolean;
|
|
@@ -410,3 +410,5 @@ export { cloneIdentityAttestation, cloneIdentityClaims, cloneIdentityToken } fro
|
|
|
410
410
|
export type { IdentityAttestation, IdentityClaims, IdentityToken } from "./identity.js";
|
|
411
411
|
export { cloneCircuitBreakDecision, cloneTrajectoryAssessment } from "./trajectory.js";
|
|
412
412
|
export type { CircuitBreakDecision, TrajectoryAssessment, TrajectorySignal } from "./trajectory.js";
|
|
413
|
+
export { ALLOWED_ACTION_TYPES, DELIVERY_MESSAGE_SCHEMA_VERSION, DELIVERY_RECORD_SCHEMA_VERSION, MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION } from "./delivery.js";
|
|
414
|
+
export type { ActionType, DeliveryMessage, DeliveryRecord, MessageKind, MessageSelectionResponse, UpdateAvailableField } from "./delivery.js";
|
|
@@ -223,3 +223,5 @@ export { DEFAULT_ROUTING_POLICY } from "./execution-policy.js";
|
|
|
223
223
|
export { cloneContextGraphSnapshot } from "./context-graph.js";
|
|
224
224
|
export { cloneIdentityAttestation, cloneIdentityClaims, cloneIdentityToken } from "./identity.js";
|
|
225
225
|
export { cloneCircuitBreakDecision, cloneTrajectoryAssessment } from "./trajectory.js";
|
|
226
|
+
// ─── R4 Delivery — M1 Contract ──────────────────────────────────────────────
|
|
227
|
+
export { ALLOWED_ACTION_TYPES, DELIVERY_MESSAGE_SCHEMA_VERSION, DELIVERY_RECORD_SCHEMA_VERSION, MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION } from "./delivery.js";
|
|
@@ -8,15 +8,10 @@ export function compilePromptPacket(request) {
|
|
|
8
8
|
const priorFailurePatterns = request.previousAttempts
|
|
9
9
|
.filter((a) => a.failureClass && a.intervention)
|
|
10
10
|
.map((a) => `${a.failureClass}:${a.intervention}`);
|
|
11
|
-
const guidanceParts =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
]
|
|
16
|
-
: [
|
|
17
|
-
"Only modify files directly required to satisfy the contract.",
|
|
18
|
-
"Do not touch files outside the allowed paths."
|
|
19
|
-
];
|
|
11
|
+
const guidanceParts = [
|
|
12
|
+
"Only modify files directly required to satisfy the contract.",
|
|
13
|
+
"Do not touch files outside the allowed paths."
|
|
14
|
+
];
|
|
20
15
|
if (request.context.allowedPaths && request.context.allowedPaths.length > 0) {
|
|
21
16
|
guidanceParts.push(`Allowed paths: ${request.context.allowedPaths.join(", ")}.`);
|
|
22
17
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { fetchSelectedMessage } from "./message-client.js";
|
|
2
|
+
export type { MessageClientOptions, MessageSelectRequest } from "./message-client.js";
|
|
3
|
+
export { cacheMessage, isCooldownExpired, isDismissed, loadDeliveryRecord, recordDismissed, recordShown, resolveDefaultLedgerPath, saveDeliveryRecord, } from "./message-ledger.js";
|
|
4
|
+
export { parseMessageSelectionResponse } from "./message-schema.js";
|
|
5
|
+
export type { ParseFailure, ParseResult, ValidationError } from "./message-schema.js";
|
|
6
|
+
export { getCliInstalledVersion, getMcpInstalledVersion, isNewerVersion } from "./update-check.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { fetchSelectedMessage } from "./message-client.js";
|
|
2
|
+
export { cacheMessage, isCooldownExpired, isDismissed, loadDeliveryRecord, recordDismissed, recordShown, resolveDefaultLedgerPath, saveDeliveryRecord, } from "./message-ledger.js";
|
|
3
|
+
export { parseMessageSelectionResponse } from "./message-schema.js";
|
|
4
|
+
export { getCliInstalledVersion, getMcpInstalledVersion, isNewerVersion } from "./update-check.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DeliveryMessage } from "../../contracts/index.js";
|
|
2
|
+
export interface MessageSelectRequest {
|
|
3
|
+
installId?: string;
|
|
4
|
+
clientVersion: string;
|
|
5
|
+
clientKind: "cli" | "mcp";
|
|
6
|
+
/** Enum signal — never a transcript or raw content */
|
|
7
|
+
trigger?: "first_verified_run" | "milestone_reached" | "version_check";
|
|
8
|
+
}
|
|
9
|
+
export interface MessageClientOptions {
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fetch a server-selected message from the Control Plane.
|
|
15
|
+
* Returns null on any network failure, timeout, or validation error.
|
|
16
|
+
* Never throws — the primary command must never be affected.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchSelectedMessage(request: MessageSelectRequest, options?: MessageClientOptions): Promise<DeliveryMessage | null>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseMessageSelectionResponse } from "./message-schema.js";
|
|
2
|
+
const DEFAULT_ENDPOINT = "https://api.martinloop.com/v1/messages/select";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 4_000;
|
|
4
|
+
/**
|
|
5
|
+
* Fetch a server-selected message from the Control Plane.
|
|
6
|
+
* Returns null on any network failure, timeout, or validation error.
|
|
7
|
+
* Never throws — the primary command must never be affected.
|
|
8
|
+
*/
|
|
9
|
+
export async function fetchSelectedMessage(request, options = {}) {
|
|
10
|
+
const endpoint = options.endpoint ?? process.env["MARTIN_MESSAGE_ENDPOINT"] ?? DEFAULT_ENDPOINT;
|
|
11
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
12
|
+
let controller;
|
|
13
|
+
let timeoutId;
|
|
14
|
+
try {
|
|
15
|
+
controller = new AbortController();
|
|
16
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
const response = await fetch(endpoint, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
|
20
|
+
body: JSON.stringify(request),
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
return null;
|
|
25
|
+
const raw = await response.text();
|
|
26
|
+
const result = parseMessageSelectionResponse(raw);
|
|
27
|
+
if (!result.ok)
|
|
28
|
+
return null;
|
|
29
|
+
return result.message ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (timeoutId !== undefined)
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type DeliveryMessage, type DeliveryRecord } from "../../contracts/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Load DeliveryRecord from disk. Returns empty record on any read/parse failure — never throws.
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadDeliveryRecord(ledgerPath: string): DeliveryRecord;
|
|
6
|
+
/**
|
|
7
|
+
* Persist DeliveryRecord atomically via temp-file + rename.
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveDeliveryRecord(ledgerPath: string, record: DeliveryRecord): void;
|
|
10
|
+
/**
|
|
11
|
+
* Returns true when the cooldown period has elapsed and the message should be shown.
|
|
12
|
+
* Clock-skew safe: if now < lastShownAtEpochMs (clock jumped back), treat cooldown as expired.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isCooldownExpired(record: DeliveryRecord, nowMs: number): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if this message id was permanently dismissed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isDismissed(record: DeliveryRecord, messageId: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Record that a message was shown. Updates cooldown window.
|
|
21
|
+
*/
|
|
22
|
+
export declare function recordShown(record: DeliveryRecord, message: DeliveryMessage, nowMs: number): DeliveryRecord;
|
|
23
|
+
/**
|
|
24
|
+
* Record that a message was permanently dismissed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function recordDismissed(record: DeliveryRecord, messageId: string): DeliveryRecord;
|
|
27
|
+
/**
|
|
28
|
+
* Cache a fetched message for offline use.
|
|
29
|
+
*/
|
|
30
|
+
export declare function cacheMessage(record: DeliveryRecord, message: DeliveryMessage, nowMs: number): DeliveryRecord;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the default ledger path: $MARTIN_STATE_DIR/delivery-record.json
|
|
33
|
+
* or ~/.martin/delivery-record.json.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveDefaultLedgerPath(): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DELIVERY_RECORD_SCHEMA_VERSION } from "../../contracts/index.js";
|
|
5
|
+
const EMPTY_RECORD = {
|
|
6
|
+
schemaVersion: DELIVERY_RECORD_SCHEMA_VERSION,
|
|
7
|
+
dismissedIds: [],
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Load DeliveryRecord from disk. Returns empty record on any read/parse failure — never throws.
|
|
11
|
+
*/
|
|
12
|
+
export function loadDeliveryRecord(ledgerPath) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(ledgerPath, "utf8");
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
if (typeof parsed === "object" &&
|
|
17
|
+
parsed !== null &&
|
|
18
|
+
!Array.isArray(parsed) &&
|
|
19
|
+
parsed["schemaVersion"] === DELIVERY_RECORD_SCHEMA_VERSION) {
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// file missing or corrupt — start fresh
|
|
25
|
+
}
|
|
26
|
+
return { ...EMPTY_RECORD, dismissedIds: [] };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Persist DeliveryRecord atomically via temp-file + rename.
|
|
30
|
+
*/
|
|
31
|
+
export function saveDeliveryRecord(ledgerPath, record) {
|
|
32
|
+
const dir = path.dirname(ledgerPath);
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
const tmp = path.join(os.tmpdir(), `martin-dlv-${process.pid}-${Date.now()}.json`);
|
|
35
|
+
fs.writeFileSync(tmp, JSON.stringify(record, null, 2), "utf8");
|
|
36
|
+
fs.renameSync(tmp, ledgerPath);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns true when the cooldown period has elapsed and the message should be shown.
|
|
40
|
+
* Clock-skew safe: if now < lastShownAtEpochMs (clock jumped back), treat cooldown as expired.
|
|
41
|
+
*/
|
|
42
|
+
export function isCooldownExpired(record, nowMs) {
|
|
43
|
+
const until = record.cooldownUntilEpochMs;
|
|
44
|
+
if (until === undefined)
|
|
45
|
+
return true;
|
|
46
|
+
const last = record.lastShownAtEpochMs ?? 0;
|
|
47
|
+
// If clock appears to have gone backwards past last-shown, treat as expired
|
|
48
|
+
if (nowMs < last)
|
|
49
|
+
return true;
|
|
50
|
+
return nowMs >= until;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns true if this message id was permanently dismissed.
|
|
54
|
+
*/
|
|
55
|
+
export function isDismissed(record, messageId) {
|
|
56
|
+
return record.dismissedIds.includes(messageId);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Record that a message was shown. Updates cooldown window.
|
|
60
|
+
*/
|
|
61
|
+
export function recordShown(record, message, nowMs) {
|
|
62
|
+
return {
|
|
63
|
+
...record,
|
|
64
|
+
lastMessageId: message.id,
|
|
65
|
+
lastShownAtEpochMs: nowMs,
|
|
66
|
+
cooldownUntilEpochMs: nowMs + message.cooldownHours * 60 * 60 * 1000,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Record that a message was permanently dismissed.
|
|
71
|
+
*/
|
|
72
|
+
export function recordDismissed(record, messageId) {
|
|
73
|
+
if (record.dismissedIds.includes(messageId))
|
|
74
|
+
return record;
|
|
75
|
+
return {
|
|
76
|
+
...record,
|
|
77
|
+
dismissedIds: [...record.dismissedIds, messageId],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Cache a fetched message for offline use.
|
|
82
|
+
*/
|
|
83
|
+
export function cacheMessage(record, message, nowMs) {
|
|
84
|
+
return {
|
|
85
|
+
...record,
|
|
86
|
+
cachedMessage: message,
|
|
87
|
+
cachedAtEpochMs: nowMs,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the default ledger path: $MARTIN_STATE_DIR/delivery-record.json
|
|
92
|
+
* or ~/.martin/delivery-record.json.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveDefaultLedgerPath() {
|
|
95
|
+
const stateDir = process.env["MARTIN_STATE_DIR"] ?? path.join(os.homedir(), ".martin");
|
|
96
|
+
return path.join(stateDir, "delivery-record.json");
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type DeliveryMessage } from "../../contracts/index.js";
|
|
2
|
+
export type ValidationError = "schema_version_unknown" | "schema_unknown_kind" | "schema_unknown_action_type" | "schema_invalid_id" | "schema_invalid_date" | "schema_invalid_cooldown" | "schema_url_not_https" | "schema_text_too_long" | "schema_response_too_large" | "schema_malformed";
|
|
3
|
+
export interface ParseResult {
|
|
4
|
+
ok: true;
|
|
5
|
+
message: DeliveryMessage | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface ParseFailure {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: ValidationError;
|
|
10
|
+
detail?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse and validate a raw response body from the message selection endpoint.
|
|
14
|
+
* Returns ok:false on any violation — caller must render nothing and log the error.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseMessageSelectionResponse(raw: string): ParseResult | ParseFailure;
|