@martinloop/mcp 0.2.0 → 0.2.7

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 (96) hide show
  1. package/README.md +118 -182
  2. package/dist/discovery-metadata.d.ts +21 -0
  3. package/dist/discovery-metadata.js +152 -0
  4. package/dist/discovery-support.d.ts +62 -0
  5. package/dist/discovery-support.js +224 -0
  6. package/dist/package-version.d.ts +1 -0
  7. package/dist/package-version.js +3 -0
  8. package/dist/prompts.d.ts +13 -3
  9. package/dist/prompts.js +537 -74
  10. package/dist/resources.d.ts +35 -5
  11. package/dist/resources.js +788 -71
  12. package/dist/server-validation.d.ts +2 -3
  13. package/dist/server-validation.js +375 -119
  14. package/dist/server.d.ts +76 -7
  15. package/dist/server.js +1478 -394
  16. package/dist/tools/doctor.d.ts +2 -0
  17. package/dist/tools/doctor.js +18 -6
  18. package/dist/tools/eval.d.ts +24 -0
  19. package/dist/tools/eval.js +65 -0
  20. package/dist/tools/get-attempt.d.ts +13 -6
  21. package/dist/tools/get-attempt.js +14 -5
  22. package/dist/tools/get-run.d.ts +19 -12
  23. package/dist/tools/get-run.js +20 -11
  24. package/dist/tools/get-status.d.ts +19 -0
  25. package/dist/tools/get-status.js +30 -2
  26. package/dist/tools/get-verification-results.d.ts +10 -7
  27. package/dist/tools/get-verification-results.js +11 -6
  28. package/dist/tools/inspect-loop.d.ts +9 -0
  29. package/dist/tools/inspect-loop.js +11 -2
  30. package/dist/tools/list-runs.d.ts +25 -5
  31. package/dist/tools/list-runs.js +21 -4
  32. package/dist/tools/logs.d.ts +25 -0
  33. package/dist/tools/logs.js +49 -0
  34. package/dist/tools/plan.d.ts +20 -0
  35. package/dist/tools/plan.js +10 -0
  36. package/dist/tools/pr-tools.d.ts +31 -0
  37. package/dist/tools/pr-tools.js +111 -0
  38. package/dist/tools/preflight.d.ts +10 -0
  39. package/dist/tools/preflight.js +18 -4
  40. package/dist/tools/run-controls.d.ts +36 -0
  41. package/dist/tools/run-controls.js +88 -0
  42. package/dist/tools/run-dossier.d.ts +51 -4
  43. package/dist/tools/run-dossier.js +100 -5
  44. package/dist/tools/run-loop.d.ts +19 -0
  45. package/dist/tools/run-loop.js +61 -4
  46. package/dist/tools/run-store.d.ts +57 -3
  47. package/dist/tools/run-store.js +404 -53
  48. package/dist/tools/tool-errors.d.ts +37 -0
  49. package/dist/tools/tool-errors.js +170 -0
  50. package/dist/tools/tool-response.d.ts +16 -0
  51. package/dist/tools/tool-response.js +34 -0
  52. package/dist/tools/tool-support.d.ts +92 -2
  53. package/dist/tools/tool-support.js +385 -63
  54. package/dist/tools/triage-runs.d.ts +33 -0
  55. package/dist/tools/triage-runs.js +138 -0
  56. package/dist/tools/workflow-governance.d.ts +133 -0
  57. package/dist/tools/workflow-governance.js +581 -0
  58. package/dist/vendor/adapters/claude-cli.js +0 -1
  59. package/dist/vendor/adapters/cli-bridge.d.ts +5 -0
  60. package/dist/vendor/adapters/cli-bridge.js +16 -9
  61. package/dist/vendor/adapters/direct-provider.js +0 -1
  62. package/dist/vendor/adapters/index.d.ts +2 -1
  63. package/dist/vendor/adapters/index.js +2 -1
  64. package/dist/vendor/adapters/openai-compatible.d.ts +47 -0
  65. package/dist/vendor/adapters/openai-compatible.js +242 -0
  66. package/dist/vendor/adapters/runtime-support.js +0 -1
  67. package/dist/vendor/adapters/stub-agent-cli.js +0 -1
  68. package/dist/vendor/adapters/stub-direct-provider.js +0 -1
  69. package/dist/vendor/adapters/verifier-only.js +0 -1
  70. package/dist/vendor/contracts/governance.js +0 -1
  71. package/dist/vendor/contracts/index.d.ts +2 -0
  72. package/dist/vendor/contracts/index.js +1 -1
  73. package/dist/vendor/contracts/operator.d.ts +19 -0
  74. package/dist/vendor/contracts/operator.js +11 -0
  75. package/dist/vendor/core/compiler.js +0 -1
  76. package/dist/vendor/core/context-integrity.js +0 -1
  77. package/dist/vendor/core/grounding.js +0 -1
  78. package/dist/vendor/core/index.js +1 -2
  79. package/dist/vendor/core/leash.js +19 -12
  80. package/dist/vendor/core/persistence/compiler.js +0 -1
  81. package/dist/vendor/core/persistence/index.js +0 -1
  82. package/dist/vendor/core/persistence/ledger.js +0 -1
  83. package/dist/vendor/core/persistence/runs-reader.js +0 -1
  84. package/dist/vendor/core/persistence/store.js +0 -1
  85. package/dist/vendor/core/policy.js +0 -1
  86. package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
  87. package/dist/vendor/core/red-blue/red-phase.js +135 -0
  88. package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
  89. package/dist/vendor/core/red-blue/risk-tiers.js +32 -0
  90. package/dist/vendor/core/rollback.js +2 -3
  91. package/dist/workflow-state.d.ts +25 -0
  92. package/dist/workflow-state.js +102 -0
  93. package/package.json +12 -7
  94. package/server.json +2 -2
  95. package/dist/tools/cockpit-support.d.ts +0 -69
  96. package/dist/tools/cockpit-support.js +0 -108
