@loopover/engine 1.0.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/CHANGELOG.md +94 -0
- package/LICENSE +661 -0
- package/README.md +676 -0
- package/dist/advisory/gate-advisory.d.ts +151 -0
- package/dist/advisory/gate-advisory.js +512 -0
- package/dist/ai-policy-map.d.ts +97 -0
- package/dist/ai-policy-map.js +377 -0
- package/dist/ams-policy-spec.d.ts +56 -0
- package/dist/ams-policy-spec.js +165 -0
- package/dist/calibration-dashboard.d.ts +23 -0
- package/dist/calibration-dashboard.js +65 -0
- package/dist/calibration-trend.d.ts +40 -0
- package/dist/calibration-trend.js +60 -0
- package/dist/contributor-fit.d.ts +24 -0
- package/dist/contributor-fit.js +50 -0
- package/dist/discovery-index-contract.d.ts +74 -0
- package/dist/discovery-index-contract.js +184 -0
- package/dist/discovery-soft-claim.d.ts +35 -0
- package/dist/discovery-soft-claim.js +50 -0
- package/dist/duplicate-winner.d.ts +57 -0
- package/dist/duplicate-winner.js +98 -0
- package/dist/feasibility.d.ts +27 -0
- package/dist/feasibility.js +86 -0
- package/dist/finding-severity-calibration.d.ts +116 -0
- package/dist/finding-severity-calibration.js +457 -0
- package/dist/fleet-run-manifest.d.ts +41 -0
- package/dist/fleet-run-manifest.js +151 -0
- package/dist/focus-manifest/guidance.d.ts +21 -0
- package/dist/focus-manifest/guidance.js +222 -0
- package/dist/focus-manifest.d.ts +826 -0
- package/dist/focus-manifest.js +2730 -0
- package/dist/gate-verdict-calibration.d.ts +111 -0
- package/dist/gate-verdict-calibration.js +411 -0
- package/dist/github/constants.d.ts +1 -0
- package/dist/github/constants.js +1 -0
- package/dist/github/sanitize-public-comment.d.ts +1 -0
- package/dist/github/sanitize-public-comment.js +26 -0
- package/dist/goal-model.d.ts +7 -0
- package/dist/goal-model.js +87 -0
- package/dist/governor/action-mode.d.ts +55 -0
- package/dist/governor/action-mode.js +74 -0
- package/dist/governor/budget-cap.d.ts +49 -0
- package/dist/governor/budget-cap.js +34 -0
- package/dist/governor/chokepoint.d.ts +58 -0
- package/dist/governor/chokepoint.js +269 -0
- package/dist/governor/kill-switch.d.ts +38 -0
- package/dist/governor/kill-switch.js +62 -0
- package/dist/governor/rate-limit.d.ts +40 -0
- package/dist/governor/rate-limit.js +63 -0
- package/dist/governor/reputation-throttle.d.ts +50 -0
- package/dist/governor/reputation-throttle.js +94 -0
- package/dist/governor/run-halt.d.ts +39 -0
- package/dist/governor/run-halt.js +83 -0
- package/dist/governor/self-plagiarism.d.ts +47 -0
- package/dist/governor/self-plagiarism.js +200 -0
- package/dist/governor/write-rate-limit.d.ts +52 -0
- package/dist/governor/write-rate-limit.js +132 -0
- package/dist/governor-ledger.d.ts +25 -0
- package/dist/governor-ledger.js +91 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +120 -0
- package/dist/issue-plan-decomposition.d.ts +18 -0
- package/dist/issue-plan-decomposition.js +56 -0
- package/dist/issue-rag-query.d.ts +12 -0
- package/dist/issue-rag-query.js +35 -0
- package/dist/local-scorer.d.ts +34 -0
- package/dist/local-scorer.js +36 -0
- package/dist/metadata-best-min-score.d.ts +9 -0
- package/dist/metadata-best-min-score.js +9 -0
- package/dist/metadata-best-pick.d.ts +9 -0
- package/dist/metadata-best-pick.js +9 -0
- package/dist/metadata-min-score.d.ts +9 -0
- package/dist/metadata-min-score.js +13 -0
- package/dist/metadata-top-min-score.d.ts +9 -0
- package/dist/metadata-top-min-score.js +13 -0
- package/dist/metadata-top-pick.d.ts +9 -0
- package/dist/metadata-top-pick.js +13 -0
- package/dist/miner/acceptance-criteria.d.ts +43 -0
- package/dist/miner/acceptance-criteria.js +77 -0
- package/dist/miner/agent-sdk-driver.d.ts +39 -0
- package/dist/miner/agent-sdk-driver.js +162 -0
- package/dist/miner/attempt-log.d.ts +29 -0
- package/dist/miner/attempt-log.js +100 -0
- package/dist/miner/attempt-metering.d.ts +39 -0
- package/dist/miner/attempt-metering.js +67 -0
- package/dist/miner/cli-subprocess-driver.d.ts +68 -0
- package/dist/miner/cli-subprocess-driver.js +272 -0
- package/dist/miner/coding-agent-driver.d.ts +33 -0
- package/dist/miner/coding-agent-driver.js +36 -0
- package/dist/miner/coding-agent-invoke.d.ts +11 -0
- package/dist/miner/coding-agent-invoke.js +80 -0
- package/dist/miner/coding-agent-mode.d.ts +25 -0
- package/dist/miner/coding-agent-mode.js +43 -0
- package/dist/miner/driver-factory.d.ts +82 -0
- package/dist/miner/driver-factory.js +169 -0
- package/dist/miner/harness-submission-trigger.d.ts +37 -0
- package/dist/miner/harness-submission-trigger.js +46 -0
- package/dist/miner/iterate-loop.d.ts +97 -0
- package/dist/miner/iterate-loop.js +278 -0
- package/dist/miner/iterate-policy.d.ts +98 -0
- package/dist/miner/iterate-policy.js +98 -0
- package/dist/miner/lint-guard.d.ts +50 -0
- package/dist/miner/lint-guard.js +68 -0
- package/dist/miner/local-write-tools.d.ts +81 -0
- package/dist/miner/local-write-tools.js +113 -0
- package/dist/miner/loop-reentry-policy.d.ts +36 -0
- package/dist/miner/loop-reentry-policy.js +53 -0
- package/dist/miner/repo-map.d.ts +51 -0
- package/dist/miner/repo-map.js +268 -0
- package/dist/miner/self-review-adapter.d.ts +106 -0
- package/dist/miner/self-review-adapter.js +83 -0
- package/dist/miner/submission-gate.d.ts +40 -0
- package/dist/miner/submission-gate.js +89 -0
- package/dist/miner/worktree-allocator.d.ts +59 -0
- package/dist/miner/worktree-allocator.js +60 -0
- package/dist/miner/worktree-pool.d.ts +55 -0
- package/dist/miner/worktree-pool.js +65 -0
- package/dist/miner-goal-lane-fit.d.ts +18 -0
- package/dist/miner-goal-lane-fit.js +76 -0
- package/dist/miner-goal-spec.d.ts +139 -0
- package/dist/miner-goal-spec.js +281 -0
- package/dist/miner-prediction-metrics.d.ts +16 -0
- package/dist/miner-prediction-metrics.js +61 -0
- package/dist/miner-telemetry.d.ts +32 -0
- package/dist/miner-telemetry.js +87 -0
- package/dist/objective-anchor.d.ts +125 -0
- package/dist/objective-anchor.js +401 -0
- package/dist/opportunity-competition.d.ts +6 -0
- package/dist/opportunity-competition.js +29 -0
- package/dist/opportunity-freshness.d.ts +18 -0
- package/dist/opportunity-freshness.js +55 -0
- package/dist/opportunity-metadata.d.ts +53 -0
- package/dist/opportunity-metadata.js +204 -0
- package/dist/opportunity-ranker.d.ts +46 -0
- package/dist/opportunity-ranker.js +74 -0
- package/dist/pairwise-calibration.d.ts +49 -0
- package/dist/pairwise-calibration.js +106 -0
- package/dist/phase7-calibration-loop.d.ts +136 -0
- package/dist/phase7-calibration-loop.js +437 -0
- package/dist/plan-blocked.d.ts +6 -0
- package/dist/plan-blocked.js +24 -0
- package/dist/plan-completed.d.ts +5 -0
- package/dist/plan-completed.js +6 -0
- package/dist/plan-completion.d.ts +6 -0
- package/dist/plan-completion.js +7 -0
- package/dist/plan-empty.d.ts +5 -0
- package/dist/plan-empty.js +6 -0
- package/dist/plan-export.d.ts +27 -0
- package/dist/plan-export.js +86 -0
- package/dist/plan-failure.d.ts +5 -0
- package/dist/plan-failure.js +6 -0
- package/dist/plan-overall-status.d.ts +6 -0
- package/dist/plan-overall-status.js +25 -0
- package/dist/plan-pending.d.ts +5 -0
- package/dist/plan-pending.js +6 -0
- package/dist/plan-progress-complete.d.ts +7 -0
- package/dist/plan-progress-complete.js +10 -0
- package/dist/plan-ready.d.ts +6 -0
- package/dist/plan-ready.js +12 -0
- package/dist/plan-running.d.ts +5 -0
- package/dist/plan-running.js +6 -0
- package/dist/plan-skipped.d.ts +5 -0
- package/dist/plan-skipped.js +6 -0
- package/dist/plan-step-count.d.ts +5 -0
- package/dist/plan-step-count.js +6 -0
- package/dist/plan-step-stats.d.ts +5 -0
- package/dist/plan-step-stats.js +6 -0
- package/dist/plan-templates.d.ts +25 -0
- package/dist/plan-templates.js +107 -0
- package/dist/plan-terminated.d.ts +6 -0
- package/dist/plan-terminated.js +9 -0
- package/dist/portfolio/non-convergence.d.ts +37 -0
- package/dist/portfolio/non-convergence.js +57 -0
- package/dist/portfolio/queue.d.ts +30 -0
- package/dist/portfolio/queue.js +150 -0
- package/dist/predicted-gate.d.ts +103 -0
- package/dist/predicted-gate.js +218 -0
- package/dist/prompt-packet.d.ts +11 -0
- package/dist/prompt-packet.js +34 -0
- package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-best-min-score.js +9 -0
- package/dist/ranked-opportunity-best-pick.d.ts +8 -0
- package/dist/ranked-opportunity-best-pick.js +9 -0
- package/dist/ranked-opportunity-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-min-score.js +13 -0
- package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-top-min-score.js +13 -0
- package/dist/review/advisory-ai-routing-config.d.ts +8 -0
- package/dist/review/advisory-ai-routing-config.js +40 -0
- package/dist/review/check-names.d.ts +1 -0
- package/dist/review/check-names.js +1 -0
- package/dist/review/cla-check.d.ts +49 -0
- package/dist/review/cla-check.js +71 -0
- package/dist/review/diff-file-priority.d.ts +1 -0
- package/dist/review/diff-file-priority.js +12 -0
- package/dist/review/enrichment-analyzer-names.d.ts +3 -0
- package/dist/review/enrichment-analyzer-names.js +63 -0
- package/dist/review/guardrail-config.d.ts +10 -0
- package/dist/review/guardrail-config.js +62 -0
- package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
- package/dist/review/linked-issue-hard-rules-config.js +79 -0
- package/dist/review/linked-issue-label-propagation.d.ts +9 -0
- package/dist/review/linked-issue-label-propagation.js +114 -0
- package/dist/review/pre-merge-checks.d.ts +28 -0
- package/dist/review/pre-merge-checks.js +66 -0
- package/dist/review/review-thread-findings.d.ts +1 -0
- package/dist/review/review-thread-findings.js +1 -0
- package/dist/review/safe-url.d.ts +5 -0
- package/dist/review/safe-url.js +137 -0
- package/dist/review/screenshot-table-gate.d.ts +87 -0
- package/dist/review/screenshot-table-gate.js +340 -0
- package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
- package/dist/review/unlinked-issue-guardrail-config.js +44 -0
- package/dist/reviewer-consensus-calibration.d.ts +113 -0
- package/dist/reviewer-consensus-calibration.js +493 -0
- package/dist/reward-risk.d.ts +223 -0
- package/dist/reward-risk.js +618 -0
- package/dist/scoring/label-match.d.ts +4 -0
- package/dist/scoring/label-match.js +145 -0
- package/dist/scoring/model.d.ts +31 -0
- package/dist/scoring/model.js +157 -0
- package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
- package/dist/scoring/pending-pr-scenarios.js +145 -0
- package/dist/scoring/preview.d.ts +246 -0
- package/dist/scoring/preview.js +1108 -0
- package/dist/scoring/types.d.ts +131 -0
- package/dist/scoring/types.js +7 -0
- package/dist/settings/auto-close-exempt.d.ts +10 -0
- package/dist/settings/auto-close-exempt.js +58 -0
- package/dist/settings/autonomy.d.ts +33 -0
- package/dist/settings/autonomy.js +80 -0
- package/dist/settings/command-authorization.d.ts +49 -0
- package/dist/settings/command-authorization.js +222 -0
- package/dist/settings/contributor-blacklist.d.ts +17 -0
- package/dist/settings/contributor-blacklist.js +86 -0
- package/dist/settings/moderation-rules.d.ts +51 -0
- package/dist/settings/moderation-rules.js +104 -0
- package/dist/settings/pr-type-label.d.ts +70 -0
- package/dist/settings/pr-type-label.js +149 -0
- package/dist/signals/change-guardrail.d.ts +49 -0
- package/dist/signals/change-guardrail.js +164 -0
- package/dist/signals/check-summary.d.ts +9 -0
- package/dist/signals/check-summary.js +12 -0
- package/dist/signals/contributor-calibration.d.ts +23 -0
- package/dist/signals/contributor-calibration.js +47 -0
- package/dist/signals/duplicate-winner.d.ts +9 -0
- package/dist/signals/duplicate-winner.js +9 -0
- package/dist/signals/path-matchers.d.ts +36 -0
- package/dist/signals/path-matchers.js +371 -0
- package/dist/signals/predicted-gate-engine.d.ts +62 -0
- package/dist/signals/predicted-gate-engine.js +895 -0
- package/dist/signals/preflight-limits.d.ts +14 -0
- package/dist/signals/preflight-limits.js +14 -0
- package/dist/signals/slop.d.ts +56 -0
- package/dist/signals/slop.js +324 -0
- package/dist/signals/test-evidence.d.ts +25 -0
- package/dist/signals/test-evidence.js +108 -0
- package/dist/subprocess-env.d.ts +23 -0
- package/dist/subprocess-env.js +74 -0
- package/dist/track-record-summary.d.ts +92 -0
- package/dist/track-record-summary.js +301 -0
- package/dist/types/manifest-deps-types.d.ts +514 -0
- package/dist/types/manifest-deps-types.js +4 -0
- package/dist/types/predicted-gate-types.d.ts +338 -0
- package/dist/types/predicted-gate-types.js +3 -0
- package/dist/types/reward-risk-types.d.ts +319 -0
- package/dist/types/reward-risk-types.js +11 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +80 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type PredictedGateInput, type PredictedGateVerdict, type GateCheckConclusion } from "../predicted-gate.js";
|
|
2
|
+
import type { FocusManifest } from "../focus-manifest/guidance.js";
|
|
3
|
+
import type { AdvisoryFinding, BountyRecord, IssueRecord, PullRequestRecord, RepositoryRecord } from "../types/predicted-gate-types.js";
|
|
4
|
+
import type { IssueQualityReport } from "../signals/predicted-gate-engine.js";
|
|
5
|
+
/** One changed file in the attempt's live worktree diff. Mirrors `SlopChangedFile` (`src/signals/slop.ts`). */
|
|
6
|
+
export type SelfReviewChangedFile = {
|
|
7
|
+
path: string;
|
|
8
|
+
additions?: number | undefined;
|
|
9
|
+
deletions?: number | undefined;
|
|
10
|
+
};
|
|
11
|
+
/** Structural mirror of `SlopBand` (`src/signals/slop.ts`). */
|
|
12
|
+
export type SelfReviewSlopBand = "clean" | "low" | "elevated" | "high";
|
|
13
|
+
/** Structural mirror of `SlopAssessmentInput` (`src/signals/slop.ts`) -- see the module doc comment on why
|
|
14
|
+
* this is a hand-kept mirror rather than an import. */
|
|
15
|
+
export type SelfReviewSlopInput = {
|
|
16
|
+
changedFiles?: SelfReviewChangedFile[] | undefined;
|
|
17
|
+
tests?: string[] | undefined;
|
|
18
|
+
testFiles?: string[] | undefined;
|
|
19
|
+
description?: string | null | undefined;
|
|
20
|
+
commitMessages?: string[] | undefined;
|
|
21
|
+
inDuplicateCluster?: boolean | undefined;
|
|
22
|
+
hasLinkedIssue?: boolean | undefined;
|
|
23
|
+
issueDiscoveryLane?: boolean | undefined;
|
|
24
|
+
};
|
|
25
|
+
/** Structural mirror of `SlopAssessment` (`src/signals/slop.ts`). Reuses the engine's own native
|
|
26
|
+
* `AdvisoryFinding` for `findings` (predicted-gate-types.ts's own comment already documents it as the mirror
|
|
27
|
+
* of `src/signals/engine.ts`'s `SignalFinding`, which slop.ts's findings are typed as). */
|
|
28
|
+
export type SelfReviewSlopAssessment = {
|
|
29
|
+
slopRisk: number;
|
|
30
|
+
band: SelfReviewSlopBand;
|
|
31
|
+
findings: AdvisoryFinding[];
|
|
32
|
+
};
|
|
33
|
+
/** Injected dependency binding the real `src/signals/slop.ts#buildSlopAssessment` -- mirrors the
|
|
34
|
+
* `RewardRiskEngineDeps` injection pattern for the identical not-yet-extracted-into-the-engine reason. */
|
|
35
|
+
export type SelfReviewAdapterDeps = {
|
|
36
|
+
runSlopAssessment: (input: SelfReviewSlopInput) => SelfReviewSlopAssessment;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The attempt-side state an iterate-loop iteration (#2333) has available each round: the live worktree diff,
|
|
40
|
+
* plus the acceptance-criteria-derived identity fields a synthetic PR needs. Nothing here requires network
|
|
41
|
+
* access -- everything is either local diff state or already resolved by an earlier phase (prompt packet /
|
|
42
|
+
* acceptance criteria).
|
|
43
|
+
*/
|
|
44
|
+
export type AttemptDiffState = {
|
|
45
|
+
repoFullName: string;
|
|
46
|
+
contributorLogin: string;
|
|
47
|
+
title: string;
|
|
48
|
+
body?: string | undefined;
|
|
49
|
+
labels?: string[] | undefined;
|
|
50
|
+
linkedIssues?: number[] | undefined;
|
|
51
|
+
authorAssociation?: string | undefined;
|
|
52
|
+
changedFiles: SelfReviewChangedFile[];
|
|
53
|
+
testFiles?: string[] | undefined;
|
|
54
|
+
commitMessages?: string[] | undefined;
|
|
55
|
+
issueDiscoveryLane?: boolean | undefined;
|
|
56
|
+
};
|
|
57
|
+
/** Repo-level context the caller supplies once per attempt (this adapter does not fetch it itself -- see the
|
|
58
|
+
* module doc comment). Mirrors `buildPredictedGateVerdict`'s own non-diff-state parameters exactly. */
|
|
59
|
+
export type SelfReviewContext = {
|
|
60
|
+
manifest: FocusManifest;
|
|
61
|
+
repo: RepositoryRecord | null;
|
|
62
|
+
issues: IssueRecord[];
|
|
63
|
+
pullRequests: PullRequestRecord[];
|
|
64
|
+
bounties?: BountyRecord[] | undefined;
|
|
65
|
+
issueQuality?: IssueQualityReport | null | undefined;
|
|
66
|
+
confirmedContributor?: boolean | undefined;
|
|
67
|
+
/** Whether this attempt's synthetic PR is itself in a duplicate cluster -- the caller computes this from
|
|
68
|
+
* `pullRequests`/`issues` the same way the live gate's collision report would. Threaded separately from
|
|
69
|
+
* `diffState` since it depends on repo-level context, not the diff itself. */
|
|
70
|
+
inDuplicateCluster?: boolean | undefined;
|
|
71
|
+
};
|
|
72
|
+
export type SelfReviewVerdict = {
|
|
73
|
+
predictedGateVerdict: PredictedGateVerdict;
|
|
74
|
+
slopAssessment: SelfReviewSlopAssessment;
|
|
75
|
+
changedPaths: string[];
|
|
76
|
+
/** The hard requirement this issue's deliverables call for: true ONLY when `predictedGateVerdict.conclusion`
|
|
77
|
+
* is a clear pass ({@link SELF_REVIEW_PASSING_CONCLUSION}). Any other conclusion -- `"failure"`,
|
|
78
|
+
* `"action_required"`, `"neutral"`, or `"skipped"` -- means false. Callers (this adapter's own consumers,
|
|
79
|
+
* and independently the iterate-loop orchestrator, #2333, as defense in depth) must never hand off to
|
|
80
|
+
* submission when this is false. */
|
|
81
|
+
passesPredictedGate: boolean;
|
|
82
|
+
};
|
|
83
|
+
/** The one literal conclusion value that counts as a clear pass. Exported so callers enforcing the same hard
|
|
84
|
+
* requirement (defense in depth, per this issue's own deliverable) check against the identical literal rather
|
|
85
|
+
* than each re-deriving their own notion of "passing". */
|
|
86
|
+
export declare const SELF_REVIEW_PASSING_CONCLUSION: GateCheckConclusion;
|
|
87
|
+
/** Build the `PredictedGateInput` (repo, contributor login, title, body, labels, linked issues) from the
|
|
88
|
+
* attempt's diff state -- the compact synthetic-PR-identity fields `buildPredictedGateVerdict` needs, as
|
|
89
|
+
* distinct from the repo-level {@link SelfReviewContext}. */
|
|
90
|
+
export declare function buildSelfReviewPredictedGateInput(diffState: AttemptDiffState): PredictedGateInput;
|
|
91
|
+
/** The real changed file paths from the diff, for the `changedPaths` argument `buildPredictedGateVerdict`
|
|
92
|
+
* needs to evaluate path-dependent checks (focus-manifest path policy, path-gated pre-merge checks, the
|
|
93
|
+
* file-count size/guardrail hold). Omitting them silently under-predicts per predicted-gate.ts's own
|
|
94
|
+
* `PREDICTED_GATE_NOTE_NO_PATHS` disclaimer -- a dangerous false-confidence bug if the miner's own loop
|
|
95
|
+
* relied on an omitted-paths call. `runSelfReview` below always threads this through; it is exported
|
|
96
|
+
* separately so a caller assembling `SelfReviewContext` can also see the exact same path list if needed. */
|
|
97
|
+
export declare function buildSelfReviewChangedPaths(diffState: AttemptDiffState): string[];
|
|
98
|
+
/** Build the slop-assessment input from the diff state + context, mirroring `SlopAssessmentInput` exactly. */
|
|
99
|
+
export declare function buildSelfReviewSlopInput(diffState: AttemptDiffState, context: SelfReviewContext): SelfReviewSlopInput;
|
|
100
|
+
/**
|
|
101
|
+
* Run the full self-review pass for one iteration: build the predicted-gate + slop inputs from the attempt's
|
|
102
|
+
* diff state, call `buildPredictedGateVerdict` with the caller-supplied repo-level context (`changedPaths`
|
|
103
|
+
* ALWAYS threaded through explicitly, never omitted), run the injected slop assessment, and combine into one
|
|
104
|
+
* verdict.
|
|
105
|
+
*/
|
|
106
|
+
export declare function runSelfReview(diffState: AttemptDiffState, context: SelfReviewContext, deps: SelfReviewAdapterDeps): SelfReviewVerdict;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Self-review adapter (#2334): turns an attempt's live worktree diff state into the SAME inputs
|
|
2
|
+
// `buildPredictedGateVerdict` (predicted-gate.ts) and the slop-signal pass (src/signals/slop.ts) expect, so
|
|
3
|
+
// the iterate-loop's self-review call (#2333) is genuinely byte-identical to what the live maintainer gate
|
|
4
|
+
// would compute post-submission -- not an approximation.
|
|
5
|
+
//
|
|
6
|
+
// SLOP INJECTION: `src/signals/slop.ts` has not been extracted into this package (it depends on several
|
|
7
|
+
// sibling `src/signals/*` modules that are also unextracted) -- mirrors the established `RewardRiskEngineDeps`
|
|
8
|
+
// injection pattern (`reward-risk.ts`, #2281) for the identical reason: this module takes the slop assessment
|
|
9
|
+
// as an INJECTED function rather than importing slop.ts directly, so the engine package keeps zero import
|
|
10
|
+
// dependency on the private `src/` tree. `SelfReviewSlopInput`/`SelfReviewSlopAssessment` below are a
|
|
11
|
+
// hand-kept structural mirror of slop.ts's `SlopAssessmentInput`/`SlopAssessment` -- same discipline as
|
|
12
|
+
// `types/predicted-gate-types.ts`'s own header comment ("Local mirrors from src/... Keep in sync by hand").
|
|
13
|
+
// The real binding (`buildSlopAssessment`) lives in whichever `src`-side shim wires a live iterate-loop.
|
|
14
|
+
import { buildPredictedGateVerdict } from "../predicted-gate.js";
|
|
15
|
+
/** The one literal conclusion value that counts as a clear pass. Exported so callers enforcing the same hard
|
|
16
|
+
* requirement (defense in depth, per this issue's own deliverable) check against the identical literal rather
|
|
17
|
+
* than each re-deriving their own notion of "passing". */
|
|
18
|
+
export const SELF_REVIEW_PASSING_CONCLUSION = "success";
|
|
19
|
+
function isClearPass(conclusion) {
|
|
20
|
+
return conclusion === SELF_REVIEW_PASSING_CONCLUSION;
|
|
21
|
+
}
|
|
22
|
+
/** Build the `PredictedGateInput` (repo, contributor login, title, body, labels, linked issues) from the
|
|
23
|
+
* attempt's diff state -- the compact synthetic-PR-identity fields `buildPredictedGateVerdict` needs, as
|
|
24
|
+
* distinct from the repo-level {@link SelfReviewContext}. */
|
|
25
|
+
export function buildSelfReviewPredictedGateInput(diffState) {
|
|
26
|
+
return {
|
|
27
|
+
repoFullName: diffState.repoFullName,
|
|
28
|
+
contributorLogin: diffState.contributorLogin,
|
|
29
|
+
title: diffState.title,
|
|
30
|
+
...(diffState.body !== undefined ? { body: diffState.body } : {}),
|
|
31
|
+
...(diffState.labels !== undefined ? { labels: diffState.labels } : {}),
|
|
32
|
+
...(diffState.linkedIssues !== undefined ? { linkedIssues: diffState.linkedIssues } : {}),
|
|
33
|
+
...(diffState.authorAssociation !== undefined ? { authorAssociation: diffState.authorAssociation } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The real changed file paths from the diff, for the `changedPaths` argument `buildPredictedGateVerdict`
|
|
37
|
+
* needs to evaluate path-dependent checks (focus-manifest path policy, path-gated pre-merge checks, the
|
|
38
|
+
* file-count size/guardrail hold). Omitting them silently under-predicts per predicted-gate.ts's own
|
|
39
|
+
* `PREDICTED_GATE_NOTE_NO_PATHS` disclaimer -- a dangerous false-confidence bug if the miner's own loop
|
|
40
|
+
* relied on an omitted-paths call. `runSelfReview` below always threads this through; it is exported
|
|
41
|
+
* separately so a caller assembling `SelfReviewContext` can also see the exact same path list if needed. */
|
|
42
|
+
export function buildSelfReviewChangedPaths(diffState) {
|
|
43
|
+
return diffState.changedFiles.map((file) => file.path);
|
|
44
|
+
}
|
|
45
|
+
/** Build the slop-assessment input from the diff state + context, mirroring `SlopAssessmentInput` exactly. */
|
|
46
|
+
export function buildSelfReviewSlopInput(diffState, context) {
|
|
47
|
+
return {
|
|
48
|
+
changedFiles: diffState.changedFiles,
|
|
49
|
+
testFiles: diffState.testFiles,
|
|
50
|
+
description: diffState.body ?? null,
|
|
51
|
+
commitMessages: diffState.commitMessages,
|
|
52
|
+
inDuplicateCluster: context.inDuplicateCluster,
|
|
53
|
+
hasLinkedIssue: (diffState.linkedIssues?.length ?? 0) > 0,
|
|
54
|
+
issueDiscoveryLane: diffState.issueDiscoveryLane,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run the full self-review pass for one iteration: build the predicted-gate + slop inputs from the attempt's
|
|
59
|
+
* diff state, call `buildPredictedGateVerdict` with the caller-supplied repo-level context (`changedPaths`
|
|
60
|
+
* ALWAYS threaded through explicitly, never omitted), run the injected slop assessment, and combine into one
|
|
61
|
+
* verdict.
|
|
62
|
+
*/
|
|
63
|
+
export function runSelfReview(diffState, context, deps) {
|
|
64
|
+
const changedPaths = buildSelfReviewChangedPaths(diffState);
|
|
65
|
+
const predictedGateVerdict = buildPredictedGateVerdict({
|
|
66
|
+
input: buildSelfReviewPredictedGateInput(diffState),
|
|
67
|
+
manifest: context.manifest,
|
|
68
|
+
repo: context.repo,
|
|
69
|
+
issues: context.issues,
|
|
70
|
+
pullRequests: context.pullRequests,
|
|
71
|
+
...(context.bounties !== undefined ? { bounties: context.bounties } : {}),
|
|
72
|
+
...(context.issueQuality !== undefined ? { issueQuality: context.issueQuality } : {}),
|
|
73
|
+
...(context.confirmedContributor !== undefined ? { confirmedContributor: context.confirmedContributor } : {}),
|
|
74
|
+
changedPaths,
|
|
75
|
+
});
|
|
76
|
+
const slopAssessment = deps.runSlopAssessment(buildSelfReviewSlopInput(diffState, context));
|
|
77
|
+
return {
|
|
78
|
+
predictedGateVerdict,
|
|
79
|
+
slopAssessment,
|
|
80
|
+
changedPaths,
|
|
81
|
+
passesPredictedGate: isClearPass(predictedGateVerdict.conclusion),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type MinerKillSwitchScope } from "../governor/kill-switch.js";
|
|
2
|
+
import type { PredictedGateVerdict } from "../predicted-gate.js";
|
|
3
|
+
import type { SelfReviewSlopAssessment, SelfReviewSlopBand } from "./self-review-adapter.js";
|
|
4
|
+
/** The one literal conclusion value that counts as a clear predicted-gate pass -- same literal self-review-
|
|
5
|
+
* adapter.ts's `SELF_REVIEW_PASSING_CONCLUSION` uses, kept as an independent constant here so this module has
|
|
6
|
+
* no runtime dependency beyond types on self-review-adapter.ts. */
|
|
7
|
+
export declare const SUBMISSION_GATE_PASSING_CONCLUSION: "success";
|
|
8
|
+
/** True when `band` is at or under `threshold`'s severity (inclusive) -- e.g. a `"low"` band is within a
|
|
9
|
+
* `"elevated"` threshold, and a band exactly equal to the threshold still passes. */
|
|
10
|
+
export declare function isSlopBandWithinThreshold(band: SelfReviewSlopBand, threshold: SelfReviewSlopBand): boolean;
|
|
11
|
+
/** `"observe"` mirrors `AUTONOMY_LEVELS`' deny-by-default floor: {@link shouldSubmit} still computes and
|
|
12
|
+
* returns the real signal-based decision (for logging), but structurally forces `allow: false` regardless --
|
|
13
|
+
* not left to an external caller to remember to also check the mode before acting on `allow: true`. */
|
|
14
|
+
export type SubmissionGateMode = "observe" | "enforce";
|
|
15
|
+
export type SubmissionGateCandidate = {
|
|
16
|
+
/** Checked FIRST, before any other field below -- see the module doc comment's KILL-SWITCH section. */
|
|
17
|
+
killSwitchScope: MinerKillSwitchScope;
|
|
18
|
+
/** `null` means the predictor was unreachable or errored -- fails closed, exactly like a genuine non-passing
|
|
19
|
+
* verdict, never treated as "no opinion, so allow". */
|
|
20
|
+
predictedGateVerdict: PredictedGateVerdict | null;
|
|
21
|
+
/** `null` means the slop check errored -- fails closed, exactly like a genuine over-threshold assessment. */
|
|
22
|
+
slopAssessment: SelfReviewSlopAssessment | null;
|
|
23
|
+
/** The maximum slop band that still permits submission (inclusive of this exact band). */
|
|
24
|
+
slopThreshold: SelfReviewSlopBand;
|
|
25
|
+
mode: SubmissionGateMode;
|
|
26
|
+
};
|
|
27
|
+
export type SubmissionGateDecision = {
|
|
28
|
+
allow: boolean;
|
|
29
|
+
/** Always populated when `allow` is `false` (including in `"observe"` mode, prefixed to distinguish a
|
|
30
|
+
* would-have-allowed dry-run from a real block) -- every decision is auditable, not just denials. */
|
|
31
|
+
reasons: string[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* THE gate: build (or invoke) `gittensory_open_pr`'s action spec ONLY when this returns `allow: true`. Requires
|
|
35
|
+
* BOTH a clean predicted-gate pass AND a slop band at or under the configured threshold; any missing signal, or
|
|
36
|
+
* `mode: "observe"`, forces `allow: false`. The kill-switch is checked FIRST, before any other logic -- an
|
|
37
|
+
* active kill-switch blocks unconditionally, regardless of otherwise-passing signals. Pure; identical inputs
|
|
38
|
+
* always yield the identical decision.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shouldSubmit(candidate: SubmissionGateCandidate): SubmissionGateDecision;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Gated-submission trigger (#2336): THE safety-critical chokepoint of Phase 4 -- the actual decision point
|
|
2
|
+
// that decides "call gittensory_open_pr NOW" for an autonomous run. Conservatively requires BOTH a predicted-
|
|
3
|
+
// gate PASS AND a slop score under a configurable threshold before a local-write open_pr action spec is ever
|
|
4
|
+
// built. Any ambiguity (a missing/errored signal) resolves to NOT submitting -- this function never defaults
|
|
5
|
+
// to allow.
|
|
6
|
+
//
|
|
7
|
+
// SEQUENCING: runs BEFORE `buildOpenPrSpec` (src/mcp/local-write-tools.ts) is ever called -- i.e. before the
|
|
8
|
+
// Governor chokepoint (#2340) ever sees an open_pr action spec to evaluate. The two are complementary, not
|
|
9
|
+
// redundant: this gates on CONTENT-QUALITY signals (predicted-gate conclusion, slop risk) specific to the
|
|
10
|
+
// candidate diff; the chokepoint gates on RESOURCE/GOVERNANCE signals (rate-limit, budget, reputation, self-
|
|
11
|
+
// plagiarism, dry-run mode) that apply to every write action class, not just open_pr. "The actual call site
|
|
12
|
+
// invoking buildOpenPrSpec / gittensory_open_pr is gated exclusively through this function" (this issue's own
|
|
13
|
+
// deliverable) is a POLICY this and every future call site must honor -- wiring a real call site is a later,
|
|
14
|
+
// separate issue (mirrors #2333/#2335's own split between loop mechanics and policy).
|
|
15
|
+
//
|
|
16
|
+
// INPUT SHAPE: `predictedGateVerdict`/`slopAssessment` are typed exactly as the fields `SelfReviewVerdict`
|
|
17
|
+
// (self-review-adapter.ts, #2334) already carries, so a caller can pass the SAME verdict the iterate-loop's
|
|
18
|
+
// own self-review (#2333) already computed at handoff time -- this is a defense-in-depth RE-CHECK of that
|
|
19
|
+
// verdict immediately before submission, not a redundant re-computation from scratch.
|
|
20
|
+
//
|
|
21
|
+
// DRY-RUN: mirrors `src/settings/autonomy.ts`'s deny-by-default dial (`AUTONOMY_LEVELS`, `"observe"` as the
|
|
22
|
+
// floor) for safe rollout of THIS function's own thresholds -- distinct from, and evaluated separately from,
|
|
23
|
+
// the Governor chokepoint's own dry-run/live action-mode dial (#2342), which gates autonomous WRITING at all
|
|
24
|
+
// for a repo. `"observe"` here is specifically for safely calibrating the predicted-gate/slop thresholds
|
|
25
|
+
// against live traffic before ever trusting them to gate a real submission.
|
|
26
|
+
//
|
|
27
|
+
// KILL-SWITCH (#2339): checked FIRST, before any other logic -- flipping the kill-switch must halt this
|
|
28
|
+
// chokepoint immediately, the same way it halts the Governor chokepoint (#2340). Reuses
|
|
29
|
+
// `isMinerKillSwitchActive` (kill-switch.ts, #2341) directly rather than a bespoke wrapper -- the exact "single
|
|
30
|
+
// shared helper, not duplicated per call site" #2339's own deliverable calls for; `loop-reentry-policy.ts`'s
|
|
31
|
+
// `shouldReenter` consults the identical function. `killSwitchScope` is REQUIRED (not optional-with-a-
|
|
32
|
+
// permissive-default) so a caller cannot forget to resolve and pass it -- the same fail-closed-by-construction
|
|
33
|
+
// discipline as every other required field here.
|
|
34
|
+
import { isMinerKillSwitchActive } from "../governor/kill-switch.js";
|
|
35
|
+
/** The one literal conclusion value that counts as a clear predicted-gate pass -- same literal self-review-
|
|
36
|
+
* adapter.ts's `SELF_REVIEW_PASSING_CONCLUSION` uses, kept as an independent constant here so this module has
|
|
37
|
+
* no runtime dependency beyond types on self-review-adapter.ts. */
|
|
38
|
+
export const SUBMISSION_GATE_PASSING_CONCLUSION = "success";
|
|
39
|
+
const SLOP_BAND_SEVERITY = Object.freeze({
|
|
40
|
+
clean: 0,
|
|
41
|
+
low: 1,
|
|
42
|
+
elevated: 2,
|
|
43
|
+
high: 3,
|
|
44
|
+
});
|
|
45
|
+
/** True when `band` is at or under `threshold`'s severity (inclusive) -- e.g. a `"low"` band is within a
|
|
46
|
+
* `"elevated"` threshold, and a band exactly equal to the threshold still passes. */
|
|
47
|
+
export function isSlopBandWithinThreshold(band, threshold) {
|
|
48
|
+
return SLOP_BAND_SEVERITY[band] <= SLOP_BAND_SEVERITY[threshold];
|
|
49
|
+
}
|
|
50
|
+
/** The pure signal check, independent of `mode` -- {@link shouldSubmit} layers the observe/enforce dial on
|
|
51
|
+
* top of this. Returns an empty array only when BOTH signals genuinely pass. */
|
|
52
|
+
function evaluateSubmissionSignals(candidate) {
|
|
53
|
+
const reasons = [];
|
|
54
|
+
if (candidate.predictedGateVerdict === null) {
|
|
55
|
+
reasons.push("predicted_gate_unavailable");
|
|
56
|
+
}
|
|
57
|
+
else if (candidate.predictedGateVerdict.conclusion !== SUBMISSION_GATE_PASSING_CONCLUSION) {
|
|
58
|
+
const blockerCodes = candidate.predictedGateVerdict.blockers.map((blocker) => blocker.code).join(",");
|
|
59
|
+
reasons.push(`predicted_gate_not_passing:${candidate.predictedGateVerdict.conclusion}${blockerCodes ? `:${blockerCodes}` : ""}`);
|
|
60
|
+
}
|
|
61
|
+
if (candidate.slopAssessment === null) {
|
|
62
|
+
reasons.push("slop_assessment_unavailable");
|
|
63
|
+
}
|
|
64
|
+
else if (!isSlopBandWithinThreshold(candidate.slopAssessment.band, candidate.slopThreshold)) {
|
|
65
|
+
reasons.push(`slop_band_exceeds_threshold:${candidate.slopAssessment.band}>${candidate.slopThreshold}`);
|
|
66
|
+
}
|
|
67
|
+
return reasons;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* THE gate: build (or invoke) `gittensory_open_pr`'s action spec ONLY when this returns `allow: true`. Requires
|
|
71
|
+
* BOTH a clean predicted-gate pass AND a slop band at or under the configured threshold; any missing signal, or
|
|
72
|
+
* `mode: "observe"`, forces `allow: false`. The kill-switch is checked FIRST, before any other logic -- an
|
|
73
|
+
* active kill-switch blocks unconditionally, regardless of otherwise-passing signals. Pure; identical inputs
|
|
74
|
+
* always yield the identical decision.
|
|
75
|
+
*/
|
|
76
|
+
export function shouldSubmit(candidate) {
|
|
77
|
+
if (isMinerKillSwitchActive(candidate.killSwitchScope)) {
|
|
78
|
+
return { allow: false, reasons: [`${candidate.killSwitchScope}_kill_switch_active`] };
|
|
79
|
+
}
|
|
80
|
+
const reasons = evaluateSubmissionSignals(candidate);
|
|
81
|
+
const signalsPass = reasons.length === 0;
|
|
82
|
+
if (candidate.mode === "observe") {
|
|
83
|
+
return {
|
|
84
|
+
allow: false,
|
|
85
|
+
reasons: signalsPass ? ["observe_mode_active:would_have_allowed"] : ["observe_mode_active:would_have_blocked", ...reasons],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { allow: signalsPass, reasons };
|
|
89
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type WorktreeExecResult = {
|
|
2
|
+
code: number | null;
|
|
3
|
+
stdout?: string;
|
|
4
|
+
stderr?: string;
|
|
5
|
+
};
|
|
6
|
+
/** The injected git exec — a real `child_process` spawn in prod, a fake in tests. */
|
|
7
|
+
export type WorktreeExecFn = (cmd: string, args: readonly string[], opts: {
|
|
8
|
+
cwd: string;
|
|
9
|
+
}) => Promise<WorktreeExecResult>;
|
|
10
|
+
/** The deterministic worktree location + branch for one attempt. */
|
|
11
|
+
export type WorktreePlan = {
|
|
12
|
+
attemptId: string;
|
|
13
|
+
worktreePath: string;
|
|
14
|
+
branchName: string;
|
|
15
|
+
};
|
|
16
|
+
/** Worktrees live under this dir inside the repo; the branch carries this prefix. */
|
|
17
|
+
export declare const WORKTREE_SUBDIR = ".gittensory-worktrees";
|
|
18
|
+
export declare const WORKTREE_BRANCH_PREFIX = "gittensory/attempt/";
|
|
19
|
+
/**
|
|
20
|
+
* Compute the deterministic worktree path + branch name for an attempt — keyed on the attempt id, never a random
|
|
21
|
+
* suffix. Pure. Two concurrent attempts with distinct ids get distinct paths/branches; the same id always maps to
|
|
22
|
+
* the same location (so a crashed attempt's worktree is identifiable and cleanable).
|
|
23
|
+
*/
|
|
24
|
+
export declare function planWorktree(input: {
|
|
25
|
+
repoPath: string;
|
|
26
|
+
attemptId: string;
|
|
27
|
+
}): WorktreePlan;
|
|
28
|
+
export type WorktreeAddResult = {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
plan: WorktreePlan;
|
|
31
|
+
error?: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Create the attempt's isolated worktree via `git worktree add -b <branch> <path> <baseBranch>`, run through the
|
|
35
|
+
* injected exec. Returns the plan (so the caller knows the path/branch) and, on failure, git's stderr.
|
|
36
|
+
*/
|
|
37
|
+
export declare function addWorktree(input: {
|
|
38
|
+
exec: WorktreeExecFn;
|
|
39
|
+
repoPath: string;
|
|
40
|
+
baseBranch: string;
|
|
41
|
+
attemptId: string;
|
|
42
|
+
}): Promise<WorktreeAddResult>;
|
|
43
|
+
export type WorktreeRemoveResult = {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
removed: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
/** Retention policy: retain a FAILED attempt's worktree for post-mortem, remove a SUCCEEDED attempt's. */
|
|
49
|
+
export declare function shouldRetainWorktree(attemptOk: boolean): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Tear down the attempt's worktree via `git worktree remove --force <path>`, through the injected exec. When
|
|
52
|
+
* `retain` is set the worktree is KEPT (no exec, `removed: false`) for post-mortem — pass `shouldRetainWorktree(ok)`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function removeWorktree(input: {
|
|
55
|
+
exec: WorktreeExecFn;
|
|
56
|
+
repoPath: string;
|
|
57
|
+
worktreePath: string;
|
|
58
|
+
retain?: boolean;
|
|
59
|
+
}): Promise<WorktreeRemoveResult>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
/** Worktrees live under this dir inside the repo; the branch carries this prefix. */
|
|
3
|
+
export const WORKTREE_SUBDIR = ".gittensory-worktrees";
|
|
4
|
+
export const WORKTREE_BRANCH_PREFIX = "gittensory/attempt/";
|
|
5
|
+
const MAX_SLUG_LENGTH = 64;
|
|
6
|
+
/** Deterministically slugify an attempt id into a filesystem- and git-ref-safe token (same id → same slug). */
|
|
7
|
+
function slugifyAttemptId(attemptId) {
|
|
8
|
+
const slug = attemptId
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
12
|
+
.replace(/^[-.]+|[-.]+$/g, "");
|
|
13
|
+
if (!slug)
|
|
14
|
+
throw new Error("invalid_attempt_id");
|
|
15
|
+
return slug.slice(0, MAX_SLUG_LENGTH);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute the deterministic worktree path + branch name for an attempt — keyed on the attempt id, never a random
|
|
19
|
+
* suffix. Pure. Two concurrent attempts with distinct ids get distinct paths/branches; the same id always maps to
|
|
20
|
+
* the same location (so a crashed attempt's worktree is identifiable and cleanable).
|
|
21
|
+
*/
|
|
22
|
+
export function planWorktree(input) {
|
|
23
|
+
const slug = slugifyAttemptId(input.attemptId);
|
|
24
|
+
return {
|
|
25
|
+
attemptId: input.attemptId,
|
|
26
|
+
worktreePath: join(input.repoPath, WORKTREE_SUBDIR, slug),
|
|
27
|
+
branchName: `${WORKTREE_BRANCH_PREFIX}${slug}`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create the attempt's isolated worktree via `git worktree add -b <branch> <path> <baseBranch>`, run through the
|
|
32
|
+
* injected exec. Returns the plan (so the caller knows the path/branch) and, on failure, git's stderr.
|
|
33
|
+
*/
|
|
34
|
+
export async function addWorktree(input) {
|
|
35
|
+
const plan = planWorktree({ repoPath: input.repoPath, attemptId: input.attemptId });
|
|
36
|
+
const result = await input.exec("git", ["worktree", "add", "-b", plan.branchName, plan.worktreePath, input.baseBranch], { cwd: input.repoPath });
|
|
37
|
+
if (result.code === 0)
|
|
38
|
+
return { ok: true, plan };
|
|
39
|
+
const detail = (result.stderr ?? "").trim() || `git_worktree_add_exit_${result.code}`;
|
|
40
|
+
return { ok: false, plan, error: detail };
|
|
41
|
+
}
|
|
42
|
+
/** Retention policy: retain a FAILED attempt's worktree for post-mortem, remove a SUCCEEDED attempt's. */
|
|
43
|
+
export function shouldRetainWorktree(attemptOk) {
|
|
44
|
+
return !attemptOk;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Tear down the attempt's worktree via `git worktree remove --force <path>`, through the injected exec. When
|
|
48
|
+
* `retain` is set the worktree is KEPT (no exec, `removed: false`) for post-mortem — pass `shouldRetainWorktree(ok)`.
|
|
49
|
+
*/
|
|
50
|
+
export async function removeWorktree(input) {
|
|
51
|
+
if (input.retain)
|
|
52
|
+
return { ok: true, removed: false };
|
|
53
|
+
const result = await input.exec("git", ["worktree", "remove", "--force", input.worktreePath], {
|
|
54
|
+
cwd: input.repoPath,
|
|
55
|
+
});
|
|
56
|
+
if (result.code === 0)
|
|
57
|
+
return { ok: true, removed: true };
|
|
58
|
+
const detail = (result.stderr ?? "").trim() || `git_worktree_remove_exit_${result.code}`;
|
|
59
|
+
return { ok: false, removed: false, error: detail };
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type WorktreePlan } from "./worktree-allocator.js";
|
|
2
|
+
/** One live allocation: which attempt holds which planned worktree. */
|
|
3
|
+
export type WorktreeAllocation = {
|
|
4
|
+
attemptId: string;
|
|
5
|
+
repoPath: string;
|
|
6
|
+
plan: WorktreePlan;
|
|
7
|
+
};
|
|
8
|
+
/** The pool's whole allocation state — a serializable snapshot the persistence wrapper stores. */
|
|
9
|
+
export type WorktreePoolState = {
|
|
10
|
+
allocations: readonly WorktreeAllocation[];
|
|
11
|
+
};
|
|
12
|
+
export type WorktreePoolConfig = {
|
|
13
|
+
/** Maximum concurrent worktrees. A non-positive cap allocates nothing. */
|
|
14
|
+
maxConcurrency: number;
|
|
15
|
+
};
|
|
16
|
+
/** The empty pool — nothing allocated. */
|
|
17
|
+
export declare const EMPTY_WORKTREE_POOL: WorktreePoolState;
|
|
18
|
+
export type AcquireWorktreeResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
state: WorktreePoolState;
|
|
21
|
+
allocation: WorktreeAllocation;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: "already_allocated" | "at_capacity";
|
|
25
|
+
state: WorktreePoolState;
|
|
26
|
+
};
|
|
27
|
+
/** True when `attemptId` currently holds an allocation. Pure. */
|
|
28
|
+
export declare function isWorktreeAllocated(state: WorktreePoolState, attemptId: string): boolean;
|
|
29
|
+
/** Slots still available before the concurrency cap (never negative). Pure. */
|
|
30
|
+
export declare function availableWorktreeSlots(state: WorktreePoolState, config: WorktreePoolConfig): number;
|
|
31
|
+
/**
|
|
32
|
+
* Acquire a worktree slot for an attempt. The slot's path/branch is derived deterministically from the
|
|
33
|
+
* attempt id via {@link planWorktree}. Fails WITHOUT mutating when the attempt already holds a slot
|
|
34
|
+
* (`already_allocated`, idempotency guard) or the pool is at its concurrency cap (`at_capacity`). Pure —
|
|
35
|
+
* returns a new state on success.
|
|
36
|
+
*/
|
|
37
|
+
export declare function acquireWorktree(state: WorktreePoolState, config: WorktreePoolConfig, input: {
|
|
38
|
+
attemptId: string;
|
|
39
|
+
repoPath: string;
|
|
40
|
+
}): AcquireWorktreeResult;
|
|
41
|
+
/**
|
|
42
|
+
* Release an attempt's slot, freeing it for reuse. Pure and idempotent — releasing an attempt that holds no
|
|
43
|
+
* slot returns an equivalent state. The caller tears down the actual worktree (via the primitive) separately.
|
|
44
|
+
*/
|
|
45
|
+
export declare function releaseWorktree(state: WorktreePoolState, attemptId: string): WorktreePoolState;
|
|
46
|
+
/**
|
|
47
|
+
* Reclaim orphaned slots: free every allocation whose attempt is no longer in the live set (e.g. after a crash
|
|
48
|
+
* left the bookkeeping ahead of reality). Returns the surviving state plus the reclaimed allocations so the
|
|
49
|
+
* caller can tear down their leaked worktrees (or flag them for manual cleanup) rather than leaking forever.
|
|
50
|
+
* Pure.
|
|
51
|
+
*/
|
|
52
|
+
export declare function reclaimOrphanedWorktrees(state: WorktreePoolState, liveAttemptIds: Iterable<string>): {
|
|
53
|
+
state: WorktreePoolState;
|
|
54
|
+
reclaimed: WorktreeAllocation[];
|
|
55
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Git-worktree POOL allocator (#4297): the pure, in-memory scheduling logic for a POOL of per-attempt git
|
|
2
|
+
// worktrees across concurrent fleet attempts — acquire/release under a concurrency cap, plus orphan
|
|
3
|
+
// reclamation. Complementary to the isolation PRIMITIVE (worktree-allocator.ts, #4269), which plans/creates/
|
|
4
|
+
// tears down ONE worktree; this manages the SET of them so two concurrent attempts never collide and a crash
|
|
5
|
+
// can't leak worktree slots forever.
|
|
6
|
+
//
|
|
7
|
+
// Per #4297, the pure allocation logic lives here in gittensory-engine so it is unit-testable WITHOUT touching
|
|
8
|
+
// a real filesystem or database. The thin bookkeeping wrapper that PERSISTS this state (local SQLite, the same
|
|
9
|
+
// way claim-ledger.js / run-state.js do) is a separate miner-package layer — it holds a WorktreePoolState,
|
|
10
|
+
// calls these pure transitions, and writes the result back. No IO here: every function takes state in and
|
|
11
|
+
// returns new state out.
|
|
12
|
+
import { planWorktree } from "./worktree-allocator.js";
|
|
13
|
+
/** The empty pool — nothing allocated. */
|
|
14
|
+
export const EMPTY_WORKTREE_POOL = { allocations: [] };
|
|
15
|
+
/** True when `attemptId` currently holds an allocation. Pure. */
|
|
16
|
+
export function isWorktreeAllocated(state, attemptId) {
|
|
17
|
+
return state.allocations.some((allocation) => allocation.attemptId === attemptId);
|
|
18
|
+
}
|
|
19
|
+
/** Slots still available before the concurrency cap (never negative). Pure. */
|
|
20
|
+
export function availableWorktreeSlots(state, config) {
|
|
21
|
+
return Math.max(0, config.maxConcurrency - state.allocations.length);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Acquire a worktree slot for an attempt. The slot's path/branch is derived deterministically from the
|
|
25
|
+
* attempt id via {@link planWorktree}. Fails WITHOUT mutating when the attempt already holds a slot
|
|
26
|
+
* (`already_allocated`, idempotency guard) or the pool is at its concurrency cap (`at_capacity`). Pure —
|
|
27
|
+
* returns a new state on success.
|
|
28
|
+
*/
|
|
29
|
+
export function acquireWorktree(state, config, input) {
|
|
30
|
+
if (isWorktreeAllocated(state, input.attemptId)) {
|
|
31
|
+
return { ok: false, reason: "already_allocated", state };
|
|
32
|
+
}
|
|
33
|
+
if (state.allocations.length >= config.maxConcurrency) {
|
|
34
|
+
return { ok: false, reason: "at_capacity", state };
|
|
35
|
+
}
|
|
36
|
+
const allocation = {
|
|
37
|
+
attemptId: input.attemptId,
|
|
38
|
+
repoPath: input.repoPath,
|
|
39
|
+
plan: planWorktree({ repoPath: input.repoPath, attemptId: input.attemptId }),
|
|
40
|
+
};
|
|
41
|
+
return { ok: true, state: { allocations: [...state.allocations, allocation] }, allocation };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Release an attempt's slot, freeing it for reuse. Pure and idempotent — releasing an attempt that holds no
|
|
45
|
+
* slot returns an equivalent state. The caller tears down the actual worktree (via the primitive) separately.
|
|
46
|
+
*/
|
|
47
|
+
export function releaseWorktree(state, attemptId) {
|
|
48
|
+
const allocations = state.allocations.filter((allocation) => allocation.attemptId !== attemptId);
|
|
49
|
+
return allocations.length === state.allocations.length ? state : { allocations };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reclaim orphaned slots: free every allocation whose attempt is no longer in the live set (e.g. after a crash
|
|
53
|
+
* left the bookkeeping ahead of reality). Returns the surviving state plus the reclaimed allocations so the
|
|
54
|
+
* caller can tear down their leaked worktrees (or flag them for manual cleanup) rather than leaking forever.
|
|
55
|
+
* Pure.
|
|
56
|
+
*/
|
|
57
|
+
export function reclaimOrphanedWorktrees(state, liveAttemptIds) {
|
|
58
|
+
const live = new Set(liveAttemptIds);
|
|
59
|
+
const reclaimed = [];
|
|
60
|
+
const remaining = [];
|
|
61
|
+
for (const allocation of state.allocations) {
|
|
62
|
+
(live.has(allocation.attemptId) ? remaining : reclaimed).push(allocation);
|
|
63
|
+
}
|
|
64
|
+
return { state: reclaimed.length === 0 ? state : { allocations: remaining }, reclaimed };
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MinerGoalSpec } from "./miner-goal-spec.js";
|
|
2
|
+
/** Whether a repo's miner goal spec permits autonomous targeting (explicit opt-out only). */
|
|
3
|
+
export declare function isMinerRepoTargetable(spec: MinerGoalSpec): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Compute a [0, 1] lane-fit score from issue labels and a parsed {@link MinerGoalSpec}. Path-based fit is
|
|
6
|
+
* intentionally omitted — discovery metadata has labels only; path gating belongs in the analyze phase.
|
|
7
|
+
*/
|
|
8
|
+
export declare function computeMinerGoalLaneFit(issue: {
|
|
9
|
+
labels: readonly string[];
|
|
10
|
+
}, spec: MinerGoalSpec): number;
|
|
11
|
+
/**
|
|
12
|
+
* Lane-fit for metadata-ranked issues. Uses full path+label {@link computeLaneFit} when
|
|
13
|
+
* `candidatePaths` are present; otherwise falls back to label-only {@link computeMinerGoalLaneFit}.
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeMetadataLaneFit(issue: {
|
|
16
|
+
labels: readonly string[];
|
|
17
|
+
candidatePaths?: readonly string[] | undefined;
|
|
18
|
+
}, spec: MinerGoalSpec): number;
|