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,30 @@
|
|
|
1
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
2
|
+
// ─── Detection ────────────────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* T14 — Efficiency Anomaly Detection
|
|
5
|
+
*
|
|
6
|
+
* Detection logic:
|
|
7
|
+
* - IF baseline.samples < 20 → DO NOT FIRE (cold-start guard)
|
|
8
|
+
* - IF actualCost < (baseline.p25 * 0.75) → fire T14 anomaly
|
|
9
|
+
* - T14 is ALWAYS warn-only (action: "logged") — never hard-rejects a run
|
|
10
|
+
*/
|
|
11
|
+
export function checkEfficiencyAnomaly(input) {
|
|
12
|
+
// Cold-start guard: never fire before 20 samples
|
|
13
|
+
if (input.baseline.samples < 20) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const threshold = input.baseline.p25CostUsd * 0.75;
|
|
17
|
+
if (input.actualCostUsd >= threshold) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const deviationPct = ((input.baseline.p25CostUsd - input.actualCostUsd) / input.baseline.p25CostUsd) * 100;
|
|
21
|
+
return {
|
|
22
|
+
trapId: "T14",
|
|
23
|
+
runId: input.runId,
|
|
24
|
+
actualCostUsd: input.actualCostUsd,
|
|
25
|
+
p25BaselineCostUsd: input.baseline.p25CostUsd,
|
|
26
|
+
deviationPct,
|
|
27
|
+
action: "logged"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=efficiency-sentinel.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stagnation detection — monotonic progress guard.
|
|
3
|
+
*
|
|
4
|
+
* Detects when an agent loop is spinning without making forward progress:
|
|
5
|
+
* the last N attempts all produced the same failure classification and
|
|
6
|
+
* the same intervention response. When stagnation is detected the caller
|
|
7
|
+
* should exit the loop with lifecycleState "stagnation_detected".
|
|
8
|
+
*
|
|
9
|
+
* This prevents the "$500 infinite loop" scenario where Claude Code
|
|
10
|
+
* enters a retry cycle for hours without human awareness.
|
|
11
|
+
*/
|
|
12
|
+
import type { LoopAttempt } from "../../contracts/index.js";
|
|
13
|
+
export interface StagnationInput {
|
|
14
|
+
readonly attempts: LoopAttempt[];
|
|
15
|
+
/** Number of consecutive attempts to inspect. Defaults to 3. */
|
|
16
|
+
readonly windowSize?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface StagnationResult {
|
|
19
|
+
readonly stagnant: boolean;
|
|
20
|
+
readonly reason?: string;
|
|
21
|
+
readonly windowSize: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Return `stagnant: true` when the last `windowSize` attempts all share:
|
|
25
|
+
* - the same `adapterId` (same adapter/model — not a recovery escalation)
|
|
26
|
+
* - the same `failureClass` value (including both-undefined)
|
|
27
|
+
* - the same `intervention` value (including both-undefined)
|
|
28
|
+
*
|
|
29
|
+
* Requiring the same adapterId prevents false positives when the runtime
|
|
30
|
+
* is legitimately cycling through different recovery paths (which leads to
|
|
31
|
+
* `diminishing_returns`, not stagnation).
|
|
32
|
+
*
|
|
33
|
+
* When fewer attempts than `windowSize` exist, always returns `stagnant: false`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectStagnation(input: StagnationInput): StagnationResult;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// detectStagnation
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
/**
|
|
5
|
+
* Return `stagnant: true` when the last `windowSize` attempts all share:
|
|
6
|
+
* - the same `adapterId` (same adapter/model — not a recovery escalation)
|
|
7
|
+
* - the same `failureClass` value (including both-undefined)
|
|
8
|
+
* - the same `intervention` value (including both-undefined)
|
|
9
|
+
*
|
|
10
|
+
* Requiring the same adapterId prevents false positives when the runtime
|
|
11
|
+
* is legitimately cycling through different recovery paths (which leads to
|
|
12
|
+
* `diminishing_returns`, not stagnation).
|
|
13
|
+
*
|
|
14
|
+
* When fewer attempts than `windowSize` exist, always returns `stagnant: false`.
|
|
15
|
+
*/
|
|
16
|
+
export function detectStagnation(input) {
|
|
17
|
+
const windowSize = input.windowSize ?? 3;
|
|
18
|
+
if (input.attempts.length < windowSize) {
|
|
19
|
+
return { stagnant: false, windowSize };
|
|
20
|
+
}
|
|
21
|
+
// Take the last `windowSize` attempts
|
|
22
|
+
const window = input.attempts.slice(-windowSize);
|
|
23
|
+
const first = window[0];
|
|
24
|
+
const referenceAdapter = first.adapterId;
|
|
25
|
+
const referenceClass = first.failureClass;
|
|
26
|
+
const referenceIntervention = first.intervention;
|
|
27
|
+
for (const attempt of window) {
|
|
28
|
+
// Different adapter = legitimate recovery escalation, not stagnation
|
|
29
|
+
if (attempt.adapterId !== referenceAdapter) {
|
|
30
|
+
return { stagnant: false, windowSize };
|
|
31
|
+
}
|
|
32
|
+
if (attempt.failureClass !== referenceClass) {
|
|
33
|
+
return { stagnant: false, windowSize };
|
|
34
|
+
}
|
|
35
|
+
if (attempt.intervention !== referenceIntervention) {
|
|
36
|
+
return { stagnant: false, windowSize };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const classLabel = referenceClass ?? "(none)";
|
|
40
|
+
const interventionLabel = referenceIntervention ?? "(none)";
|
|
41
|
+
const reason = `Stagnation detected: last ${windowSize} attempts on adapter "${referenceAdapter}" all produced ` +
|
|
42
|
+
`failureClass="${classLabel}" with intervention="${interventionLabel}". ` +
|
|
43
|
+
`Loop is not making forward progress.`;
|
|
44
|
+
return { stagnant: true, reason, windowSize };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=progress-guard.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* siem-emitter.ts — SLICE-16
|
|
3
|
+
*
|
|
4
|
+
* SIEM integration: emits Martin Loop gate events in OCSF or CEF format.
|
|
5
|
+
*
|
|
6
|
+
* Events are batched (up to 50 events or 5s) then POSTed to the configured
|
|
7
|
+
* endpoint. On transport failure the events are written to
|
|
8
|
+
* ~/.martin/siem-queue/ for later retry via `martin siem drain`.
|
|
9
|
+
*
|
|
10
|
+
* Configuration (policy file or env):
|
|
11
|
+
* siem.endpoint — HTTP POST target (required to enable)
|
|
12
|
+
* siem.format — "ocsf" | "cef" (default: "ocsf")
|
|
13
|
+
* siem.apiKey — Bearer token for endpoint auth (optional)
|
|
14
|
+
*/
|
|
15
|
+
export type SiemEventType = "attempt_started" | "attempt_completed" | "leash_blocked" | "policy_violated" | "budget_exceeded" | "patch_accepted" | "patch_rejected" | "rollback_triggered";
|
|
16
|
+
export type SiemFormat = "ocsf" | "cef";
|
|
17
|
+
export interface SiemEventInput {
|
|
18
|
+
type: SiemEventType;
|
|
19
|
+
loopId: string;
|
|
20
|
+
attemptId?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
severity?: "info" | "low" | "medium" | "high" | "critical";
|
|
23
|
+
metadata?: Record<string, string | number | boolean>;
|
|
24
|
+
}
|
|
25
|
+
export declare function formatOcsf(event: SiemEventInput, timestamp: number): object;
|
|
26
|
+
export declare function formatCef(event: SiemEventInput, timestamp: number): string;
|
|
27
|
+
export interface SiemConfig {
|
|
28
|
+
endpoint: string;
|
|
29
|
+
format: SiemFormat;
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
flushIntervalMs?: number;
|
|
32
|
+
maxBatchSize?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class SiemEmitter {
|
|
35
|
+
private readonly config;
|
|
36
|
+
private queue;
|
|
37
|
+
private flushTimer;
|
|
38
|
+
private fetchFn;
|
|
39
|
+
constructor(config: SiemConfig, fetchFn?: typeof fetch);
|
|
40
|
+
emit(event: SiemEventInput): void;
|
|
41
|
+
flush(): Promise<{
|
|
42
|
+
sent: number;
|
|
43
|
+
queued: number;
|
|
44
|
+
}>;
|
|
45
|
+
/** Returns all pending (unflushed) events. Used in tests. */
|
|
46
|
+
pendingCount(): number;
|
|
47
|
+
destroy(): void;
|
|
48
|
+
}
|
|
49
|
+
export declare function createSiemEmitter(config: Partial<SiemConfig> | undefined, fetchFn?: typeof fetch): SiemEmitter | null;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* siem-emitter.ts — SLICE-16
|
|
3
|
+
*
|
|
4
|
+
* SIEM integration: emits Martin Loop gate events in OCSF or CEF format.
|
|
5
|
+
*
|
|
6
|
+
* Events are batched (up to 50 events or 5s) then POSTed to the configured
|
|
7
|
+
* endpoint. On transport failure the events are written to
|
|
8
|
+
* ~/.martin/siem-queue/ for later retry via `martin siem drain`.
|
|
9
|
+
*
|
|
10
|
+
* Configuration (policy file or env):
|
|
11
|
+
* siem.endpoint — HTTP POST target (required to enable)
|
|
12
|
+
* siem.format — "ocsf" | "cef" (default: "ocsf")
|
|
13
|
+
* siem.apiKey — Bearer token for endpoint auth (optional)
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// OCSF formatter
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const OCSF_SEVERITY_MAP = {
|
|
20
|
+
info: 1, low: 2, medium: 4, high: 6, critical: 8
|
|
21
|
+
};
|
|
22
|
+
const OCSF_ACTIVITY_MAP = {
|
|
23
|
+
attempt_started: 1,
|
|
24
|
+
attempt_completed: 2,
|
|
25
|
+
leash_blocked: 3,
|
|
26
|
+
policy_violated: 4,
|
|
27
|
+
budget_exceeded: 5,
|
|
28
|
+
patch_accepted: 6,
|
|
29
|
+
patch_rejected: 7,
|
|
30
|
+
rollback_triggered: 8
|
|
31
|
+
};
|
|
32
|
+
export function formatOcsf(event, timestamp) {
|
|
33
|
+
return {
|
|
34
|
+
class_uid: 300101,
|
|
35
|
+
category_uid: 3,
|
|
36
|
+
type_uid: 300101,
|
|
37
|
+
time: timestamp,
|
|
38
|
+
severity_id: OCSF_SEVERITY_MAP[event.severity ?? "info"] ?? 1,
|
|
39
|
+
actor: { user: { name: "martin-loop" } },
|
|
40
|
+
metadata: {
|
|
41
|
+
version: "1.0.0",
|
|
42
|
+
product: { name: "Martin Loop" },
|
|
43
|
+
uid: createHash("sha256")
|
|
44
|
+
.update(`${event.loopId}:${event.attemptId ?? ""}:${timestamp}`)
|
|
45
|
+
.digest("hex")
|
|
46
|
+
.slice(0, 16)
|
|
47
|
+
},
|
|
48
|
+
activity_id: OCSF_ACTIVITY_MAP[event.type] ?? 0,
|
|
49
|
+
message: event.message,
|
|
50
|
+
unmapped: {
|
|
51
|
+
event_type: event.type,
|
|
52
|
+
loop_id: event.loopId,
|
|
53
|
+
attempt_id: event.attemptId ?? "",
|
|
54
|
+
...(event.metadata ?? {})
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// CEF formatter
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
const CEF_SEVERITY_MAP = {
|
|
62
|
+
info: 0, low: 2, medium: 5, high: 7, critical: 10
|
|
63
|
+
};
|
|
64
|
+
function cefEscape(val) {
|
|
65
|
+
return val.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/=/g, "\\=");
|
|
66
|
+
}
|
|
67
|
+
export function formatCef(event, timestamp) {
|
|
68
|
+
const sev = CEF_SEVERITY_MAP[event.severity ?? "info"] ?? 0;
|
|
69
|
+
const ext = [
|
|
70
|
+
`rt=${timestamp}`,
|
|
71
|
+
`loopId=${cefEscape(event.loopId)}`,
|
|
72
|
+
`attemptId=${cefEscape(event.attemptId ?? "")}`,
|
|
73
|
+
`msg=${cefEscape(event.message)}`
|
|
74
|
+
];
|
|
75
|
+
if (event.metadata) {
|
|
76
|
+
for (const [k, v] of Object.entries(event.metadata)) {
|
|
77
|
+
ext.push(`${cefEscape(String(k))}=${cefEscape(String(v))}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return `CEF:0|MartinLoop|martin-loop|1.0|${event.type}|${event.message}|${sev}| ${ext.join(" ")}`;
|
|
81
|
+
}
|
|
82
|
+
export class SiemEmitter {
|
|
83
|
+
config;
|
|
84
|
+
queue = [];
|
|
85
|
+
flushTimer = null;
|
|
86
|
+
fetchFn;
|
|
87
|
+
constructor(config, fetchFn) {
|
|
88
|
+
this.config = {
|
|
89
|
+
endpoint: config.endpoint,
|
|
90
|
+
format: config.format,
|
|
91
|
+
apiKey: config.apiKey,
|
|
92
|
+
flushIntervalMs: config.flushIntervalMs ?? 5000,
|
|
93
|
+
maxBatchSize: config.maxBatchSize ?? 50
|
|
94
|
+
};
|
|
95
|
+
this.fetchFn = fetchFn ?? fetch;
|
|
96
|
+
}
|
|
97
|
+
emit(event) {
|
|
98
|
+
this.queue.push({ event, timestamp: Date.now() });
|
|
99
|
+
if (this.queue.length >= this.config.maxBatchSize) {
|
|
100
|
+
void this.flush();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (this.config.flushIntervalMs > 0 && !this.flushTimer) {
|
|
104
|
+
this.flushTimer = setTimeout(() => {
|
|
105
|
+
void this.flush();
|
|
106
|
+
}, this.config.flushIntervalMs);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async flush() {
|
|
110
|
+
if (this.flushTimer) {
|
|
111
|
+
clearTimeout(this.flushTimer);
|
|
112
|
+
this.flushTimer = null;
|
|
113
|
+
}
|
|
114
|
+
const batch = this.queue.splice(0, this.config.maxBatchSize);
|
|
115
|
+
if (batch.length === 0)
|
|
116
|
+
return { sent: 0, queued: 0 };
|
|
117
|
+
const formatted = batch.map(({ event, timestamp }) => this.config.format === "cef"
|
|
118
|
+
? formatCef(event, timestamp)
|
|
119
|
+
: formatOcsf(event, timestamp));
|
|
120
|
+
const headers = { "Content-Type": "application/json" };
|
|
121
|
+
if (this.config.apiKey)
|
|
122
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
123
|
+
try {
|
|
124
|
+
const res = await this.fetchFn(this.config.endpoint, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers,
|
|
127
|
+
body: JSON.stringify({ events: formatted }),
|
|
128
|
+
signal: AbortSignal.timeout(10_000)
|
|
129
|
+
});
|
|
130
|
+
if (!res.ok)
|
|
131
|
+
throw new Error(`SIEM endpoint returned HTTP ${res.status}`);
|
|
132
|
+
return { sent: batch.length, queued: 0 };
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// On failure: keep events in front of queue for retry
|
|
136
|
+
this.queue.unshift(...batch);
|
|
137
|
+
return { sent: 0, queued: this.queue.length };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Returns all pending (unflushed) events. Used in tests. */
|
|
141
|
+
pendingCount() {
|
|
142
|
+
return this.queue.length;
|
|
143
|
+
}
|
|
144
|
+
destroy() {
|
|
145
|
+
if (this.flushTimer)
|
|
146
|
+
clearTimeout(this.flushTimer);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Factory — returns null when siem.endpoint is not configured
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
export function createSiemEmitter(config, fetchFn) {
|
|
153
|
+
if (!config?.endpoint)
|
|
154
|
+
return null;
|
|
155
|
+
return new SiemEmitter({ endpoint: config.endpoint, format: config.format ?? "ocsf", ...config }, fetchFn);
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=siem-emitter.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LoopAttempt, LoopTask } from "../../contracts/index.js";
|
|
2
|
+
export interface AttemptFocusInput {
|
|
3
|
+
task: Pick<LoopTask, "objective" | "acceptanceCriteria">;
|
|
4
|
+
attempts: LoopAttempt[];
|
|
5
|
+
remainingBudgetUsd: number;
|
|
6
|
+
}
|
|
7
|
+
export interface RetryEconomicsInput {
|
|
8
|
+
previousAttempts: LoopAttempt[];
|
|
9
|
+
projectedUsd: number;
|
|
10
|
+
remainingBudgetUsd: number;
|
|
11
|
+
remainingIterations: number;
|
|
12
|
+
novelRecoveryPath?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface RetryEconomicsDecision {
|
|
15
|
+
allowRetry: boolean;
|
|
16
|
+
reason?: string;
|
|
17
|
+
repeatedVerifierSignature?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function buildAttemptFocus(input: AttemptFocusInput): string;
|
|
20
|
+
export declare function evaluateRetryEconomics(input: RetryEconomicsInput): RetryEconomicsDecision;
|
|
21
|
+
export declare function detectRepeatedVerifierSignature(attempts: Pick<LoopAttempt, "verifierSummary">[], window?: number): string | undefined;
|
|
22
|
+
export declare function normalizeVerifierSignature(summary?: string): string | undefined;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export function buildAttemptFocus(input) {
|
|
2
|
+
const parts = [`Primary objective: ${input.task.objective}.`];
|
|
3
|
+
const acceptanceCriteria = input.task.acceptanceCriteria ?? [];
|
|
4
|
+
const lastAttempt = input.attempts.at(-1);
|
|
5
|
+
const repeatedSignature = detectRepeatedVerifierSignature(input.attempts);
|
|
6
|
+
if (acceptanceCriteria.length > 0) {
|
|
7
|
+
parts.push(`Acceptance criteria: ${acceptanceCriteria.join("; ")}.`);
|
|
8
|
+
}
|
|
9
|
+
if (lastAttempt?.failureClass) {
|
|
10
|
+
parts.push(`Last failure class: ${lastAttempt.failureClass}. Resolve that exact gap before widening scope.`);
|
|
11
|
+
}
|
|
12
|
+
if (lastAttempt?.summary) {
|
|
13
|
+
parts.push(`Do not repeat this failed pattern: ${truncateSentence(lastAttempt.summary, 160)}.`);
|
|
14
|
+
}
|
|
15
|
+
if (repeatedSignature) {
|
|
16
|
+
parts.push(`Repeated verifier signal: ${repeatedSignature}. Target the smallest change that clears this exact failure.`);
|
|
17
|
+
}
|
|
18
|
+
else if (lastAttempt?.verifierSummary) {
|
|
19
|
+
parts.push(`Verifier focus: ${normalizeVerifierSignature(lastAttempt.verifierSummary)}.`);
|
|
20
|
+
}
|
|
21
|
+
if (input.remainingBudgetUsd <= 2) {
|
|
22
|
+
parts.push("Budget is tight. Prefer one narrow, verifier-led fix over a broad rewrite.");
|
|
23
|
+
}
|
|
24
|
+
return parts.join(" ");
|
|
25
|
+
}
|
|
26
|
+
export function evaluateRetryEconomics(input) {
|
|
27
|
+
if (input.novelRecoveryPath) {
|
|
28
|
+
return { allowRetry: true };
|
|
29
|
+
}
|
|
30
|
+
const lastTwo = input.previousAttempts.slice(-2);
|
|
31
|
+
const repeatedFailureClass = lastTwo.length === 2 &&
|
|
32
|
+
lastTwo[0]?.failureClass &&
|
|
33
|
+
lastTwo[0].failureClass === lastTwo[1]?.failureClass;
|
|
34
|
+
const repeatedVerifierSignature = detectRepeatedVerifierSignature(input.previousAttempts);
|
|
35
|
+
const remainingAfterRetry = roundUsd(input.remainingBudgetUsd - input.projectedUsd);
|
|
36
|
+
const tightBudget = remainingAfterRetry < Math.max(input.projectedUsd * 0.5, 0.25);
|
|
37
|
+
if (repeatedFailureClass && repeatedVerifierSignature && tightBudget && input.remainingIterations <= 2) {
|
|
38
|
+
return {
|
|
39
|
+
allowRetry: false,
|
|
40
|
+
reason: `Repeated verifier signature "${repeatedVerifierSignature}" under tight budget. ` +
|
|
41
|
+
"Escalate instead of paying for another near-identical retry.",
|
|
42
|
+
repeatedVerifierSignature
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
allowRetry: true,
|
|
47
|
+
...(repeatedVerifierSignature ? { repeatedVerifierSignature } : {})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function detectRepeatedVerifierSignature(attempts, window = 2) {
|
|
51
|
+
const signatures = attempts
|
|
52
|
+
.map((attempt) => normalizeVerifierSignature(attempt.verifierSummary))
|
|
53
|
+
.filter((signature) => Boolean(signature));
|
|
54
|
+
if (signatures.length < window) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const recent = signatures.slice(-window);
|
|
58
|
+
const first = recent[0];
|
|
59
|
+
if (!first) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return recent.every((signature) => signature === first) ? first : undefined;
|
|
63
|
+
}
|
|
64
|
+
export function normalizeVerifierSignature(summary) {
|
|
65
|
+
if (!summary) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const normalized = summary
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/\s+/g, " ")
|
|
71
|
+
.replace(/\b\d+\b/g, "#")
|
|
72
|
+
.replace(/[^a-z0-9# :._-]/g, "")
|
|
73
|
+
.trim();
|
|
74
|
+
if (!normalized) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const firstClause = normalized.split(/(?:;|\.|,|\n)/u)[0]?.trim();
|
|
78
|
+
return firstClause ? truncateSentence(firstClause, 96) : undefined;
|
|
79
|
+
}
|
|
80
|
+
function truncateSentence(value, maxLength) {
|
|
81
|
+
if (value.length <= maxLength) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return `${value.slice(0, maxLength - 1).trimEnd()}...`;
|
|
85
|
+
}
|
|
86
|
+
function roundUsd(value) {
|
|
87
|
+
return Math.round(value * 100) / 100;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=attempt-brief.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diff-summary.ts
|
|
3
|
+
*
|
|
4
|
+
* Produces a one-line human-readable summary of a unified diff relative to the
|
|
5
|
+
* loop objective. Uses a cheap haiku-tier LLM call when an adapter is available;
|
|
6
|
+
* falls back to a deterministic heuristic when no adapter is configured or the
|
|
7
|
+
* model call fails.
|
|
8
|
+
*
|
|
9
|
+
* Quality gate: the generated one-liner MUST contain at least one concrete
|
|
10
|
+
* identifier (file name, function name, symbol) extracted from the diff itself.
|
|
11
|
+
* If the model output fails that gate the fallback is used instead.
|
|
12
|
+
*/
|
|
13
|
+
export interface DiffSummaryInput {
|
|
14
|
+
/** The raw unified diff text (output of git diff or similar). */
|
|
15
|
+
diff: string;
|
|
16
|
+
/** The original loop objective supplied by the user. */
|
|
17
|
+
objective: string;
|
|
18
|
+
}
|
|
19
|
+
export interface DiffSummary {
|
|
20
|
+
/** Single-line summary suitable for a commit message or PR description. */
|
|
21
|
+
oneLiner: string;
|
|
22
|
+
/** How the summary was produced. */
|
|
23
|
+
method: "model" | "heuristic";
|
|
24
|
+
/** Concrete identifier extracted from the diff that validates the summary. */
|
|
25
|
+
anchorIdentifier: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Summarize a unified diff in the context of a loop objective.
|
|
29
|
+
*
|
|
30
|
+
* @param input Diff text + objective string.
|
|
31
|
+
* @param callModelFn Optional async function that accepts a prompt string and
|
|
32
|
+
* returns a model response string. When omitted, or when
|
|
33
|
+
* the call throws, the deterministic heuristic is used.
|
|
34
|
+
*/
|
|
35
|
+
export declare function summarizeDiff(input: DiffSummaryInput, callModelFn?: (prompt: string) => Promise<string>): Promise<DiffSummary>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diff-summary.ts
|
|
3
|
+
*
|
|
4
|
+
* Produces a one-line human-readable summary of a unified diff relative to the
|
|
5
|
+
* loop objective. Uses a cheap haiku-tier LLM call when an adapter is available;
|
|
6
|
+
* falls back to a deterministic heuristic when no adapter is configured or the
|
|
7
|
+
* model call fails.
|
|
8
|
+
*
|
|
9
|
+
* Quality gate: the generated one-liner MUST contain at least one concrete
|
|
10
|
+
* identifier (file name, function name, symbol) extracted from the diff itself.
|
|
11
|
+
* If the model output fails that gate the fallback is used instead.
|
|
12
|
+
*/
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Public entry point
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Summarize a unified diff in the context of a loop objective.
|
|
18
|
+
*
|
|
19
|
+
* @param input Diff text + objective string.
|
|
20
|
+
* @param callModelFn Optional async function that accepts a prompt string and
|
|
21
|
+
* returns a model response string. When omitted, or when
|
|
22
|
+
* the call throws, the deterministic heuristic is used.
|
|
23
|
+
*/
|
|
24
|
+
export async function summarizeDiff(input, callModelFn) {
|
|
25
|
+
const anchor = extractAnchorIdentifier(input.diff);
|
|
26
|
+
if (callModelFn && input.diff.trim().length > 0) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await callModelFn(buildPrompt(input, anchor));
|
|
29
|
+
const cleaned = cleanModelOutput(raw);
|
|
30
|
+
if (isQualityGatePassing(cleaned, anchor)) {
|
|
31
|
+
return { oneLiner: cleaned, method: "model", anchorIdentifier: anchor };
|
|
32
|
+
}
|
|
33
|
+
// Model output failed quality gate — fall through to heuristic
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-fatal — fall through to heuristic
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
oneLiner: buildHeuristicSummary(input, anchor),
|
|
41
|
+
method: "heuristic",
|
|
42
|
+
anchorIdentifier: anchor
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Prompt construction
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function buildPrompt(input, anchor) {
|
|
49
|
+
// Keep the diff excerpt short — we only need enough for the model to name
|
|
50
|
+
// the primary change. Hard cap at 3000 chars to stay within haiku context.
|
|
51
|
+
const diffExcerpt = input.diff.length > 3000
|
|
52
|
+
? `${input.diff.slice(0, 2900)}\n... (truncated)`
|
|
53
|
+
: input.diff;
|
|
54
|
+
return [
|
|
55
|
+
"You are a precise technical writer generating a single-line git commit summary.",
|
|
56
|
+
"",
|
|
57
|
+
`Objective: ${input.objective}`,
|
|
58
|
+
"",
|
|
59
|
+
"Diff:",
|
|
60
|
+
"```",
|
|
61
|
+
diffExcerpt,
|
|
62
|
+
"```",
|
|
63
|
+
"",
|
|
64
|
+
`The summary MUST mention "${anchor}" or a closely related identifier.`,
|
|
65
|
+
"Respond with ONLY the one-line summary — no preamble, no punctuation at the end.",
|
|
66
|
+
"Keep it under 72 characters.",
|
|
67
|
+
"Format: <verb in imperative mood> <what changed> in <where>",
|
|
68
|
+
"Example: add retry guard to fetchWithTimeout in http-client.ts"
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Quality gate
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
/**
|
|
75
|
+
* Returns true when the model output contains the anchor identifier or a
|
|
76
|
+
* plausible stem of it (e.g. "retry" from "retryCount").
|
|
77
|
+
*/
|
|
78
|
+
function isQualityGatePassing(summary, anchor) {
|
|
79
|
+
if (summary.trim().length === 0)
|
|
80
|
+
return false;
|
|
81
|
+
if (summary.length > 120)
|
|
82
|
+
return false; // unreasonably long
|
|
83
|
+
const lowerSummary = summary.toLowerCase();
|
|
84
|
+
const lowerAnchor = anchor.toLowerCase();
|
|
85
|
+
// Direct match
|
|
86
|
+
if (lowerSummary.includes(lowerAnchor))
|
|
87
|
+
return true;
|
|
88
|
+
// Stem match: try the first 6+ chars of the anchor
|
|
89
|
+
const stem = lowerAnchor.slice(0, Math.max(6, Math.floor(lowerAnchor.length * 0.6)));
|
|
90
|
+
if (stem.length >= 4 && lowerSummary.includes(stem))
|
|
91
|
+
return true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Deterministic heuristic
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
/**
|
|
98
|
+
* Builds a summary without a model call by inspecting the diff structure.
|
|
99
|
+
* Always passes the quality gate because we embed the anchor directly.
|
|
100
|
+
*/
|
|
101
|
+
function buildHeuristicSummary(input, anchor) {
|
|
102
|
+
const stats = parseDiffStats(input.diff);
|
|
103
|
+
const verb = selectVerb(stats);
|
|
104
|
+
const filesChanged = stats.files.length;
|
|
105
|
+
if (filesChanged === 0) {
|
|
106
|
+
return `${verb} changes related to ${anchor}`;
|
|
107
|
+
}
|
|
108
|
+
if (filesChanged === 1) {
|
|
109
|
+
const file = stats.files[0] ?? "change";
|
|
110
|
+
const location = stripPath(file);
|
|
111
|
+
return `${verb} ${anchor} in ${location}`;
|
|
112
|
+
}
|
|
113
|
+
const primary = stripPath(stats.files[0] ?? "change");
|
|
114
|
+
return `${verb} ${anchor} across ${filesChanged} files (primary: ${primary})`;
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Anchor identifier extraction
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
/**
|
|
120
|
+
* Extracts the most informative concrete identifier from a unified diff.
|
|
121
|
+
*
|
|
122
|
+
* Priority order:
|
|
123
|
+
* 1. First function/method name from a hunk header (@@...@@ <name>)
|
|
124
|
+
* 2. First added symbol-looking token from a + line
|
|
125
|
+
* 3. First changed file base name (no extension)
|
|
126
|
+
* 4. Literal "change" as a safe fallback
|
|
127
|
+
*/
|
|
128
|
+
function extractAnchorIdentifier(diff) {
|
|
129
|
+
// 1. Hunk header function hint:
|
|
130
|
+
// @@ -l,n +l,n @@ function validateToken(
|
|
131
|
+
const hunkHeaderFn = diff.match(/@@[^@]+@@\s+(?:(?:export|async)\s+)*(?:function\s+)?(\w[\w.]*)\s*\(/u);
|
|
132
|
+
if (hunkHeaderFn?.[1] && hunkHeaderFn[1].length >= 3) {
|
|
133
|
+
return hunkHeaderFn[1];
|
|
134
|
+
}
|
|
135
|
+
// 2. First added function/method-like declaration. This intentionally
|
|
136
|
+
// precedes local const extraction so a method body's first local variable
|
|
137
|
+
// does not become the summary anchor.
|
|
138
|
+
const addedCallable = diff.match(/^\+[^+].*?\b(?:export\s+)?(?:async\s+)?(?:function\s+)?(\w[\w]*)\s*\(/mu);
|
|
139
|
+
if (addedCallable?.[1] && addedCallable[1].length >= 3) {
|
|
140
|
+
return addedCallable[1];
|
|
141
|
+
}
|
|
142
|
+
// 3. First export/const/class/type on an added line
|
|
143
|
+
const addedSymbol = diff.match(/^\+[^+].*?\b(?:export\s+)?(?:const|let|var|class|interface|type)\s+(\w[\w]*)/mu);
|
|
144
|
+
if (addedSymbol?.[1] && addedSymbol[1].length >= 3) {
|
|
145
|
+
return addedSymbol[1];
|
|
146
|
+
}
|
|
147
|
+
// 4. First changed file name from diff --git a/<path> b/<path>
|
|
148
|
+
const fileMatch = diff.match(/^diff --git a\/(.+?) b\//mu);
|
|
149
|
+
if (fileMatch?.[1]) {
|
|
150
|
+
const base = fileMatch[1].split("/").pop() ?? fileMatch[1];
|
|
151
|
+
return base.replace(/\.\w+$/, ""); // strip extension
|
|
152
|
+
}
|
|
153
|
+
// 5. First +++ b/ file name
|
|
154
|
+
const plusFile = diff.match(/^\+\+\+ b\/(.+)$/mu);
|
|
155
|
+
if (plusFile?.[1]) {
|
|
156
|
+
const base = plusFile[1].split("/").pop() ?? plusFile[1];
|
|
157
|
+
return base.replace(/\.\w+$/, "");
|
|
158
|
+
}
|
|
159
|
+
return "change";
|
|
160
|
+
}
|
|
161
|
+
function parseDiffStats(diff) {
|
|
162
|
+
const files = [];
|
|
163
|
+
let addedLines = 0;
|
|
164
|
+
let removedLines = 0;
|
|
165
|
+
for (const line of diff.split("\n")) {
|
|
166
|
+
if (line.startsWith("diff --git ")) {
|
|
167
|
+
const match = line.match(/^diff --git a\/(.+?) b\//u);
|
|
168
|
+
if (match?.[1])
|
|
169
|
+
files.push(match[1]);
|
|
170
|
+
}
|
|
171
|
+
else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
172
|
+
addedLines += 1;
|
|
173
|
+
}
|
|
174
|
+
else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
175
|
+
removedLines += 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { files, addedLines, removedLines };
|
|
179
|
+
}
|
|
180
|
+
function selectVerb(stats) {
|
|
181
|
+
if (stats.addedLines > 0 && stats.removedLines === 0)
|
|
182
|
+
return "add";
|
|
183
|
+
if (stats.removedLines > 0 && stats.addedLines === 0)
|
|
184
|
+
return "remove";
|
|
185
|
+
if (stats.addedLines > stats.removedLines * 3)
|
|
186
|
+
return "add";
|
|
187
|
+
if (stats.removedLines > stats.addedLines * 3)
|
|
188
|
+
return "remove";
|
|
189
|
+
return "update";
|
|
190
|
+
}
|
|
191
|
+
function stripPath(filePath) {
|
|
192
|
+
return filePath.split("/").pop() ?? filePath;
|
|
193
|
+
}
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Model output cleanup
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
function cleanModelOutput(raw) {
|
|
198
|
+
return raw
|
|
199
|
+
.trim()
|
|
200
|
+
.replace(/^["']|["']$/g, "") // strip surrounding quotes
|
|
201
|
+
.replace(/\.+$/, "") // strip trailing dots
|
|
202
|
+
.replace(/\s+/g, " "); // collapse whitespace
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=diff-summary.js.map
|