@@ -1,19 +1,32 @@
1
- import { extname, isAbsolute, relative, resolve } from "node:path";
1
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
2
+ import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
2
3
  import { resolveRunsRoot } from "./vendor/core/index.js";
4
+ import { invalidArgumentsError, invalidPathError, invalidSelectorError } from "./tools/tool-errors.js";
5
+ export { sanitizeToolErrorMessage } from "./tools/tool-errors.js";
3
6
  export function validateToolInput(name, args) {
4
7
  switch (name) {
5
- case "martin_doctor":
6
- return validateDoctorInput(args);
7
- case "martin_preflight":
8
- return validatePreflightInput(args);
9
8
  case "martin_run":
10
9
  return validateRunInput(args);
11
10
  case "martin_inspect":
12
11
  return validateInspectInput(args);
13
12
  case "martin_status":
14
13
  return validateStatusInput(args);
14
+ case "martin_doctor":
15
+ return validateDoctorInput(args);
16
+ case "martin_plan":
17
+ return validatePlanInput(args);
18
+ case "martin_preflight":
19
+ return validatePreflightInput(args);
20
+ case "martin_logs":
21
+ return validateLogsInput(args);
22
+ case "martin_cancel":
23
+ case "martin_pause":
24
+ case "martin_continue":
25
+ return validateRunControlInput(args);
15
26
  case "martin_list_runs":
16
27
  return validateListRunsInput(args);
28
+ case "martin_triage_runs":
29
+ return validateTriageRunsInput(args);
17
30
  case "martin_get_run":
18
31
  return validateGetRunInput(args);
19
32
  case "martin_get_attempt":
@@ -21,97 +34,79 @@ export function validateToolInput(name, args) {
21
34
  case "martin_get_verification_results":
22
35
  return validateGetVerificationResultsInput(args);
23
36
  case "martin_run_dossier":
37
+ case "martin_dossier":
38
+ return validateRunDossierInput(args);
39
+ case "martin_eval":
40
+ return validateEvalInput(args);
41
+ case "martin_pr_summary":
24
42
  return validateRunDossierInput(args);
43
+ case "martin_create_pr":
44
+ return validateCreatePrInput(args);
45
+ case "martin_review_pr":
46
+ return validateReviewPrInput(args);
25
47
  default:
26
- throw new Error(`Unknown tool: ${name}`);
48
+ throw invalidArgumentsError(`Unknown tool: ${name}`, "Refresh the Martin tool manifest and retry.");
27
49
  }
28
50
  }
29
- export function sanitizeToolErrorMessage(error) {
30
- const message = error instanceof Error ? error.message : String(error);
31
- return /([A-Za-z]:\\|\/|policy\.rego|policy\.wasm|\.pem|\.env)/u.test(message)
32
- ? "Tool execution failed."
33
- : message;
34
- }
35
- function validateDoctorInput(args) {
36
- const record = requireObject(args);
37
- assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
38
- const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
39
- return {
40
- ...(record.workingDirectory !== undefined
41
- ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
42
- : {}),
43
- ...(record.runsDir !== undefined
44
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
45
- : {}),
46
- ...(engine ? { engine } : {})
47
- };
48
- }
49
- function validatePreflightInput(args) {
50
- const record = requireObject(args);
51
- assertAllowedKeys(record, [
52
- "objective",
53
- "workingDirectory",
54
- "engine",
55
- "model",
56
- "maxUsd",
57
- "maxIterations",
58
- "maxTokens",
59
- "verificationPlan",
60
- "allowedPaths",
61
- "deniedPaths",
62
- "workspaceId",
63
- "projectId"
64
- ]);
65
- const engine = optionalEnum(record.engine, "engine", ["claude", "codex"]);
66
- return {
67
- objective: requireString(record.objective, "objective"),
68
- ...(record.workingDirectory !== undefined
69
- ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
70
- : {}),
71
- ...(engine ? { engine } : {}),
72
- ...optionalString(record.model, "model"),
73
- ...optionalPositiveNumber(record.maxUsd, "maxUsd"),
74
- ...optionalPositiveInteger(record.maxIterations, "maxIterations"),
75
- ...optionalPositiveInteger(record.maxTokens, "maxTokens"),
76
- ...optionalStringArrayAsObject(record.verificationPlan, "verificationPlan"),
77
- ...optionalPathPatternArrayAsObject(record.allowedPaths, "allowedPaths"),
78
- ...optionalPathPatternArrayAsObject(record.deniedPaths, "deniedPaths"),
79
- ...optionalString(record.workspaceId, "workspaceId"),
80
- ...optionalString(record.projectId, "projectId")
81
- };
82
- }
83
51
  export function resolveSafeRepoRoot(repoRoot, workspaceRoot = process.env.MARTIN_MCP_WORKSPACE_ROOT ?? process.cwd()) {
84
52
  const baseRoot = resolve(workspaceRoot);
85
53
  const candidate = repoRoot ? resolve(baseRoot, repoRoot) : baseRoot;
86
- assertPathWithinRoot(candidate, baseRoot, "workingDirectory");
54
+ assertPathWithinRoot(candidate, baseRoot, "workingDirectory", {
55
+ requireExistingCandidate: true,
56
+ requireExistingRoot: true
57
+ });
87
58
  return candidate;
88
59
  }
89
60
  export function resolveSafeRunsJsonPath(file, runsRoot = resolveRunsRoot(process.env)) {
90
61
  const baseRoot = resolve(runsRoot);
91
62
  const candidate = resolve(baseRoot, file);
92
- assertPathWithinRoot(candidate, baseRoot, "file");
63
+ assertPathWithinRoot(candidate, baseRoot, "file", {
64
+ requireExistingCandidate: true,
65
+ requireExistingRoot: true
66
+ });
93
67
  const extension = extname(candidate).toLowerCase();
94
68
  if (extension !== ".json" && extension !== ".jsonl") {
95
- throw new Error("Invalid file.");
69
+ throw invalidPathError("Invalid file.", "Point file at a loop-record.json, a legacy .jsonl file, or a run directory under the runs root.");
96
70
  }
97
71
  return candidate;
98
72
  }
99
73
  export function resolveSafeRunsPath(file, runsRoot = resolveRunsRoot(process.env)) {
100
74
  const baseRoot = resolve(runsRoot);
101
75
  const candidate = resolve(baseRoot, file);
102
- assertPathWithinRoot(candidate, baseRoot, "file");
76
+ assertPathWithinRoot(candidate, baseRoot, "file", {
77
+ requireExistingCandidate: true,
78
+ requireExistingRoot: true
79
+ });
103
80
  const extension = extname(candidate).toLowerCase();
104
81
  if (extension && extension !== ".json" && extension !== ".jsonl") {
105
- throw new Error("Invalid file.");
82
+ throw invalidPathError("Invalid file.", "Point file at a loop-record.json, a legacy .jsonl file, or a run directory under the runs root.");
106
83
  }
107
84
  return candidate;
108
85
  }
109
86
  export function resolveSafeRunsRootPath(runsRoot, fallbackRunsRoot = resolveRunsRoot(process.env)) {
110
87
  const baseRoot = resolve(fallbackRunsRoot);
111
88
  const candidate = runsRoot ? resolve(baseRoot, runsRoot) : baseRoot;
112
- assertPathWithinRoot(candidate, baseRoot, "runsDir");
89
+ if (runsRoot && !existsSync(candidate)) {
90
+ if (!isAbsolute(runsRoot)) {
91
+ assertRawPathWithinRoot(candidate, baseRoot, "runsDir");
92
+ }
93
+ return candidate;
94
+ }
95
+ assertPathWithinRoot(candidate, baseRoot, "runsDir", {
96
+ requireExistingCandidate: false,
97
+ requireExistingRoot: false
98
+ });
113
99
  return candidate;
114
100
  }
101
+ function assertRawPathWithinRoot(candidatePath, rootPath, name) {
102
+ const relativePath = relative(resolve(rootPath), resolve(candidatePath));
103
+ if (relativePath === "" || relativePath === ".") {
104
+ return;
105
+ }
106
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
107
+ throw invalidPathError(`Invalid ${name}.`);
108
+ }
109
+ }
115
110
  export function resolveSafeLoopRecordPath(loopId, runsRoot = resolveRunsRoot(process.env)) {
116
111
  const normalizedLoopId = requireLoopId(loopId, "loopId");
117
112
  return resolveSafeRunsJsonPath(`${normalizedLoopId}/loop-record.json`, runsRoot);
@@ -127,7 +122,7 @@ export function normalizeSafePathPatterns(value, name) {
127
122
  normalized.startsWith("/") ||
128
123
  /^[A-Za-z]:\//u.test(normalized) ||
129
124
  normalized.split("/").includes("..")) {
130
- throw new Error(`Invalid ${name}.`);
125
+ throw invalidPathError(`Invalid ${name}.`);
131
126
  }
132
127
  return normalized;
133
128
  });
