@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,278 @@
|
|
|
1
|
+
// Local create->score->self-review->decide iterate-loop orchestrator (#2333): the actual autonomous control
|
|
2
|
+
// flow Phase 3 exists to build. Repeatedly invokes a `CodingAgentDriver` (coding-agent-driver.ts), self-reviews
|
|
3
|
+
// the resulting diff against the byte-identical predicted-gate target (self-review-adapter.ts, #2334), and
|
|
4
|
+
// consults the pure policy (iterate-policy.ts, #2335) to decide -- autonomously, no human in the loop at this
|
|
5
|
+
// stage -- whether to keep iterating, hand off to Phase 4 submission, or abandon.
|
|
6
|
+
//
|
|
7
|
+
// TAGGED maintainer (not contributor) per the phase brief: this orchestration control flow is the precise
|
|
8
|
+
// chokepoint the fixed architecture skeleton's safety-tier system reserves for the owner -- it is the trigger
|
|
9
|
+
// surface for "does the system keep trying, or does it eventually open a PR" without a human approving each
|
|
10
|
+
// step, and is adjacent to the #1 slop-at-scale strategic risk (an autonomous fleet maximizing gate-pass rate
|
|
11
|
+
// can mass-produce gate-passing-but-low-value PRs).
|
|
12
|
+
//
|
|
13
|
+
// FAIL CLOSED ON AMBIGUITY: a driver run that does not complete successfully, or a self-review call that
|
|
14
|
+
// itself throws, is treated identically to a `SelfReviewOutcome` of `"ambiguous"` -- iterate-policy.ts's own
|
|
15
|
+
// precedence then abandons rather than optimistically continuing or handing off. The loop never fabricates a
|
|
16
|
+
// "pass" from anything other than a genuinely successful `runSelfReview` call.
|
|
17
|
+
//
|
|
18
|
+
// BOUNDED INSIDE THE LOOP: both the iteration ceiling (`input.maxIterations`) and the optional cumulative
|
|
19
|
+
// budget (`input.budget`, evaluated every iteration via attempt-metering.ts's `accumulateAttemptUsage`/
|
|
20
|
+
// `evaluateAttemptBudget` against real per-iteration turns/costUsd/wallClockMs -- tokens stays an honest 0,
|
|
21
|
+
// no driver reports a real token count today, #5395) are enforced here every iteration -- not left to an
|
|
22
|
+
// external caller to remember, and not just capped after the fact between loop cycles (loop-cli.js's own
|
|
23
|
+
// governor cap usage). A `maxIterations <= 0` input abandons immediately, before ever invoking the driver.
|
|
24
|
+
//
|
|
25
|
+
// AUDITABLE: every iteration's decision (continue / handoff / abandon) is recorded via the injected
|
|
26
|
+
// `appendAttemptLogEvent` dependency (attempt-log.ts's normalized event shape) before this function returns
|
|
27
|
+
// control to its caller for that iteration -- the decision trail survives independently of this function's own
|
|
28
|
+
// return value. A logging failure never alters the loop's decision (mirrors the governor-ledger and
|
|
29
|
+
// pretooluse-hook append-failure handling elsewhere in this package).
|
|
30
|
+
import { codingAgentModeExecutes } from "./coding-agent-mode.js";
|
|
31
|
+
import { invokeCodingAgentDriver } from "./coding-agent-invoke.js";
|
|
32
|
+
import { runSelfReview } from "./self-review-adapter.js";
|
|
33
|
+
import { decideNextActionWithReason, deriveSelfReviewOutcome } from "./iterate-policy.js";
|
|
34
|
+
import { accumulateAttemptUsage, evaluateAttemptBudget } from "./attempt-metering.js";
|
|
35
|
+
function buildAttemptDiffState(input, driverResult) {
|
|
36
|
+
return {
|
|
37
|
+
repoFullName: input.repoFullName,
|
|
38
|
+
contributorLogin: input.contributorLogin,
|
|
39
|
+
title: input.title,
|
|
40
|
+
...(input.body !== undefined ? { body: input.body } : {}),
|
|
41
|
+
...(input.labels !== undefined ? { labels: input.labels } : {}),
|
|
42
|
+
...(input.linkedIssues !== undefined ? { linkedIssues: input.linkedIssues } : {}),
|
|
43
|
+
...(input.authorAssociation !== undefined ? { authorAssociation: input.authorAssociation } : {}),
|
|
44
|
+
changedFiles: driverResult.changedFiles.map((path) => ({ path })),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Turn one iteration's driver result into a policy-ready {@link SelfReviewOutcome}. A driver run that did not
|
|
48
|
+
* complete successfully, or a `runSelfReview` call that itself throws, both become `"ambiguous"` -- this loop
|
|
49
|
+
* never fabricates a pass/fail from anything other than a genuinely successful self-review call. */
|
|
50
|
+
function evaluateSelfReviewOutcome(input, driverResult, deps) {
|
|
51
|
+
if (!driverResult.ok) {
|
|
52
|
+
return {
|
|
53
|
+
outcome: { kind: "ambiguous", reason: `driver run did not complete successfully${driverResult.error ? `: ${driverResult.error}` : "."}` },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const verdict = runSelfReview(buildAttemptDiffState(input, driverResult), input.reviewContext, { runSlopAssessment: deps.runSlopAssessment });
|
|
58
|
+
return { outcome: deriveSelfReviewOutcome(verdict), verdict };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return { outcome: { kind: "ambiguous", reason: `self_review_error: ${error instanceof Error ? error.message : String(error)}` } };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** A thrown driver error is normalized into the same `{ ok: false }` shape a driver returning gracefully would
|
|
65
|
+
* produce, so {@link evaluateSelfReviewOutcome} has exactly one failure path to handle, not two. Non-live modes
|
|
66
|
+
* are also resolved here, at the driver boundary, so paused/dry-run attempts never spawn the underlying agent. */
|
|
67
|
+
async function runDriverSafely(input, deps, task) {
|
|
68
|
+
if (!codingAgentModeExecutes(input.mode)) {
|
|
69
|
+
return invokeCodingAgentDriver(deps.driver, input.mode, task, {
|
|
70
|
+
append: (event) => safeAppendAttemptLogEvent(deps, event),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return await deps.driver.run(task);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return { ok: false, changedFiles: [], summary: "", error: `driver_threw: ${error instanceof Error ? error.message : String(error)}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function attemptLogEventTypeForDecision(decision) {
|
|
81
|
+
if (decision.action === "continue")
|
|
82
|
+
return "attempt_tool_edit";
|
|
83
|
+
if (decision.action === "handoff")
|
|
84
|
+
return "attempt_succeeded";
|
|
85
|
+
// abandon: a deliberate early disengagement (rejection signaled, or the self-review itself was inconclusive)
|
|
86
|
+
// reads as aborted; a genuine failure to converge (ceiling reached, or stuck with no progress) reads as
|
|
87
|
+
// failed. Both are still `action: "abandon"` in the decision itself -- this is only a coarser attempt-log
|
|
88
|
+
// classification layered on top, for the fixed six-value ATTEMPT_LOG_EVENT_TYPES vocabulary.
|
|
89
|
+
if (decision.abandonReason === "rejection_signaled" || decision.abandonReason === "self_review_ambiguous")
|
|
90
|
+
return "attempt_aborted";
|
|
91
|
+
return "attempt_failed";
|
|
92
|
+
}
|
|
93
|
+
/** A logging failure must never crash the loop or alter its decision -- mirrors the governor-ledger and
|
|
94
|
+
* pretooluse-hook append-failure handling elsewhere in this package. */
|
|
95
|
+
function safeAppendAttemptLogEvent(deps, event) {
|
|
96
|
+
try {
|
|
97
|
+
deps.appendAttemptLogEvent(event);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Deliberately swallowed -- see doc comment above.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function logDecision(input, deps, iterationNumber, decision, budgetBreaches) {
|
|
104
|
+
safeAppendAttemptLogEvent(deps, {
|
|
105
|
+
eventType: attemptLogEventTypeForDecision(decision),
|
|
106
|
+
attemptId: input.attemptId,
|
|
107
|
+
actionClass: "iterate_loop",
|
|
108
|
+
mode: input.mode,
|
|
109
|
+
reason: decision.reason,
|
|
110
|
+
payload: {
|
|
111
|
+
iterationNumber,
|
|
112
|
+
action: decision.action,
|
|
113
|
+
...(decision.abandonReason !== undefined ? { abandonReason: decision.abandonReason } : {}),
|
|
114
|
+
// Which real axis (or axes) breached, on the hard-budget-ceiling abandon path (#5395, checked directly
|
|
115
|
+
// in the loop body before self-review even runs -- see that check site's own comment) -- an operator
|
|
116
|
+
// reading the attempt log back otherwise has no way to see which axis actually tripped.
|
|
117
|
+
...(budgetBreaches.length > 0 ? { budgetBreaches } : {}),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Extract the blocker codes to carry into the next iteration's no-progress comparison. Only ever called after
|
|
123
|
+
* `decideNextActionWithReason` has returned `"continue"` for this exact `outcome` -- that function's own
|
|
124
|
+
* precedence ladder short-circuits BOTH the `"ambiguous"` and `"pass"` variants (to abandon and handoff
|
|
125
|
+
* respectively) before ever reaching its `"continue"` fallthrough, so `outcome.kind === "fail"` is guaranteed
|
|
126
|
+
* whenever this is reached from the real call site below, not just the common case.
|
|
127
|
+
*/
|
|
128
|
+
function blockerCodesFromContinuingOutcome(outcome) {
|
|
129
|
+
if (outcome.kind === "fail")
|
|
130
|
+
return outcome.blockerCodes;
|
|
131
|
+
/* v8 ignore next -- unreachable: see this function's own doc comment above. */
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
function buildHandoffPacket(input, verdict, driverResult) {
|
|
135
|
+
return {
|
|
136
|
+
worktreePath: input.workingDirectory,
|
|
137
|
+
...(input.branchRef !== undefined ? { branchRef: input.branchRef } : {}),
|
|
138
|
+
diffSummary: driverResult.summary,
|
|
139
|
+
selfReviewVerdict: verdict,
|
|
140
|
+
attemptLogReference: input.attemptId,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const ZERO_METER_TOTALS = { tokens: 0, turns: 0, wallClockMs: 0, costUsd: 0 };
|
|
144
|
+
function immediateAbandonNoIterationsPermitted(input, deps) {
|
|
145
|
+
const decision = {
|
|
146
|
+
action: "abandon",
|
|
147
|
+
abandonReason: "max_iterations_reached",
|
|
148
|
+
reason: `maxIterations (${input.maxIterations}) permits no iterations; abandoning without invoking the driver.`,
|
|
149
|
+
};
|
|
150
|
+
safeAppendAttemptLogEvent(deps, {
|
|
151
|
+
eventType: "attempt_aborted",
|
|
152
|
+
attemptId: input.attemptId,
|
|
153
|
+
actionClass: "iterate_loop",
|
|
154
|
+
mode: input.mode,
|
|
155
|
+
reason: decision.reason,
|
|
156
|
+
payload: { iterationNumber: 0, action: decision.action, abandonReason: decision.abandonReason },
|
|
157
|
+
});
|
|
158
|
+
return { outcome: "abandon", finalDecision: decision, iterationsUsed: 0, totalTurnsUsed: 0, totalCostUsd: 0, iterations: [] };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Run the full create->score->self-review->decide loop for one attempt, iteration by iteration, until
|
|
162
|
+
* iterate-policy.ts's {@link decideNextActionWithReason} reaches a terminal `"handoff"` or `"abandon"`.
|
|
163
|
+
*
|
|
164
|
+
* Every iteration: invoke the driver, self-review the resulting diff (never fabricating a pass from a failed
|
|
165
|
+
* or errored driver/self-review run), consult the policy with the running iteration/cost/no-progress state,
|
|
166
|
+
* and record the decision via the attempt-log. `"continue"` decisions loop again; `"handoff"`/`"abandon"`
|
|
167
|
+
* return immediately. See {@link IterateLoopCoreResult}'s own doc comment for why this doesn't carry
|
|
168
|
+
* `finalMeterTotals`/`budgetBreaches` itself -- {@link runIterateLoop} attaches those.
|
|
169
|
+
*/
|
|
170
|
+
async function runIterateLoopCore(input, deps, tracker) {
|
|
171
|
+
// Truncated toward zero rather than used as-is: a fractional maxIterations (a caller bug -- "how many times
|
|
172
|
+
// to run a coding agent" has no fractional meaning) would otherwise let this loop's own `for` bound and
|
|
173
|
+
// iterate-policy.ts's `iterationNumber >= maxIterations` ceiling check disagree by less than one iteration
|
|
174
|
+
// (e.g. 2.5 lets the `for` loop run a 3rd time that the ceiling check, comparing against 2.5, would not yet
|
|
175
|
+
// reject), silently permitting one extra iteration beyond the caller's intent. Normalizing once here keeps
|
|
176
|
+
// both checks watching the exact same integer ceiling.
|
|
177
|
+
const maxIterations = Math.max(0, Math.trunc(input.maxIterations));
|
|
178
|
+
if (maxIterations <= 0)
|
|
179
|
+
return immediateAbandonNoIterationsPermitted(input, deps);
|
|
180
|
+
safeAppendAttemptLogEvent(deps, {
|
|
181
|
+
eventType: "attempt_started",
|
|
182
|
+
attemptId: input.attemptId,
|
|
183
|
+
actionClass: "iterate_loop",
|
|
184
|
+
mode: input.mode,
|
|
185
|
+
reason: "iterate_loop_started",
|
|
186
|
+
payload: { maxIterations, maxTurnsPerIteration: input.maxTurnsPerIteration },
|
|
187
|
+
});
|
|
188
|
+
const nowMs = deps.nowMs ?? Date.now;
|
|
189
|
+
const iterations = [];
|
|
190
|
+
let previousBlockerCodes = null;
|
|
191
|
+
let totalTurnsUsed = 0;
|
|
192
|
+
let totalCostUsd = 0;
|
|
193
|
+
for (let iterationNumber = 1; iterationNumber <= maxIterations; iterationNumber += 1) {
|
|
194
|
+
const iterationStartMs = nowMs();
|
|
195
|
+
const driverResult = await runDriverSafely(input, deps, {
|
|
196
|
+
attemptId: input.attemptId,
|
|
197
|
+
workingDirectory: input.workingDirectory,
|
|
198
|
+
acceptanceCriteriaPath: input.acceptanceCriteriaPath,
|
|
199
|
+
instructions: input.instructions,
|
|
200
|
+
maxTurns: input.maxTurnsPerIteration,
|
|
201
|
+
});
|
|
202
|
+
const iterationElapsedMs = Math.max(0, nowMs() - iterationStartMs);
|
|
203
|
+
totalTurnsUsed += driverResult.turnsUsed ?? 0;
|
|
204
|
+
totalCostUsd += driverResult.costUsd ?? 0;
|
|
205
|
+
// tokens stays an honest 0: no CodingAgentDriver reports a real per-iteration token count today (#5395) --
|
|
206
|
+
// an absence, never a fabricated number, matching this package's costUsd discipline elsewhere.
|
|
207
|
+
tracker.totals = accumulateAttemptUsage(tracker.totals, {
|
|
208
|
+
tokens: 0,
|
|
209
|
+
turns: driverResult.turnsUsed ?? 0,
|
|
210
|
+
wallClockMs: iterationElapsedMs,
|
|
211
|
+
costUsd: driverResult.costUsd ?? 0,
|
|
212
|
+
});
|
|
213
|
+
const budgetVerdict = input.budget !== undefined ? evaluateAttemptBudget(tracker.totals, input.budget) : undefined;
|
|
214
|
+
tracker.breaches = budgetVerdict?.breaches ?? [];
|
|
215
|
+
// A reached budget ceiling is a HARD, unconditional stop -- checked and, if breached, acted on BEFORE
|
|
216
|
+
// self-review even runs, so a same-iteration pass can never bypass it. The spend/turns/time on this
|
|
217
|
+
// iteration are sunk either way (the driver already ran), but handing off anyway would let a single
|
|
218
|
+
// over-budget iteration silently defeat the entire point of wiring a ceiling in (#5395's own mid-attempt
|
|
219
|
+
// abort goal) -- so this abandons regardless of what this iteration's own result looks like.
|
|
220
|
+
if (budgetVerdict !== undefined && !budgetVerdict.withinBudget) {
|
|
221
|
+
const decision = {
|
|
222
|
+
action: "abandon",
|
|
223
|
+
abandonReason: "cost_ceiling_reached",
|
|
224
|
+
reason: `Reached the attempt's budget ceiling (${tracker.breaches.join(", ")}) on iteration ${iterationNumber}; abandoning regardless of this iteration's own result.`,
|
|
225
|
+
};
|
|
226
|
+
logDecision(input, deps, iterationNumber, decision, tracker.breaches);
|
|
227
|
+
iterations.push({ iterationNumber, driverResult, decision });
|
|
228
|
+
return { outcome: "abandon", finalDecision: decision, iterationsUsed: iterationNumber, totalTurnsUsed, totalCostUsd, iterations };
|
|
229
|
+
}
|
|
230
|
+
const { outcome: selfReview, verdict } = evaluateSelfReviewOutcome(input, driverResult, deps);
|
|
231
|
+
const state = {
|
|
232
|
+
iterationNumber,
|
|
233
|
+
maxIterations,
|
|
234
|
+
selfReview,
|
|
235
|
+
previousBlockerCodes,
|
|
236
|
+
rejectionSignaled: input.rejectionSignaled,
|
|
237
|
+
};
|
|
238
|
+
const decision = decideNextActionWithReason(state);
|
|
239
|
+
logDecision(input, deps, iterationNumber, decision, []);
|
|
240
|
+
iterations.push({ iterationNumber, driverResult, decision });
|
|
241
|
+
if (decision.action === "handoff") {
|
|
242
|
+
// Guaranteed defined: decideNextActionWithReason only reaches `"handoff"` from `selfReview.kind ===
|
|
243
|
+
// "pass"`, which evaluateSelfReviewOutcome only ever returns alongside a real, successfully computed
|
|
244
|
+
// verdict (never from the ambiguous/driver-failure path).
|
|
245
|
+
return {
|
|
246
|
+
outcome: "handoff",
|
|
247
|
+
finalDecision: decision,
|
|
248
|
+
iterationsUsed: iterationNumber,
|
|
249
|
+
totalTurnsUsed,
|
|
250
|
+
totalCostUsd,
|
|
251
|
+
iterations,
|
|
252
|
+
handoffPacket: buildHandoffPacket(input, verdict, driverResult),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (decision.action === "abandon") {
|
|
256
|
+
return { outcome: "abandon", finalDecision: decision, iterationsUsed: iterationNumber, totalTurnsUsed, totalCostUsd, iterations };
|
|
257
|
+
}
|
|
258
|
+
previousBlockerCodes = blockerCodesFromContinuingOutcome(selfReview);
|
|
259
|
+
}
|
|
260
|
+
/* v8 ignore next 8 -- unreachable in practice: decideNextActionWithReason's own `iterationNumber >=
|
|
261
|
+
* maxIterations` check guarantees an abandon by the time iterationNumber reaches the (now-integer, per the
|
|
262
|
+
* truncation above) maxIterations ceiling inside the loop above, so the for-loop above always returns.
|
|
263
|
+
* Retained as an explicit fail-closed fallback rather than an implicit `undefined` return, consistent with
|
|
264
|
+
* this package's fail-closed discipline, in case a future edit to the precedence ladder ever removes that
|
|
265
|
+
* guarantee. */
|
|
266
|
+
const fallbackDecision = { action: "abandon", abandonReason: "max_iterations_reached", reason: "Iterate loop exhausted its iteration budget." };
|
|
267
|
+
return { outcome: "abandon", finalDecision: fallbackDecision, iterationsUsed: maxIterations, totalTurnsUsed, totalCostUsd, iterations };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Thin wrapper over {@link runIterateLoopCore} that attaches the real accumulated
|
|
271
|
+
* {@link AttemptMeterTotals}/breached axes (#5395) once, at this single always-reached return point -- see
|
|
272
|
+
* {@link IterateLoopCoreResult}'s own doc comment for why the core itself doesn't carry these fields.
|
|
273
|
+
*/
|
|
274
|
+
export async function runIterateLoop(input, deps) {
|
|
275
|
+
const tracker = { totals: ZERO_METER_TOTALS, breaches: [] };
|
|
276
|
+
const core = await runIterateLoopCore(input, deps, tracker);
|
|
277
|
+
return { ...core, finalMeterTotals: tracker.totals, budgetBreaches: tracker.breaches };
|
|
278
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { SelfReviewVerdict } from "./self-review-adapter.js";
|
|
2
|
+
/** The three outcomes `decideNextAction` may reach. */
|
|
3
|
+
export type IterateLoopAction = "continue" | "handoff" | "abandon";
|
|
4
|
+
/** Every distinct reason `decideNextAction` can abandon for -- kept as a closed literal union so a caller
|
|
5
|
+
* recording the decision (the attempt-log primitive, per #2333) has a stable, exhaustive vocabulary. */
|
|
6
|
+
export type AbandonReason = "rejection_signaled" | "self_review_ambiguous" | "max_iterations_reached" | "cost_ceiling_reached" | "no_progress";
|
|
7
|
+
/**
|
|
8
|
+
* The self-review outcome as the policy needs it -- narrower than the full {@link SelfReviewVerdict} (self-
|
|
9
|
+
* review-adapter.ts, #2334) so `IterationState` stays a minimal, cheap-to-construct synthetic fixture in
|
|
10
|
+
* tests. `"ambiguous"` is NOT something {@link deriveSelfReviewOutcome} ever produces from a successfully
|
|
11
|
+
* returned verdict (a real verdict object always has a definite conclusion) -- it is constructed directly by
|
|
12
|
+
* the caller's own error handling when the self-review call itself throws (e.g. a calculator inside it errors),
|
|
13
|
+
* per #2333's own "self-review itself errors" abandon trigger.
|
|
14
|
+
*/
|
|
15
|
+
export type SelfReviewOutcome = {
|
|
16
|
+
readonly kind: "pass";
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "fail";
|
|
19
|
+
readonly blockerCodes: readonly string[];
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "ambiguous";
|
|
22
|
+
readonly reason?: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
/** Derive the policy-relevant {@link SelfReviewOutcome} from a real, successfully computed
|
|
25
|
+
* {@link SelfReviewVerdict}. Only ever returns `"pass"` or `"fail"` -- see that variant's own doc comment for
|
|
26
|
+
* why `"ambiguous"` is constructed elsewhere. */
|
|
27
|
+
export declare function deriveSelfReviewOutcome(verdict: SelfReviewVerdict): SelfReviewOutcome;
|
|
28
|
+
/**
|
|
29
|
+
* Everything `decideNextAction` needs for one iteration's decision. Deliberately minimal and synthetic-
|
|
30
|
+
* fixture-friendly -- no driver, no worktree, no IO.
|
|
31
|
+
*/
|
|
32
|
+
export type IterationState = {
|
|
33
|
+
/** 1-indexed count of iterations attempted so far, INCLUDING this one. */
|
|
34
|
+
iterationNumber: number;
|
|
35
|
+
/** Hard ceiling enforced INSIDE this policy (#2333's own deliverable: not left to an external caller to
|
|
36
|
+
* remember to enforce). `iterationNumber >= maxIterations` abandons regardless of self-review outcome. */
|
|
37
|
+
maxIterations: number;
|
|
38
|
+
/** True when the loop's own cumulative cost ceiling (e.g. total driver turns spent across every iteration of
|
|
39
|
+
* this attempt so far, not just this one) has been reached or exceeded -- the loop mechanics' (#2333) OWN
|
|
40
|
+
* "max-cost ceiling enforced inside the loop" deliverable, alongside the iteration ceiling above. This
|
|
41
|
+
* policy has no notion of what "cost" means; the caller computes the boolean from whatever cost signal it
|
|
42
|
+
* tracks. Optional and defaults to not-reached, so `IterationState` fixtures that predate this field remain
|
|
43
|
+
* valid. */
|
|
44
|
+
costCeilingReached?: boolean | undefined;
|
|
45
|
+
selfReview: SelfReviewOutcome;
|
|
46
|
+
/** The prior iteration's `fail` blocker codes, for the no-progress detector -- `null` when there is no prior
|
|
47
|
+
* iteration to compare (the first iteration, or the prior iteration did not reach a `fail` outcome). */
|
|
48
|
+
previousBlockerCodes: readonly string[] | null;
|
|
49
|
+
/** True when the target repo (or this contributor's history with it) has signaled it does not want
|
|
50
|
+
* automated/AI-authored contributions -- an explicit AI-usage-policy ban, or a prior submission from this
|
|
51
|
+
* same miner was closed/rejected on this exact repo. The caller resolves this (e.g. via the AI-policy-map
|
|
52
|
+
* signals or the rejection-state-machine primitive already shipped in `packages/gittensory-miner/lib/`) and
|
|
53
|
+
* passes it in; this policy does not compute it itself. */
|
|
54
|
+
rejectionSignaled: boolean;
|
|
55
|
+
};
|
|
56
|
+
/** Forward-looking INTERFACE for Phase 4 (submission), not an implementation of it -- Phase 4 lands as a later,
|
|
57
|
+
* separate issue. Gives it a stable target instead of reverse-engineering the shape from the loop's internals. */
|
|
58
|
+
export type HandoffPacket = {
|
|
59
|
+
/** Absolute path to (or a branch ref identifying) the worktree holding the passing attempt's changes. */
|
|
60
|
+
worktreePath: string;
|
|
61
|
+
branchRef?: string | undefined;
|
|
62
|
+
/** Human-readable summary of the final diff, for the submission's own PR description. */
|
|
63
|
+
diffSummary: string;
|
|
64
|
+
/** The PASSING self-review verdict that authorized this handoff -- always has `passesPredictedGate: true`
|
|
65
|
+
* (constructing a packet from anything else is a caller bug, not something this type can prevent statically,
|
|
66
|
+
* since `decideNextAction` is the actual enforcement point). */
|
|
67
|
+
selfReviewVerdict: SelfReviewVerdict;
|
|
68
|
+
/** Reference into the attempt-log primitive (`packages/gittensory-engine/src/miner/attempt-log.ts`) for this
|
|
69
|
+
* attempt's full decision trail. */
|
|
70
|
+
attemptLogReference: string;
|
|
71
|
+
};
|
|
72
|
+
export type IterateLoopDecision = {
|
|
73
|
+
action: IterateLoopAction;
|
|
74
|
+
/** Machine-stable, human-readable reason -- always populated, including for `"continue"` and `"handoff"`, so
|
|
75
|
+
* every decision (not just abandons) has an auditable reason string for the attempt-log. */
|
|
76
|
+
reason: string;
|
|
77
|
+
/** Populated only when `action === "abandon"`. */
|
|
78
|
+
abandonReason?: AbandonReason | undefined;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Decide the next action for one iteration. Pure; identical inputs always yield the identical decision.
|
|
82
|
+
*
|
|
83
|
+
* Precedence (each check short-circuits the ones below it):
|
|
84
|
+
* 1. `rejectionSignaled` -- ALWAYS abandons, even over an otherwise-passing self-review (disengage silently).
|
|
85
|
+
* 2. `selfReview.kind === "ambiguous"` -- abandons; never optimistically continues or hands off on ambiguity.
|
|
86
|
+
* 3. `selfReview.kind === "pass"` -- the ONLY path to `"handoff"`.
|
|
87
|
+
* 4. `iterationNumber >= maxIterations` -- abandons at the hard ceiling regardless of whether the blocker set
|
|
88
|
+
* was still changing (genuine incremental progress does not buy unlimited iterations).
|
|
89
|
+
* 5. `costCeilingReached` -- abandons at the hard cost ceiling, same rationale as the iteration ceiling above.
|
|
90
|
+
* 6. The current `fail` blocker set is identical to `previousBlockerCodes` -- abandons (no progress, stop
|
|
91
|
+
* wasting turns).
|
|
92
|
+
* 7. Otherwise -- continue.
|
|
93
|
+
*/
|
|
94
|
+
export declare function decideNextActionWithReason(state: IterationState): IterateLoopDecision;
|
|
95
|
+
/** The bare `decideNextAction(state) -> "continue" | "handoff" | "abandon"` signature this issue's deliverable
|
|
96
|
+
* calls for. For the WHY behind a decision (the attempt-log needs a reason string, not just the action), use
|
|
97
|
+
* {@link decideNextActionWithReason} -- this is a thin projection over the same logic. */
|
|
98
|
+
export declare function decideNextAction(state: IterationState): IterateLoopAction;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Iterate-loop stop/abandon/handoff policy (#2335): the explicit POLICY the orchestration loop's control flow
|
|
2
|
+
// (#2333, sibling issue) consults each iteration to decide among exactly three outcomes. Deliberately split
|
|
3
|
+
// from the loop MECHANICS (#2333) so the actual thresholds/rules are one small, individually-reviewable, pure
|
|
4
|
+
// artifact -- `decideNextAction` needs no driver, no worktree, no IO to test.
|
|
5
|
+
//
|
|
6
|
+
// STRATEGIC CONSTRAINTS this policy encodes (gittensory-miner-autonomy-roadmap):
|
|
7
|
+
// - "never auto-submit (P4) before governor+caps (P5)" -- a mandatory clean predicted-gate PASS is the ONLY
|
|
8
|
+
// path to `"handoff"`; an ambiguous or errored self-review downgrades to abandon, never optimistically
|
|
9
|
+
// hands off.
|
|
10
|
+
// - "disengage SILENTLY on rejection" (the Matplotlib "MJ Rathbun" cautionary tale) -- a rejection signal
|
|
11
|
+
// wins over EVERYTHING else, including a self-review that would otherwise pass. Continuing to submit to a
|
|
12
|
+
// repo that has already shown it does not want automated contributions is the exact anti-pattern this
|
|
13
|
+
// guards against, regardless of how good any individual attempt looks.
|
|
14
|
+
// - "reward MERGED net-positive (never submission volume)" -- the no-progress detector and the iteration/cost
|
|
15
|
+
// ceilings exist so a stuck loop stops wasting turns (or spend) chasing a submission that was never going
|
|
16
|
+
// to land, rather than grinding toward *a* submission for its own sake.
|
|
17
|
+
//
|
|
18
|
+
// AUTONOMY DIAL (not yet wired): `src/settings/autonomy.ts`'s `resolveAutonomy`/`isActingAutonomyLevel` is the
|
|
19
|
+
// existing reusable deny-by-default pattern this policy's eventual live autonomy-level check should consult
|
|
20
|
+
// once `.gittensory-miner.yml` defines autonomy fields -- that wiring is explicitly left to a later phase; this
|
|
21
|
+
// module's `decideNextAction` is autonomy-level-agnostic today.
|
|
22
|
+
/** Derive the policy-relevant {@link SelfReviewOutcome} from a real, successfully computed
|
|
23
|
+
* {@link SelfReviewVerdict}. Only ever returns `"pass"` or `"fail"` -- see that variant's own doc comment for
|
|
24
|
+
* why `"ambiguous"` is constructed elsewhere. */
|
|
25
|
+
export function deriveSelfReviewOutcome(verdict) {
|
|
26
|
+
if (verdict.passesPredictedGate)
|
|
27
|
+
return { kind: "pass" };
|
|
28
|
+
return { kind: "fail", blockerCodes: verdict.predictedGateVerdict.blockers.map((blocker) => blocker.code) };
|
|
29
|
+
}
|
|
30
|
+
function blockerSetsEqual(current, previous) {
|
|
31
|
+
if (current.length !== previous.length)
|
|
32
|
+
return false;
|
|
33
|
+
const currentSet = new Set(current);
|
|
34
|
+
const previousSet = new Set(previous);
|
|
35
|
+
if (currentSet.size !== previousSet.size)
|
|
36
|
+
return false;
|
|
37
|
+
for (const code of currentSet)
|
|
38
|
+
if (!previousSet.has(code))
|
|
39
|
+
return false;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Decide the next action for one iteration. Pure; identical inputs always yield the identical decision.
|
|
44
|
+
*
|
|
45
|
+
* Precedence (each check short-circuits the ones below it):
|
|
46
|
+
* 1. `rejectionSignaled` -- ALWAYS abandons, even over an otherwise-passing self-review (disengage silently).
|
|
47
|
+
* 2. `selfReview.kind === "ambiguous"` -- abandons; never optimistically continues or hands off on ambiguity.
|
|
48
|
+
* 3. `selfReview.kind === "pass"` -- the ONLY path to `"handoff"`.
|
|
49
|
+
* 4. `iterationNumber >= maxIterations` -- abandons at the hard ceiling regardless of whether the blocker set
|
|
50
|
+
* was still changing (genuine incremental progress does not buy unlimited iterations).
|
|
51
|
+
* 5. `costCeilingReached` -- abandons at the hard cost ceiling, same rationale as the iteration ceiling above.
|
|
52
|
+
* 6. The current `fail` blocker set is identical to `previousBlockerCodes` -- abandons (no progress, stop
|
|
53
|
+
* wasting turns).
|
|
54
|
+
* 7. Otherwise -- continue.
|
|
55
|
+
*/
|
|
56
|
+
export function decideNextActionWithReason(state) {
|
|
57
|
+
if (state.rejectionSignaled) {
|
|
58
|
+
return { action: "abandon", abandonReason: "rejection_signaled", reason: "Repo or contributor has signaled it does not want automated contributions; disengaging silently rather than retry-hammering." };
|
|
59
|
+
}
|
|
60
|
+
if (state.selfReview.kind === "ambiguous") {
|
|
61
|
+
return {
|
|
62
|
+
action: "abandon",
|
|
63
|
+
abandonReason: "self_review_ambiguous",
|
|
64
|
+
reason: `Self-review could not conclusively determine pass/fail${state.selfReview.reason ? `: ${state.selfReview.reason}` : "."} Downgrading to abandon rather than optimistically handing off.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (state.selfReview.kind === "pass") {
|
|
68
|
+
return { action: "handoff", reason: "Self-review reached a clean predicted-gate pass." };
|
|
69
|
+
}
|
|
70
|
+
if (state.iterationNumber >= state.maxIterations) {
|
|
71
|
+
return {
|
|
72
|
+
action: "abandon",
|
|
73
|
+
abandonReason: "max_iterations_reached",
|
|
74
|
+
reason: `Reached the iteration ceiling (${state.maxIterations}) without a clean predicted-gate pass.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (state.costCeilingReached === true) {
|
|
78
|
+
return {
|
|
79
|
+
action: "abandon",
|
|
80
|
+
abandonReason: "cost_ceiling_reached",
|
|
81
|
+
reason: "Reached the attempt's cost ceiling (cumulative driver spend across every iteration so far) without a clean predicted-gate pass.",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (state.previousBlockerCodes !== null && blockerSetsEqual(state.selfReview.blockerCodes, state.previousBlockerCodes)) {
|
|
85
|
+
return {
|
|
86
|
+
action: "abandon",
|
|
87
|
+
abandonReason: "no_progress",
|
|
88
|
+
reason: `Blocker set unchanged from the prior iteration (${state.selfReview.blockerCodes.join(", ") || "no blockers listed"}); stopping rather than repeating an attempt that is not converging.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { action: "continue", reason: "Self-review still failing but the blocker set changed since the prior iteration; continuing." };
|
|
92
|
+
}
|
|
93
|
+
/** The bare `decideNextAction(state) -> "continue" | "handoff" | "abandon"` signature this issue's deliverable
|
|
94
|
+
* calls for. For the WHY behind a decision (the attempt-log needs a reason string, not just the action), use
|
|
95
|
+
* {@link decideNextActionWithReason} -- this is a thin projection over the same logic. */
|
|
96
|
+
export function decideNextAction(state) {
|
|
97
|
+
return decideNextActionWithReason(state).action;
|
|
98
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CodingAgentDriverResult } from "./coding-agent-driver.js";
|
|
2
|
+
/** Which existing check governs a changed file. `root` covers `src/**` and any non-`.js` file under
|
|
3
|
+
* `packages/gittensory-miner`/`packages/gittensory-mcp` (e.g. a hand-written `.d.ts`), since those are
|
|
4
|
+
* type-checked by the root `tsc --noEmit`, not `node --check`. */
|
|
5
|
+
export type LintGuardPackage = "ui" | "engine" | "miner-js" | "mcp-js" | "root";
|
|
6
|
+
/** Classify a changed file path (POSIX or Windows separators) into the package whose existing check governs
|
|
7
|
+
* it. Pure path matching -- no filesystem access. */
|
|
8
|
+
export declare function classifyLintGuardPackage(path: string): LintGuardPackage;
|
|
9
|
+
/** Injected process runner -- real IO lives here, not in `guardChangedFiles`, so tests never spawn a real
|
|
10
|
+
* subprocess. `ok` is derived from the exit code by the caller of this function, not by this type. */
|
|
11
|
+
export type LintGuardSpawnFn = (cmd: string, args: readonly string[], opts: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
}) => Promise<{
|
|
14
|
+
code: number | null;
|
|
15
|
+
output: string;
|
|
16
|
+
}>;
|
|
17
|
+
export type LintGuardCheckResult = {
|
|
18
|
+
package: LintGuardPackage;
|
|
19
|
+
file: string;
|
|
20
|
+
command: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
output: string;
|
|
23
|
+
};
|
|
24
|
+
/** Structured result -- never a thrown exception -- so a caller (the self-review loop, #2333) can
|
|
25
|
+
* distinguish "the edit doesn't typecheck" (a `checks` entry with `ok: false`) from "the coding agent
|
|
26
|
+
* itself failed" (a separate concern entirely, see {@link guardCodingAgentDriverResult}). */
|
|
27
|
+
export type LintGuardResult = {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
checks: readonly LintGuardCheckResult[];
|
|
30
|
+
};
|
|
31
|
+
export type LintGuardOptions = {
|
|
32
|
+
spawn: LintGuardSpawnFn;
|
|
33
|
+
/** Repo root the checks run from. Default: `process.cwd()`. */
|
|
34
|
+
cwd?: string | undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Run the existing check for every package a changed file belongs to. One check per package group, not one
|
|
38
|
+
* per file (except `node --check`, which is inherently per-file) -- `tsc`/`ui:typecheck` validate a whole
|
|
39
|
+
* package at once, so re-running them per file would be redundant work, not extra coverage.
|
|
40
|
+
*/
|
|
41
|
+
export declare function guardChangedFiles(changedFiles: readonly string[], options: LintGuardOptions): Promise<LintGuardResult>;
|
|
42
|
+
export type LintGuardedDriverResult = CodingAgentDriverResult & {
|
|
43
|
+
lintGuard: LintGuardResult;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Decorate a `CodingAgentDriver` result with its lint-guard verdict. Skips the guard entirely (an empty,
|
|
47
|
+
* passing `lintGuard`) when the driver itself failed or reported no changed files -- there is nothing to
|
|
48
|
+
* check, and running checks against an untouched tree would only produce a misleading unrelated result.
|
|
49
|
+
*/
|
|
50
|
+
export declare function guardCodingAgentDriverResult(result: CodingAgentDriverResult, options: LintGuardOptions): Promise<LintGuardedDriverResult>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const MINER_JS_EXTENSION = /\.(js|mjs|cjs)$/;
|
|
2
|
+
/** Classify a changed file path (POSIX or Windows separators) into the package whose existing check governs
|
|
3
|
+
* it. Pure path matching -- no filesystem access. */
|
|
4
|
+
export function classifyLintGuardPackage(path) {
|
|
5
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6
|
+
if (normalized.startsWith("apps/gittensory-ui/"))
|
|
7
|
+
return "ui";
|
|
8
|
+
if (normalized.startsWith("packages/gittensory-engine/"))
|
|
9
|
+
return "engine";
|
|
10
|
+
if (normalized.startsWith("packages/gittensory-miner/") && MINER_JS_EXTENSION.test(normalized))
|
|
11
|
+
return "miner-js";
|
|
12
|
+
if (normalized.startsWith("packages/gittensory-mcp/") && MINER_JS_EXTENSION.test(normalized))
|
|
13
|
+
return "mcp-js";
|
|
14
|
+
return "root";
|
|
15
|
+
}
|
|
16
|
+
const PACKAGE_COMMAND = Object.freeze({
|
|
17
|
+
root: Object.freeze(["npm", "run", "typecheck"]),
|
|
18
|
+
engine: Object.freeze(["npm", "run", "build", "--workspace", "@loopover/engine"]),
|
|
19
|
+
ui: Object.freeze(["npm", "run", "ui:typecheck"]),
|
|
20
|
+
});
|
|
21
|
+
async function runPackageCheck(pkg, files, spawn, cwd) {
|
|
22
|
+
if (pkg === "miner-js" || pkg === "mcp-js") {
|
|
23
|
+
// node --check is inherently per-file, unlike the whole-package tsc/ui:lint commands below.
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const { code, output } = await spawn("node", ["--check", file], { cwd });
|
|
27
|
+
results.push({ package: pkg, file, command: `node --check ${file}`, ok: code === 0, output });
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
const command = PACKAGE_COMMAND[pkg];
|
|
32
|
+
const { code, output } = await spawn(command[0], command.slice(1), { cwd });
|
|
33
|
+
return [{ package: pkg, file: files.join(", "), command: command.join(" "), ok: code === 0, output }];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run the existing check for every package a changed file belongs to. One check per package group, not one
|
|
37
|
+
* per file (except `node --check`, which is inherently per-file) -- `tsc`/`ui:typecheck` validate a whole
|
|
38
|
+
* package at once, so re-running them per file would be redundant work, not extra coverage.
|
|
39
|
+
*/
|
|
40
|
+
export async function guardChangedFiles(changedFiles, options) {
|
|
41
|
+
const cwd = options.cwd ?? process.cwd();
|
|
42
|
+
const byPackage = new Map();
|
|
43
|
+
for (const file of changedFiles) {
|
|
44
|
+
const pkg = classifyLintGuardPackage(file);
|
|
45
|
+
const list = byPackage.get(pkg);
|
|
46
|
+
if (list)
|
|
47
|
+
list.push(file);
|
|
48
|
+
else
|
|
49
|
+
byPackage.set(pkg, [file]);
|
|
50
|
+
}
|
|
51
|
+
const checks = [];
|
|
52
|
+
for (const [pkg, files] of byPackage) {
|
|
53
|
+
checks.push(...(await runPackageCheck(pkg, files, options.spawn, cwd)));
|
|
54
|
+
}
|
|
55
|
+
return { ok: checks.every((check) => check.ok), checks };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decorate a `CodingAgentDriver` result with its lint-guard verdict. Skips the guard entirely (an empty,
|
|
59
|
+
* passing `lintGuard`) when the driver itself failed or reported no changed files -- there is nothing to
|
|
60
|
+
* check, and running checks against an untouched tree would only produce a misleading unrelated result.
|
|
61
|
+
*/
|
|
62
|
+
export async function guardCodingAgentDriverResult(result, options) {
|
|
63
|
+
if (!result.ok || result.changedFiles.length === 0) {
|
|
64
|
+
return { ...result, lintGuard: { ok: true, checks: [] } };
|
|
65
|
+
}
|
|
66
|
+
const lintGuard = await guardChangedFiles(result.changedFiles, options);
|
|
67
|
+
return { ...result, ok: result.ok && lintGuard.ok, lintGuard };
|
|
68
|
+
}
|