martin-loop 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +331 -58
- package/dist/bin/martin-loop.js +12 -8
- package/dist/index.d.ts +21 -8
- package/dist/index.js +31 -9
- package/dist/vendor/adapters/claude-cli.d.ts +89 -0
- package/dist/vendor/adapters/claude-cli.js +555 -0
- package/dist/vendor/adapters/cli-bridge.d.ts +28 -0
- package/dist/vendor/adapters/cli-bridge.js +127 -0
- package/dist/vendor/adapters/direct-provider.d.ts +10 -0
- package/dist/vendor/adapters/direct-provider.js +41 -0
- package/dist/vendor/adapters/index.d.ts +5 -0
- package/dist/vendor/adapters/index.js +5 -0
- package/dist/vendor/adapters/runtime-support.d.ts +14 -0
- package/dist/vendor/adapters/runtime-support.js +52 -0
- package/dist/vendor/adapters/stub-agent-cli.d.ts +8 -0
- package/dist/vendor/adapters/stub-agent-cli.js +41 -0
- package/dist/vendor/adapters/stub-direct-provider.d.ts +8 -0
- package/dist/vendor/adapters/stub-direct-provider.js +10 -0
- package/dist/vendor/cli/bin/martin.js +19 -0
- package/dist/vendor/cli/index.d.ts +39 -0
- package/dist/vendor/cli/index.js +634 -0
- package/dist/vendor/cli/persistence.d.ts +34 -0
- package/dist/vendor/cli/persistence.js +71 -0
- package/dist/vendor/contracts/governance.d.ts +21 -0
- package/dist/vendor/contracts/governance.js +12 -0
- package/dist/vendor/contracts/index.d.ts +330 -0
- package/dist/vendor/contracts/index.js +203 -0
- package/dist/vendor/core/compiler.d.ts +50 -0
- package/dist/vendor/core/compiler.js +47 -0
- package/dist/vendor/core/grounding.d.ts +37 -0
- package/dist/vendor/core/grounding.js +270 -0
- package/dist/vendor/core/index.d.ts +145 -0
- package/dist/vendor/core/index.js +1099 -0
- package/dist/vendor/core/leash.d.ts +48 -0
- package/dist/vendor/core/leash.js +408 -0
- package/dist/vendor/core/persistence/compiler.d.ts +18 -0
- package/dist/vendor/core/persistence/compiler.js +35 -0
- package/dist/vendor/core/persistence/index.d.ts +6 -0
- package/dist/vendor/core/persistence/index.js +4 -0
- package/dist/vendor/core/persistence/ledger.d.ts +23 -0
- package/dist/vendor/core/persistence/ledger.js +10 -0
- package/dist/vendor/core/persistence/store.d.ts +77 -0
- package/dist/vendor/core/persistence/store.js +84 -0
- package/dist/vendor/core/policy.d.ts +126 -0
- package/dist/vendor/core/policy.js +625 -0
- package/dist/vendor/core/rollback.d.ts +11 -0
- package/dist/vendor/core/rollback.js +219 -0
- package/docs/oss/EXAMPLES.md +126 -0
- package/docs/oss/OSS-BOUNDARY-REPORT.json +113 -0
- package/docs/oss/OSS-BOUNDARY-REPORT.md +48 -0
- package/docs/oss/QUICKSTART.md +135 -0
- package/docs/{README.md → oss/README.md} +17 -13
- package/docs/oss/RELEASE-SURFACE-REPORT.json +45 -0
- package/docs/oss/RELEASE-SURFACE-REPORT.md +35 -0
- package/package.json +27 -35
- package/dist/bin/martin-loop.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/docs/EXAMPLES.md +0 -96
- package/docs/QUICKSTART.md +0 -127
- package/docs/release/CLAIM-TO-CAPABILITY.md +0 -19
- /package/dist/{bin/martin-loop.d.ts → vendor/cli/bin/martin.d.ts} +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export function resolveRunsRoot(env = process.env) {
|
|
5
|
+
return env["MARTIN_RUNS_DIR"]?.trim() ??
|
|
6
|
+
join(homedir(), ".martin", "runs");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Write all loop artifacts to disk at the end of a run.
|
|
10
|
+
* Uses the Phase 3 flat path: ~/.martin/runs/<loopId>/
|
|
11
|
+
* - contract.json (task + budget, immutable)
|
|
12
|
+
* - state.json (status, cost, metrics summary)
|
|
13
|
+
* - ledger.jsonl (all events, one JSON per line)
|
|
14
|
+
* - attempts/ (per-attempt JSON files)
|
|
15
|
+
*/
|
|
16
|
+
export async function persistLoopArtifacts(loop, options = {}) {
|
|
17
|
+
const runsRoot = options.runsRoot ?? resolveRunsRoot();
|
|
18
|
+
const loopRoot = join(runsRoot, loop.loopId);
|
|
19
|
+
const attemptsRoot = join(loopRoot, "attempts");
|
|
20
|
+
await mkdir(attemptsRoot, { recursive: true });
|
|
21
|
+
const state = buildLoopState(loop);
|
|
22
|
+
const contract = {
|
|
23
|
+
loopId: loop.loopId,
|
|
24
|
+
workspaceId: loop.workspaceId,
|
|
25
|
+
projectId: loop.projectId,
|
|
26
|
+
task: loop.task,
|
|
27
|
+
budget: loop.budget,
|
|
28
|
+
metadata: loop.metadata,
|
|
29
|
+
createdAt: loop.createdAt
|
|
30
|
+
};
|
|
31
|
+
await Promise.all([
|
|
32
|
+
writeJsonFile(join(loopRoot, "contract.json"), contract),
|
|
33
|
+
writeJsonFile(join(loopRoot, "state.json"), state),
|
|
34
|
+
writeJsonFile(join(loopRoot, "loop.json"), loop),
|
|
35
|
+
writeEvents(join(loopRoot, "ledger.jsonl"), loop.events),
|
|
36
|
+
...loop.attempts.map((attempt) => writeJsonFile(join(attemptsRoot, `${String(attempt.index).padStart(3, "0")}-${attempt.attemptId}.json`), attempt))
|
|
37
|
+
]);
|
|
38
|
+
// Append summary to workspace-level index
|
|
39
|
+
await appendFile(join(runsRoot, `${loop.workspaceId}.jsonl`), `${JSON.stringify({ loopId: loop.loopId, status: loop.status, cost: loop.cost, updatedAt: loop.updatedAt })}\n`, "utf8");
|
|
40
|
+
}
|
|
41
|
+
function buildLoopState(loop) {
|
|
42
|
+
return {
|
|
43
|
+
loopId: loop.loopId,
|
|
44
|
+
workspaceId: loop.workspaceId,
|
|
45
|
+
projectId: loop.projectId,
|
|
46
|
+
status: loop.status,
|
|
47
|
+
lifecycleState: loop.lifecycleState,
|
|
48
|
+
createdAt: loop.createdAt,
|
|
49
|
+
updatedAt: loop.updatedAt,
|
|
50
|
+
task: {
|
|
51
|
+
title: loop.task.title,
|
|
52
|
+
objective: loop.task.objective,
|
|
53
|
+
...(loop.task.repoRoot ? { repoRoot: loop.task.repoRoot } : {})
|
|
54
|
+
},
|
|
55
|
+
budget: loop.budget,
|
|
56
|
+
cost: loop.cost,
|
|
57
|
+
metrics: {
|
|
58
|
+
attemptCount: loop.attempts.length,
|
|
59
|
+
eventCount: loop.events.length,
|
|
60
|
+
failureCount: loop.events.filter((e) => e.type === "failure.classified").length
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function writeJsonFile(path, value) {
|
|
65
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
66
|
+
}
|
|
67
|
+
async function writeEvents(path, events) {
|
|
68
|
+
const body = events.map((e) => JSON.stringify(e)).join("\n");
|
|
69
|
+
await writeFile(path, body.length > 0 ? `${body}\n` : "", "utf8");
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=persistence.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type GovernanceSnapshot = {
|
|
2
|
+
policyProfile: "strict" | "balanced" | "overnight" | "debug";
|
|
3
|
+
maxUsd: number;
|
|
4
|
+
softLimitUsd: number;
|
|
5
|
+
maxTokens: number;
|
|
6
|
+
maxIterations: number;
|
|
7
|
+
allowedAdapters: string[];
|
|
8
|
+
allowedModels: string[];
|
|
9
|
+
destructiveActionPolicy: "never" | "approval" | "allowed";
|
|
10
|
+
approvalRequired: boolean;
|
|
11
|
+
verifierRules: string[];
|
|
12
|
+
escalationRoute: string;
|
|
13
|
+
telemetryDestination: "local-only" | "control-plane";
|
|
14
|
+
retentionPolicy: string;
|
|
15
|
+
provenance: Array<{
|
|
16
|
+
field: string;
|
|
17
|
+
value: string;
|
|
18
|
+
source: string;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
export declare function createGovernanceSnapshot(snapshot: GovernanceSnapshot): GovernanceSnapshot;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createGovernanceSnapshot(snapshot) {
|
|
2
|
+
return {
|
|
3
|
+
...snapshot,
|
|
4
|
+
allowedAdapters: [...snapshot.allowedAdapters],
|
|
5
|
+
allowedModels: [...snapshot.allowedModels],
|
|
6
|
+
verifierRules: [...snapshot.verifierRules],
|
|
7
|
+
provenance: snapshot.provenance.map((entry) => ({
|
|
8
|
+
...entry
|
|
9
|
+
}))
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=governance.js.map
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export type LoopStatus = "queued" | "running" | "verifying" | "completed" | "failed" | "exited";
|
|
2
|
+
export type LoopLifecycleState = "created" | "running" | "verifying" | "completed" | "budget_exit" | "diminishing_returns" | "stuck_exit" | "human_escalation";
|
|
3
|
+
export type FailureClass = "logic_error" | "hallucination" | "syntax_error" | "type_error" | "test_regression" | "scope_creep" | "no_progress" | "repo_grounding_failure" | "verification_failure" | "environment_mismatch" | "budget_pressure";
|
|
4
|
+
export type InterventionType = "compress_context" | "change_model" | "tighten_task" | "switch_adapter" | "run_verifier" | "escalate_human" | "stop_loop";
|
|
5
|
+
export type LoopEventType = "run.started" | "attempt.started" | "attempt.completed" | "failure.classified" | "intervention.selected" | "verification.completed" | "budget.updated" | "run.completed";
|
|
6
|
+
export interface LoopTask {
|
|
7
|
+
title: string;
|
|
8
|
+
objective: string;
|
|
9
|
+
repoRoot?: string;
|
|
10
|
+
verificationPlan: string[];
|
|
11
|
+
verificationStack?: VerificationStep[];
|
|
12
|
+
executionProfile?: ExecutionProfile;
|
|
13
|
+
allowedNetworkDomains?: string[];
|
|
14
|
+
approvalPolicy?: ApprovalPolicy;
|
|
15
|
+
/** Glob patterns for files the agent is allowed to modify. Empty = no restriction. */
|
|
16
|
+
allowedPaths?: string[];
|
|
17
|
+
/** Glob patterns for files the agent must never modify. */
|
|
18
|
+
deniedPaths?: string[];
|
|
19
|
+
/** Human-readable acceptance criteria injected into the prompt as a checklist. */
|
|
20
|
+
acceptanceCriteria?: string[];
|
|
21
|
+
}
|
|
22
|
+
export type ExecutionProfile = "strict_local" | "ci_safe" | "staging_controlled" | "research_untrusted";
|
|
23
|
+
export interface ApprovalPolicy {
|
|
24
|
+
dependencyAdds?: boolean;
|
|
25
|
+
migrations?: boolean;
|
|
26
|
+
configChanges?: boolean;
|
|
27
|
+
externalWrites?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface VerificationStep {
|
|
30
|
+
/** Shell command to run. */
|
|
31
|
+
command: string;
|
|
32
|
+
/** Classification for reporting and intervention selection. */
|
|
33
|
+
type: "lint" | "typecheck" | "test_targeted" | "test_full" | "custom";
|
|
34
|
+
/** Stop the verification stack immediately on failure. Defaults to true. */
|
|
35
|
+
fastFail?: boolean;
|
|
36
|
+
/** Relative weight for partial scoring (0.0-1.0). */
|
|
37
|
+
weight?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface LoopBudget {
|
|
40
|
+
maxUsd: number;
|
|
41
|
+
softLimitUsd: number;
|
|
42
|
+
maxIterations: number;
|
|
43
|
+
maxTokens: number;
|
|
44
|
+
}
|
|
45
|
+
export interface LoopCost {
|
|
46
|
+
actualUsd: number;
|
|
47
|
+
avoidedUsd: number;
|
|
48
|
+
tokensIn: number;
|
|
49
|
+
tokensOut: number;
|
|
50
|
+
}
|
|
51
|
+
export interface LoopArtifact {
|
|
52
|
+
artifactId: string;
|
|
53
|
+
kind: "diff" | "trace" | "report" | "transcript" | "screenshot" | "other";
|
|
54
|
+
label: string;
|
|
55
|
+
uri: string;
|
|
56
|
+
}
|
|
57
|
+
export interface LoopAttempt {
|
|
58
|
+
attemptId: string;
|
|
59
|
+
index: number;
|
|
60
|
+
adapterId: string;
|
|
61
|
+
model: string;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
completedAt?: string;
|
|
64
|
+
summary?: string;
|
|
65
|
+
failureClass?: FailureClass;
|
|
66
|
+
intervention?: InterventionType;
|
|
67
|
+
}
|
|
68
|
+
export interface LoopEvent {
|
|
69
|
+
eventId: string;
|
|
70
|
+
type: LoopEventType;
|
|
71
|
+
timestamp: string;
|
|
72
|
+
lifecycleState: LoopLifecycleState;
|
|
73
|
+
payload: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
export interface LoopRecord {
|
|
76
|
+
loopId: string;
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
projectId: string;
|
|
79
|
+
teamId?: string;
|
|
80
|
+
status: LoopStatus;
|
|
81
|
+
lifecycleState: LoopLifecycleState;
|
|
82
|
+
task: LoopTask;
|
|
83
|
+
budget: LoopBudget;
|
|
84
|
+
cost: LoopCost;
|
|
85
|
+
artifacts: LoopArtifact[];
|
|
86
|
+
attempts: LoopAttempt[];
|
|
87
|
+
events: LoopEvent[];
|
|
88
|
+
metadata: Record<string, string>;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
updatedAt: string;
|
|
91
|
+
}
|
|
92
|
+
export interface LoopRecordDraft {
|
|
93
|
+
loopId?: string;
|
|
94
|
+
workspaceId: string;
|
|
95
|
+
projectId: string;
|
|
96
|
+
teamId?: string;
|
|
97
|
+
status?: LoopStatus;
|
|
98
|
+
lifecycleState?: LoopLifecycleState;
|
|
99
|
+
task: LoopTask;
|
|
100
|
+
budget?: Partial<LoopBudget>;
|
|
101
|
+
cost?: Partial<LoopCost>;
|
|
102
|
+
artifacts?: LoopArtifact[];
|
|
103
|
+
attempts?: LoopAttempt[];
|
|
104
|
+
events?: LoopEvent[];
|
|
105
|
+
metadata?: Record<string, string>;
|
|
106
|
+
createdAt?: string;
|
|
107
|
+
updatedAt?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface LoopEventDraft {
|
|
110
|
+
type: LoopEventType;
|
|
111
|
+
lifecycleState?: LoopLifecycleState;
|
|
112
|
+
payload: Record<string, unknown>;
|
|
113
|
+
timestamp?: string;
|
|
114
|
+
}
|
|
115
|
+
export type TelemetryEnvironment = "local" | "ci" | "staging" | "production";
|
|
116
|
+
export interface TelemetrySource {
|
|
117
|
+
runtimeVersion: string;
|
|
118
|
+
adapterId: string;
|
|
119
|
+
provider: string;
|
|
120
|
+
model: string;
|
|
121
|
+
}
|
|
122
|
+
export interface TelemetryEvent {
|
|
123
|
+
eventId: string;
|
|
124
|
+
loopId: string;
|
|
125
|
+
attemptId?: string;
|
|
126
|
+
type: LoopEventType;
|
|
127
|
+
lifecycleState: LoopLifecycleState;
|
|
128
|
+
timestamp: string;
|
|
129
|
+
sequence: number;
|
|
130
|
+
payload: Record<string, unknown>;
|
|
131
|
+
}
|
|
132
|
+
export interface TelemetryEventDraft {
|
|
133
|
+
loopId: string;
|
|
134
|
+
attemptId?: string;
|
|
135
|
+
type: LoopEventType;
|
|
136
|
+
lifecycleState: LoopLifecycleState;
|
|
137
|
+
timestamp?: string;
|
|
138
|
+
payload: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
export interface TelemetryEnvelope {
|
|
141
|
+
schemaVersion: string;
|
|
142
|
+
envelopeId: string;
|
|
143
|
+
workspaceId: string;
|
|
144
|
+
projectId: string;
|
|
145
|
+
ingestKeyId: string;
|
|
146
|
+
environment: TelemetryEnvironment;
|
|
147
|
+
emittedAt: string;
|
|
148
|
+
sequence: number;
|
|
149
|
+
source: TelemetrySource;
|
|
150
|
+
events: TelemetryEvent[];
|
|
151
|
+
}
|
|
152
|
+
export interface TelemetryEnvelopeDraft {
|
|
153
|
+
schemaVersion?: string;
|
|
154
|
+
workspaceId: string;
|
|
155
|
+
projectId: string;
|
|
156
|
+
ingestKeyId: string;
|
|
157
|
+
environment: TelemetryEnvironment;
|
|
158
|
+
source: TelemetrySource;
|
|
159
|
+
events: TelemetryEventDraft[];
|
|
160
|
+
}
|
|
161
|
+
export interface TelemetryLoopSnapshot {
|
|
162
|
+
loopId: string;
|
|
163
|
+
status: LoopStatus;
|
|
164
|
+
lifecycleState: LoopLifecycleState;
|
|
165
|
+
cost: LoopCost;
|
|
166
|
+
}
|
|
167
|
+
export interface TelemetryBatch {
|
|
168
|
+
workspaceId: string;
|
|
169
|
+
projectId: string;
|
|
170
|
+
loops: TelemetryLoopSnapshot[];
|
|
171
|
+
}
|
|
172
|
+
export interface ValidationResult {
|
|
173
|
+
ok: boolean;
|
|
174
|
+
errors: string[];
|
|
175
|
+
}
|
|
176
|
+
export interface PortfolioSnapshot {
|
|
177
|
+
totalActualUsd: number;
|
|
178
|
+
totalAvoidedUsd: number;
|
|
179
|
+
totalTokensIn: number;
|
|
180
|
+
totalTokensOut: number;
|
|
181
|
+
activeLoops: number;
|
|
182
|
+
optimizedLoops: number;
|
|
183
|
+
failuresCaught: number;
|
|
184
|
+
averageExitSeconds: number;
|
|
185
|
+
}
|
|
186
|
+
export interface ContractOptions {
|
|
187
|
+
now?: string;
|
|
188
|
+
idFactory?: (prefix: string) => string;
|
|
189
|
+
}
|
|
190
|
+
export declare const DEFAULT_BUDGET: LoopBudget;
|
|
191
|
+
export declare const EMPTY_COST: LoopCost;
|
|
192
|
+
export declare function createLoopRecord(draft: LoopRecordDraft, options?: ContractOptions): LoopRecord;
|
|
193
|
+
export declare function appendLoopEvent(loop: LoopRecord, eventDraft: LoopEventDraft, options?: ContractOptions): LoopRecord;
|
|
194
|
+
export declare function validateTelemetryBatch(batch: TelemetryBatch): ValidationResult;
|
|
195
|
+
export declare function buildPortfolioSnapshot(loops: LoopRecord[]): PortfolioSnapshot;
|
|
196
|
+
export declare function createTelemetryEnvelope(draft: TelemetryEnvelopeDraft, options?: ContractOptions): TelemetryEnvelope;
|
|
197
|
+
export declare function validateTelemetryEnvelope(envelope: TelemetryEnvelope): ValidationResult;
|
|
198
|
+
export type PolicyPhase = "GATHER" | "ADMIT" | "PATCH" | "VERIFY" | "RECOVER" | "ESCALATE" | "ABORT" | "HANDOFF";
|
|
199
|
+
export interface EvidenceVector {
|
|
200
|
+
/** Count of compile/build errors in the last attempt output. */
|
|
201
|
+
compileErrors: number;
|
|
202
|
+
/** Count of TypeScript type errors. */
|
|
203
|
+
typeErrors: number;
|
|
204
|
+
/** Count of failing test cases. */
|
|
205
|
+
failingTests: number;
|
|
206
|
+
/** Verifier score 0.0–1.0 (1.0 = full pass). */
|
|
207
|
+
verifierScore: number;
|
|
208
|
+
/** Patch novelty 0.0–1.0 (0.0 = identical to previous attempt). */
|
|
209
|
+
diffNovelty: number;
|
|
210
|
+
/** Number of forbidden files touched by the last patch. */
|
|
211
|
+
forbiddenTouchedFileCount: number;
|
|
212
|
+
/** Count of unresolvable symbols/modules referenced in the patch. */
|
|
213
|
+
missingSymbolCount: number;
|
|
214
|
+
/** Actual USD cost divided by verifier score improvement. */
|
|
215
|
+
costPerProgressUnit: number;
|
|
216
|
+
/** How many times this failure surface has been retried. */
|
|
217
|
+
retryCountForSurface: number;
|
|
218
|
+
/** Safety risk score 0.0–1.0 from leash evaluation. */
|
|
219
|
+
safetyRiskScore: number;
|
|
220
|
+
}
|
|
221
|
+
export interface MachineState {
|
|
222
|
+
phase: PolicyPhase;
|
|
223
|
+
currentAttempt: number;
|
|
224
|
+
activeModel: string;
|
|
225
|
+
remainingBudgetUsd: number;
|
|
226
|
+
/** Retry counter per FailureClass surface. */
|
|
227
|
+
attemptCountersBySurface: Record<string, number>;
|
|
228
|
+
lastFailureSurface?: FailureClass;
|
|
229
|
+
lastVerifierScore: number;
|
|
230
|
+
openAlerts: string[];
|
|
231
|
+
policyHistory: Array<{
|
|
232
|
+
phase: PolicyPhase;
|
|
233
|
+
reason: string;
|
|
234
|
+
timestamp: string;
|
|
235
|
+
}>;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Cost provenance label — every budget metric must carry this.
|
|
239
|
+
* Never conflate actual with estimated or unavailable.
|
|
240
|
+
*/
|
|
241
|
+
export type CostProvenance = "actual" | "estimated" | "unavailable";
|
|
242
|
+
/**
|
|
243
|
+
* Per-attempt budget preflight estimate produced before admission.
|
|
244
|
+
* Used to gate attempts before any tokens are spent.
|
|
245
|
+
*/
|
|
246
|
+
export interface BudgetPreflightEstimate {
|
|
247
|
+
estimatedPromptTokens: number;
|
|
248
|
+
estimatedToolOverheadTokens: number;
|
|
249
|
+
estimatedOutputTokensMax: number;
|
|
250
|
+
estimatedVerifierCostUsd: number;
|
|
251
|
+
estimatedAttemptCostUsd: number;
|
|
252
|
+
provenance: CostProvenance;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Actual cost settlement written to the ledger after an attempt completes.
|
|
256
|
+
* Separates patch cost from verification cost.
|
|
257
|
+
*/
|
|
258
|
+
export interface BudgetSettlement {
|
|
259
|
+
runId: string;
|
|
260
|
+
attemptIndex: number;
|
|
261
|
+
patchCost: {
|
|
262
|
+
usd: number;
|
|
263
|
+
tokensIn: number;
|
|
264
|
+
tokensOut: number;
|
|
265
|
+
provenance: CostProvenance;
|
|
266
|
+
};
|
|
267
|
+
verificationCost: {
|
|
268
|
+
usd: number;
|
|
269
|
+
provenance: CostProvenance;
|
|
270
|
+
};
|
|
271
|
+
totalActualUsd: number;
|
|
272
|
+
preflightEstimateUsd: number;
|
|
273
|
+
varianceUsd: number;
|
|
274
|
+
settledAt: string;
|
|
275
|
+
}
|
|
276
|
+
export type PatchDecision = "KEEP" | "DISCARD" | "ESCALATE" | "HANDOFF";
|
|
277
|
+
export type PatchDecisionReasonCode = "verifier_passed" | "verifier_regressed" | "grounding_failure" | "scope_violation" | "no_code_change" | "large_diff_no_improvement" | "low_novelty_no_progress" | "human_approval_required" | "safety_violation" | "verifier_not_improved";
|
|
278
|
+
export interface PatchScore {
|
|
279
|
+
score: number;
|
|
280
|
+
verifierScore: number;
|
|
281
|
+
verifierDelta: number;
|
|
282
|
+
groundingViolationCount: number;
|
|
283
|
+
scopeViolationCount: number;
|
|
284
|
+
safetyViolationCount: number;
|
|
285
|
+
changedFileCount: number;
|
|
286
|
+
diffRiskScore: number;
|
|
287
|
+
noveltyScore: number;
|
|
288
|
+
costUsd: number;
|
|
289
|
+
reasonCodes: PatchDecisionReasonCode[];
|
|
290
|
+
}
|
|
291
|
+
export interface PatchDecisionArtifact {
|
|
292
|
+
decision: PatchDecision;
|
|
293
|
+
summary: string;
|
|
294
|
+
reasonCodes: PatchDecisionReasonCode[];
|
|
295
|
+
}
|
|
296
|
+
export type RollbackBoundaryStrategy = "git_head_plus_snapshot" | "no_repo_root";
|
|
297
|
+
export interface RollbackFileSnapshot {
|
|
298
|
+
path: string;
|
|
299
|
+
existed: boolean;
|
|
300
|
+
encoding: "base64";
|
|
301
|
+
contentBase64?: string;
|
|
302
|
+
}
|
|
303
|
+
export interface RollbackBoundaryArtifact {
|
|
304
|
+
strategy: RollbackBoundaryStrategy;
|
|
305
|
+
capturedAt: string;
|
|
306
|
+
headRef?: string;
|
|
307
|
+
trackedDirtyFiles: string[];
|
|
308
|
+
untrackedFiles: string[];
|
|
309
|
+
snapshots: RollbackFileSnapshot[];
|
|
310
|
+
}
|
|
311
|
+
export type RollbackOutcomeStatus = "restored" | "not_required" | "failed" | "unavailable";
|
|
312
|
+
export interface RollbackOutcomeArtifact {
|
|
313
|
+
attempted: boolean;
|
|
314
|
+
status: RollbackOutcomeStatus;
|
|
315
|
+
restoredAt: string;
|
|
316
|
+
decision: PatchDecision;
|
|
317
|
+
before: {
|
|
318
|
+
trackedDirtyFiles: string[];
|
|
319
|
+
untrackedFiles: string[];
|
|
320
|
+
};
|
|
321
|
+
after: {
|
|
322
|
+
trackedDirtyFiles: string[];
|
|
323
|
+
untrackedFiles: string[];
|
|
324
|
+
};
|
|
325
|
+
restoredFiles: string[];
|
|
326
|
+
deletedFiles: string[];
|
|
327
|
+
error?: string;
|
|
328
|
+
}
|
|
329
|
+
export { createGovernanceSnapshot } from "./governance.js";
|
|
330
|
+
export type { GovernanceSnapshot } from "./governance.js";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export const DEFAULT_BUDGET = {
|
|
2
|
+
maxUsd: 25,
|
|
3
|
+
softLimitUsd: 15,
|
|
4
|
+
maxIterations: 8,
|
|
5
|
+
maxTokens: 80_000
|
|
6
|
+
};
|
|
7
|
+
export const EMPTY_COST = {
|
|
8
|
+
actualUsd: 0,
|
|
9
|
+
avoidedUsd: 0,
|
|
10
|
+
tokensIn: 0,
|
|
11
|
+
tokensOut: 0
|
|
12
|
+
};
|
|
13
|
+
export function createLoopRecord(draft, options = {}) {
|
|
14
|
+
const now = options.now ?? new Date().toISOString();
|
|
15
|
+
return {
|
|
16
|
+
loopId: draft.loopId ?? makeId("loop", options),
|
|
17
|
+
workspaceId: draft.workspaceId,
|
|
18
|
+
projectId: draft.projectId,
|
|
19
|
+
status: draft.status ?? "queued",
|
|
20
|
+
lifecycleState: draft.lifecycleState ?? "created",
|
|
21
|
+
task: draft.task,
|
|
22
|
+
budget: {
|
|
23
|
+
...DEFAULT_BUDGET,
|
|
24
|
+
...draft.budget
|
|
25
|
+
},
|
|
26
|
+
cost: {
|
|
27
|
+
...EMPTY_COST,
|
|
28
|
+
...draft.cost
|
|
29
|
+
},
|
|
30
|
+
artifacts: [...(draft.artifacts ?? [])],
|
|
31
|
+
attempts: [...(draft.attempts ?? [])],
|
|
32
|
+
events: [...(draft.events ?? [])],
|
|
33
|
+
metadata: {
|
|
34
|
+
...(draft.metadata ?? {})
|
|
35
|
+
},
|
|
36
|
+
createdAt: draft.createdAt ?? now,
|
|
37
|
+
updatedAt: draft.updatedAt ?? now,
|
|
38
|
+
...(draft.teamId ? { teamId: draft.teamId } : {})
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function appendLoopEvent(loop, eventDraft, options = {}) {
|
|
42
|
+
const timestamp = eventDraft.timestamp ?? options.now ?? new Date().toISOString();
|
|
43
|
+
const lifecycleState = eventDraft.lifecycleState ?? loop.lifecycleState;
|
|
44
|
+
const event = {
|
|
45
|
+
eventId: makeId("evt", options),
|
|
46
|
+
type: eventDraft.type,
|
|
47
|
+
timestamp,
|
|
48
|
+
lifecycleState,
|
|
49
|
+
payload: eventDraft.payload
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
...loop,
|
|
53
|
+
lifecycleState,
|
|
54
|
+
status: nextStatus(loop.status, event.type),
|
|
55
|
+
events: [...loop.events, event],
|
|
56
|
+
updatedAt: timestamp
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function validateTelemetryBatch(batch) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
if (!hasText(batch.workspaceId)) {
|
|
62
|
+
errors.push("workspaceId is required");
|
|
63
|
+
}
|
|
64
|
+
if (!hasText(batch.projectId)) {
|
|
65
|
+
errors.push("projectId is required");
|
|
66
|
+
}
|
|
67
|
+
batch.loops.forEach((loop, index) => {
|
|
68
|
+
if (!hasText(loop.loopId)) {
|
|
69
|
+
errors.push(`loop[${index}].loopId is required`);
|
|
70
|
+
}
|
|
71
|
+
if (loop.cost.actualUsd < 0) {
|
|
72
|
+
errors.push(`loop[${index}].cost.actualUsd must be greater than or equal to 0`);
|
|
73
|
+
}
|
|
74
|
+
if (loop.cost.avoidedUsd < 0) {
|
|
75
|
+
errors.push(`loop[${index}].cost.avoidedUsd must be greater than or equal to 0`);
|
|
76
|
+
}
|
|
77
|
+
if (loop.cost.tokensIn < 0) {
|
|
78
|
+
errors.push(`loop[${index}].cost.tokensIn must be greater than or equal to 0`);
|
|
79
|
+
}
|
|
80
|
+
if (loop.cost.tokensOut < 0) {
|
|
81
|
+
errors.push(`loop[${index}].cost.tokensOut must be greater than or equal to 0`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
ok: errors.length === 0,
|
|
86
|
+
errors
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function buildPortfolioSnapshot(loops) {
|
|
90
|
+
const exitedLoops = loops.filter((loop) => ["completed", "budget_exit", "diminishing_returns", "stuck_exit", "human_escalation"].includes(loop.lifecycleState));
|
|
91
|
+
const totalExitSeconds = exitedLoops.reduce((total, loop) => {
|
|
92
|
+
const created = Date.parse(loop.createdAt);
|
|
93
|
+
const updated = Date.parse(loop.updatedAt);
|
|
94
|
+
if (Number.isNaN(created) || Number.isNaN(updated) || updated < created) {
|
|
95
|
+
return total;
|
|
96
|
+
}
|
|
97
|
+
return total + Math.round((updated - created) / 1000);
|
|
98
|
+
}, 0);
|
|
99
|
+
return {
|
|
100
|
+
totalActualUsd: loops.reduce((total, loop) => total + loop.cost.actualUsd, 0),
|
|
101
|
+
totalAvoidedUsd: loops.reduce((total, loop) => total + loop.cost.avoidedUsd, 0),
|
|
102
|
+
totalTokensIn: loops.reduce((total, loop) => total + loop.cost.tokensIn, 0),
|
|
103
|
+
totalTokensOut: loops.reduce((total, loop) => total + loop.cost.tokensOut, 0),
|
|
104
|
+
activeLoops: loops.filter((loop) => ["queued", "running", "verifying"].includes(loop.status))
|
|
105
|
+
.length,
|
|
106
|
+
optimizedLoops: loops.filter((loop) => loop.cost.avoidedUsd > loop.cost.actualUsd).length,
|
|
107
|
+
failuresCaught: loops.reduce((total, loop) => total + loop.events.filter((event) => event.type === "failure.classified").length, 0),
|
|
108
|
+
averageExitSeconds: exitedLoops.length === 0 ? 0 : Math.round(totalExitSeconds / exitedLoops.length)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function createTelemetryEnvelope(draft, options = {}) {
|
|
112
|
+
const emittedAt = options.now ?? new Date().toISOString();
|
|
113
|
+
const usedIds = new Set();
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: draft.schemaVersion ?? "martin.telemetry.v1",
|
|
116
|
+
envelopeId: makeId("env", options),
|
|
117
|
+
workspaceId: draft.workspaceId,
|
|
118
|
+
projectId: draft.projectId,
|
|
119
|
+
ingestKeyId: draft.ingestKeyId,
|
|
120
|
+
environment: draft.environment,
|
|
121
|
+
emittedAt,
|
|
122
|
+
sequence: draft.events.length,
|
|
123
|
+
source: draft.source,
|
|
124
|
+
events: draft.events.map((event, index) => {
|
|
125
|
+
const baseId = makeId("evt", options);
|
|
126
|
+
const eventId = usedIds.has(baseId) ? `${baseId}_${index + 1}` : baseId;
|
|
127
|
+
usedIds.add(eventId);
|
|
128
|
+
return {
|
|
129
|
+
eventId,
|
|
130
|
+
loopId: event.loopId,
|
|
131
|
+
type: event.type,
|
|
132
|
+
lifecycleState: event.lifecycleState,
|
|
133
|
+
timestamp: event.timestamp ?? emittedAt,
|
|
134
|
+
sequence: index + 1,
|
|
135
|
+
payload: event.payload,
|
|
136
|
+
...(event.attemptId ? { attemptId: event.attemptId } : {})
|
|
137
|
+
};
|
|
138
|
+
})
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export function validateTelemetryEnvelope(envelope) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
if (!hasText(envelope.schemaVersion)) {
|
|
144
|
+
errors.push("schemaVersion is required");
|
|
145
|
+
}
|
|
146
|
+
if (!hasText(envelope.workspaceId)) {
|
|
147
|
+
errors.push("workspaceId is required");
|
|
148
|
+
}
|
|
149
|
+
if (!hasText(envelope.projectId)) {
|
|
150
|
+
errors.push("projectId is required");
|
|
151
|
+
}
|
|
152
|
+
if (!hasText(envelope.ingestKeyId)) {
|
|
153
|
+
errors.push("ingestKeyId is required");
|
|
154
|
+
}
|
|
155
|
+
if (envelope.sequence !== envelope.events.length) {
|
|
156
|
+
errors.push("sequence must equal the number of events in the envelope");
|
|
157
|
+
}
|
|
158
|
+
const sequenceLooksValid = envelope.events.every((event, index) => event.sequence === index + 1);
|
|
159
|
+
if (!sequenceLooksValid) {
|
|
160
|
+
errors.push("events must use a strictly increasing sequence starting at 1");
|
|
161
|
+
}
|
|
162
|
+
envelope.events.forEach((event, index) => {
|
|
163
|
+
if (!hasText(event.eventId)) {
|
|
164
|
+
errors.push(`events[${index}].eventId is required`);
|
|
165
|
+
}
|
|
166
|
+
if (!hasText(event.loopId)) {
|
|
167
|
+
errors.push(`events[${index}].loopId is required`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
ok: errors.length === 0,
|
|
172
|
+
errors
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function makeId(prefix, options) {
|
|
176
|
+
if (options.idFactory) {
|
|
177
|
+
return options.idFactory(prefix);
|
|
178
|
+
}
|
|
179
|
+
const entropy = Math.random().toString(36).slice(2, 10);
|
|
180
|
+
return `${prefix}_${entropy}`;
|
|
181
|
+
}
|
|
182
|
+
function nextStatus(current, eventType) {
|
|
183
|
+
switch (eventType) {
|
|
184
|
+
case "run.started":
|
|
185
|
+
case "attempt.started":
|
|
186
|
+
case "attempt.completed":
|
|
187
|
+
case "failure.classified":
|
|
188
|
+
case "intervention.selected":
|
|
189
|
+
case "budget.updated":
|
|
190
|
+
return "running";
|
|
191
|
+
case "verification.completed":
|
|
192
|
+
return "verifying";
|
|
193
|
+
case "run.completed":
|
|
194
|
+
return current === "failed" ? "failed" : "completed";
|
|
195
|
+
default:
|
|
196
|
+
return current;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function hasText(value) {
|
|
200
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
201
|
+
}
|
|
202
|
+
export { createGovernanceSnapshot } from "./governance.js";
|
|
203
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LoopAttempt, LoopTask } from "../contracts/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The subset of MartinAdapterRequest fields needed by the prompt packet
|
|
4
|
+
* compiler. Defined here (not in index.ts) to avoid circular imports:
|
|
5
|
+
* index.ts → ./persistence/index.js → ./persistence/compiler.js → ./compiler.js (no cycle)
|
|
6
|
+
*/
|
|
7
|
+
export interface CompilerAdapterRequest {
|
|
8
|
+
loopId: string;
|
|
9
|
+
attemptId: string;
|
|
10
|
+
context: {
|
|
11
|
+
taskTitle: string;
|
|
12
|
+
objective: string;
|
|
13
|
+
verificationPlan: string[];
|
|
14
|
+
verificationStack?: LoopTask["verificationStack"];
|
|
15
|
+
repoRoot?: string;
|
|
16
|
+
allowedPaths?: string[];
|
|
17
|
+
deniedPaths?: string[];
|
|
18
|
+
acceptanceCriteria?: string[];
|
|
19
|
+
focus: string;
|
|
20
|
+
remainingBudgetUsd: number;
|
|
21
|
+
remainingIterations: number;
|
|
22
|
+
remainingTokens: number;
|
|
23
|
+
};
|
|
24
|
+
previousAttempts: LoopAttempt[];
|
|
25
|
+
}
|
|
26
|
+
export interface PromptPacket {
|
|
27
|
+
loopId: string;
|
|
28
|
+
attemptNumber: number;
|
|
29
|
+
contract: {
|
|
30
|
+
objective: string;
|
|
31
|
+
verificationPlan: string[];
|
|
32
|
+
allowedPaths?: string[];
|
|
33
|
+
deniedPaths?: string[];
|
|
34
|
+
acceptanceCriteria?: string[];
|
|
35
|
+
};
|
|
36
|
+
/** Prior failure/intervention pairs as "failureClass:intervention" strings. */
|
|
37
|
+
priorFailurePatterns: string[];
|
|
38
|
+
guidance: string;
|
|
39
|
+
budgetEnvelope: {
|
|
40
|
+
remainingBudgetUsd: number;
|
|
41
|
+
remainingIterations: number;
|
|
42
|
+
remainingTokens: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compiles a deterministic PromptPacket from a MartinAdapterRequest.
|
|
47
|
+
* This is the context compiler — takes structured request state and produces
|
|
48
|
+
* a reconstructable packet (no chat history required).
|
|
49
|
+
*/
|
|
50
|
+
export declare function compilePromptPacket(request: CompilerAdapterRequest): PromptPacket;
|