@martinloop/mcp 0.3.5 → 0.3.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.
- package/README.md +137 -137
- package/dist/discovery-metadata.d.ts +1 -1
- package/dist/discovery-metadata.js +2 -0
- package/dist/prompts.js +19 -1
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +125 -17
- package/dist/server-validation.d.ts +1 -1
- package/dist/server-validation.js +12 -0
- package/dist/server.js +18 -7
- package/dist/tools/doctor.js +6 -0
- package/dist/tools/run-loop.d.ts +2 -1
- package/dist/tools/run-loop.js +5 -1
- package/dist/tools/tool-support.d.ts +1 -1
- package/dist/tools/tool-support.js +2 -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 +21 -20
- package/dist/vendor/adapters/cli-bridge.d.ts +7 -0
- package/dist/vendor/adapters/cli-bridge.js +94 -8
- package/dist/vendor/adapters/codex-launcher.js +18 -7
- 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 +84 -51
- package/dist/vendor/adapters/verifier-only.js +2 -2
- package/dist/vendor/contracts/index.d.ts +2 -2
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/core/compiler.js +4 -9
- package/dist/vendor/core/index.d.ts +10 -2
- package/dist/vendor/core/index.js +32 -91
- package/dist/vendor/core/persistence/memory-store.d.ts +63 -0
- package/dist/vendor/core/persistence/memory-store.js +115 -0
- package/dist/vendor/core/persistence/trace-store.d.ts +85 -0
- package/dist/vendor/core/persistence/trace-store.js +125 -0
- package/dist/vendor/core/repo-analyzer.d.ts +53 -0
- package/dist/vendor/core/repo-analyzer.js +263 -0
- package/dist/vendor/core/rollback.js +2 -3
- package/dist/vendor/core/routing.d.ts +42 -0
- package/dist/vendor/core/routing.js +156 -5
- package/dist/vendor/core/test-integrity.d.ts +64 -0
- package/dist/vendor/core/test-integrity.js +197 -0
- package/dist/workflow-state.d.ts +1 -1
- package/dist/workflow-state.js +12 -4
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -13,7 +13,11 @@ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, ver
|
|
|
13
13
|
export { compileExecutionPolicy } from "./policy-compiler.js";
|
|
14
14
|
export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
|
|
15
15
|
// ─── Routing economics ─────────────────────────────────────────────────────
|
|
16
|
-
export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
|
|
16
|
+
export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
|
|
17
|
+
// ─── Test Integrity Validation ──────────────────────────────────────────────
|
|
18
|
+
export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
|
|
19
|
+
// ─── Repo Style Analyzer ────────────────────────────────────────────────────
|
|
20
|
+
export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
|
|
17
21
|
// ─── Context Integrity Pre-gate ──────────────────────────────────────────────
|
|
18
22
|
export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
19
23
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
@@ -21,6 +25,10 @@ export { compilePromptPacket } from "./compiler.js";
|
|
|
21
25
|
// ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
|
|
22
26
|
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
23
27
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
28
|
+
// ─── MartinLoop Memory Store ────────────────────────────────────────────────
|
|
29
|
+
export { appendMemory, readMemoryEntries, getPreference, buildMemorySummary, recordPreference, recordConsent } from "./persistence/memory-store.js";
|
|
30
|
+
// ─── Trace Intelligence Store ───────────────────────────────────────────────
|
|
31
|
+
export { appendTraceEntry, readTraceEntries, aggregateTraces, getHistoricalDirectSuccessRate } from "./persistence/trace-store.js";
|
|
24
32
|
/**
|
|
25
33
|
* Admission gate — must pass before any attempt is executed.
|
|
26
34
|
* Evaluates budget headroom, oscillation, and repetitive failure patterns.
|
|
@@ -178,7 +186,6 @@ export async function runMartin(input) {
|
|
|
178
186
|
let currentAdapterIndex = 0;
|
|
179
187
|
let currentAdapter = adapterChain[currentAdapterIndex] ?? input.adapter;
|
|
180
188
|
let useCompressedContext = false;
|
|
181
|
-
const isVerifyOnly = input.task.mutationMode === "verify_only";
|
|
182
189
|
const executionProfile = resolveExecutionProfile({
|
|
183
190
|
executionProfile: input.task.executionProfile,
|
|
184
191
|
allowedNetworkDomains: input.task.allowedNetworkDomains
|
|
@@ -676,93 +683,7 @@ export async function runMartin(input) {
|
|
|
676
683
|
// a git repo) and silently return [], which would falsely trigger no_code_change.
|
|
677
684
|
const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
|
|
678
685
|
const isVerifierOnlyAdapter = executingAdapter.adapterId === "direct:verifier:verify-only";
|
|
679
|
-
const patchTruthCountsEdits = !
|
|
680
|
-
if (isVerifyOnly && changedFiles.length > 0) {
|
|
681
|
-
const patchDecision = evaluatePatchDecision({
|
|
682
|
-
verificationPassed: verification.passed,
|
|
683
|
-
previousVerifierScore,
|
|
684
|
-
verifierScore: verification.passed ? 1 : 0,
|
|
685
|
-
scopeViolationCount: changedFiles.length,
|
|
686
|
-
changedFileCount: changedFiles.length,
|
|
687
|
-
diffNovelty: 1,
|
|
688
|
-
diffStats: result.execution?.diffStats,
|
|
689
|
-
costUsd: getUsageUsd(result.usage),
|
|
690
|
-
summary: result.summary
|
|
691
|
-
});
|
|
692
|
-
const verifyOnlyExitDecision = {
|
|
693
|
-
shouldExit: true,
|
|
694
|
-
lifecycleState: "human_escalation",
|
|
695
|
-
status: "exited",
|
|
696
|
-
reason: "Verify-only mode forbids file changes.",
|
|
697
|
-
failureClass: "safety_leash_blocked",
|
|
698
|
-
safetySurface: "filesystem",
|
|
699
|
-
reasonCode: "verify_only_write_attempt"
|
|
700
|
-
};
|
|
701
|
-
const rollbackOutcome = await restoreRollbackBoundary({
|
|
702
|
-
repoRoot: request.context.repoRoot,
|
|
703
|
-
boundary: rollbackBoundary,
|
|
704
|
-
restoredAt: attemptCompletedAt,
|
|
705
|
-
decision: patchDecision.decision
|
|
706
|
-
});
|
|
707
|
-
if (input.store) {
|
|
708
|
-
const verifyOnlyViolation = {
|
|
709
|
-
kind: "path_not_allowed",
|
|
710
|
-
message: `Verify-only mode forbids changed files: ${changedFiles.join(", ")}`,
|
|
711
|
-
file: changedFiles[0]
|
|
712
|
-
};
|
|
713
|
-
await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
|
|
714
|
-
compiledContext,
|
|
715
|
-
leash: createLeashArtifact({
|
|
716
|
-
surface: "filesystem",
|
|
717
|
-
reason: verifyOnlyExitDecision.reason,
|
|
718
|
-
violations: [verifyOnlyViolation]
|
|
719
|
-
}, currentAttemptIndex),
|
|
720
|
-
patchScore: patchDecision.score,
|
|
721
|
-
patchDecision: toPatchDecisionArtifact(patchDecision),
|
|
722
|
-
...(rollbackBoundary ? { rollbackBoundary } : {}),
|
|
723
|
-
...(rollbackOutcome ? { rollbackOutcome } : {})
|
|
724
|
-
});
|
|
725
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
726
|
-
kind: "safety.violations_found",
|
|
727
|
-
runId: loop.loopId,
|
|
728
|
-
attemptIndex: currentAttemptIndex,
|
|
729
|
-
payload: {
|
|
730
|
-
surface: "filesystem",
|
|
731
|
-
blocked: true,
|
|
732
|
-
attemptIndex: currentAttemptIndex,
|
|
733
|
-
violations: [
|
|
734
|
-
{
|
|
735
|
-
kind: "path_not_allowed",
|
|
736
|
-
message: verifyOnlyExitDecision.reason,
|
|
737
|
-
files: changedFiles
|
|
738
|
-
}
|
|
739
|
-
]
|
|
740
|
-
}
|
|
741
|
-
}));
|
|
742
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
743
|
-
kind: "attempt.discarded",
|
|
744
|
-
runId: loop.loopId,
|
|
745
|
-
attemptIndex: currentAttemptIndex,
|
|
746
|
-
payload: {
|
|
747
|
-
decision: patchDecision.decision,
|
|
748
|
-
reason: patchDecision.summary,
|
|
749
|
-
reasonCodes: patchDecision.reasonCodes,
|
|
750
|
-
score: patchDecision.score.score
|
|
751
|
-
}
|
|
752
|
-
}));
|
|
753
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
754
|
-
kind: "run.exited",
|
|
755
|
-
runId: loop.loopId,
|
|
756
|
-
payload: createRunExitPayload(verifyOnlyExitDecision)
|
|
757
|
-
}));
|
|
758
|
-
}
|
|
759
|
-
const finalizedLoop = finalizeLoop(loop, verifyOnlyExitDecision, now(), idFactory);
|
|
760
|
-
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
761
|
-
return {
|
|
762
|
-
loop: finalizedLoop,
|
|
763
|
-
decision: verifyOnlyExitDecision
|
|
764
|
-
};
|
|
765
|
-
}
|
|
686
|
+
const patchTruthCountsEdits = !isVerifierOnlyAdapter && changedFileEvidenceAvailable;
|
|
766
687
|
const filesystemDecision = evaluateFilesystemLeash({
|
|
767
688
|
repoRoot: request.context.repoRoot,
|
|
768
689
|
changedFiles,
|
|
@@ -1009,7 +930,7 @@ export async function runMartin(input) {
|
|
|
1009
930
|
}
|
|
1010
931
|
}
|
|
1011
932
|
if (patchDecision && patchDecision.decision !== "KEEP" && !failure) {
|
|
1012
|
-
failure = classifyPatchDecisionFailure(patchDecision);
|
|
933
|
+
failure = classifyPatchDecisionFailure(patchDecision, result);
|
|
1013
934
|
loop = applyPatchFailureToLoop(loop, {
|
|
1014
935
|
attemptId,
|
|
1015
936
|
summary: patchDecision.summary,
|
|
@@ -1403,7 +1324,19 @@ function toPatchDecisionArtifact(decision) {
|
|
|
1403
1324
|
reasonCodes: decision.reasonCodes
|
|
1404
1325
|
};
|
|
1405
1326
|
}
|
|
1406
|
-
|
|
1327
|
+
const SANDBOX_WRITE_BLOCKED_PATTERNS = [
|
|
1328
|
+
/createprocessasuserw failed:\s*\d+/iu,
|
|
1329
|
+
/windows sandbox: runner error/iu,
|
|
1330
|
+
/writing is blocked by read-only sandbox/iu,
|
|
1331
|
+
/read-only filesystem sandbox/iu,
|
|
1332
|
+
/approval is disabled/iu
|
|
1333
|
+
];
|
|
1334
|
+
function detectSandboxWriteBlocked(result) {
|
|
1335
|
+
const sources = [result.summary, result.failure?.message]
|
|
1336
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
1337
|
+
return sources.some((source) => SANDBOX_WRITE_BLOCKED_PATTERNS.some((pattern) => pattern.test(source)));
|
|
1338
|
+
}
|
|
1339
|
+
function classifyPatchDecisionFailure(decision, result) {
|
|
1407
1340
|
if (decision.reasonCodes.includes("grounding_failure")) {
|
|
1408
1341
|
return {
|
|
1409
1342
|
failureClass: "repo_grounding_failure",
|
|
@@ -1445,6 +1378,14 @@ function classifyPatchDecisionFailure(decision) {
|
|
|
1445
1378
|
recommendedIntervention: "escalate_human"
|
|
1446
1379
|
};
|
|
1447
1380
|
}
|
|
1381
|
+
if (result && detectSandboxWriteBlocked(result)) {
|
|
1382
|
+
return {
|
|
1383
|
+
failureClass: "sandbox_write_blocked",
|
|
1384
|
+
rationale: "The adapter reported a completed patch but the workspace-write sandbox blocked the underlying file write, so no trustworthy code change landed on disk.",
|
|
1385
|
+
retryable: true,
|
|
1386
|
+
recommendedIntervention: "switch_adapter"
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1448
1389
|
return {
|
|
1449
1390
|
failureClass: "no_progress",
|
|
1450
1391
|
rationale: "Patch truth discarded the attempt because it did not produce a trustworthy code change.",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MartinLoop Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Append-only, never-overwriting persistent memory. Records user consent
|
|
5
|
+
* signals, preferences, behavioral patterns, and learning outcomes so
|
|
6
|
+
* MartinLoop gets smarter with every session.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Append-only JSONL — entries are never deleted or overwritten
|
|
10
|
+
* - Structured by kind — each memory entry has a type and a key
|
|
11
|
+
* - Source-aware — distinguishes explicit user input from inferred patterns
|
|
12
|
+
* - Confidence-scored — inferred memories degrade gracefully
|
|
13
|
+
*
|
|
14
|
+
* Storage: <runsRoot>/_martin/memory.jsonl
|
|
15
|
+
*/
|
|
16
|
+
export type MemoryKind = "consent" | "preference" | "pattern" | "feedback" | "budget" | "model";
|
|
17
|
+
export interface MemoryEntry {
|
|
18
|
+
/** ISO-8601 timestamp */
|
|
19
|
+
timestamp: string;
|
|
20
|
+
/** What kind of memory this is */
|
|
21
|
+
kind: MemoryKind;
|
|
22
|
+
/** Dot-notated key, e.g. "model.direct.claude", "budget.auth.usd", "gate.bypassed" */
|
|
23
|
+
key: string;
|
|
24
|
+
/** The memory payload */
|
|
25
|
+
value: unknown;
|
|
26
|
+
/** Did the user say it explicitly, or did Martin observe it? */
|
|
27
|
+
source: "explicit" | "inferred";
|
|
28
|
+
/** Confidence score 0-1 — inferred memories should be < 0.8 */
|
|
29
|
+
confidence: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MemorySummary {
|
|
32
|
+
totalEntries: number;
|
|
33
|
+
byKind: Partial<Record<MemoryKind, MemoryEntry[]>>;
|
|
34
|
+
recentPreferences: MemoryEntry[];
|
|
35
|
+
recentConsents: MemoryEntry[];
|
|
36
|
+
recentFeedback: MemoryEntry[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Append a memory entry. Never overwrites — always appends.
|
|
40
|
+
*/
|
|
41
|
+
export declare function appendMemory(runsRoot: string, entry: MemoryEntry): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Read all memory entries in order (oldest first).
|
|
44
|
+
*/
|
|
45
|
+
export declare function readMemoryEntries(runsRoot: string): Promise<MemoryEntry[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Get the most recent memory entry for a specific key.
|
|
48
|
+
* Returns undefined if no entry exists for that key.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getPreference(runsRoot: string, key: string): Promise<MemoryEntry | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Build a summary of memory entries for MCP resource and MartinLoop context injection.
|
|
53
|
+
* Returns the top 20 most recent entries per kind.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildMemorySummary(entries: MemoryEntry[]): MemorySummary;
|
|
56
|
+
/**
|
|
57
|
+
* Record a preference from user input.
|
|
58
|
+
*/
|
|
59
|
+
export declare function recordPreference(runsRoot: string, key: string, value: unknown, source?: "explicit" | "inferred"): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Record a consent signal (user approved or denied a governance action).
|
|
62
|
+
*/
|
|
63
|
+
export declare function recordConsent(runsRoot: string, action: string, approved: boolean, context?: Record<string, unknown>): Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MartinLoop Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Append-only, never-overwriting persistent memory. Records user consent
|
|
5
|
+
* signals, preferences, behavioral patterns, and learning outcomes so
|
|
6
|
+
* MartinLoop gets smarter with every session.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - Append-only JSONL — entries are never deleted or overwritten
|
|
10
|
+
* - Structured by kind — each memory entry has a type and a key
|
|
11
|
+
* - Source-aware — distinguishes explicit user input from inferred patterns
|
|
12
|
+
* - Confidence-scored — inferred memories degrade gracefully
|
|
13
|
+
*
|
|
14
|
+
* Storage: <runsRoot>/_martin/memory.jsonl
|
|
15
|
+
*/
|
|
16
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
const MEMORY_DIRECTORY = "_martin";
|
|
19
|
+
const MEMORY_FILENAME = "memory.jsonl";
|
|
20
|
+
function resolveMemoryPath(runsRoot) {
|
|
21
|
+
return join(resolve(runsRoot), MEMORY_DIRECTORY, MEMORY_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append a memory entry. Never overwrites — always appends.
|
|
25
|
+
*/
|
|
26
|
+
export async function appendMemory(runsRoot, entry) {
|
|
27
|
+
const memPath = resolveMemoryPath(runsRoot);
|
|
28
|
+
await mkdir(join(resolve(runsRoot), MEMORY_DIRECTORY), { recursive: true });
|
|
29
|
+
await appendFile(memPath, JSON.stringify(entry) + "\n", "utf8");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read all memory entries in order (oldest first).
|
|
33
|
+
*/
|
|
34
|
+
export async function readMemoryEntries(runsRoot) {
|
|
35
|
+
const memPath = resolveMemoryPath(runsRoot);
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(memPath, "utf8");
|
|
38
|
+
return raw
|
|
39
|
+
.split("\n")
|
|
40
|
+
.filter((line) => line.trim().length > 0)
|
|
41
|
+
.map((line) => {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.filter((e) => e !== null);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get the most recent memory entry for a specific key.
|
|
57
|
+
* Returns undefined if no entry exists for that key.
|
|
58
|
+
*/
|
|
59
|
+
export async function getPreference(runsRoot, key) {
|
|
60
|
+
const entries = await readMemoryEntries(runsRoot);
|
|
61
|
+
// Return the most recent entry for this key (last in file)
|
|
62
|
+
const matches = entries.filter((e) => e.key === key);
|
|
63
|
+
return matches[matches.length - 1];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a summary of memory entries for MCP resource and MartinLoop context injection.
|
|
67
|
+
* Returns the top 20 most recent entries per kind.
|
|
68
|
+
*/
|
|
69
|
+
export function buildMemorySummary(entries) {
|
|
70
|
+
const byKind = {};
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!byKind[entry.kind]) {
|
|
73
|
+
byKind[entry.kind] = [];
|
|
74
|
+
}
|
|
75
|
+
byKind[entry.kind].push(entry);
|
|
76
|
+
}
|
|
77
|
+
// Keep only the 20 most recent per kind
|
|
78
|
+
for (const kind of Object.keys(byKind)) {
|
|
79
|
+
byKind[kind] = byKind[kind].slice(-20);
|
|
80
|
+
}
|
|
81
|
+
const recent = entries.slice(-50);
|
|
82
|
+
return {
|
|
83
|
+
totalEntries: entries.length,
|
|
84
|
+
byKind,
|
|
85
|
+
recentPreferences: recent.filter((e) => e.kind === "preference"),
|
|
86
|
+
recentConsents: recent.filter((e) => e.kind === "consent"),
|
|
87
|
+
recentFeedback: recent.filter((e) => e.kind === "feedback")
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Record a preference from user input.
|
|
92
|
+
*/
|
|
93
|
+
export async function recordPreference(runsRoot, key, value, source = "explicit") {
|
|
94
|
+
await appendMemory(runsRoot, {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
kind: "preference",
|
|
97
|
+
key,
|
|
98
|
+
value,
|
|
99
|
+
source,
|
|
100
|
+
confidence: source === "explicit" ? 1.0 : 0.6
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Record a consent signal (user approved or denied a governance action).
|
|
105
|
+
*/
|
|
106
|
+
export async function recordConsent(runsRoot, action, approved, context) {
|
|
107
|
+
await appendMemory(runsRoot, {
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
kind: "consent",
|
|
110
|
+
key: `consent.${action}`,
|
|
111
|
+
value: { approved, context },
|
|
112
|
+
source: "explicit",
|
|
113
|
+
confidence: 1.0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-Only Trace Store
|
|
3
|
+
*
|
|
4
|
+
* Aggregates run data over time to build institutional memory. Every governed
|
|
5
|
+
* run's routing decision, cost, failure class, and outcome is appended to a
|
|
6
|
+
* local JSONL trace file. Data is NEVER overwritten — only appended.
|
|
7
|
+
*
|
|
8
|
+
* The trace store feeds:
|
|
9
|
+
* - classifyRoute() with historicalDirectSuccessRate from real past runs
|
|
10
|
+
* - Cost prediction models with actual spend data
|
|
11
|
+
* - Failure pattern detection across runs
|
|
12
|
+
* - Team analytics and optimization recommendations
|
|
13
|
+
*
|
|
14
|
+
* Storage: `<runsRoot>/_martin/trace-log.jsonl` (one JSON object per line)
|
|
15
|
+
*/
|
|
16
|
+
export interface TraceEntry {
|
|
17
|
+
/** ISO-8601 timestamp */
|
|
18
|
+
timestamp: string;
|
|
19
|
+
/** Unique loop identifier */
|
|
20
|
+
loopId: string;
|
|
21
|
+
/** The objective that was governed */
|
|
22
|
+
objective: string;
|
|
23
|
+
/** Which engine executed the run */
|
|
24
|
+
engine: string;
|
|
25
|
+
/** Route selected by classifyRoute */
|
|
26
|
+
selectedRoute: "direct" | "manager" | "consensus";
|
|
27
|
+
/** Route classifier confidence (0-1) */
|
|
28
|
+
routeConfidence: number;
|
|
29
|
+
/** Budget allocated (USD) */
|
|
30
|
+
budgetUsd: number;
|
|
31
|
+
/** Actual spend (USD) */
|
|
32
|
+
actualCostUsd: number;
|
|
33
|
+
/** Pre-work burn percentage */
|
|
34
|
+
preWorkBurnPct: number;
|
|
35
|
+
/** Number of attempts */
|
|
36
|
+
attempts: number;
|
|
37
|
+
/** Final loop status */
|
|
38
|
+
status: string;
|
|
39
|
+
/** Final lifecycle state */
|
|
40
|
+
lifecycleState: string;
|
|
41
|
+
/** Failure class of last attempt (if failed) */
|
|
42
|
+
failureClass?: string;
|
|
43
|
+
/** Whether verification passed */
|
|
44
|
+
verificationPassed: boolean;
|
|
45
|
+
/** Number of files changed */
|
|
46
|
+
filesChanged: number;
|
|
47
|
+
/** Working directory (hashed for privacy) */
|
|
48
|
+
workspaceHash: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TraceAggregation {
|
|
51
|
+
totalRuns: number;
|
|
52
|
+
directRuns: number;
|
|
53
|
+
managerRuns: number;
|
|
54
|
+
consensusRuns: number;
|
|
55
|
+
directSuccessRate: number;
|
|
56
|
+
managerSuccessRate: number;
|
|
57
|
+
averageCostUsd: number;
|
|
58
|
+
averagePreWorkBurnPct: number;
|
|
59
|
+
totalSpendUsd: number;
|
|
60
|
+
totalSavedUsd: number;
|
|
61
|
+
topFailureClasses: Array<{
|
|
62
|
+
failureClass: string;
|
|
63
|
+
count: number;
|
|
64
|
+
}>;
|
|
65
|
+
averageAttemptsPerRun: number;
|
|
66
|
+
lastUpdated: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Append a trace entry to the local trace store. Never overwrites.
|
|
70
|
+
*/
|
|
71
|
+
export declare function appendTraceEntry(runsRoot: string, entry: TraceEntry): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Read all trace entries from the local trace store.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readTraceEntries(runsRoot: string): Promise<TraceEntry[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Aggregate trace entries into summary statistics.
|
|
78
|
+
* Used by classifyRoute() to get historicalDirectSuccessRate.
|
|
79
|
+
*/
|
|
80
|
+
export declare function aggregateTraces(entries: TraceEntry[]): TraceAggregation;
|
|
81
|
+
/**
|
|
82
|
+
* Get the historical direct success rate for use in route classification.
|
|
83
|
+
* Returns undefined if insufficient data (< 5 runs).
|
|
84
|
+
*/
|
|
85
|
+
export declare function getHistoricalDirectSuccessRate(runsRoot: string): Promise<number | undefined>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-Only Trace Store
|
|
3
|
+
*
|
|
4
|
+
* Aggregates run data over time to build institutional memory. Every governed
|
|
5
|
+
* run's routing decision, cost, failure class, and outcome is appended to a
|
|
6
|
+
* local JSONL trace file. Data is NEVER overwritten — only appended.
|
|
7
|
+
*
|
|
8
|
+
* The trace store feeds:
|
|
9
|
+
* - classifyRoute() with historicalDirectSuccessRate from real past runs
|
|
10
|
+
* - Cost prediction models with actual spend data
|
|
11
|
+
* - Failure pattern detection across runs
|
|
12
|
+
* - Team analytics and optimization recommendations
|
|
13
|
+
*
|
|
14
|
+
* Storage: `<runsRoot>/_martin/trace-log.jsonl` (one JSON object per line)
|
|
15
|
+
*/
|
|
16
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
const TRACE_DIRECTORY = "_martin";
|
|
19
|
+
const TRACE_FILENAME = "trace-log.jsonl";
|
|
20
|
+
function resolveTracePath(runsRoot) {
|
|
21
|
+
return join(resolve(runsRoot), TRACE_DIRECTORY, TRACE_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append a trace entry to the local trace store. Never overwrites.
|
|
25
|
+
*/
|
|
26
|
+
export async function appendTraceEntry(runsRoot, entry) {
|
|
27
|
+
const tracePath = resolveTracePath(runsRoot);
|
|
28
|
+
await mkdir(join(resolve(runsRoot), TRACE_DIRECTORY), { recursive: true });
|
|
29
|
+
const line = JSON.stringify(entry) + "\n";
|
|
30
|
+
await appendFile(tracePath, line, "utf8");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read all trace entries from the local trace store.
|
|
34
|
+
*/
|
|
35
|
+
export async function readTraceEntries(runsRoot) {
|
|
36
|
+
const tracePath = resolveTracePath(runsRoot);
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(tracePath, "utf8");
|
|
39
|
+
return raw
|
|
40
|
+
.split("\n")
|
|
41
|
+
.filter((line) => line.trim().length > 0)
|
|
42
|
+
.map((line) => {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(line);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.filter((entry) => entry !== null);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Aggregate trace entries into summary statistics.
|
|
58
|
+
* Used by classifyRoute() to get historicalDirectSuccessRate.
|
|
59
|
+
*/
|
|
60
|
+
export function aggregateTraces(entries) {
|
|
61
|
+
if (entries.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
totalRuns: 0,
|
|
64
|
+
directRuns: 0,
|
|
65
|
+
managerRuns: 0,
|
|
66
|
+
consensusRuns: 0,
|
|
67
|
+
directSuccessRate: 0,
|
|
68
|
+
managerSuccessRate: 0,
|
|
69
|
+
averageCostUsd: 0,
|
|
70
|
+
averagePreWorkBurnPct: 0,
|
|
71
|
+
totalSpendUsd: 0,
|
|
72
|
+
totalSavedUsd: 0,
|
|
73
|
+
topFailureClasses: [],
|
|
74
|
+
averageAttemptsPerRun: 0,
|
|
75
|
+
lastUpdated: new Date().toISOString()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const direct = entries.filter((e) => e.selectedRoute === "direct");
|
|
79
|
+
const manager = entries.filter((e) => e.selectedRoute === "manager");
|
|
80
|
+
const consensus = entries.filter((e) => e.selectedRoute === "consensus");
|
|
81
|
+
const directSuccesses = direct.filter((e) => e.status === "completed" && e.verificationPassed);
|
|
82
|
+
const managerSuccesses = manager.filter((e) => e.status === "completed" && e.verificationPassed);
|
|
83
|
+
const totalCost = entries.reduce((sum, e) => sum + e.actualCostUsd, 0);
|
|
84
|
+
const totalBudget = entries.reduce((sum, e) => sum + e.budgetUsd, 0);
|
|
85
|
+
const totalAttempts = entries.reduce((sum, e) => sum + e.attempts, 0);
|
|
86
|
+
const totalBurnPct = entries.reduce((sum, e) => sum + e.preWorkBurnPct, 0);
|
|
87
|
+
// Count failure classes
|
|
88
|
+
const failureCounts = new Map();
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.failureClass) {
|
|
91
|
+
failureCounts.set(entry.failureClass, (failureCounts.get(entry.failureClass) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const topFailureClasses = [...failureCounts.entries()]
|
|
95
|
+
.map(([failureClass, count]) => ({ failureClass, count }))
|
|
96
|
+
.sort((a, b) => b.count - a.count)
|
|
97
|
+
.slice(0, 10);
|
|
98
|
+
return {
|
|
99
|
+
totalRuns: entries.length,
|
|
100
|
+
directRuns: direct.length,
|
|
101
|
+
managerRuns: manager.length,
|
|
102
|
+
consensusRuns: consensus.length,
|
|
103
|
+
directSuccessRate: direct.length > 0 ? directSuccesses.length / direct.length : 0,
|
|
104
|
+
managerSuccessRate: manager.length > 0 ? managerSuccesses.length / manager.length : 0,
|
|
105
|
+
averageCostUsd: Math.round((totalCost / entries.length) * 100) / 100,
|
|
106
|
+
averagePreWorkBurnPct: Math.round(totalBurnPct / entries.length),
|
|
107
|
+
totalSpendUsd: Math.round(totalCost * 100) / 100,
|
|
108
|
+
totalSavedUsd: Math.round((totalBudget - totalCost) * 100) / 100,
|
|
109
|
+
topFailureClasses,
|
|
110
|
+
averageAttemptsPerRun: Math.round((totalAttempts / entries.length) * 10) / 10,
|
|
111
|
+
lastUpdated: entries[entries.length - 1]?.timestamp ?? new Date().toISOString()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the historical direct success rate for use in route classification.
|
|
116
|
+
* Returns undefined if insufficient data (< 5 runs).
|
|
117
|
+
*/
|
|
118
|
+
export async function getHistoricalDirectSuccessRate(runsRoot) {
|
|
119
|
+
const entries = await readTraceEntries(runsRoot);
|
|
120
|
+
const directRuns = entries.filter((e) => e.selectedRoute === "direct");
|
|
121
|
+
if (directRuns.length < 5)
|
|
122
|
+
return undefined;
|
|
123
|
+
const successes = directRuns.filter((e) => e.status === "completed" && e.verificationPassed);
|
|
124
|
+
return successes.length / directRuns.length;
|
|
125
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Style Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Scans a repository to detect coding conventions, patterns, and style
|
|
5
|
+
* so that governed agent runs produce code matching the existing codebase.
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Naming conventions (camelCase, snake_case, PascalCase, kebab-case)
|
|
9
|
+
* - Test framework (Vitest, Jest, Mocha, node:test)
|
|
10
|
+
* - Package manager (pnpm, npm, yarn, bun)
|
|
11
|
+
* - Module system (ESM, CJS, mixed)
|
|
12
|
+
* - File organization pattern (feature folders, type folders, flat)
|
|
13
|
+
* - TypeScript strictness level
|
|
14
|
+
* - Comment density
|
|
15
|
+
* - Formatting (tabs vs spaces, semicolons, quotes)
|
|
16
|
+
*/
|
|
17
|
+
export interface RepoStyleProfile {
|
|
18
|
+
/** Detected package manager */
|
|
19
|
+
packageManager: "pnpm" | "npm" | "yarn" | "bun" | "unknown";
|
|
20
|
+
/** Detected module system */
|
|
21
|
+
moduleSystem: "esm" | "cjs" | "mixed" | "unknown";
|
|
22
|
+
/** Detected test framework */
|
|
23
|
+
testFramework: "vitest" | "jest" | "mocha" | "node-test" | "unknown";
|
|
24
|
+
/** Primary language */
|
|
25
|
+
language: "typescript" | "javascript" | "mixed";
|
|
26
|
+
/** TypeScript strict mode enabled */
|
|
27
|
+
typescriptStrict: boolean;
|
|
28
|
+
/** Dominant naming convention for files */
|
|
29
|
+
fileNaming: "kebab-case" | "camelCase" | "PascalCase" | "snake_case" | "mixed";
|
|
30
|
+
/** Dominant naming convention for variables/functions */
|
|
31
|
+
codeNaming: "camelCase" | "snake_case" | "mixed";
|
|
32
|
+
/** Uses semicolons */
|
|
33
|
+
semicolons: boolean;
|
|
34
|
+
/** Single vs double quotes */
|
|
35
|
+
quotes: "single" | "double" | "mixed";
|
|
36
|
+
/** Indentation style */
|
|
37
|
+
indentation: "tabs" | "2-spaces" | "4-spaces" | "mixed";
|
|
38
|
+
/** Average lines per file (source files) */
|
|
39
|
+
avgLinesPerFile: number;
|
|
40
|
+
/** File organization pattern */
|
|
41
|
+
organization: "feature-folders" | "type-folders" | "flat" | "mixed";
|
|
42
|
+
/** Raw signals for prompt injection */
|
|
43
|
+
promptDirectives: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Analyze a repository and return a style profile.
|
|
47
|
+
*/
|
|
48
|
+
export declare function analyzeRepoStyle(rootDir: string): RepoStyleProfile;
|
|
49
|
+
/**
|
|
50
|
+
* Build prompt directives from the style profile.
|
|
51
|
+
* These get injected into the agent's prompt to match repo conventions.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildPromptDirectives(profile: RepoStyleProfile): string[];
|