martin-loop 0.1.5 → 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/LICENSE +21 -21
- package/README.md +307 -398
- package/demo/seeded-workspace/README.md +35 -35
- package/demo/seeded-workspace/TASKS.md +29 -29
- package/demo/seeded-workspace/martin.config.yaml +11 -11
- package/demo/seeded-workspace/package.json +8 -8
- package/demo/seeded-workspace/src/invoice-summary.js +11 -11
- package/demo/seeded-workspace/test/invoice-summary.test.js +20 -20
- package/dist/bin/martin-loop.js +0 -0
- 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/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/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/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/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/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-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 +2 -2
- package/dist/vendor/core/index.js +38 -12
- 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/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 -142
- package/docs/oss/EXAMPLES.md +134 -134
- package/docs/oss/OSS-BOUNDARY-REPORT.json +1 -1
- package/docs/oss/OSS-BOUNDARY-REPORT.md +1 -1
- package/docs/oss/QUICKSTART.md +170 -165
- package/docs/oss/RALPH-LOOP-SAFETY.md +113 -113
- package/docs/oss/README.md +96 -96
- package/docs/oss/RELEASE-SURFACE-REPORT.json +2 -1
- package/docs/oss/RELEASE-SURFACE-REPORT.md +2 -1
- package/package.json +130 -58
- package/docs/distribution/DIRECTORY-SUBMISSIONS.md +0 -89
- package/docs/distribution/INTEGRATION-OUTREACH.md +0 -61
- package/docs/distribution/UNDER-3-CHALLENGE.md +0 -65
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale run TTL cleanup and active run resolution.
|
|
3
|
+
*
|
|
4
|
+
* Fixes the "zombie session" problem where interrupted runs leave state.json
|
|
5
|
+
* stuck in a non-terminal lifecycleState. New sessions that inspect the runs
|
|
6
|
+
* directory would see these as active when they're abandoned.
|
|
7
|
+
*
|
|
8
|
+
* Default TTL: 4 hours (MARTIN_SESSION_TTL_MS env override).
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Constants
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const DEFAULT_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
16
|
+
/** LoopLifecycleState values that represent a finished (non-resumable) run. */
|
|
17
|
+
const TERMINAL_STATES = new Set([
|
|
18
|
+
"completed",
|
|
19
|
+
"budget_exit",
|
|
20
|
+
"diminishing_returns",
|
|
21
|
+
"stuck_exit",
|
|
22
|
+
"human_escalation",
|
|
23
|
+
"session_timeout",
|
|
24
|
+
"stagnation_detected",
|
|
25
|
+
"wall_clock_exceeded"
|
|
26
|
+
]);
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// cleanStaleRuns
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Scan runsRoot for abandoned non-terminal runs and mark them session_timeout.
|
|
32
|
+
*
|
|
33
|
+
* A run is stale when:
|
|
34
|
+
* - Its state.json lifecycleState is not in TERMINAL_STATES, AND
|
|
35
|
+
* - Its state.json updatedAt timestamp is older than ttlMs
|
|
36
|
+
*
|
|
37
|
+
* The TTL defaults to MARTIN_SESSION_TTL_MS env var, then 4 hours.
|
|
38
|
+
*/
|
|
39
|
+
export async function cleanStaleRuns(runsRoot, ttlMs) {
|
|
40
|
+
const resolvedTtl = ttlMs ??
|
|
41
|
+
(process.env["MARTIN_SESSION_TTL_MS"]
|
|
42
|
+
? parseInt(process.env["MARTIN_SESSION_TTL_MS"], 10)
|
|
43
|
+
: DEFAULT_TTL_MS);
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const errors = [];
|
|
46
|
+
let cleaned = 0;
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(runsRoot);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// runsRoot doesn't exist yet — nothing to clean
|
|
53
|
+
return { cleaned: 0, errors: [] };
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const statePath = join(runsRoot, entry, "state.json");
|
|
57
|
+
try {
|
|
58
|
+
const raw = await readFile(statePath, "utf8");
|
|
59
|
+
const state = JSON.parse(raw);
|
|
60
|
+
const lifecycleState = state.lifecycleState;
|
|
61
|
+
const updatedAt = state.updatedAt;
|
|
62
|
+
// Skip terminal runs regardless of age
|
|
63
|
+
if (!lifecycleState || TERMINAL_STATES.has(lifecycleState)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Skip runs updated recently
|
|
67
|
+
if (!updatedAt)
|
|
68
|
+
continue;
|
|
69
|
+
const elapsed = now - new Date(updatedAt).getTime();
|
|
70
|
+
if (elapsed < resolvedTtl)
|
|
71
|
+
continue;
|
|
72
|
+
// Mark as session_timeout
|
|
73
|
+
const updated = {
|
|
74
|
+
...state,
|
|
75
|
+
lifecycleState: "session_timeout",
|
|
76
|
+
updatedAt: new Date().toISOString()
|
|
77
|
+
};
|
|
78
|
+
await writeFile(statePath, `${JSON.stringify(updated, null, 2)}\n`, "utf8");
|
|
79
|
+
cleaned++;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// state.json missing or unreadable — skip silently, record error
|
|
83
|
+
if (err.code !== "ENOENT") {
|
|
84
|
+
errors.push(`${entry}: ${String(err)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { cleaned, errors };
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// resolveActiveRuns
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
/**
|
|
94
|
+
* Return RunContracts for all runs currently in a non-terminal lifecycleState.
|
|
95
|
+
* Reads both state.json (for lifecycleState) and contract.json (for RunContract).
|
|
96
|
+
* Entries missing either file are silently skipped.
|
|
97
|
+
*/
|
|
98
|
+
export async function resolveActiveRuns(runsRoot) {
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = await readdir(runsRoot);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const active = [];
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
try {
|
|
109
|
+
const statePath = join(runsRoot, entry, "state.json");
|
|
110
|
+
const contractPath = join(runsRoot, entry, "contract.json");
|
|
111
|
+
const stateRaw = await readFile(statePath, "utf8");
|
|
112
|
+
const state = JSON.parse(stateRaw);
|
|
113
|
+
if (!state.lifecycleState || TERMINAL_STATES.has(state.lifecycleState)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const contractRaw = await readFile(contractPath, "utf8");
|
|
117
|
+
const contract = JSON.parse(contractRaw);
|
|
118
|
+
active.push(contract);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Missing state.json or contract.json — skip
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return active;
|
|
125
|
+
}
|
|
126
|
+
// Ensure the runsRoot directory exists (best-effort, used by callers that
|
|
127
|
+
// want to guarantee the directory before scanning)
|
|
128
|
+
export async function ensureRunsRoot(runsRoot) {
|
|
129
|
+
await mkdir(runsRoot, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=cleanup.js.map
|
|
@@ -2,5 +2,7 @@ export { makeLedgerEvent } from "./ledger.js";
|
|
|
2
2
|
export type { LedgerEvent, LedgerEventDraft, LedgerEventKind } from "./ledger.js";
|
|
3
3
|
export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
|
|
4
4
|
export type { AttemptArtifacts, RunContract, RunStore } from "./store.js";
|
|
5
|
+
export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
|
|
6
|
+
export type { LoopAttemptRecord, LoopRunRecord } from "./runs-reader.js";
|
|
5
7
|
export { compileAndPersistContext } from "./compiler.js";
|
|
6
8
|
export type { CompileResult } from "./compiler.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { makeLedgerEvent } from "./ledger.js";
|
|
2
2
|
export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
|
|
3
|
+
export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
|
|
3
4
|
export { compileAndPersistContext } from "./compiler.js";
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads completed loop records from ~/.martin/runs/ for analysis.
|
|
3
|
+
* Used by the Trust Calibration Engine and other offline analytics.
|
|
4
|
+
*
|
|
5
|
+
* Supports both storage layouts:
|
|
6
|
+
* - legacy root JSONL files: <runsRoot>/*.jsonl
|
|
7
|
+
* - canonical run trees: <runsRoot>/<loopId>/loop-record.json
|
|
8
|
+
*/
|
|
9
|
+
export interface LoopAttemptRecord {
|
|
10
|
+
index: number;
|
|
11
|
+
model?: string;
|
|
12
|
+
adapterId?: string;
|
|
13
|
+
failureClass?: string;
|
|
14
|
+
intervention?: string;
|
|
15
|
+
startedAt?: string;
|
|
16
|
+
completedAt?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface LoopRunRecord {
|
|
19
|
+
loopId: string;
|
|
20
|
+
status: string;
|
|
21
|
+
lifecycleState: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
budget: {
|
|
25
|
+
maxUsd: number;
|
|
26
|
+
softLimitUsd: number;
|
|
27
|
+
maxIterations: number;
|
|
28
|
+
maxTokens: number;
|
|
29
|
+
};
|
|
30
|
+
cost: {
|
|
31
|
+
actualUsd: number;
|
|
32
|
+
tokensIn: number;
|
|
33
|
+
tokensOut: number;
|
|
34
|
+
avoidedUsd?: number;
|
|
35
|
+
};
|
|
36
|
+
attempts: LoopAttemptRecord[];
|
|
37
|
+
task: {
|
|
38
|
+
title: string;
|
|
39
|
+
objective: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export declare function readLoopRecordsFromFile(file: string): Promise<LoopRunRecord[]>;
|
|
43
|
+
export declare function readLatestLoopRecordFromFile(file: string): Promise<LoopRunRecord | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Reads all loop records from the given directory (default: ~/.martin/runs/).
|
|
46
|
+
* Returns an empty array if the directory doesn't exist or has no records.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readAllLoopRecords(runsDir?: string): Promise<LoopRunRecord[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Returns the most recently updated loop record, or null if none exist.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readLatestLoopRecord(runsDir?: string): Promise<LoopRunRecord | null>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads completed loop records from ~/.martin/runs/ for analysis.
|
|
3
|
+
* Used by the Trust Calibration Engine and other offline analytics.
|
|
4
|
+
*
|
|
5
|
+
* Supports both storage layouts:
|
|
6
|
+
* - legacy root JSONL files: <runsRoot>/*.jsonl
|
|
7
|
+
* - canonical run trees: <runsRoot>/<loopId>/loop-record.json
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { extname, join } from "node:path";
|
|
12
|
+
export async function readLoopRecordsFromFile(file) {
|
|
13
|
+
const text = await readFile(file, "utf8");
|
|
14
|
+
const extension = extname(file).toLowerCase();
|
|
15
|
+
if (extension === ".jsonl") {
|
|
16
|
+
return text
|
|
17
|
+
.split(/\r?\n/u)
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((line) => JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
const parsed = JSON.parse(text);
|
|
23
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
24
|
+
}
|
|
25
|
+
export async function readLatestLoopRecordFromFile(file) {
|
|
26
|
+
const records = await readLoopRecordsFromFile(file);
|
|
27
|
+
if (records.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
return records.reduce((latest, record) => {
|
|
30
|
+
const currentTimestamp = new Date(record.updatedAt ?? record.createdAt).getTime();
|
|
31
|
+
const latestTimestamp = new Date(latest.updatedAt ?? latest.createdAt).getTime();
|
|
32
|
+
return currentTimestamp > latestTimestamp ? record : latest;
|
|
33
|
+
}, records[0]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads all loop records from the given directory (default: ~/.martin/runs/).
|
|
37
|
+
* Returns an empty array if the directory doesn't exist or has no records.
|
|
38
|
+
*/
|
|
39
|
+
export async function readAllLoopRecords(runsDir) {
|
|
40
|
+
const dir = runsDir ?? join(homedir(), ".martin", "runs");
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const records = [];
|
|
49
|
+
const jsonlFiles = entries
|
|
50
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
51
|
+
.map((entry) => entry.name);
|
|
52
|
+
for (const file of jsonlFiles) {
|
|
53
|
+
try {
|
|
54
|
+
records.push(...(await readLoopRecordsFromFile(join(dir, file))));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// skip malformed files or lines
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const runDirectories = entries.filter((entry) => entry.isDirectory());
|
|
61
|
+
for (const entry of runDirectories) {
|
|
62
|
+
try {
|
|
63
|
+
records.push(...(await readLoopRecordsFromFile(join(dir, entry.name, "loop-record.json"))));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// skip missing or malformed canonical records
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return records;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns the most recently updated loop record, or null if none exist.
|
|
73
|
+
*/
|
|
74
|
+
export async function readLatestLoopRecord(runsDir) {
|
|
75
|
+
const records = await readAllLoopRecords(runsDir);
|
|
76
|
+
if (records.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
return records.reduce((latest, r) => {
|
|
79
|
+
const a = new Date(r.updatedAt ?? r.createdAt).getTime();
|
|
80
|
+
const b = new Date(latest.updatedAt ?? latest.createdAt).getTime();
|
|
81
|
+
return a > b ? r : latest;
|
|
82
|
+
}, records[0]);
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=runs-reader.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LoopBudget, LoopTask, MachineState } from "../../contracts/index.js";
|
|
1
|
+
import type { LoopBudget, LoopRecord, LoopTask, MachineState } from "../../contracts/index.js";
|
|
2
2
|
import { type LedgerEvent } from "./ledger.js";
|
|
3
3
|
export interface RunContract {
|
|
4
4
|
runId: string;
|
|
@@ -53,6 +53,11 @@ export interface RunStore {
|
|
|
53
53
|
* Write artifacts for a completed attempt to artifacts/attempt-<n>/.
|
|
54
54
|
*/
|
|
55
55
|
writeAttemptArtifacts(runId: string, attemptIndex: number, artifacts: AttemptArtifacts): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Persist the latest canonical loop record snapshot when the caller has one.
|
|
58
|
+
* Optional to avoid breaking custom RunStore implementations.
|
|
59
|
+
*/
|
|
60
|
+
writeLoopRecord?(runId: string, loop: LoopRecord): Promise<void>;
|
|
56
61
|
}
|
|
57
62
|
export declare function resolveRunsRoot(env?: NodeJS.ProcessEnv): string;
|
|
58
63
|
export declare function runDir(runsRoot: string, runId: string): string;
|
|
@@ -74,6 +74,11 @@ export function createFileRunStore(options = {}) {
|
|
|
74
74
|
if (artifacts.rollbackOutcome !== undefined) {
|
|
75
75
|
await writeJsonFile(join(dir, "rollback-outcome.json"), artifacts.rollbackOutcome);
|
|
76
76
|
}
|
|
77
|
+
},
|
|
78
|
+
async writeLoopRecord(runId, loop) {
|
|
79
|
+
const dir = runDir(runsRoot, runId);
|
|
80
|
+
await mkdir(dir, { recursive: true });
|
|
81
|
+
await writeJsonFile(join(dir, "loop-record.json"), loop);
|
|
77
82
|
}
|
|
78
83
|
};
|
|
79
84
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-touch-quota.ts — SLICE-07
|
|
3
|
+
*
|
|
4
|
+
* File-touch quota per attempt.
|
|
5
|
+
*
|
|
6
|
+
* A single attempt is bounded in how many files it may modify. The quota is
|
|
7
|
+
* derived from the objective complexity heuristic unless an explicit override
|
|
8
|
+
* is provided in the policy.
|
|
9
|
+
*
|
|
10
|
+
* Enforcement happens at the patch-application layer (AFTER diff generation,
|
|
11
|
+
* BEFORE any file is written) — not at the prompt layer.
|
|
12
|
+
*/
|
|
13
|
+
export interface FileTouchQuotaResult {
|
|
14
|
+
/** Whether the patch is allowed to proceed. */
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
/** Number of files the patch touched. */
|
|
17
|
+
touchedCount: number;
|
|
18
|
+
/** The active quota limit. */
|
|
19
|
+
quota: number;
|
|
20
|
+
/** Populated when allowed:false. */
|
|
21
|
+
reasonCode?: "file_touch_quota_exceeded";
|
|
22
|
+
/** Human-readable explanation. */
|
|
23
|
+
reason?: string;
|
|
24
|
+
/** true when quota was exceeded but an explicit override allowed it anyway. */
|
|
25
|
+
overridden?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface ResolveFileTouchQuotaInput {
|
|
28
|
+
objective: string;
|
|
29
|
+
/** When set, ignores the heuristic and uses this value directly. */
|
|
30
|
+
explicitOverride?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface CheckFileTouchQuotaInput {
|
|
33
|
+
/** List of file paths touched by the patch (deduped). */
|
|
34
|
+
touchedFiles: string[];
|
|
35
|
+
/** The resolved quota for this attempt. */
|
|
36
|
+
quota: number;
|
|
37
|
+
/** When true, quota enforcement is bypassed (override path). */
|
|
38
|
+
override?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the file-touch quota for an attempt.
|
|
42
|
+
*
|
|
43
|
+
* Default heuristics:
|
|
44
|
+
* - explicit override → that value
|
|
45
|
+
* - objective contains multi-file keywords → 10
|
|
46
|
+
* - otherwise → 3
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveFileTouchQuota(input: ResolveFileTouchQuotaInput): number;
|
|
49
|
+
/**
|
|
50
|
+
* Checks whether a patch's file touch count is within the quota.
|
|
51
|
+
*
|
|
52
|
+
* Returns allowed:true when within quota OR when override:true.
|
|
53
|
+
* Returns allowed:false with reasonCode "file_touch_quota_exceeded" otherwise.
|
|
54
|
+
*/
|
|
55
|
+
export declare function checkFileTouchQuota(input: CheckFileTouchQuotaInput): FileTouchQuotaResult;
|
|
56
|
+
/**
|
|
57
|
+
* Extracts a deduplicated list of b-side file paths from a unified diff.
|
|
58
|
+
* Only parses the `diff --git` header lines for efficiency.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseTouchedFilesFromDiff(diff: string): string[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-touch-quota.ts — SLICE-07
|
|
3
|
+
*
|
|
4
|
+
* File-touch quota per attempt.
|
|
5
|
+
*
|
|
6
|
+
* A single attempt is bounded in how many files it may modify. The quota is
|
|
7
|
+
* derived from the objective complexity heuristic unless an explicit override
|
|
8
|
+
* is provided in the policy.
|
|
9
|
+
*
|
|
10
|
+
* Enforcement happens at the patch-application layer (AFTER diff generation,
|
|
11
|
+
* BEFORE any file is written) — not at the prompt layer.
|
|
12
|
+
*/
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Public API
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the file-touch quota for an attempt.
|
|
18
|
+
*
|
|
19
|
+
* Default heuristics:
|
|
20
|
+
* - explicit override → that value
|
|
21
|
+
* - objective contains multi-file keywords → 10
|
|
22
|
+
* - otherwise → 3
|
|
23
|
+
*/
|
|
24
|
+
export function resolveFileTouchQuota(input) {
|
|
25
|
+
if (input.explicitOverride !== undefined && input.explicitOverride > 0) {
|
|
26
|
+
return input.explicitOverride;
|
|
27
|
+
}
|
|
28
|
+
const lower = input.objective.toLowerCase();
|
|
29
|
+
const MULTI_FILE_KEYWORDS = [
|
|
30
|
+
"all",
|
|
31
|
+
"across",
|
|
32
|
+
"refactor",
|
|
33
|
+
"migration",
|
|
34
|
+
"migrate",
|
|
35
|
+
"rename",
|
|
36
|
+
"move",
|
|
37
|
+
"reorganize",
|
|
38
|
+
"restructure",
|
|
39
|
+
"every",
|
|
40
|
+
"each",
|
|
41
|
+
"bulk",
|
|
42
|
+
"batch",
|
|
43
|
+
"update all",
|
|
44
|
+
"fix all"
|
|
45
|
+
];
|
|
46
|
+
const isMultiFile = MULTI_FILE_KEYWORDS.some((kw) => lower.includes(kw));
|
|
47
|
+
if (isMultiFile)
|
|
48
|
+
return 10;
|
|
49
|
+
// Short single-symbol objectives default to 3
|
|
50
|
+
return Math.max(1, 3);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Checks whether a patch's file touch count is within the quota.
|
|
54
|
+
*
|
|
55
|
+
* Returns allowed:true when within quota OR when override:true.
|
|
56
|
+
* Returns allowed:false with reasonCode "file_touch_quota_exceeded" otherwise.
|
|
57
|
+
*/
|
|
58
|
+
export function checkFileTouchQuota(input) {
|
|
59
|
+
const touchedCount = input.touchedFiles.length;
|
|
60
|
+
const withinQuota = touchedCount <= input.quota;
|
|
61
|
+
if (withinQuota) {
|
|
62
|
+
return {
|
|
63
|
+
allowed: true,
|
|
64
|
+
touchedCount,
|
|
65
|
+
quota: input.quota
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (input.override) {
|
|
69
|
+
return {
|
|
70
|
+
allowed: true,
|
|
71
|
+
touchedCount,
|
|
72
|
+
quota: input.quota,
|
|
73
|
+
overridden: true,
|
|
74
|
+
reason: `File touch quota (${input.quota}) exceeded by ${touchedCount} files — ` +
|
|
75
|
+
`allowed via explicit override`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
allowed: false,
|
|
80
|
+
touchedCount,
|
|
81
|
+
quota: input.quota,
|
|
82
|
+
reasonCode: "file_touch_quota_exceeded",
|
|
83
|
+
reason: `Patch touches ${touchedCount} files but the quota for this attempt is ${input.quota}. ` +
|
|
84
|
+
`Set fileTouchQuota in the policy file or pass --override-file-quota to bypass.`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Diff parser
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
/**
|
|
91
|
+
* Extracts a deduplicated list of b-side file paths from a unified diff.
|
|
92
|
+
* Only parses the `diff --git` header lines for efficiency.
|
|
93
|
+
*/
|
|
94
|
+
export function parseTouchedFilesFromDiff(diff) {
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for (const line of diff.split("\n")) {
|
|
97
|
+
// Match: diff --git a/<path> b/<path>
|
|
98
|
+
const match = line.match(/^diff --git a\/.+? b\/(.+)$/u);
|
|
99
|
+
if (match?.[1]) {
|
|
100
|
+
seen.add(match[1]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...seen];
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=file-touch-quota.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* policy-loader.ts — SLICE-15
|
|
3
|
+
*
|
|
4
|
+
* Loads and merges policy files in order:
|
|
5
|
+
* built-in defaults → home dir → repo root → CLI flag overrides
|
|
6
|
+
*
|
|
7
|
+
* Supports .json and a simple subset of .yaml (scalar values only).
|
|
8
|
+
* No external deps — uses only node:fs and node:path.
|
|
9
|
+
*/
|
|
10
|
+
import { type PolicyFile, type ResolvedPolicy } from "./policy-schema.js";
|
|
11
|
+
export declare function parseSimpleYaml(src: string): Record<string, unknown>;
|
|
12
|
+
export declare function findPolicyFile(dir: string): string | null;
|
|
13
|
+
export declare function loadPolicyFile(filePath: string): {
|
|
14
|
+
policy: PolicyFile;
|
|
15
|
+
errors: string[];
|
|
16
|
+
};
|
|
17
|
+
export interface PolicyLoadOptions {
|
|
18
|
+
/** Repo root directory. Defaults to process.cwd(). */
|
|
19
|
+
cwd?: string;
|
|
20
|
+
/** Home directory. Defaults to process.env.HOME / USERPROFILE. */
|
|
21
|
+
homeDir?: string;
|
|
22
|
+
/** CLI flag overrides — these always win. */
|
|
23
|
+
cliOverrides?: PolicyFile;
|
|
24
|
+
}
|
|
25
|
+
export interface PolicyLoadResult {
|
|
26
|
+
resolved: ResolvedPolicy;
|
|
27
|
+
sources: string[];
|
|
28
|
+
errors: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function loadPolicy(options?: PolicyLoadOptions): PolicyLoadResult;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* policy-loader.ts — SLICE-15
|
|
3
|
+
*
|
|
4
|
+
* Loads and merges policy files in order:
|
|
5
|
+
* built-in defaults → home dir → repo root → CLI flag overrides
|
|
6
|
+
*
|
|
7
|
+
* Supports .json and a simple subset of .yaml (scalar values only).
|
|
8
|
+
* No external deps — uses only node:fs and node:path.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { BUILTIN_DEFAULTS, mergePolicies, validatePolicyFile } from "./policy-schema.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Simple YAML parser (scalar values only — handles martin.policy.yaml)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
export function parseSimpleYaml(src) {
|
|
17
|
+
const result = {};
|
|
18
|
+
let currentKey = null;
|
|
19
|
+
let currentObj = null;
|
|
20
|
+
for (const rawLine of src.split("\n")) {
|
|
21
|
+
const line = rawLine.split("#")[0] ?? ""; // strip comments
|
|
22
|
+
if (!line.trim())
|
|
23
|
+
continue;
|
|
24
|
+
// Detect indented key-value (nested object)
|
|
25
|
+
const nestedMatch = line.match(/^ (\w[\w.]*)\s*:\s*(.*)$/);
|
|
26
|
+
const topMatch = line.match(/^(\w[\w.]*)\s*:\s*(.*)$/);
|
|
27
|
+
if (nestedMatch && currentKey) {
|
|
28
|
+
const nestedKey = nestedMatch[1];
|
|
29
|
+
if (!nestedKey)
|
|
30
|
+
continue;
|
|
31
|
+
if (!currentObj) {
|
|
32
|
+
currentObj = {};
|
|
33
|
+
result[currentKey] = currentObj;
|
|
34
|
+
}
|
|
35
|
+
const val = parseYamlValue((nestedMatch[2] ?? "").trim());
|
|
36
|
+
if (val !== undefined)
|
|
37
|
+
currentObj[nestedKey] = val;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (topMatch) {
|
|
41
|
+
const key = topMatch[1];
|
|
42
|
+
const rest = topMatch[2] ?? "";
|
|
43
|
+
if (!key)
|
|
44
|
+
continue;
|
|
45
|
+
const trimmed = rest.trim();
|
|
46
|
+
if (trimmed === "" || trimmed === "|" || trimmed === ">") {
|
|
47
|
+
// Nested block follows
|
|
48
|
+
currentKey = key;
|
|
49
|
+
currentObj = null;
|
|
50
|
+
}
|
|
51
|
+
else if (trimmed.startsWith("-")) {
|
|
52
|
+
// Array shorthand on same line — not supported; reset
|
|
53
|
+
currentKey = key;
|
|
54
|
+
currentObj = null;
|
|
55
|
+
result[key] = [];
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
currentKey = key;
|
|
59
|
+
currentObj = null;
|
|
60
|
+
const val = parseYamlValue(trimmed);
|
|
61
|
+
if (val !== undefined)
|
|
62
|
+
result[key] = val;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Array item
|
|
67
|
+
const arrMatch = line.match(/^ -\s+(.+)$/);
|
|
68
|
+
if (arrMatch && currentKey) {
|
|
69
|
+
if (!Array.isArray(result[currentKey]))
|
|
70
|
+
result[currentKey] = [];
|
|
71
|
+
result[currentKey].push(parseYamlValue((arrMatch[1] ?? "").trim()));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function parseYamlValue(s) {
|
|
77
|
+
if (s === "true")
|
|
78
|
+
return true;
|
|
79
|
+
if (s === "false")
|
|
80
|
+
return false;
|
|
81
|
+
if (s === "null" || s === "~")
|
|
82
|
+
return null;
|
|
83
|
+
if (/^-?\d+(\.\d+)?$/.test(s))
|
|
84
|
+
return Number(s);
|
|
85
|
+
// Strip surrounding quotes
|
|
86
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
87
|
+
return s.slice(1, -1);
|
|
88
|
+
}
|
|
89
|
+
if (s.startsWith("$ENV{") && s.endsWith("}")) {
|
|
90
|
+
return process.env[s.slice(5, -1)] ?? "";
|
|
91
|
+
}
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// File loader
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
const POLICY_FILENAMES = ["martin.policy.json", "martin.policy.yaml", "martin.policy.yml"];
|
|
98
|
+
export function findPolicyFile(dir) {
|
|
99
|
+
for (const name of POLICY_FILENAMES) {
|
|
100
|
+
const p = join(dir, name);
|
|
101
|
+
if (existsSync(p))
|
|
102
|
+
return p;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
export function loadPolicyFile(filePath) {
|
|
107
|
+
const src = readFileSync(filePath, "utf-8");
|
|
108
|
+
let raw;
|
|
109
|
+
if (filePath.endsWith(".json")) {
|
|
110
|
+
try {
|
|
111
|
+
raw = JSON.parse(src);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return { policy: {}, errors: [`JSON parse error: ${String(e)}`] };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
raw = parseSimpleYaml(src);
|
|
119
|
+
}
|
|
120
|
+
const errors = validatePolicyFile(raw);
|
|
121
|
+
return { policy: errors.length ? {} : raw, errors };
|
|
122
|
+
}
|
|
123
|
+
export function loadPolicy(options) {
|
|
124
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
125
|
+
const home = options?.homeDir ?? (process.env.HOME ?? process.env.USERPROFILE ?? "");
|
|
126
|
+
let resolved = { ...BUILTIN_DEFAULTS };
|
|
127
|
+
const sources = ["built-in defaults"];
|
|
128
|
+
const errors = [];
|
|
129
|
+
// Layer 1: home dir
|
|
130
|
+
if (home) {
|
|
131
|
+
const homeFile = findPolicyFile(home);
|
|
132
|
+
if (homeFile) {
|
|
133
|
+
const { policy, errors: errs } = loadPolicyFile(homeFile);
|
|
134
|
+
if (errs.length)
|
|
135
|
+
errors.push(...errs.map(e => `${homeFile}: ${e}`));
|
|
136
|
+
else {
|
|
137
|
+
resolved = mergePolicies(resolved, policy);
|
|
138
|
+
sources.push(homeFile);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Layer 2: repo root (walk up from cwd until we find a policy file or hit root)
|
|
143
|
+
let dir = resolve(cwd);
|
|
144
|
+
let walked = 0;
|
|
145
|
+
while (walked < 10) {
|
|
146
|
+
const found = findPolicyFile(dir);
|
|
147
|
+
if (found) {
|
|
148
|
+
const { policy, errors: errs } = loadPolicyFile(found);
|
|
149
|
+
if (errs.length)
|
|
150
|
+
errors.push(...errs.map(e => `${found}: ${e}`));
|
|
151
|
+
else {
|
|
152
|
+
resolved = mergePolicies(resolved, policy);
|
|
153
|
+
sources.push(found);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
const parent = resolve(dir, "..");
|
|
158
|
+
if (parent === dir)
|
|
159
|
+
break;
|
|
160
|
+
dir = parent;
|
|
161
|
+
walked++;
|
|
162
|
+
}
|
|
163
|
+
// Layer 3: CLI flag overrides
|
|
164
|
+
if (options?.cliOverrides) {
|
|
165
|
+
resolved = mergePolicies(resolved, options.cliOverrides);
|
|
166
|
+
sources.push("CLI flags");
|
|
167
|
+
}
|
|
168
|
+
return { resolved, sources, errors };
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=policy-loader.js.map
|