@martinloop/mcp 0.8.4 → 0.8.6
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/dist/arcade/capabilities.d.ts +1 -1
- package/dist/arcade/capabilities.js +2 -1
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/server-validation.js +9 -4
- package/dist/server.js +45 -3
- package/dist/tools/pr-tools.js +12 -0
- package/dist/tools/run-controls.d.ts +5 -0
- package/dist/tools/run-controls.js +6 -1
- package/dist/vendor/adapters/claude-cli.js +53 -1
- package/dist/vendor/adapters/process-supervisor.d.ts +7 -0
- package/dist/vendor/adapters/process-supervisor.js +76 -2
- package/dist/vendor/core/grounding.js +31 -1
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -54,7 +54,7 @@ export declare function buildArcadeToolDefinitions(capabilities: unknown): reado
|
|
|
54
54
|
readonly displayOutcome: {
|
|
55
55
|
readonly anyOf: readonly [{
|
|
56
56
|
readonly type: "string";
|
|
57
|
-
readonly enum: readonly ["VERIFIED", "STOPPED", "
|
|
57
|
+
readonly enum: readonly ["VERIFIED", "STOPPED", "NEEDS_REVIEW"];
|
|
58
58
|
}, {
|
|
59
59
|
readonly type: "null";
|
|
60
60
|
}];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { VERIFIED_HANDOFF_OUTCOMES } from "../vendor/contracts/index.js";
|
|
1
2
|
export const MARTIN_ARCADE_UI_EXTENSION_ID = "io.modelcontextprotocol/ui";
|
|
2
3
|
export const MARTIN_ARCADE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
3
4
|
export const MARTIN_ARCADE_RESOURCE_URI = "ui://martinloop/arcade/index.html";
|
|
@@ -51,7 +52,7 @@ export function buildArcadeToolDefinitions(capabilities) {
|
|
|
51
52
|
completed: { type: "boolean", description: "Whether the run is outside an active lifecycle state." },
|
|
52
53
|
displayOutcome: {
|
|
53
54
|
anyOf: [
|
|
54
|
-
{ type: "string", enum: [
|
|
55
|
+
{ type: "string", enum: [...VERIFIED_HANDOFF_OUTCOMES] },
|
|
55
56
|
{ type: "null" }
|
|
56
57
|
],
|
|
57
58
|
description: "Terminal verified-handoff outcome, or null while the run is active."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.
|
|
1
|
+
export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.6";
|
package/dist/package-version.js
CHANGED
|
@@ -435,7 +435,7 @@ function validateGetVerificationResultsInput(args) {
|
|
|
435
435
|
function validateRunDossierInput(args) {
|
|
436
436
|
const record = requireObject(args);
|
|
437
437
|
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format"]);
|
|
438
|
-
const base = validateGetRunInput(
|
|
438
|
+
const base = validateGetRunInput(pickKeys(record, RUN_SELECTOR_KEYS));
|
|
439
439
|
return {
|
|
440
440
|
...base,
|
|
441
441
|
...optionalEnumAsObject(record.format, "format", ["json", "md", "github-pr"])
|
|
@@ -447,7 +447,7 @@ function validateEvalInput(args) {
|
|
|
447
447
|
function validateRunControlInput(args) {
|
|
448
448
|
const record = requireObject(args);
|
|
449
449
|
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "reason", "requestedBy"]);
|
|
450
|
-
const base = validateGetRunInput(
|
|
450
|
+
const base = validateGetRunInput(pickKeys(record, RUN_SELECTOR_KEYS));
|
|
451
451
|
return {
|
|
452
452
|
...base,
|
|
453
453
|
...optionalString(record.reason, "reason"),
|
|
@@ -457,7 +457,7 @@ function validateRunControlInput(args) {
|
|
|
457
457
|
function validateCreatePrInput(args) {
|
|
458
458
|
const record = requireObject(args);
|
|
459
459
|
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format", "title", "base", "execute"]);
|
|
460
|
-
const base = validateRunDossierInput(
|
|
460
|
+
const base = validateRunDossierInput(pickKeys(record, [...RUN_SELECTOR_KEYS, "format"]));
|
|
461
461
|
return {
|
|
462
462
|
...base,
|
|
463
463
|
...optionalString(record.title, "title"),
|
|
@@ -468,12 +468,17 @@ function validateCreatePrInput(args) {
|
|
|
468
468
|
function validateReviewPrInput(args) {
|
|
469
469
|
const record = requireObject(args);
|
|
470
470
|
assertAllowedKeys(record, ["file", "loopId", "runsDir", "latest", "format", "prBody"]);
|
|
471
|
-
const base = validateRunDossierInput(
|
|
471
|
+
const base = validateRunDossierInput(pickKeys(record, [...RUN_SELECTOR_KEYS, "format"]));
|
|
472
472
|
return {
|
|
473
473
|
...base,
|
|
474
474
|
...optionalString(record.prBody, "prBody")
|
|
475
475
|
};
|
|
476
476
|
}
|
|
477
|
+
const RUN_SELECTOR_KEYS = ["file", "loopId", "runsDir", "latest"];
|
|
478
|
+
// Base validators check their own allowed keys, so composed validators hand them only those keys.
|
|
479
|
+
function pickKeys(record, keys) {
|
|
480
|
+
return Object.fromEntries(Object.entries(record).filter(([key]) => keys.includes(key)));
|
|
481
|
+
}
|
|
477
482
|
function requireObject(value) {
|
|
478
483
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
479
484
|
throw invalidArgumentsError("Tool arguments must be an object.");
|
package/dist/server.js
CHANGED
|
@@ -501,6 +501,25 @@ const preflightOutputSchema = {
|
|
|
501
501
|
},
|
|
502
502
|
required: ["available", "detail"]
|
|
503
503
|
},
|
|
504
|
+
// Present when the Codex launch probe ran on a live host.
|
|
505
|
+
codexDiagnostics: {
|
|
506
|
+
type: "object",
|
|
507
|
+
additionalProperties: true,
|
|
508
|
+
properties: {
|
|
509
|
+
selectedPath: { type: "string" },
|
|
510
|
+
hostPlatform: { type: "string" },
|
|
511
|
+
installKind: { type: "string" },
|
|
512
|
+
nativeInstallValid: { type: "boolean" },
|
|
513
|
+
invocationMode: { type: "string" },
|
|
514
|
+
sandboxMode: { type: "string" },
|
|
515
|
+
sandboxCompatible: { type: "boolean" },
|
|
516
|
+
launchReady: { type: "boolean" },
|
|
517
|
+
summary: { type: "string" },
|
|
518
|
+
remediation: { type: "string" },
|
|
519
|
+
candidateProbeResults: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
520
|
+
},
|
|
521
|
+
required: ["launchReady", "summary"]
|
|
522
|
+
},
|
|
504
523
|
runsRoot: { type: "string" },
|
|
505
524
|
pathScope: {
|
|
506
525
|
type: "object",
|
|
@@ -775,9 +794,21 @@ const controlOutputSchema = {
|
|
|
775
794
|
controlId: { type: "string", description: "Unique receipt ID for this control action." },
|
|
776
795
|
requestedAt: { type: "string", description: "ISO 8601 timestamp when the control was recorded." },
|
|
777
796
|
requestedBy: { type: "string", description: "Identity that requested the control, if provided." },
|
|
778
|
-
reason: { type: "string", description: "Human-readable reason for the control, if provided." }
|
|
797
|
+
reason: { type: "string", description: "Human-readable reason for the control, if provided." },
|
|
798
|
+
ok: { type: "boolean", description: "Whether the control receipt was recorded." },
|
|
799
|
+
summary: { type: "string", description: "Human-readable result of the control request." },
|
|
800
|
+
requestedAction: {
|
|
801
|
+
type: "string",
|
|
802
|
+
enum: ["pause", "cancel", "continue"],
|
|
803
|
+
description: "The control action that was requested."
|
|
804
|
+
},
|
|
805
|
+
state: {
|
|
806
|
+
type: "object",
|
|
807
|
+
additionalProperties: true,
|
|
808
|
+
description: "Run control state after this request, including all recorded receipts."
|
|
809
|
+
}
|
|
779
810
|
},
|
|
780
|
-
required: ["loopId", "action", "controlId", "requestedAt"]
|
|
811
|
+
required: ["loopId", "action", "controlId", "requestedAt", "ok", "summary", "requestedAction", "state"]
|
|
781
812
|
};
|
|
782
813
|
const evalOutputSchema = {
|
|
783
814
|
type: "object",
|
|
@@ -830,6 +861,17 @@ const prSummaryOutputSchema = {
|
|
|
830
861
|
},
|
|
831
862
|
required: ["loopId", "title", "body", "grade", "score"]
|
|
832
863
|
};
|
|
864
|
+
const createPrOutputSchema = {
|
|
865
|
+
...prSummaryOutputSchema,
|
|
866
|
+
properties: {
|
|
867
|
+
...prSummaryOutputSchema.properties,
|
|
868
|
+
execute: { type: "boolean", description: "Whether gh was asked to create the pull request." },
|
|
869
|
+
created: { type: "boolean", description: "Whether a pull request was created." },
|
|
870
|
+
branch: { type: "string", description: "Current branch of the run's repository, when known." },
|
|
871
|
+
url: { type: "string", description: "URL of the created pull request." }
|
|
872
|
+
},
|
|
873
|
+
required: [...prSummaryOutputSchema.required, "execute", "created"]
|
|
874
|
+
};
|
|
833
875
|
const prReviewOutputSchema = {
|
|
834
876
|
type: "object",
|
|
835
877
|
additionalProperties: false,
|
|
@@ -1505,7 +1547,7 @@ export function createMartinMcpServer(serverInfo) {
|
|
|
1505
1547
|
execute: { type: "boolean" }
|
|
1506
1548
|
}
|
|
1507
1549
|
},
|
|
1508
|
-
outputSchema:
|
|
1550
|
+
outputSchema: createPrOutputSchema
|
|
1509
1551
|
},
|
|
1510
1552
|
{
|
|
1511
1553
|
name: "martin_review_pr",
|
package/dist/tools/pr-tools.js
CHANGED
|
@@ -33,6 +33,18 @@ export async function martinCreatePrTool(input) {
|
|
|
33
33
|
...(branch ? { branch } : {})
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
// Opening a PR claims the work is done. Only a VERIFIED run with intact receipts qualifies.
|
|
37
|
+
const dossier = await martinRunDossierTool(input);
|
|
38
|
+
const outcome = dossier.verifiedHandoff.outcome;
|
|
39
|
+
const integrity = dossier.receiptIntegrity.state;
|
|
40
|
+
if (outcome !== "VERIFIED" || integrity !== "verified") {
|
|
41
|
+
throw new MartinToolError("policy_blocked", `Refusing to open a PR: run ${summary.loopId} is ${outcome} with receipt integrity ${integrity}.`, {
|
|
42
|
+
category: "policy_blocked",
|
|
43
|
+
suggestion: "Open a PR only from a VERIFIED run, or rerun martin_create_pr with execute=false to preview it.",
|
|
44
|
+
retryable: false,
|
|
45
|
+
details: { outcome, receiptIntegrity: integrity }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
36
48
|
const gh = detectCliAvailability("gh");
|
|
37
49
|
if (!gh.available) {
|
|
38
50
|
throw new MartinToolError("engine_unavailable", "GitHub CLI is not available on PATH.", {
|
|
@@ -29,6 +29,11 @@ export interface MartinRunControlOutput {
|
|
|
29
29
|
loopId: string;
|
|
30
30
|
requestedAction: MartinControlAction;
|
|
31
31
|
state: MartinRunControlState;
|
|
32
|
+
action: MartinControlAction;
|
|
33
|
+
controlId: string;
|
|
34
|
+
requestedAt: string;
|
|
35
|
+
reason?: string;
|
|
36
|
+
requestedBy?: string;
|
|
32
37
|
}
|
|
33
38
|
export declare function createRunControlReceipt(action: MartinControlAction, input: MartinRunControlRequestInput): Promise<MartinRunControlOutput>;
|
|
34
39
|
export declare function readRunControlState(detailOrInput: DetailedLoopSource | MartinRunControlRequestInput): Promise<MartinRunControlState>;
|
|
@@ -27,7 +27,12 @@ export async function createRunControlReceipt(action, input) {
|
|
|
27
27
|
: `Continue request recorded for ${detail.loop.loopId}.`,
|
|
28
28
|
loopId: detail.loop.loopId,
|
|
29
29
|
requestedAction: action,
|
|
30
|
-
state
|
|
30
|
+
state,
|
|
31
|
+
action: receipt.action,
|
|
32
|
+
controlId: receipt.controlId,
|
|
33
|
+
requestedAt: receipt.requestedAt,
|
|
34
|
+
...(receipt.reason ? { reason: receipt.reason } : {}),
|
|
35
|
+
...(receipt.requestedBy ? { requestedBy: receipt.requestedBy } : {})
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
export async function readRunControlState(detailOrInput) {
|
|
@@ -210,6 +210,39 @@ function extractUsage(parsed, modelLabel, modelSource) {
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Returns the provider's failure message when the Codex JSONL stream shows a failed turn:
|
|
215
|
+
* an explicit `turn.failed`, or an `error` event with no `turn.completed`. Undefined otherwise.
|
|
216
|
+
*/
|
|
217
|
+
function findCodexTurnFailure(stdout) {
|
|
218
|
+
let completed = false;
|
|
219
|
+
let turnFailed = false;
|
|
220
|
+
let message;
|
|
221
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
222
|
+
let event;
|
|
223
|
+
try {
|
|
224
|
+
event = JSON.parse(line.trim());
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (event.type === "turn.completed")
|
|
230
|
+
completed = true;
|
|
231
|
+
if (event.type === "turn.failed") {
|
|
232
|
+
turnFailed = true;
|
|
233
|
+
message = String(event.error?.message ?? message ?? "Codex turn failed.");
|
|
234
|
+
}
|
|
235
|
+
else if (event.type === "error") {
|
|
236
|
+
message ??= String(event.message ?? "Codex reported an error.");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (turnFailed)
|
|
240
|
+
return message;
|
|
241
|
+
return message !== undefined && !completed ? message : undefined;
|
|
242
|
+
}
|
|
243
|
+
function codexTurnCompleted(stdout) {
|
|
244
|
+
return /"type"\s*:\s*"turn\.completed"/u.test(stdout);
|
|
245
|
+
}
|
|
213
246
|
function extractCodexJsonlResult(stdout, modelLabel) {
|
|
214
247
|
const events = stdout
|
|
215
248
|
.split(/\r?\n/u)
|
|
@@ -829,8 +862,27 @@ export function createAgentCliAdapter(options) {
|
|
|
829
862
|
const geminiJsonResult = !supportsJsonOutput && options.command === "gemini"
|
|
830
863
|
? extractGeminiJsonResult(agentResult.stdout, options.model)
|
|
831
864
|
: undefined;
|
|
865
|
+
// A failed Codex turn did no verified work: never let the verifier run against an
|
|
866
|
+
// untouched workspace and certify it.
|
|
867
|
+
const codexTurnFailure = options.command === "codex" ? findCodexTurnFailure(agentResult.stdout) : undefined;
|
|
868
|
+
if (codexTurnFailure !== undefined) {
|
|
869
|
+
const failureMessage = `codex turn failed: ${codexTurnFailure}`;
|
|
870
|
+
return {
|
|
871
|
+
status: "failed",
|
|
872
|
+
summary: "codex turn failed before verifier execution.",
|
|
873
|
+
usage: codexJsonlResult?.usage ?? normalizeUsage({
|
|
874
|
+
actualUsd: 0,
|
|
875
|
+
tokensIn: 0,
|
|
876
|
+
tokensOut: 0,
|
|
877
|
+
provenance: "unavailable"
|
|
878
|
+
}),
|
|
879
|
+
verification: { passed: false, summary: `Verifier not run: ${failureMessage}` },
|
|
880
|
+
failure: { message: failureMessage }
|
|
881
|
+
};
|
|
882
|
+
}
|
|
832
883
|
const producedStructuredCompletion = parsed?.result !== undefined ||
|
|
833
|
-
codexJsonlResult !== undefined
|
|
884
|
+
(codexJsonlResult !== undefined &&
|
|
885
|
+
(agentResult.exitCode === 0 || codexTurnCompleted(agentResult.stdout))) ||
|
|
834
886
|
geminiJsonResult !== undefined;
|
|
835
887
|
if (agentResult.exitCode !== 0 && !producedStructuredCompletion) {
|
|
836
888
|
const failureMessage = formatPreVerifierSubprocessFailure(options.command, agentResult.stderr || agentResult.stdout, agentResult.exitCode);
|
|
@@ -57,4 +57,11 @@ export declare function spawnSupervisedProcess(command: string, args: readonly s
|
|
|
57
57
|
export declare function spawnSupervisedProcessForTest(command: string, args: readonly string[], options: SpawnSupervisedProcessOptions & {
|
|
58
58
|
spawnImpl: ProcessSpawnLike;
|
|
59
59
|
}, seams: ProcessSupervisorTestSeams): Promise<SupervisedProcessResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Classify an OS process-table snapshot with PID, PGID and STAT columns.
|
|
62
|
+
* POSIX Z means terminated but not yet reaped; other states remain live.
|
|
63
|
+
* Malformed or empty output must never assert cleanup success.
|
|
64
|
+
* @internal - deterministic pure classification for process-supervisor tests
|
|
65
|
+
*/
|
|
66
|
+
export declare function classifyPosixProcessGroupSnapshot(output: string, ownedGroupPid: number): "closed" | "alive" | "absent" | "invalid";
|
|
60
67
|
export {};
|
|
@@ -327,14 +327,21 @@ async function terminatePosixGroupAfterRootExit(pid, killGroup, graceMs, waitImp
|
|
|
327
327
|
const initial = probePosixProcessGroup(target, killGroup);
|
|
328
328
|
if (initial === "closed")
|
|
329
329
|
return { state: "closed" };
|
|
330
|
-
if (initial instanceof Error)
|
|
330
|
+
if (initial instanceof Error) {
|
|
331
|
+
if (isErrno(initial, "EPERM") && await confirmInactivePosixGroup(pid, killGroup)) {
|
|
332
|
+
return { state: "closed" };
|
|
333
|
+
}
|
|
331
334
|
return cleanupFailure("termination_failed", initial);
|
|
335
|
+
}
|
|
332
336
|
try {
|
|
333
337
|
killGroup(target, "SIGTERM");
|
|
334
338
|
}
|
|
335
339
|
catch (error) {
|
|
336
340
|
if (isErrno(error, "ESRCH"))
|
|
337
341
|
return { state: "closed" };
|
|
342
|
+
if (isErrno(error, "EPERM") && await confirmInactivePosixGroup(pid, killGroup)) {
|
|
343
|
+
return { state: "closed" };
|
|
344
|
+
}
|
|
338
345
|
return cleanupFailure("termination_failed", error);
|
|
339
346
|
}
|
|
340
347
|
if (await waitForPosixGroupExit(target, killGroup, graceMs, waitImpl))
|
|
@@ -345,14 +352,30 @@ async function terminatePosixGroupAfterRootExit(pid, killGroup, graceMs, waitImp
|
|
|
345
352
|
catch (error) {
|
|
346
353
|
if (isErrno(error, "ESRCH"))
|
|
347
354
|
return { state: "closed" };
|
|
355
|
+
if (isErrno(error, "EPERM") && await confirmInactivePosixGroup(pid, killGroup)) {
|
|
356
|
+
return { state: "closed" };
|
|
357
|
+
}
|
|
348
358
|
return cleanupFailure("termination_failed", error);
|
|
349
359
|
}
|
|
350
360
|
if (await waitForPosixGroupExit(target, killGroup, graceMs, waitImpl))
|
|
351
361
|
return { state: "closed" };
|
|
362
|
+
// A reparented, already-terminated descendant can remain as a zombie in the
|
|
363
|
+
// process table. On POSIX, kill(-pgid, 0) still sees that process group even
|
|
364
|
+
// though its remaining members cannot execute code. Prove the absence of
|
|
365
|
+
// *runnable* group members rather than treating an unreapable zombie as live.
|
|
366
|
+
// Do not turn an unreadable/ambiguous process snapshot into success.
|
|
367
|
+
const snapshot = await inspectPosixProcessGroupMembers(pid);
|
|
368
|
+
if (snapshot === "closed" || (snapshot === "absent" && probePosixProcessGroup(target, killGroup) === "closed")) {
|
|
369
|
+
return { state: "closed" };
|
|
370
|
+
}
|
|
371
|
+
if (snapshot instanceof Error)
|
|
372
|
+
return cleanupFailure("termination_failed", snapshot);
|
|
352
373
|
return {
|
|
353
374
|
state: "failed",
|
|
354
375
|
code: "tree_close_timeout",
|
|
355
|
-
message:
|
|
376
|
+
message: snapshot === "invalid"
|
|
377
|
+
? "Could not prove owned POSIX group closure from the process table."
|
|
378
|
+
: "Owned POSIX process group still has live members after SIGTERM and SIGKILL."
|
|
356
379
|
};
|
|
357
380
|
}
|
|
358
381
|
async function waitForPosixGroupExit(target, killGroup, durationMs, waitImpl) {
|
|
@@ -367,6 +390,14 @@ async function waitForPosixGroupExit(target, killGroup, durationMs, waitImpl) {
|
|
|
367
390
|
}
|
|
368
391
|
return probePosixProcessGroup(target, killGroup) === "closed";
|
|
369
392
|
}
|
|
393
|
+
async function confirmInactivePosixGroup(ownedGroupPid, killGroup) {
|
|
394
|
+
const snapshot = await inspectPosixProcessGroupMembers(ownedGroupPid);
|
|
395
|
+
if (snapshot === "closed")
|
|
396
|
+
return true; // Observed owned members are all defunct zombies.
|
|
397
|
+
// An absent group is not proof on its own after EPERM. Require an ESRCH re-probe.
|
|
398
|
+
return snapshot === "absent"
|
|
399
|
+
&& probePosixProcessGroup(-ownedGroupPid, killGroup) === "closed";
|
|
400
|
+
}
|
|
370
401
|
function probePosixProcessGroup(target, killGroup) {
|
|
371
402
|
try {
|
|
372
403
|
killGroup(target, 0);
|
|
@@ -378,6 +409,49 @@ function probePosixProcessGroup(target, killGroup) {
|
|
|
378
409
|
return error instanceof Error ? error : new Error(String(error));
|
|
379
410
|
}
|
|
380
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Classify an OS process-table snapshot with PID, PGID and STAT columns.
|
|
414
|
+
* POSIX Z means terminated but not yet reaped; other states remain live.
|
|
415
|
+
* Malformed or empty output must never assert cleanup success.
|
|
416
|
+
* @internal - deterministic pure classification for process-supervisor tests
|
|
417
|
+
*/
|
|
418
|
+
export function classifyPosixProcessGroupSnapshot(output, ownedGroupPid) {
|
|
419
|
+
if (!isOwnedPid(ownedGroupPid))
|
|
420
|
+
return "invalid";
|
|
421
|
+
let observed = 0;
|
|
422
|
+
let matchingGroupMembers = 0;
|
|
423
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
424
|
+
if (!line.trim())
|
|
425
|
+
continue;
|
|
426
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/u.exec(line);
|
|
427
|
+
if (!match)
|
|
428
|
+
return "invalid";
|
|
429
|
+
observed += 1;
|
|
430
|
+
if (Number(match[2]) === ownedGroupPid) {
|
|
431
|
+
matchingGroupMembers += 1;
|
|
432
|
+
if (!match[3].startsWith("Z"))
|
|
433
|
+
return "alive";
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (observed === 0)
|
|
437
|
+
return "invalid";
|
|
438
|
+
return matchingGroupMembers > 0 ? "closed" : "absent";
|
|
439
|
+
}
|
|
440
|
+
async function inspectPosixProcessGroupMembers(ownedGroupPid) {
|
|
441
|
+
return new Promise((resolveResult) => {
|
|
442
|
+
execFile("ps", ["-axo", "pid=,pgid=,stat="], {
|
|
443
|
+
encoding: "utf8",
|
|
444
|
+
timeout: 2_000,
|
|
445
|
+
maxBuffer: 8 * 1024 * 1024
|
|
446
|
+
}, (error, stdout) => {
|
|
447
|
+
if (error) {
|
|
448
|
+
resolveResult(error);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
resolveResult(classifyPosixProcessGroupSnapshot(stdout, ownedGroupPid));
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
381
455
|
function isErrno(error, code) {
|
|
382
456
|
return typeof error === "object" && error !== null && "code" in error
|
|
383
457
|
&& String(error.code) === code;
|
|
@@ -5,6 +5,21 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { extname, join, relative } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
+
// Callables provided by the JavaScript language, Node.js, browsers, and common test runners.
|
|
9
|
+
// They are never declared in the repository, so they must not count as ungrounded symbols.
|
|
10
|
+
const STANDARD_CALLABLE_GLOBALS = new Set([
|
|
11
|
+
// CommonJS / Node.js
|
|
12
|
+
"require", "setTimeout", "setInterval", "setImmediate", "clearTimeout", "clearInterval",
|
|
13
|
+
"clearImmediate", "queueMicrotask", "structuredClone", "fetch",
|
|
14
|
+
// ECMAScript globals
|
|
15
|
+
"parseInt", "parseFloat", "isNaN", "isFinite", "encodeURIComponent", "decodeURIComponent",
|
|
16
|
+
"encodeURI", "decodeURI", "escape", "unescape", "eval",
|
|
17
|
+
// Browser globals
|
|
18
|
+
"alert", "confirm", "prompt", "atob", "btoa", "requestAnimationFrame", "cancelAnimationFrame",
|
|
19
|
+
// Test runners (node:test, vitest, jest, mocha, chai)
|
|
20
|
+
"describe", "suite", "test", "it", "expect", "assert", "before", "after", "beforeEach",
|
|
21
|
+
"afterEach", "beforeAll", "afterAll", "context", "specify", "should"
|
|
22
|
+
]);
|
|
8
23
|
const TEXT_EXTENSIONS = new Set([
|
|
9
24
|
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
10
25
|
".json", ".md", ".txt", ".yaml", ".yml", ".py", ".go",
|
|
@@ -284,17 +299,30 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
|
|
|
284
299
|
"while",
|
|
285
300
|
"switch",
|
|
286
301
|
"catch",
|
|
287
|
-
"typeof"
|
|
302
|
+
"typeof",
|
|
303
|
+
"super",
|
|
304
|
+
"yield",
|
|
305
|
+
"delete",
|
|
306
|
+
"throw",
|
|
307
|
+
"instanceof",
|
|
308
|
+
"constructor"
|
|
288
309
|
]);
|
|
289
310
|
const addedLines = [...diff.matchAll(addedLinePattern)].map((match) => match[1] ?? "");
|
|
290
311
|
const declaredSymbols = new Set();
|
|
291
312
|
const declarationPattern = /\b(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
313
|
+
// Class and object-literal method definitions: `name(args) {`, optionally async/static/get/set.
|
|
314
|
+
const methodDefinitionPattern = /(?:^|[{,])\s*(?:(?:public|private|protected|static|async|get|set|override)\s+)*([A-Za-z_][A-Za-z0-9_]*)\s*\([^()]*\)\s*(?::[^{=]+)?\{/gu;
|
|
292
315
|
for (const line of addedLines) {
|
|
293
316
|
for (const declarationMatch of line.matchAll(declarationPattern)) {
|
|
294
317
|
const symbol = declarationMatch[1];
|
|
295
318
|
if (symbol)
|
|
296
319
|
declaredSymbols.add(symbol.toLowerCase());
|
|
297
320
|
}
|
|
321
|
+
for (const methodMatch of line.matchAll(methodDefinitionPattern)) {
|
|
322
|
+
const methodName = methodMatch[1];
|
|
323
|
+
if (methodName && !symbolKeywords.has(methodName))
|
|
324
|
+
declaredSymbols.add(methodName.toLowerCase());
|
|
325
|
+
}
|
|
298
326
|
if (/^\s*import\b/u.test(line)) {
|
|
299
327
|
const importClause = line.match(/^\s*import\s+(.+?)\s+from\s+['"]/u)?.[1] ?? "";
|
|
300
328
|
for (const importSymbol of importClause.matchAll(/\b(?:as\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
|
|
@@ -334,6 +362,8 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
|
|
|
334
362
|
continue;
|
|
335
363
|
if (symbolKeywords.has(symbol))
|
|
336
364
|
continue;
|
|
365
|
+
if (STANDARD_CALLABLE_GLOBALS.has(symbol))
|
|
366
|
+
continue;
|
|
337
367
|
if (/^[A-Z]/u.test(symbol))
|
|
338
368
|
continue;
|
|
339
369
|
if (declaredSymbols.has(symbol.toLowerCase()))
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/Keesan12/martin-loop",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.8.
|
|
10
|
+
"version": "0.8.6",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@martinloop/mcp",
|
|
15
|
-
"version": "0.8.
|
|
15
|
+
"version": "0.8.6",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|