@@ -139,9 +134,14 @@ function validateRunInput(args) {
139
134
  "workingDirectory",
140
135
  "engine",
141
136
  "model",
137
+ "context",
138
+ "policyPack",
142
139
  "maxUsd",
143
140
  "maxIterations",
144
141
  "maxTokens",
142
+ "maxMinutes",
143
+ "maxFilesChanged",
144
+ "maxCommands",
145
145
  "verificationPlan",
146
146
  "allowedPaths",
147
147
  "deniedPaths",
@@ -156,9 +156,20 @@ function validateRunInput(args) {
156
156
  : {}),
157
157
  ...(engine ? { engine } : {}),
158
158
  ...optionalString(record.model, "model"),
159
+ ...optionalString(record.context, "context"),
160
+ ...optionalEnumAsObject(record.policyPack, "policyPack", [
161
+ "solo-founder",
162
+ "startup-team",
163
+ "enterprise-strict",
164
+ "oss-maintainer",
165
+ "security-sensitive"
166
+ ]),
159
167
  ...optionalPositiveNumber(record.maxUsd, "maxUsd"),
160
168
  ...optionalPositiveInteger(record.maxIterations, "maxIterations"),
161
169
  ...optionalPositiveInteger(record.maxTokens, "maxTokens"),
170
+ ...optionalPositiveInteger(record.maxMinutes, "maxMinutes"),
171
+ ...optionalPositiveInteger(record.maxFilesChanged, "maxFilesChanged"),
172
+ ...optionalPositiveInteger(record.maxCommands, "maxCommands"),
162
173
  ...optionalStringArrayAsObject(record.verificationPlan, "verificationPlan"),
163
174
  ...optionalPathPatternArrayAsObject(record.allowedPaths, "allowedPaths"),
164
175
  ...optionalPathPatternArrayAsObject(record.deniedPaths, "deniedPaths"),
@@ -169,18 +180,24 @@ function validateRunInput(args) {
169
180
  function validateInspectInput(args) {
170
181
  const record = requireObject(args);
171
182
  assertAllowedKeys(record, ["file", "runsDir"]);
183
+ const resolvedRunsDir = record.runsDir !== undefined
184
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
185
+ : undefined;
172
186
  return {
173
187
  ...(record.file !== undefined
174
- ? { file: resolveSafeRunsPath(requireString(record.file, "file")) }
188
+ ? {
189
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
190
+ }
175
191
  : {}),
176
- ...(record.runsDir !== undefined
177
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
178
- : {})
192
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {})
179
193
  };
180
194
  }
181
195
  function validateStatusInput(args) {
182
196
  const record = requireObject(args);
183
197
  assertAllowedKeys(record, ["loopJson", "file", "loopId", "runsDir", "latest"]);
198
+ const resolvedRunsDir = record.runsDir !== undefined
199
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
200
+ : undefined;
184
201
  const selectors = [
185
202
  record.loopJson !== undefined ? "loopJson" : null,
186
203
  record.file !== undefined ? "file" : null,
@@ -188,113 +205,334 @@ function validateStatusInput(args) {
188
205
  record.latest !== undefined ? "latest" : null
189
206
  ].filter((value) => value !== null);
190
207
  if (selectors.length !== 1) {
191
- throw new Error("Provide exactly one of loopJson, file, loopId, or latest.");
208
+ throw invalidSelectorError("Provide exactly one of loopJson, file, loopId, or latest.", "Choose exactly one status selector per call.");
192
209
  }
193
210
  if (record.latest !== undefined && record.latest !== true) {
194
- throw new Error("Invalid latest.");
211
+ throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
195
212
  }
196
213
  return {
197
214
  ...(record.loopJson !== undefined
198
215
  ? { loopJson: requireString(record.loopJson, "loopJson") }
199
216
  : {}),
200
217
  ...(record.file !== undefined
201
- ? { file: resolveSafeRunsPath(requireString(record.file, "file")) }
218
+ ? {
219
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
220
+ }
202
221
  : {}),
203
222
  ...(record.loopId !== undefined
204
- ? { loopId: requireLoopId(record.loopId, "loopId") }
205
- : {}),
206
- ...(record.runsDir !== undefined
207
- ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
223
+ ? {
224
+ loopId: requireLoopId(record.loopId, "loopId")
225
+ }
208
226
  : {}),
227
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
209
228
  ...(record.latest === true ? { latest: true } : {})
210
229
  };
211
230
  }
212
- function validateListRunsInput(args) {
231
+ function validateDoctorInput(args) {
213
232
  const record = requireObject(args);
214
- assertAllowedKeys(record, ["runsDir", "limit"]);
233
+ assertAllowedKeys(record, ["workingDirectory", "runsDir", "engine"]);
215
234
  return {
235
+ ...(record.workingDirectory !== undefined
236
+ ? { workingDirectory: resolveSafeRepoRoot(requireString(record.workingDirectory, "workingDirectory")) }
237
+ : {}),
216
238
  ...(record.runsDir !== undefined
217
239
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
218
240
  : {}),
219
- ...(record.limit !== undefined ? { limit: requirePositiveInteger(record.limit, "limit") } : {})
241
+ ...optionalEnumAsObject(record.engine, "engine", ["claude", "codex"])
220
242
  };
221
243
  }
222
- function validateGetRunInput(args) {
223
- const record = requireObject(args);
224
- assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
225
- return validateRunSelector(record);
244
+ function validatePreflightInput(args) {
245
+ return validateRunInput(args);
226
246
  }
227
- function validateGetVerificationResultsInput(args) {
228
- const record = requireObject(args);
229
- assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
230
- return validateRunSelector(record);
247
+ function validatePlanInput(args) {
248
+ return validateRunInput(args);
231
249
  }
232
- function validateRunDossierInput(args) {
250
+ function validateLogsInput(args) {
233
251
  const record = requireObject(args);
234
- assertAllowedKeys(record, ["loopId", "runsDir", "latest"]);
235
- return validateRunSelector(record);
252
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "limit"]);
253
+ const resolvedRunsDir = record.runsDir !== undefined
254
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
255
+ : undefined;
256
+ const selectors = [
257
+ record.file !== undefined ? "file" : null,
258
+ record.loopId !== undefined ? "loopId" : null,
259
+ record.latest !== undefined ? "latest" : null
260
+ ].filter((value) => value !== null);
261
+ if (selectors.length !== 1) {
262
+ throw invalidSelectorError("Provide exactly one of file, loopId, or latest.", "Choose exactly one run selector per call.");
263
+ }
264
+ return {
265
+ ...(record.file !== undefined
266
+ ? {
267
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
268
+ }
269
+ : {}),
270
+ ...(record.loopId !== undefined ? { loopId: requireLoopId(record.loopId, "loopId") } : {}),
271
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
272
+ ...(record.latest === true ? { latest: true } : {}),
273
+ ...optionalPositiveInteger(record.limit, "limit")
274
+ };
236
275
  }
237
- function validateGetAttemptInput(args) {
276
+ function validateListRunsInput(args) {
238
277
  const record = requireObject(args);
239
- assertAllowedKeys(record, ["loopId", "attemptIndex", "runsDir"]);
278
+ assertAllowedKeys(record, [
279
+ "runsDir",
280
+ "limit",
281
+ "status",
282
+ "lifecycleState",
283
+ "adapterId",
284
+ "model",
285
+ "updatedAfter"
286
+ ]);
240
287
  return {
241
- loopId: requireLoopId(record.loopId, "loopId"),
242
- attemptIndex: requirePositiveInteger(record.attemptIndex, "attemptIndex"),
243
288
  ...(record.runsDir !== undefined
244
289
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
245
- : {})
290
+ : {}),
291
+ ...optionalPositiveInteger(record.limit, "limit"),
292
+ ...optionalString(record.status, "status"),
293
+ ...optionalString(record.lifecycleState, "lifecycleState"),
294
+ ...optionalString(record.adapterId, "adapterId"),
295
+ ...optionalString(record.model, "model"),
296
+ ...optionalString(record.updatedAfter, "updatedAfter")
246
297
  };
247
298
  }
248
- function validateRunSelector(record) {
299
+ function validateGetRunInput(args) {
300
+ const record = requireObject(args);
301
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest"]);
302
+ const resolvedRunsDir = record.runsDir !== undefined
303
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
304
+ : undefined;
249
305
  const selectors = [
306
+ record.file !== undefined ? "file" : null,
250
307
  record.loopId !== undefined ? "loopId" : null,
251
308
  record.latest !== undefined ? "latest" : null
252
309
  ].filter((value) => value !== null);
253
310
  if (selectors.length !== 1) {
254
- throw new Error("Provide exactly one of loopId or latest.");
311
+ throw invalidSelectorError("Provide exactly one of file, loopId, or latest.", "Choose exactly one run selector per call.");
255
312
  }
256
313
  if (record.latest !== undefined && record.latest !== true) {
257
- throw new Error("Invalid latest.");
314
+ throw invalidArgumentsError("Invalid latest.", "latest must be the literal boolean value true.");
258
315
  }
259
316
  return {
260
- ...(record.loopId !== undefined ? { loopId: requireLoopId(record.loopId, "loopId") } : {}),
261
- ...(record.latest === true ? { latest: true } : {}),
317
+ ...(record.file !== undefined
318
+ ? {
319
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
320
+ }
321
+ : {}),
322
+ ...(record.loopId !== undefined
323
+ ? { loopId: requireLoopId(record.loopId, "loopId") }
324
+ : {}),
325
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
326
+ ...(record.latest === true ? { latest: true } : {})
327
+ };
328
+ }
329
+ function validateTriageRunsInput(args) {
330
+ const record = requireObject(args);
331
+ assertAllowedKeys(record, [
332
+ "runsDir",
333
+ "limit",
334
+ "status",
335
+ "lifecycleState",
336
+ "adapterId",
337
+ "model",
338
+ "updatedAfter",
339
+ "includeHealthy"
340
+ ]);
341
+ return {
262
342
  ...(record.runsDir !== undefined
263
343
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
264
- : {})
344
+ : {}),
345
+ ...optionalPositiveInteger(record.limit, "limit"),
346
+ ...optionalString(record.status, "status"),
347
+ ...optionalString(record.lifecycleState, "lifecycleState"),
348
+ ...optionalString(record.adapterId, "adapterId"),
349
+ ...optionalString(record.model, "model"),
350
+ ...optionalString(record.updatedAfter, "updatedAfter"),
351
+ ...optionalBoolean(record.includeHealthy, "includeHealthy")
352
+ };
353
+ }
354
+ function validateGetAttemptInput(args) {
355
+ const record = requireObject(args);
356
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "attemptIndex"]);
357
+ const resolvedRunsDir = record.runsDir !== undefined
358
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
359
+ : undefined;
360
+ const selectors = [
361
+ record.file !== undefined ? "file" : null,
362
+ record.loopId !== undefined ? "loopId" : null
363
+ ].filter((value) => value !== null);
364
+ if (selectors.length !== 1) {
365
+ throw invalidSelectorError("Provide exactly one of file or loopId.", "Choose exactly one run selector per call.");
366
+ }
367
+ return {
368
+ ...(record.file !== undefined
369
+ ? {
370
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
371
+ }
372
+ : {}),
373
+ ...(record.loopId !== undefined
374
+ ? { loopId: requireLoopId(record.loopId, "loopId") }
375
+ : {}),
376
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {}),
377
+ ...optionalPositiveInteger(record.attemptIndex, "attemptIndex")
378
+ };
379
+ }
380
+ function validateGetVerificationResultsInput(args) {
381
+ const record = requireObject(args);
382
+ assertAllowedKeys(record, ["file", "loopId", "runsDir"]);
383
+ const resolvedRunsDir = record.runsDir !== undefined
384
+ ? resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir"))
385
+ : undefined;
386
+ const selectors = [
387
+ record.file !== undefined ? "file" : null,
388
+ record.loopId !== undefined ? "loopId" : null
389
+ ].filter((value) => value !== null);
390
+ if (selectors.length !== 1) {
391
+ throw invalidSelectorError("Provide exactly one of file or loopId.", "Choose exactly one run selector per call.");
392
+ }
393
+ return {
394
+ ...(record.file !== undefined
395
+ ? {
396
+ file: resolveSafeRunsPath(requireString(record.file, "file"), resolvedRunsDir ?? resolveRunsRoot(process.env))
397
+ }
398
+ : {}),
399
+ ...(record.loopId !== undefined
400
+ ? { loopId: requireLoopId(record.loopId, "loopId") }
401
+ : {}),
402
+ ...(resolvedRunsDir ? { runsDir: resolvedRunsDir } : {})
403
+ };
404
+ }
405
+ function validateRunDossierInput(args) {
406
+ const record = requireObject(args);
407
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format"]);
408
+ const base = validateGetRunInput(args);
409
+ return {
410
+ ...base,
411
+ ...optionalEnumAsObject(record.format, "format", ["json", "md", "github-pr"])
412
+ };
413
+ }
414
+ function validateEvalInput(args) {
415
+ return validateGetRunInput(args);
416
+ }
417
+ function validateRunControlInput(args) {
418
+ const record = requireObject(args);
419
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "reason", "requestedBy"]);
420
+ const base = validateGetRunInput(args);
421
+ return {
422
+ ...base,
423
+ ...optionalString(record.reason, "reason"),
424
+ ...optionalString(record.requestedBy, "requestedBy")
425
+ };
426
+ }
427
+ function validateCreatePrInput(args) {
428
+ const record = requireObject(args);
429
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format", "title", "base", "execute"]);
430
+ const base = validateRunDossierInput(args);
431
+ return {
432
+ ...base,
433
+ ...optionalString(record.title, "title"),
434
+ ...optionalString(record.base, "base"),
435
+ ...optionalBoolean(record.execute, "execute")
436
+ };
437
+ }
438
+ function validateReviewPrInput(args) {
439
+ const record = requireObject(args);
440
+ assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format", "prBody"]);
441
+ const base = validateRunDossierInput(args);
442
+ return {
443
+ ...base,
444
+ ...optionalString(record.prBody, "prBody")
265
445
  };
266
446
  }
