martin-loop 0.1.4 → 1.3.0
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/CODE_OF_CONDUCT.md +32 -0
- package/README.md +172 -227
- package/demo/seeded-workspace/README.md +35 -0
- package/demo/seeded-workspace/TASKS.md +29 -0
- package/demo/seeded-workspace/martin.config.yaml +11 -0
- package/demo/seeded-workspace/package.json +8 -0
- package/demo/seeded-workspace/src/invoice-summary.js +11 -0
- package/demo/seeded-workspace/test/invoice-summary.test.js +20 -0
- package/dist/bin/martin-loop.js +0 -0
- package/dist/vendor/adapters/claude-cli.d.ts +19 -4
- package/dist/vendor/adapters/claude-cli.js +55 -24
- package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
- package/dist/vendor/adapters/cli-bridge.js +154 -28
- package/dist/vendor/adapters/counter.d.ts +1 -0
- package/dist/vendor/adapters/counter.js +4 -0
- package/dist/vendor/adapters/git-baseline.d.ts +50 -0
- package/dist/vendor/adapters/git-baseline.js +233 -0
- package/dist/vendor/adapters/index.d.ts +1 -0
- package/dist/vendor/adapters/index.js +1 -0
- package/dist/vendor/adapters/openrouter-adapter.d.ts +15 -0
- package/dist/vendor/adapters/openrouter-adapter.js +302 -0
- package/dist/vendor/adapters/usage.d.ts +48 -0
- package/dist/vendor/adapters/usage.js +66 -0
- package/dist/vendor/adapters/verifier-only.d.ts +7 -0
- package/dist/vendor/adapters/verifier-only.js +57 -0
- package/dist/vendor/cli/bin/exit.d.ts +12 -0
- package/dist/vendor/cli/bin/exit.js +28 -0
- package/dist/vendor/cli/commands/analyze.d.ts +5 -0
- package/dist/vendor/cli/commands/analyze.js +58 -0
- package/dist/vendor/cli/commands/audit-log-verify.d.ts +34 -0
- package/dist/vendor/cli/commands/audit-log-verify.js +99 -0
- package/dist/vendor/cli/commands/audit.d.ts +8 -0
- package/dist/vendor/cli/commands/audit.js +199 -0
- package/dist/vendor/cli/commands/corpus.d.ts +5 -0
- package/dist/vendor/cli/commands/corpus.js +60 -0
- package/dist/vendor/cli/commands/doctor.d.ts +8 -0
- package/dist/vendor/cli/commands/doctor.js +219 -0
- package/dist/vendor/cli/commands/explain.d.ts +17 -0
- package/dist/vendor/cli/commands/explain.js +176 -0
- package/dist/vendor/cli/commands/export.d.ts +5 -0
- package/dist/vendor/cli/commands/export.js +60 -0
- package/dist/vendor/cli/commands/governance.d.ts +8 -0
- package/dist/vendor/cli/commands/governance.js +95 -0
- package/dist/vendor/cli/commands/improve.d.ts +18 -0
- package/dist/vendor/cli/commands/improve.js +396 -0
- package/dist/vendor/cli/commands/init.d.ts +8 -0
- package/dist/vendor/cli/commands/init.js +281 -0
- package/dist/vendor/cli/commands/migration.d.ts +8 -0
- package/dist/vendor/cli/commands/migration.js +67 -0
- package/dist/vendor/cli/commands/prior.d.ts +23 -0
- package/dist/vendor/cli/commands/prior.js +145 -0
- package/dist/vendor/cli/commands/resume.d.ts +21 -0
- package/dist/vendor/cli/commands/resume.js +73 -0
- package/dist/vendor/cli/commands/verify.d.ts +6 -0
- package/dist/vendor/cli/commands/verify.js +43 -0
- package/dist/vendor/cli/index.d.ts +6 -1
- package/dist/vendor/cli/index.js +124 -7
- package/dist/vendor/cli/research/public-corpus.d.ts +43 -0
- package/dist/vendor/cli/research/public-corpus.js +151 -0
- package/dist/vendor/cli/ui/error-card.d.ts +38 -0
- package/dist/vendor/cli/ui/error-card.js +103 -0
- package/dist/vendor/cli/ui/mission-brief.d.ts +41 -0
- package/dist/vendor/cli/ui/mission-brief.js +173 -0
- package/dist/vendor/cli/ui/summary-card.d.ts +34 -0
- package/dist/vendor/cli/ui/summary-card.js +102 -0
- package/dist/vendor/contracts/audit.d.ts +46 -0
- package/dist/vendor/contracts/audit.js +360 -0
- package/dist/vendor/contracts/index.d.ts +3 -1
- package/dist/vendor/contracts/post-phase15.d.ts +240 -0
- package/dist/vendor/contracts/post-phase15.js +166 -0
- package/dist/vendor/core/agent/mandates.d.ts +46 -0
- package/dist/vendor/core/agent/mandates.js +178 -0
- package/dist/vendor/core/agent/receipts.d.ts +38 -0
- package/dist/vendor/core/agent/receipts.js +131 -0
- package/dist/vendor/core/agent/signing.d.ts +17 -0
- package/dist/vendor/core/agent/signing.js +91 -0
- package/dist/vendor/core/attestation/sign.d.ts +25 -0
- package/dist/vendor/core/attestation/sign.js +216 -0
- package/dist/vendor/core/autonomy/autonomous-promotion.d.ts +120 -0
- package/dist/vendor/core/autonomy/autonomous-promotion.js +346 -0
- package/dist/vendor/core/autonomy/envelope-v2.d.ts +29 -0
- package/dist/vendor/core/autonomy/envelope-v2.js +60 -0
- package/dist/vendor/core/autonomy/envelope.d.ts +17 -0
- package/dist/vendor/core/autonomy/envelope.js +27 -0
- package/dist/vendor/core/autonomy/escalation-ledger.d.ts +20 -0
- package/dist/vendor/core/autonomy/escalation-ledger.js +18 -0
- package/dist/vendor/core/autonomy/resume.d.ts +15 -0
- package/dist/vendor/core/autonomy/resume.js +23 -0
- package/dist/vendor/core/circuit/circuit-breaker.d.ts +60 -0
- package/dist/vendor/core/circuit/circuit-breaker.js +143 -0
- package/dist/vendor/core/compiler.d.ts +2 -0
- package/dist/vendor/core/compiler.js +10 -4
- package/dist/vendor/core/context-distillation.d.ts +3 -0
- package/dist/vendor/core/context-distillation.js +44 -0
- package/dist/vendor/core/context-flow/compile-context.d.ts +8 -0
- package/dist/vendor/core/context-flow/compile-context.js +111 -0
- package/dist/vendor/core/context-flow/entities.d.ts +2 -0
- package/dist/vendor/core/context-flow/entities.js +44 -0
- package/dist/vendor/core/context-flow/evaluate-policy.d.ts +2 -0
- package/dist/vendor/core/context-flow/evaluate-policy.js +42 -0
- package/dist/vendor/core/context-flow/index.d.ts +11 -0
- package/dist/vendor/core/context-flow/index.js +24 -0
- package/dist/vendor/core/context-flow/labels.d.ts +3 -0
- package/dist/vendor/core/context-flow/labels.js +17 -0
- package/dist/vendor/core/context-flow/normalizer.d.ts +9 -0
- package/dist/vendor/core/context-flow/normalizer.js +69 -0
- package/dist/vendor/core/context-flow/profiles.d.ts +33 -0
- package/dist/vendor/core/context-flow/profiles.js +36 -0
- package/dist/vendor/core/context-flow/redaction.d.ts +1 -0
- package/dist/vendor/core/context-flow/redaction.js +6 -0
- package/dist/vendor/core/context-flow/sensitivity.d.ts +2 -0
- package/dist/vendor/core/context-flow/sensitivity.js +27 -0
- package/dist/vendor/core/context-flow/sync-preview.d.ts +2 -0
- package/dist/vendor/core/context-flow/sync-preview.js +22 -0
- package/dist/vendor/core/context-flow/token-estimator.d.ts +3 -0
- package/dist/vendor/core/context-flow/token-estimator.js +13 -0
- package/dist/vendor/core/context-flow/types.d.ts +91 -0
- package/dist/vendor/core/context-flow/types.js +2 -0
- package/dist/vendor/core/context-integrity.d.ts +26 -0
- package/dist/vendor/core/context-integrity.js +56 -0
- package/dist/vendor/core/context-utility.d.ts +47 -0
- package/dist/vendor/core/context-utility.js +405 -0
- package/dist/vendor/core/cost/pipeline.d.ts +92 -0
- package/dist/vendor/core/cost/pipeline.js +141 -0
- package/dist/vendor/core/cost/tagged-cost.d.ts +27 -0
- package/dist/vendor/core/cost/tagged-cost.js +55 -0
- package/dist/vendor/core/cost-governor.d.ts +2 -0
- package/dist/vendor/core/cost-governor.js +50 -0
- package/dist/vendor/core/cve/cve-check.d.ts +80 -0
- package/dist/vendor/core/cve/cve-check.js +172 -0
- package/dist/vendor/core/digital-twin/index.d.ts +27 -0
- package/dist/vendor/core/digital-twin/index.js +90 -0
- package/dist/vendor/core/drift/drift-graph.d.ts +47 -0
- package/dist/vendor/core/drift/drift-graph.js +100 -0
- package/dist/vendor/core/drift/objective-lock.d.ts +69 -0
- package/dist/vendor/core/drift/objective-lock.js +88 -0
- package/dist/vendor/core/drift/scope.d.ts +46 -0
- package/dist/vendor/core/drift/scope.js +102 -0
- package/dist/vendor/core/drift/signature-lock.d.ts +48 -0
- package/dist/vendor/core/drift/signature-lock.js +202 -0
- package/dist/vendor/core/drift/stale-proof-gate.d.ts +21 -0
- package/dist/vendor/core/drift/stale-proof-gate.js +19 -0
- package/dist/vendor/core/eval/known-bad-world-runner.d.ts +24 -0
- package/dist/vendor/core/eval/known-bad-world-runner.js +256 -0
- package/dist/vendor/core/evidence/claim-audit.d.ts +18 -0
- package/dist/vendor/core/evidence/claim-audit.js +89 -0
- package/dist/vendor/core/exit-intelligence.d.ts +2 -0
- package/dist/vendor/core/exit-intelligence.js +58 -0
- package/dist/vendor/core/explain/formatter.d.ts +42 -0
- package/dist/vendor/core/explain/formatter.js +171 -0
- package/dist/vendor/core/explain/timeline.d.ts +29 -0
- package/dist/vendor/core/explain/timeline.js +213 -0
- package/dist/vendor/core/failure-taxonomy.d.ts +2 -0
- package/dist/vendor/core/failure-taxonomy.js +76 -0
- package/dist/vendor/core/gateway/index.d.ts +10 -0
- package/dist/vendor/core/gateway/index.js +12 -0
- package/dist/vendor/core/gateway/registry.d.ts +40 -0
- package/dist/vendor/core/gateway/registry.js +97 -0
- package/dist/vendor/core/gateway/transport.d.ts +31 -0
- package/dist/vendor/core/gateway/transport.js +82 -0
- package/dist/vendor/core/gateway/vault.d.ts +19 -0
- package/dist/vendor/core/gateway/vault.js +29 -0
- package/dist/vendor/core/graph/adapters.d.ts +43 -0
- package/dist/vendor/core/graph/adapters.js +91 -0
- package/dist/vendor/core/graph/hotspots.d.ts +22 -0
- package/dist/vendor/core/graph/hotspots.js +30 -0
- package/dist/vendor/core/graph/index.d.ts +1 -0
- package/dist/vendor/core/graph/index.js +2 -0
- package/dist/vendor/core/honey/honey-tokens.d.ts +32 -0
- package/dist/vendor/core/honey/honey-tokens.js +44 -0
- package/dist/vendor/core/index.d.ts +7 -4
- package/dist/vendor/core/index.js +222 -64
- package/dist/vendor/core/learning/bayesian-update.d.ts +31 -0
- package/dist/vendor/core/learning/bayesian-update.js +60 -0
- package/dist/vendor/core/learning/prior-sets.d.ts +42 -0
- package/dist/vendor/core/learning/prior-sets.js +111 -0
- package/dist/vendor/core/learning/promotion-gate.d.ts +17 -0
- package/dist/vendor/core/learning/promotion-gate.js +23 -0
- package/dist/vendor/core/leash/blast-radius.d.ts +42 -0
- package/dist/vendor/core/leash/blast-radius.js +156 -0
- package/dist/vendor/core/leash/policy-leash.d.ts +31 -0
- package/dist/vendor/core/leash/policy-leash.js +117 -0
- package/dist/vendor/core/memo/memo.d.ts +63 -0
- package/dist/vendor/core/memo/memo.js +97 -0
- package/dist/vendor/core/memory/learning-pipeline.d.ts +154 -0
- package/dist/vendor/core/memory/learning-pipeline.js +391 -0
- package/dist/vendor/core/memory/palace.d.ts +84 -0
- package/dist/vendor/core/memory/palace.js +379 -0
- package/dist/vendor/core/merge/ast-merge.d.ts +22 -0
- package/dist/vendor/core/merge/ast-merge.js +350 -0
- package/dist/vendor/core/merge/text-merge.d.ts +12 -0
- package/dist/vendor/core/merge/text-merge.js +182 -0
- package/dist/vendor/core/otel/tracer.d.ts +45 -0
- package/dist/vendor/core/otel/tracer.js +116 -0
- package/dist/vendor/core/parallel/parallel-attempts.d.ts +28 -0
- package/dist/vendor/core/parallel/parallel-attempts.js +41 -0
- package/dist/vendor/core/parallel/scorer.d.ts +24 -0
- package/dist/vendor/core/parallel/scorer.js +65 -0
- package/dist/vendor/core/pattern-detection.d.ts +64 -0
- package/dist/vendor/core/pattern-detection.js +108 -0
- package/dist/vendor/core/persistence/checkpoint.d.ts +44 -0
- package/dist/vendor/core/persistence/checkpoint.js +156 -0
- package/dist/vendor/core/persistence/cleanup.d.ts +22 -0
- package/dist/vendor/core/persistence/cleanup.js +131 -0
- package/dist/vendor/core/persistence/index.d.ts +2 -0
- package/dist/vendor/core/persistence/index.js +1 -0
- package/dist/vendor/core/persistence/runs-reader.d.ts +52 -0
- package/dist/vendor/core/persistence/runs-reader.js +84 -0
- package/dist/vendor/core/persistence/store.d.ts +6 -1
- package/dist/vendor/core/persistence/store.js +5 -0
- package/dist/vendor/core/policy/file-touch-quota.d.ts +60 -0
- package/dist/vendor/core/policy/file-touch-quota.js +105 -0
- package/dist/vendor/core/policy/policy-loader.d.ts +30 -0
- package/dist/vendor/core/policy/policy-loader.js +170 -0
- package/dist/vendor/core/policy/policy-schema.d.ts +55 -0
- package/dist/vendor/core/policy/policy-schema.js +78 -0
- package/dist/vendor/core/policy.d.ts +6 -0
- package/dist/vendor/core/probe/probe.d.ts +49 -0
- package/dist/vendor/core/probe/probe.js +115 -0
- package/dist/vendor/core/proof/patch-proof.d.ts +58 -0
- package/dist/vendor/core/proof/patch-proof.js +84 -0
- package/dist/vendor/core/proof/semantic-probe.d.ts +25 -0
- package/dist/vendor/core/proof/semantic-probe.js +82 -0
- package/dist/vendor/core/recovery/failure-mode-runner.d.ts +29 -0
- package/dist/vendor/core/recovery/failure-mode-runner.js +39 -0
- package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
- package/dist/vendor/core/red-blue/red-phase.js +141 -0
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
- package/dist/vendor/core/red-blue/risk-tiers.js +33 -0
- package/dist/vendor/core/replay/replay.d.ts +85 -0
- package/dist/vendor/core/replay/replay.js +109 -0
- package/dist/vendor/core/router/engine.d.ts +54 -0
- package/dist/vendor/core/router/engine.js +131 -0
- package/dist/vendor/core/router/index.d.ts +1 -0
- package/dist/vendor/core/router/index.js +2 -0
- package/dist/vendor/core/router/trust-calibration.d.ts +57 -0
- package/dist/vendor/core/router/trust-calibration.js +127 -0
- package/dist/vendor/core/run-martin.d.ts +2 -0
- package/dist/vendor/core/run-martin.js +287 -0
- package/dist/vendor/core/security/cve-scanner.d.ts +62 -0
- package/dist/vendor/core/security/cve-scanner.js +178 -0
- package/dist/vendor/core/sentinel/efficiency-sentinel.d.ts +29 -0
- package/dist/vendor/core/sentinel/efficiency-sentinel.js +30 -0
- package/dist/vendor/core/sentinel/progress-guard.d.ts +35 -0
- package/dist/vendor/core/sentinel/progress-guard.js +46 -0
- package/dist/vendor/core/siem/siem-emitter.d.ts +49 -0
- package/dist/vendor/core/siem/siem-emitter.js +157 -0
- package/dist/vendor/core/strategy/attempt-brief.d.ts +22 -0
- package/dist/vendor/core/strategy/attempt-brief.js +89 -0
- package/dist/vendor/core/summarize/diff-summary.d.ts +35 -0
- package/dist/vendor/core/summarize/diff-summary.js +204 -0
- package/dist/vendor/core/surface-signals.d.ts +21 -0
- package/dist/vendor/core/surface-signals.js +139 -0
- package/dist/vendor/core/truth/truth-wall.d.ts +51 -0
- package/dist/vendor/core/truth/truth-wall.js +69 -0
- package/dist/vendor/core/truth-spine.d.ts +26 -0
- package/dist/vendor/core/truth-spine.js +62 -0
- package/dist/vendor/core/types.d.ts +115 -0
- package/dist/vendor/core/types.js +2 -0
- package/dist/vendor/core/verification/tiered-verify.d.ts +17 -0
- package/dist/vendor/core/verification/tiered-verify.js +29 -0
- package/dist/vendor/core/verifier-pyramid.d.ts +32 -0
- package/dist/vendor/core/verifier-pyramid.js +111 -0
- package/dist/vendor/core/workflow-artifacts.d.ts +99 -0
- package/dist/vendor/core/workflow-artifacts.js +668 -0
- package/dist/vendor/core/wrap/supervised-run.d.ts +96 -0
- package/dist/vendor/core/wrap/supervised-run.js +178 -0
- package/docs/assets/cli-animated.svg +139 -0
- package/docs/assets/cli-static.svg +34 -0
- package/docs/assets/github-hero-v2.svg +23 -0
- package/docs/assets/martin-raplph.png.jpg +0 -0
- package/docs/assets/martinloop-logo.png +0 -0
- package/docs/assets/nvidia-inception-program-light.png +0 -0
- package/docs/assets/nvidia-inception-program.png +0 -0
- package/docs/assets/phase3c-sidesidebyside-demo.html +228 -0
- package/docs/assets/side-by-side.svg +134 -0
- package/docs/oss/CLAUDE-CODE-WALKTHROUGH.md +142 -0
- package/docs/oss/EXAMPLES.md +9 -1
- package/docs/oss/OSS-BOUNDARY-REPORT.json +109 -113
- package/docs/oss/OSS-BOUNDARY-REPORT.md +48 -48
- package/docs/oss/QUICKSTART.md +39 -4
- package/docs/oss/RALPH-LOOP-SAFETY.md +113 -0
- package/docs/oss/README.md +7 -4
- package/docs/oss/RELEASE-SURFACE-REPORT.json +46 -45
- package/docs/oss/RELEASE-SURFACE-REPORT.md +36 -35
- package/package.json +129 -49
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SLICE-18 — OpenTelemetry traces (noop implementation)
|
|
2
|
+
// Provides a stable tracer interface with an in-memory noop fallback.
|
|
3
|
+
// Actual OTLP export is a future enhancement — the interface is stable now.
|
|
4
|
+
// No @opentelemetry packages are required; this is a pure noop implementation.
|
|
5
|
+
// All valid span names for runtime validation
|
|
6
|
+
export const VALID_SPAN_NAMES = [
|
|
7
|
+
"martin.run",
|
|
8
|
+
"martin.attempt",
|
|
9
|
+
"martin.adapter_call",
|
|
10
|
+
"martin.patch_proof",
|
|
11
|
+
"martin.leash_check",
|
|
12
|
+
"martin.grounding_scan",
|
|
13
|
+
"martin.rollback",
|
|
14
|
+
"martin.gateway_policy",
|
|
15
|
+
"martin.router_decision"
|
|
16
|
+
];
|
|
17
|
+
// ─── In-memory span store (for testing and noop mode) ────────────────────────
|
|
18
|
+
let collectedSpans = [];
|
|
19
|
+
/**
|
|
20
|
+
* Returns all spans collected in the current process.
|
|
21
|
+
* Useful for test assertions.
|
|
22
|
+
*/
|
|
23
|
+
export function getCollectedSpans() {
|
|
24
|
+
return [...collectedSpans];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resets the in-memory span array.
|
|
28
|
+
* Call this in test beforeEach/afterEach to isolate tests.
|
|
29
|
+
*/
|
|
30
|
+
export function clearCollectedSpans() {
|
|
31
|
+
collectedSpans = [];
|
|
32
|
+
}
|
|
33
|
+
// ─── ID generation helpers ───────────────────────────────────────────────────
|
|
34
|
+
function randomHex(bytes) {
|
|
35
|
+
// Use crypto.getRandomValues in Node.js / browser environments
|
|
36
|
+
// Fall back to Math.random if unavailable (test environments)
|
|
37
|
+
try {
|
|
38
|
+
// Node.js crypto module
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const { randomBytes } = require("node:crypto");
|
|
41
|
+
return randomBytes(bytes).toString("hex");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Fallback: Math.random-based hex (not cryptographically secure, but fine for noop)
|
|
45
|
+
let result = "";
|
|
46
|
+
const chars = "0123456789abcdef";
|
|
47
|
+
for (let i = 0; i < bytes * 2; i++) {
|
|
48
|
+
result += chars[Math.floor(Math.random() * 16)];
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ─── Noop tracer implementation ───────────────────────────────────────────────
|
|
54
|
+
function createNoopTracer() {
|
|
55
|
+
return {
|
|
56
|
+
startSpan(name, attributes = {}) {
|
|
57
|
+
const span = {
|
|
58
|
+
spanId: randomHex(8),
|
|
59
|
+
traceId: randomHex(16),
|
|
60
|
+
startTime: new Date().toISOString(),
|
|
61
|
+
name,
|
|
62
|
+
status: "UNSET",
|
|
63
|
+
attributes: { ...attributes }
|
|
64
|
+
};
|
|
65
|
+
collectedSpans.push(span);
|
|
66
|
+
return span;
|
|
67
|
+
},
|
|
68
|
+
endSpan(span, status = "OK") {
|
|
69
|
+
// Mutate the collected span reference
|
|
70
|
+
span.endTime = new Date().toISOString();
|
|
71
|
+
span.status = status;
|
|
72
|
+
return span;
|
|
73
|
+
},
|
|
74
|
+
recordException(span, error) {
|
|
75
|
+
span.attributes["exception.type"] = error.name;
|
|
76
|
+
span.attributes["exception.message"] = error.message;
|
|
77
|
+
if (error.stack) {
|
|
78
|
+
span.attributes["exception.stacktrace"] = error.stack.slice(0, 500);
|
|
79
|
+
}
|
|
80
|
+
span.status = "ERROR";
|
|
81
|
+
return span;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Creates a tracer instance. When endpoint is undefined or empty, uses the
|
|
87
|
+
* noop implementation (stores spans in memory, no network calls).
|
|
88
|
+
* When endpoint is set, the same noop is used — actual OTLP export is a
|
|
89
|
+
* future enhancement.
|
|
90
|
+
*/
|
|
91
|
+
export function createTracer(options = {}) {
|
|
92
|
+
// In both cases (endpoint set or not), we use the noop implementation.
|
|
93
|
+
// The interface is stable; OTLP export will be added when OTel peer deps
|
|
94
|
+
// are installed.
|
|
95
|
+
void options.endpoint; // acknowledged, not yet used
|
|
96
|
+
return createNoopTracer();
|
|
97
|
+
}
|
|
98
|
+
// ─── Default module-level singleton ──────────────────────────────────────────
|
|
99
|
+
let _defaultTracer = null;
|
|
100
|
+
/**
|
|
101
|
+
* Returns the default module-level tracer singleton.
|
|
102
|
+
* Initializes with a noop tracer if not already set.
|
|
103
|
+
*/
|
|
104
|
+
export function getTracer() {
|
|
105
|
+
if (!_defaultTracer) {
|
|
106
|
+
_defaultTracer = createTracer();
|
|
107
|
+
}
|
|
108
|
+
return _defaultTracer;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resets the default tracer singleton (for testing).
|
|
112
|
+
*/
|
|
113
|
+
export function resetDefaultTracer() {
|
|
114
|
+
_defaultTracer = null;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=tracer.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type AttemptScore, type ScorerWeights } from "./scorer.js";
|
|
2
|
+
export interface ParallelAttemptInput {
|
|
3
|
+
attemptId: string;
|
|
4
|
+
grade: "A" | "B" | "C";
|
|
5
|
+
groundingScore: number;
|
|
6
|
+
costUsd: number;
|
|
7
|
+
diff: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ParallelConsensusArtifact {
|
|
10
|
+
runId: string;
|
|
11
|
+
parallelN: number;
|
|
12
|
+
attempts: AttemptScore[];
|
|
13
|
+
winner: AttemptScore | null;
|
|
14
|
+
allFailed: boolean;
|
|
15
|
+
totalCostUsd: number;
|
|
16
|
+
producedAt: string;
|
|
17
|
+
featureFlag: "parallel.enabled";
|
|
18
|
+
}
|
|
19
|
+
export interface ParallelResult {
|
|
20
|
+
winner: ParallelAttemptInput | null;
|
|
21
|
+
allFailed: boolean;
|
|
22
|
+
totalCostUsd: number;
|
|
23
|
+
artifact: ParallelConsensusArtifact;
|
|
24
|
+
}
|
|
25
|
+
export declare function runParallelConsensus(attempts: ParallelAttemptInput[], options: {
|
|
26
|
+
runId: string;
|
|
27
|
+
weights?: ScorerWeights;
|
|
28
|
+
}): ParallelResult;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ─── Parallel Consensus ───────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Scores N pre-computed attempt results, selects a winner, builds the
|
|
4
|
+
// consensus artifact, and returns a ParallelResult.
|
|
5
|
+
//
|
|
6
|
+
// Feature flag: "parallel.enabled" — this module is behind a flag.
|
|
7
|
+
// The actual adapter concurrency (Promise.allSettled) lives in index.ts.
|
|
8
|
+
// This function is synchronous and pure: score, select, artifact — no I/O.
|
|
9
|
+
import { scoreAttempt, selectWinner } from "./scorer.js";
|
|
10
|
+
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
11
|
+
export function runParallelConsensus(attempts, options) {
|
|
12
|
+
const scores = attempts.map((a) => scoreAttempt({
|
|
13
|
+
attemptId: a.attemptId,
|
|
14
|
+
grade: a.grade,
|
|
15
|
+
groundingScore: a.groundingScore,
|
|
16
|
+
costUsd: a.costUsd
|
|
17
|
+
}, options.weights));
|
|
18
|
+
const winnerScore = selectWinner(scores);
|
|
19
|
+
const allFailed = winnerScore === null;
|
|
20
|
+
const totalCostUsd = attempts.reduce((sum, a) => sum + a.costUsd, 0);
|
|
21
|
+
const winnerInput = winnerScore
|
|
22
|
+
? (attempts.find((a) => a.attemptId === winnerScore.attemptId) ?? null)
|
|
23
|
+
: null;
|
|
24
|
+
const artifact = {
|
|
25
|
+
runId: options.runId,
|
|
26
|
+
parallelN: attempts.length,
|
|
27
|
+
attempts: scores,
|
|
28
|
+
winner: winnerScore,
|
|
29
|
+
allFailed,
|
|
30
|
+
totalCostUsd,
|
|
31
|
+
producedAt: new Date().toISOString(),
|
|
32
|
+
featureFlag: "parallel.enabled"
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
winner: winnerInput,
|
|
36
|
+
allFailed,
|
|
37
|
+
totalCostUsd,
|
|
38
|
+
artifact
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=parallel-attempts.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface AttemptScore {
|
|
2
|
+
attemptId: string;
|
|
3
|
+
grade: "A" | "B" | "C";
|
|
4
|
+
groundingScore: number;
|
|
5
|
+
costUsd: number;
|
|
6
|
+
compositeScore: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ScorerWeights {
|
|
9
|
+
grounding: number;
|
|
10
|
+
verifier: number;
|
|
11
|
+
cost: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function scoreAttempt(attempt: {
|
|
14
|
+
attemptId: string;
|
|
15
|
+
grade: "A" | "B" | "C";
|
|
16
|
+
groundingScore: number;
|
|
17
|
+
costUsd: number;
|
|
18
|
+
}, weights?: ScorerWeights): AttemptScore;
|
|
19
|
+
/**
|
|
20
|
+
* Selects the winning attempt from a scored list.
|
|
21
|
+
* Returns null if all attempts have grade C (all failed).
|
|
22
|
+
* Tiebreak order: highest compositeScore → lowest costUsd → alphabetical attemptId.
|
|
23
|
+
*/
|
|
24
|
+
export declare function selectWinner(scores: AttemptScore[]): AttemptScore | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ─── Attempt Scorer ───────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Computes a composite score for a parallel attempt using:
|
|
4
|
+
// compositeScore = (groundingScore * grounding)
|
|
5
|
+
// + (verifierConfidenceProxy * verifier)
|
|
6
|
+
// + (1 / (costUsd + 0.01) * cost)
|
|
7
|
+
//
|
|
8
|
+
// verifierConfidenceProxy:
|
|
9
|
+
// grade A or B → 1.0
|
|
10
|
+
// grade C → 0.0
|
|
11
|
+
//
|
|
12
|
+
// selectWinner returns null when ALL grades are C (all failed).
|
|
13
|
+
// Tiebreak: lowest costUsd first; if still tied, alphabetical attemptId.
|
|
14
|
+
const DEFAULT_WEIGHTS = {
|
|
15
|
+
grounding: 0.4,
|
|
16
|
+
verifier: 0.4,
|
|
17
|
+
cost: 0.2
|
|
18
|
+
};
|
|
19
|
+
function verifierProxy(grade) {
|
|
20
|
+
return grade === "A" || grade === "B" ? 1.0 : 0.0;
|
|
21
|
+
}
|
|
22
|
+
export function scoreAttempt(attempt, weights = DEFAULT_WEIGHTS) {
|
|
23
|
+
const w = { ...DEFAULT_WEIGHTS, ...weights };
|
|
24
|
+
const vp = verifierProxy(attempt.grade);
|
|
25
|
+
const compositeScore = attempt.groundingScore * w.grounding +
|
|
26
|
+
vp * w.verifier +
|
|
27
|
+
(1 / (attempt.costUsd + 0.01)) * w.cost;
|
|
28
|
+
return {
|
|
29
|
+
attemptId: attempt.attemptId,
|
|
30
|
+
grade: attempt.grade,
|
|
31
|
+
groundingScore: attempt.groundingScore,
|
|
32
|
+
costUsd: attempt.costUsd,
|
|
33
|
+
compositeScore
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Selects the winning attempt from a scored list.
|
|
38
|
+
* Returns null if all attempts have grade C (all failed).
|
|
39
|
+
* Tiebreak order: highest compositeScore → lowest costUsd → alphabetical attemptId.
|
|
40
|
+
*/
|
|
41
|
+
export function selectWinner(scores) {
|
|
42
|
+
if (scores.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
// All-failed check: all grades are C
|
|
45
|
+
const allFailed = scores.every((s) => s.grade === "C");
|
|
46
|
+
if (allFailed)
|
|
47
|
+
return null;
|
|
48
|
+
// Filter out grade C — they can't win
|
|
49
|
+
const eligible = scores.filter((s) => s.grade !== "C");
|
|
50
|
+
if (eligible.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
return eligible.slice().sort((a, b) => {
|
|
53
|
+
// Primary: highest composite score (descending)
|
|
54
|
+
if (b.compositeScore !== a.compositeScore) {
|
|
55
|
+
return b.compositeScore - a.compositeScore;
|
|
56
|
+
}
|
|
57
|
+
// Secondary: lowest cost (ascending)
|
|
58
|
+
if (a.costUsd !== b.costUsd) {
|
|
59
|
+
return a.costUsd - b.costUsd;
|
|
60
|
+
}
|
|
61
|
+
// Tertiary: alphabetical attemptId (ascending)
|
|
62
|
+
return a.attemptId.localeCompare(b.attemptId);
|
|
63
|
+
})[0] ?? null;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=scorer.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { FailureClass, LoopAttempt } from "../contracts/index.js";
|
|
2
|
+
export type LoopPatternId = "no_op_success" | "repo_worktree_mismatch" | "broken_tool_loop" | "stale_state_drift";
|
|
3
|
+
export interface LoopPatternDetection {
|
|
4
|
+
attemptId: string;
|
|
5
|
+
patternId: LoopPatternId;
|
|
6
|
+
severity: "warn";
|
|
7
|
+
message: string;
|
|
8
|
+
evidence: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
interface PatternDetectionExecution {
|
|
11
|
+
changedFiles?: string[];
|
|
12
|
+
diffStats?: {
|
|
13
|
+
filesChanged: number;
|
|
14
|
+
addedLines: number;
|
|
15
|
+
deletedLines: number;
|
|
16
|
+
};
|
|
17
|
+
baseline?: {
|
|
18
|
+
startHeadSha: string;
|
|
19
|
+
startTrackedStatus: string;
|
|
20
|
+
startUntrackedSet: string[];
|
|
21
|
+
startTimestamp: string;
|
|
22
|
+
worktreeClean: boolean;
|
|
23
|
+
pilotRepoClass: string | null;
|
|
24
|
+
};
|
|
25
|
+
gitNormalization?: {
|
|
26
|
+
startHeadSha: string;
|
|
27
|
+
endHeadShaBeforeNormalization: string;
|
|
28
|
+
normalizationApplied: boolean;
|
|
29
|
+
normalizationMode: "reset_mixed_to_start_head" | "none";
|
|
30
|
+
normalizationSucceeded: boolean;
|
|
31
|
+
headAfterNormalization: string;
|
|
32
|
+
};
|
|
33
|
+
scopeSurface?: {
|
|
34
|
+
trackedChangedPaths: string[];
|
|
35
|
+
deletedPaths: string[];
|
|
36
|
+
newUntrackedPaths: string[];
|
|
37
|
+
allChangedPaths: string[];
|
|
38
|
+
baselineWasClean: boolean;
|
|
39
|
+
noCodeChange: boolean;
|
|
40
|
+
diffStats?: {
|
|
41
|
+
filesChanged: number;
|
|
42
|
+
addedLines: number;
|
|
43
|
+
deletedLines: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
interface PatternDetectionResult {
|
|
48
|
+
status: "completed" | "failed";
|
|
49
|
+
summary: string;
|
|
50
|
+
verification: {
|
|
51
|
+
passed: boolean;
|
|
52
|
+
summary: string;
|
|
53
|
+
};
|
|
54
|
+
execution?: PatternDetectionExecution;
|
|
55
|
+
}
|
|
56
|
+
export interface DetectStreamingAnomaliesInput {
|
|
57
|
+
attemptId: string;
|
|
58
|
+
result: PatternDetectionResult;
|
|
59
|
+
previousAttempts: LoopAttempt[];
|
|
60
|
+
resolvedChangedFiles: string[];
|
|
61
|
+
failureClass?: FailureClass;
|
|
62
|
+
}
|
|
63
|
+
export declare function detectStreamingAnomalies(input: DetectStreamingAnomaliesInput): LoopPatternDetection[];
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const CHANGE_CLAIM_RE = /\b(fix(?:ed)?|update(?:d)?|implement(?:ed)?|change(?:d)?|patch(?:ed)?|add(?:ed)?|remove(?:d)?|repair(?:ed)?|refactor(?:ed)?)\b/i;
|
|
2
|
+
export function detectStreamingAnomalies(input) {
|
|
3
|
+
const patterns = [];
|
|
4
|
+
const groundedChangedFiles = uniqueNormalizedPaths(input.resolvedChangedFiles);
|
|
5
|
+
const reportedChangedFiles = uniqueNormalizedPaths(input.result.execution?.changedFiles);
|
|
6
|
+
const scopeSurfaceFiles = uniqueNormalizedPaths(input.result.execution?.scopeSurface?.allChangedPaths);
|
|
7
|
+
const effectiveChangedFiles = groundedChangedFiles.length > 0 ? groundedChangedFiles : scopeSurfaceFiles;
|
|
8
|
+
const summaryClaimsChange = CHANGE_CLAIM_RE.test(input.result.summary);
|
|
9
|
+
const diffStats = input.result.execution?.diffStats ?? input.result.execution?.scopeSurface?.diffStats;
|
|
10
|
+
const baselineDirty = input.result.execution?.baseline?.worktreeClean === false ||
|
|
11
|
+
input.result.execution?.scopeSurface?.baselineWasClean === false;
|
|
12
|
+
const normalizationApplied = input.result.execution?.gitNormalization?.normalizationApplied === true ||
|
|
13
|
+
(input.result.execution?.gitNormalization?.startHeadSha !== undefined &&
|
|
14
|
+
input.result.execution?.gitNormalization?.endHeadShaBeforeNormalization !== undefined &&
|
|
15
|
+
input.result.execution.gitNormalization.startHeadSha !==
|
|
16
|
+
input.result.execution.gitNormalization.endHeadShaBeforeNormalization);
|
|
17
|
+
if (input.result.status === "completed" &&
|
|
18
|
+
input.result.verification.passed &&
|
|
19
|
+
summaryClaimsChange &&
|
|
20
|
+
effectiveChangedFiles.length === 0 &&
|
|
21
|
+
(input.result.execution?.scopeSurface?.noCodeChange ?? true) &&
|
|
22
|
+
!hasSubstantiveDiffStats(diffStats)) {
|
|
23
|
+
patterns.push({
|
|
24
|
+
attemptId: input.attemptId,
|
|
25
|
+
patternId: "no_op_success",
|
|
26
|
+
severity: "warn",
|
|
27
|
+
message: "Verification passed, but the run summary claimed a fix without repo-backed change evidence.",
|
|
28
|
+
evidence: {
|
|
29
|
+
summary: input.result.summary,
|
|
30
|
+
verificationSummary: input.result.verification.summary,
|
|
31
|
+
resolvedChangedFiles: groundedChangedFiles,
|
|
32
|
+
reportedChangedFiles
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (reportedChangedFiles.length > 0 &&
|
|
37
|
+
effectiveChangedFiles.length > 0 &&
|
|
38
|
+
!samePathSet(reportedChangedFiles, effectiveChangedFiles)) {
|
|
39
|
+
patterns.push({
|
|
40
|
+
attemptId: input.attemptId,
|
|
41
|
+
patternId: "repo_worktree_mismatch",
|
|
42
|
+
severity: "warn",
|
|
43
|
+
message: "The adapter-reported changed files diverged from the grounded repo/worktree state.",
|
|
44
|
+
evidence: {
|
|
45
|
+
reportedChangedFiles,
|
|
46
|
+
resolvedChangedFiles: effectiveChangedFiles
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const currentVerifierSignature = normalizeText(input.result.verification.summary || input.result.summary);
|
|
51
|
+
const repeatedFailureSignature = input.failureClass !== undefined &&
|
|
52
|
+
currentVerifierSignature.length > 0 &&
|
|
53
|
+
input.previousAttempts.slice(-2).length === 2 &&
|
|
54
|
+
input.previousAttempts.slice(-2).every((attempt) => {
|
|
55
|
+
const priorSignature = normalizeText(attempt.verifierSummary ?? attempt.summary);
|
|
56
|
+
return attempt.failureClass === input.failureClass && priorSignature === currentVerifierSignature;
|
|
57
|
+
});
|
|
58
|
+
if (repeatedFailureSignature) {
|
|
59
|
+
patterns.push({
|
|
60
|
+
attemptId: input.attemptId,
|
|
61
|
+
patternId: "broken_tool_loop",
|
|
62
|
+
severity: "warn",
|
|
63
|
+
message: "The loop is repeating the same failed verifier signature without a new recovery signal.",
|
|
64
|
+
evidence: {
|
|
65
|
+
failureClass: input.failureClass,
|
|
66
|
+
verifierSummary: input.result.verification.summary,
|
|
67
|
+
repeatedAttempts: input.previousAttempts.slice(-2).map((attempt) => attempt.attemptId)
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (input.result.status === "completed" &&
|
|
72
|
+
input.result.verification.passed &&
|
|
73
|
+
(baselineDirty || normalizationApplied)) {
|
|
74
|
+
patterns.push({
|
|
75
|
+
attemptId: input.attemptId,
|
|
76
|
+
patternId: "stale_state_drift",
|
|
77
|
+
severity: "warn",
|
|
78
|
+
message: "The attempt passed verification on a dirty or normalized baseline, so state drift may be masking the true change source.",
|
|
79
|
+
evidence: {
|
|
80
|
+
baselineWasClean: input.result.execution?.scopeSurface?.baselineWasClean,
|
|
81
|
+
worktreeClean: input.result.execution?.baseline?.worktreeClean,
|
|
82
|
+
normalizationApplied: input.result.execution?.gitNormalization?.normalizationApplied ?? false,
|
|
83
|
+
startHeadSha: input.result.execution?.gitNormalization?.startHeadSha,
|
|
84
|
+
endHeadShaBeforeNormalization: input.result.execution?.gitNormalization?.endHeadShaBeforeNormalization
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return patterns;
|
|
89
|
+
}
|
|
90
|
+
function normalizeText(value) {
|
|
91
|
+
return (value ?? "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
92
|
+
}
|
|
93
|
+
function normalizePath(path) {
|
|
94
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
95
|
+
}
|
|
96
|
+
function uniqueNormalizedPaths(paths) {
|
|
97
|
+
return [...new Set((paths ?? []).map(normalizePath).filter((path) => path.length > 0))].sort();
|
|
98
|
+
}
|
|
99
|
+
function samePathSet(left, right) {
|
|
100
|
+
return left.length === right.length && left.every((path, index) => path === right[index]);
|
|
101
|
+
}
|
|
102
|
+
function hasSubstantiveDiffStats(diffStats) {
|
|
103
|
+
if (!diffStats) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return diffStats.filesChanged > 0 || diffStats.addedLines > 0 || diffStats.deletedLines > 0;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=pattern-detection.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type CheckpointPhase = "run_start" | "attempt_start" | "attempt_complete" | "run_complete";
|
|
2
|
+
export interface Checkpoint {
|
|
3
|
+
loopId: string;
|
|
4
|
+
phase: CheckpointPhase;
|
|
5
|
+
attemptNumber: number;
|
|
6
|
+
lastCompletedAttempt: number;
|
|
7
|
+
costSoFar: number;
|
|
8
|
+
fileHashSnapshot: Record<string, string>;
|
|
9
|
+
lastAttemptRolledBack: boolean;
|
|
10
|
+
writtenAt: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class WorkspaceModifiedError extends Error {
|
|
13
|
+
readonly modifiedFiles: string[];
|
|
14
|
+
constructor(modifiedFiles: string[]);
|
|
15
|
+
}
|
|
16
|
+
export declare function getCheckpointStorageDir(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Writes checkpoint.json using writeFileSync (synchronous — survives crash).
|
|
19
|
+
* Path: <storageDir>/<loopId>/checkpoint.json
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeCheckpoint(checkpoint: Checkpoint, storageDir: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Returns null if the checkpoint file does not exist.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readCheckpoint(loopId: string, storageDir: string): Checkpoint | null;
|
|
26
|
+
/**
|
|
27
|
+
* Returns SHA-256 of file contents for each path that exists.
|
|
28
|
+
* Files that do not exist are omitted from the result.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hashFiles(filePaths: string[]): Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* Compares current file hashes to the checkpoint snapshot.
|
|
33
|
+
* If force=true: always returns valid:true (but still lists modified files).
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateWorkspaceHashes(checkpoint: Checkpoint, force?: boolean): {
|
|
36
|
+
valid: boolean;
|
|
37
|
+
modifiedFiles: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Deletes checkpoint files older than maxAgeMs (default 30 days).
|
|
41
|
+
* Returns count of deleted checkpoint directories.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cleanupOldCheckpoints(storageDir: string, maxAgeMs?: number): number;
|
|
44
|
+
export declare function assertSafeLoopId(loopId: string): void;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
const SAFE_LOOP_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
7
|
+
// ─── WorkspaceModifiedError ───────────────────────────────────────────────────
|
|
8
|
+
export class WorkspaceModifiedError extends Error {
|
|
9
|
+
modifiedFiles;
|
|
10
|
+
constructor(modifiedFiles) {
|
|
11
|
+
super(`Workspace modified since checkpoint. Modified: ${modifiedFiles.join(", ")}. Use --force to resume anyway.`);
|
|
12
|
+
this.modifiedFiles = modifiedFiles;
|
|
13
|
+
this.name = "WorkspaceModifiedError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// ─── Storage path resolution ──────────────────────────────────────────────────
|
|
17
|
+
export function getCheckpointStorageDir() {
|
|
18
|
+
const env = process.env["MARTIN_STORAGE"];
|
|
19
|
+
return typeof env === "string" && env.trim().length > 0
|
|
20
|
+
? env.trim()
|
|
21
|
+
: join(homedir(), ".martin", "loops");
|
|
22
|
+
}
|
|
23
|
+
// ─── Write ────────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Writes checkpoint.json using writeFileSync (synchronous — survives crash).
|
|
26
|
+
* Path: <storageDir>/<loopId>/checkpoint.json
|
|
27
|
+
*/
|
|
28
|
+
export function writeCheckpoint(checkpoint, storageDir) {
|
|
29
|
+
assertSafeLoopId(checkpoint.loopId);
|
|
30
|
+
const dir = join(storageDir, checkpoint.loopId);
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
const filePath = join(dir, "checkpoint.json");
|
|
33
|
+
writeFileSync(filePath, `${JSON.stringify(checkpoint, null, 2)}\n`, "utf8");
|
|
34
|
+
}
|
|
35
|
+
// ─── Read ─────────────────────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Returns null if the checkpoint file does not exist.
|
|
38
|
+
*/
|
|
39
|
+
export function readCheckpoint(loopId, storageDir) {
|
|
40
|
+
assertSafeLoopId(loopId);
|
|
41
|
+
const filePath = join(storageDir, loopId, "checkpoint.json");
|
|
42
|
+
if (!existsSync(filePath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const raw = readFileSync(filePath, "utf8");
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ─── File hashing ─────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Returns SHA-256 of file contents for each path that exists.
|
|
56
|
+
* Files that do not exist are omitted from the result.
|
|
57
|
+
*/
|
|
58
|
+
export function hashFiles(filePaths) {
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const filePath of filePaths) {
|
|
61
|
+
if (!existsSync(filePath)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const contents = readFileSync(filePath);
|
|
66
|
+
result[filePath] = createHash("sha256").update(contents).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Skip files that cannot be read (permission error, etc.)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
// ─── Workspace validation ─────────────────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Compares current file hashes to the checkpoint snapshot.
|
|
77
|
+
* If force=true: always returns valid:true (but still lists modified files).
|
|
78
|
+
*/
|
|
79
|
+
export function validateWorkspaceHashes(checkpoint, force) {
|
|
80
|
+
const snapshot = checkpoint.fileHashSnapshot;
|
|
81
|
+
const modifiedFiles = [];
|
|
82
|
+
for (const [filePath, savedHash] of Object.entries(snapshot)) {
|
|
83
|
+
if (!existsSync(filePath)) {
|
|
84
|
+
modifiedFiles.push(filePath);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const currentContents = readFileSync(filePath);
|
|
89
|
+
const currentHash = createHash("sha256").update(currentContents).digest("hex");
|
|
90
|
+
if (currentHash !== savedHash) {
|
|
91
|
+
modifiedFiles.push(filePath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
modifiedFiles.push(filePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (force === true) {
|
|
99
|
+
return { valid: true, modifiedFiles };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
valid: modifiedFiles.length === 0,
|
|
103
|
+
modifiedFiles
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// ─── Cleanup ──────────────────────────────────────────────────────────────────
|
|
107
|
+
const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
108
|
+
/**
|
|
109
|
+
* Deletes checkpoint files older than maxAgeMs (default 30 days).
|
|
110
|
+
* Returns count of deleted checkpoint directories.
|
|
111
|
+
*/
|
|
112
|
+
export function cleanupOldCheckpoints(storageDir, maxAgeMs) {
|
|
113
|
+
const cutoffMs = maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
let deletedCount = 0;
|
|
116
|
+
if (!existsSync(storageDir)) {
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = readdirSync(storageDir);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const checkpointPath = join(storageDir, entry, "checkpoint.json");
|
|
128
|
+
if (!existsSync(checkpointPath)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const stat = statSync(checkpointPath);
|
|
133
|
+
if (now - stat.mtimeMs > cutoffMs) {
|
|
134
|
+
// Read checkpoint to get writtenAt for more accurate age check
|
|
135
|
+
const raw = readFileSync(checkpointPath, "utf8");
|
|
136
|
+
const cp = JSON.parse(raw);
|
|
137
|
+
const writtenAt = new Date(cp.writtenAt).getTime();
|
|
138
|
+
if (!Number.isNaN(writtenAt) && now - writtenAt > cutoffMs) {
|
|
139
|
+
// Remove the checkpoint file (not the whole dir — may have other artifacts)
|
|
140
|
+
rmSync(checkpointPath);
|
|
141
|
+
deletedCount += 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Skip on any error
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return deletedCount;
|
|
150
|
+
}
|
|
151
|
+
export function assertSafeLoopId(loopId) {
|
|
152
|
+
if (!SAFE_LOOP_ID_PATTERN.test(loopId)) {
|
|
153
|
+
throw new Error(`unsafe loop id: ${loopId}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=checkpoint.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RunContract } from "./store.js";
|
|
2
|
+
export interface CleanupResult {
|
|
3
|
+
readonly cleaned: number;
|
|
4
|
+
readonly errors: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Scan runsRoot for abandoned non-terminal runs and mark them session_timeout.
|
|
8
|
+
*
|
|
9
|
+
* A run is stale when:
|
|
10
|
+
* - Its state.json lifecycleState is not in TERMINAL_STATES, AND
|
|
11
|
+
* - Its state.json updatedAt timestamp is older than ttlMs
|
|
12
|
+
*
|
|
13
|
+
* The TTL defaults to MARTIN_SESSION_TTL_MS env var, then 4 hours.
|
|
14
|
+
*/
|
|
15
|
+
export declare function cleanStaleRuns(runsRoot: string, ttlMs?: number): Promise<CleanupResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Return RunContracts for all runs currently in a non-terminal lifecycleState.
|
|
18
|
+
* Reads both state.json (for lifecycleState) and contract.json (for RunContract).
|
|
19
|
+
* Entries missing either file are silently skipped.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveActiveRuns(runsRoot: string): Promise<RunContract[]>;
|
|
22
|
+
export declare function ensureRunsRoot(runsRoot: string): Promise<void>;
|