267
447
  function requireObject(value) {
268
448
  if (!value || typeof value !== "object" || Array.isArray(value)) {
269
- throw new Error("Tool arguments must be an object.");
449
+ throw invalidArgumentsError("Tool arguments must be an object.");
270
450
  }
271
451
  return value;
272
452
  }
273
453
  function assertAllowedKeys(record, allowed) {
274
454
  const unknownKeys = Object.keys(record).filter((key) => !allowed.includes(key));
275
455
  if (unknownKeys.length > 0) {
276
- throw new Error(`Unknown arguments: ${unknownKeys.join(", ")}`);
456
+ throw invalidArgumentsError(`Unknown arguments: ${unknownKeys.join(", ")}`);
277
457
  }
278
458
  }
279
- function assertPathWithinRoot(candidatePath, rootPath, name) {
280
- const relativePath = relative(rootPath, candidatePath);
459
+ function assertPathWithinRoot(candidatePath, rootPath, name, options = {}) {
460
+ assertNoSymbolicLinkSegments(candidatePath, name, rootPath);
461
+ const canonicalRoot = canonicalizePath(rootPath, name, options.requireExistingRoot ?? false);
462
+ const canonicalCandidate = canonicalizePath(candidatePath, name, options.requireExistingCandidate ?? false);
463
+ const relativePath = relative(canonicalRoot, canonicalCandidate);
281
464
  if (relativePath === "" || relativePath === ".") {
282
465
  return;
283
466
  }
284
467
  if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
285
- throw new Error(`Invalid ${name}.`);
468
+ throw invalidPathError(`Invalid ${name}.`);
469
+ }
470
+ }
471
+ function assertNoSymbolicLinkSegments(pathValue, name, stopAtPath) {
472
+ const stopAt = stopAtPath ? resolve(stopAtPath) : undefined;
473
+ let current = resolve(pathValue);
474
+ while (true) {
475
+ if (existsSync(current)) {
476
+ try {
477
+ const stats = lstatSync(current);
478
+ if (stats.isSymbolicLink()) {
479
+ throw invalidPathError(`Invalid ${name}.`);
480
+ }
481
+ }
482
+ catch (error) {
483
+ if (error instanceof Error) {
484
+ throw error;
485
+ }
486
+ throw invalidPathError(`Invalid ${name}.`);
487
+ }
488
+ }
489
+ if (stopAt && relative(stopAt, current) === "") {
490
+ break;
491
+ }
492
+ const parent = dirname(current);
493
+ if (parent === current) {
494
+ break;
495
+ }
496
+ current = parent;
497
+ }
498
+ }
499
+ function canonicalizePath(pathValue, name, requireExisting) {
500
+ const resolvedPath = resolve(pathValue);
501
+ if (!existsSync(resolvedPath)) {
502
+ if (requireExisting) {
503
+ throw invalidPathError(`Invalid ${name}.`);
504
+ }
505
+ return resolvedPath;
506
+ }
507
+ try {
508
+ const stats = lstatSync(resolvedPath);
509
+ if (stats.isSymbolicLink()) {
510
+ throw invalidPathError(`Invalid ${name}.`);
511
+ }
512
+ }
513
+ catch (error) {
514
+ if (error instanceof Error) {
515
+ throw error;
516
+ }
517
+ throw invalidPathError(`Invalid ${name}.`);
518
+ }
519
+ try {
520
+ return realpathSync.native(resolvedPath);
521
+ }
522
+ catch {
523
+ throw invalidPathError(`Invalid ${name}.`);
286
524
  }
287
525
  }
288
526
  function requireString(value, name) {
289
527
  if (typeof value !== "string" || value.trim().length === 0) {
290
- throw new Error(`Invalid ${name}.`);
528
+ throw invalidArgumentsError(`Invalid ${name}.`);
291
529
  }
292
530
  return value.trim();
293
531
  }
294
532
  function requireLoopId(value, name) {
295
533
  const loopId = requireString(value, name);
296
534
  if (!/^[A-Za-z0-9._-]+$/u.test(loopId)) {
297
- throw new Error(`Invalid ${name}.`);
535
+ throw invalidPathError(`Invalid ${name}.`, "loopId may only include letters, numbers, dots, underscores, and hyphens.");
298
536
  }
299
537
  return loopId;
300
538
  }
@@ -309,7 +547,7 @@ function optionalPositiveNumber(value, name) {
309
547
  return {};
310
548
  }
311
549
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
312
- throw new Error(`Invalid ${name}.`);
550
+ throw invalidArgumentsError(`Invalid ${name}.`);
313
551
  }
314
552
  return { [name]: value };
315
553
  }
@@ -317,20 +555,35 @@ function optionalPositiveInteger(value, name) {
317
555
  if (value === undefined) {
318
556
  return {};
319
557
  }
320
- return { [name]: requirePositiveInteger(value, name) };
321
- }
322
- function requirePositiveInteger(value, name) {
323
558
  if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
324
- throw new Error(`Invalid ${name}.`);
559
+ throw invalidArgumentsError(`Invalid ${name}.`);
325
560
  }
326
- return value;
561
+ return { [name]: value };
562
+ }
563
+ function optionalNonNegativeInteger(value, name) {
564
+ if (value === undefined) {
565
+ return {};
566
+ }
567
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
568
+ throw invalidArgumentsError(`Invalid ${name}.`);
569
+ }
570
+ return { [name]: value };
571
+ }
572
+ function optionalBoolean(value, name) {
573
+ if (value === undefined) {
574
+ return {};
575
+ }
576
+ if (typeof value !== "boolean") {
577
+ throw invalidArgumentsError(`Invalid ${name}.`);
578
+ }
579
+ return { [name]: value };
327
580
  }
328
581
  function optionalStringArray(value, name) {
329
582
  if (value === undefined) {
330
583
  return undefined;
331
584
  }
332
585
  if (!Array.isArray(value)) {
333
- throw new Error(`Invalid ${name}.`);
586
+ throw invalidArgumentsError(`Invalid ${name}.`);
334
587
  }
335
588
  return value.map((item) => requireString(item, name));
336
589
  }
@@ -347,8 +600,11 @@ function optionalEnum(value, name, allowed) {
347
600
  return undefined;
348
601
  }
349
602
  if (typeof value !== "string" || !allowed.includes(value)) {
350
- throw new Error(`Invalid ${name}.`);
603
+ throw invalidArgumentsError(`Invalid ${name}.`);
351
604
  }
352
605
  return value;
353
606
  }
354
- //# sourceMappingURL=server-validation.js.map
607
+ function optionalEnumAsObject(value, name, allowed) {
608
+ const enumValue = optionalEnum(value, name, allowed);
609
+ return enumValue ? { [name]: enumValue } : {};
610
+ }