@dev-loops/core 0.1.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/bin/capture-deep-persona-signals.mjs +143 -0
- package/bin/ensure-phase-files.mjs +7 -0
- package/bin/log-bash-exit-1.mjs +7 -0
- package/bin/parse-review-threads.mjs +7 -0
- package/package.json +78 -0
- package/src/analysis/change-classifier.mjs +146 -0
- package/src/analysis/diff-analyzer.mjs +285 -0
- package/src/bash-exit-one.mjs +130 -0
- package/src/cli/helpers.mjs +22 -0
- package/src/cli/primitives.mjs +70 -0
- package/src/cli/retry-wrapper.mjs +169 -0
- package/src/cli/subcommand-runner.mjs +246 -0
- package/src/config/config.mjs +965 -0
- package/src/debt/cluster.mjs +240 -0
- package/src/debt/debt-finding.mjs +68 -0
- package/src/debt/debt-signal.mjs +46 -0
- package/src/debt/deep-persona-signals.mjs +266 -0
- package/src/debt/remediation-to-issue.mjs +121 -0
- package/src/debt/score.mjs +127 -0
- package/src/debt/shape.mjs +214 -0
- package/src/github/copilot-helpers.mjs +343 -0
- package/src/github/repo-slug.mjs +105 -0
- package/src/github/review-threads.mjs +343 -0
- package/src/harness/adapter.mjs +57 -0
- package/src/harness/index.mjs +3 -0
- package/src/harness/noop-adapter.mjs +22 -0
- package/src/harness/pi-adapter.mjs +47 -0
- package/src/loop/async-start-contract.mjs +170 -0
- package/src/loop/conductor-routing.mjs +817 -0
- package/src/loop/copilot-ci-status.mjs +255 -0
- package/src/loop/copilot-loop-iterations.mjs +161 -0
- package/src/loop/copilot-loop-state.mjs +510 -0
- package/src/loop/handoff-envelope.mjs +800 -0
- package/src/loop/issue-refinement-artifact.mjs +268 -0
- package/src/loop/lifecycle-state.mjs +342 -0
- package/src/loop/phase-files.mjs +187 -0
- package/src/loop/policy-constants.mjs +17 -0
- package/src/loop/pr-gate-coordination.mjs +1278 -0
- package/src/loop/public-dev-loop-routing-contract.mjs +277 -0
- package/src/loop/public-dev-loop-routing.mjs +1746 -0
- package/src/loop/queue-board-ordering.mjs +38 -0
- package/src/loop/queue-board-sync.mjs +223 -0
- package/src/loop/queue-driver.mjs +164 -0
- package/src/loop/queue-parallel.mjs +190 -0
- package/src/loop/queue-state.mjs +230 -0
- package/src/loop/retrospective-checkpoint.mjs +178 -0
- package/src/loop/reviewer-loop-state.mjs +456 -0
- package/src/loop/run-inspection.mjs +604 -0
- package/src/loop/steering.mjs +793 -0
- package/src/loop/timeout-policy.mjs +73 -0
- package/src/loop/tracker-first-loop-state.mjs +87 -0
- package/src/loop/tracker-pr-state.mjs +301 -0
- package/src/loop/worktree-guard.mjs +141 -0
- package/src/refinement/ac-dod-matrix.mjs +95 -0
|
@@ -0,0 +1,1746 @@
|
|
|
1
|
+
import {
|
|
2
|
+
evaluateRetrospectiveGate,
|
|
3
|
+
normalizeRetrospectiveCheckpointState,
|
|
4
|
+
} from "./retrospective-checkpoint.mjs";
|
|
5
|
+
import {
|
|
6
|
+
EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
|
|
7
|
+
PERSISTENT_INTERNAL_WAIT_TIMEOUT_POLICY,
|
|
8
|
+
} from "./timeout-policy.mjs";
|
|
9
|
+
import {
|
|
10
|
+
DEV_LOOP_ACTOR,
|
|
11
|
+
DEV_LOOP_ARTIFACT_STATE,
|
|
12
|
+
DEV_LOOP_AUTHORIZATION,
|
|
13
|
+
DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION,
|
|
14
|
+
DEV_LOOP_EXECUTION_MODE,
|
|
15
|
+
DEV_LOOP_GATE,
|
|
16
|
+
DEV_LOOP_ISSUE_ASSIGNMENT_SEAM,
|
|
17
|
+
DEV_LOOP_ISSUE_ASSIGNMENT_STATE,
|
|
18
|
+
DEV_LOOP_ISSUE_LINKAGE_RESOLUTION,
|
|
19
|
+
DEV_LOOP_ISSUE_READINESS,
|
|
20
|
+
DEV_LOOP_PUBLIC_INTENT,
|
|
21
|
+
DEV_LOOP_ROUTE_KIND,
|
|
22
|
+
DEV_LOOP_STARTUP_RESUME_BUNDLE_KIND,
|
|
23
|
+
DEV_LOOP_STATUS,
|
|
24
|
+
DEV_LOOP_STATUS_REPORT_KIND,
|
|
25
|
+
DEV_LOOP_TARGET_KIND,
|
|
26
|
+
DEV_LOOP_TARGET_PREFERENCE,
|
|
27
|
+
DEV_LOOP_VARIATION_PARAMETER_CONTRACT,
|
|
28
|
+
DEV_LOOP_WAIT_SEMANTICS,
|
|
29
|
+
INTERNAL_DEV_LOOP_STRATEGY,
|
|
30
|
+
PUBLIC_DEV_LOOP_ENTRYPOINT,
|
|
31
|
+
} from "./public-dev-loop-routing-contract.mjs";
|
|
32
|
+
|
|
33
|
+
export * from "./public-dev-loop-routing-contract.mjs";
|
|
34
|
+
|
|
35
|
+
const COPILOT_ISSUE_ASSIGNEE = "copilot-swe-agent";
|
|
36
|
+
|
|
37
|
+
const TARGET_KIND_SET = new Set(Object.values(DEV_LOOP_TARGET_KIND));
|
|
38
|
+
const ACTOR_SET = new Set(Object.values(DEV_LOOP_ACTOR));
|
|
39
|
+
const STATUS_SET = new Set(Object.values(DEV_LOOP_STATUS));
|
|
40
|
+
const AUTHORIZATION_SET = new Set(Object.values(DEV_LOOP_AUTHORIZATION));
|
|
41
|
+
const INTENT_SET = new Set(Object.values(DEV_LOOP_PUBLIC_INTENT));
|
|
42
|
+
const ARTIFACT_STATE_SET = new Set(Object.values(DEV_LOOP_ARTIFACT_STATE));
|
|
43
|
+
const ISSUE_LINKAGE_RESOLUTION_SET = new Set(Object.values(DEV_LOOP_ISSUE_LINKAGE_RESOLUTION));
|
|
44
|
+
const ISSUE_READINESS_SET = new Set(Object.values(DEV_LOOP_ISSUE_READINESS));
|
|
45
|
+
const ISSUE_ASSIGNMENT_STATE_SET = new Set(Object.values(DEV_LOOP_ISSUE_ASSIGNMENT_STATE));
|
|
46
|
+
const VARIATION_MODE_SET = new Set(DEV_LOOP_VARIATION_PARAMETER_CONTRACT.allowedModeValues);
|
|
47
|
+
const TARGET_PREFERENCE_SET = new Set(DEV_LOOP_VARIATION_PARAMETER_CONTRACT.allowedTargetPreferenceValues);
|
|
48
|
+
const GATE_REVIEW_VERDICT_SET = new Set(["clean", "findings_present", "blocked"]);
|
|
49
|
+
const ALLOWED_MODE_VALUES_TEXT = DEV_LOOP_VARIATION_PARAMETER_CONTRACT.allowedModeValues.join(", ");
|
|
50
|
+
const ALLOWED_TARGET_PREFERENCE_VALUES_TEXT = DEV_LOOP_VARIATION_PARAMETER_CONTRACT.allowedTargetPreferenceValues.join(", ");
|
|
51
|
+
const LINKED_PR_READY_FOR_FOLLOWUP_LOOP_STATE = "linked_pr_ready_for_followup";
|
|
52
|
+
const PRIOR_LINKED_PR_CLOSED_UNMERGED_LOOP_STATE = "prior_linked_pr_closed_unmerged";
|
|
53
|
+
|
|
54
|
+
function normalizeIntent(intent) {
|
|
55
|
+
const normalized = typeof intent === "string" ? intent.trim().toLowerCase() : "";
|
|
56
|
+
return INTENT_SET.has(normalized) ? normalized : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeTarget(target) {
|
|
60
|
+
if (!target || typeof target !== "object") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const kind = typeof target.kind === "string" ? target.kind.trim().toLowerCase() : "";
|
|
65
|
+
if (!TARGET_KIND_SET.has(kind)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const issue = Number.isInteger(target.issue) && target.issue > 0 ? target.issue : null;
|
|
70
|
+
const hasPr = Object.hasOwn(target, "pr") && target.pr !== null && target.pr !== undefined;
|
|
71
|
+
const pr = Number.isInteger(target.pr) && target.pr > 0 ? target.pr : null;
|
|
72
|
+
const hasLinkedPr = Object.hasOwn(target, "linkedPr") && target.linkedPr !== null && target.linkedPr !== undefined;
|
|
73
|
+
const linkedPr = Number.isInteger(target.linkedPr) && target.linkedPr > 0 ? target.linkedPr : null;
|
|
74
|
+
const branch = typeof target.branch === "string" && target.branch.trim().length > 0 ? target.branch.trim() : null;
|
|
75
|
+
const phase = typeof target.phase === "string" && target.phase.trim().length > 0 ? target.phase.trim() : null;
|
|
76
|
+
|
|
77
|
+
if (kind === DEV_LOOP_TARGET_KIND.ISSUE && issue === null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (kind === DEV_LOOP_TARGET_KIND.ISSUE && hasLinkedPr && linkedPr === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (kind === DEV_LOOP_TARGET_KIND.ISSUE && hasPr) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (kind === DEV_LOOP_TARGET_KIND.PR && pr === null) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (kind === DEV_LOOP_TARGET_KIND.PR && hasLinkedPr) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (kind === DEV_LOOP_TARGET_KIND.LOCAL_BRANCH && branch === null) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (kind === DEV_LOOP_TARGET_KIND.LOCAL_PHASE && phase === null && issue === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { kind, issue, pr, linkedPr, branch, phase };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeActor(value) {
|
|
103
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
104
|
+
return ACTOR_SET.has(normalized) ? normalized : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeSha(value) {
|
|
108
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeGateReviewVerdict(value) {
|
|
112
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
113
|
+
return GATE_REVIEW_VERDICT_SET.has(normalized) ? normalized : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeGateReviewEvidence(evidence) {
|
|
117
|
+
if (evidence === undefined || evidence === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (typeof evidence !== "object") {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const preApprovalGate = evidence.preApprovalGate;
|
|
125
|
+
if (!preApprovalGate || typeof preApprovalGate !== "object") {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
currentHeadSha: normalizeSha(evidence.currentHeadSha),
|
|
131
|
+
preApprovalGate: {
|
|
132
|
+
visible: preApprovalGate.visible === true,
|
|
133
|
+
headSha: normalizeSha(preApprovalGate.headSha),
|
|
134
|
+
verdict: normalizeGateReviewVerdict(preApprovalGate.verdict),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isFinalApprovalState(canonicalState) {
|
|
140
|
+
return canonicalState.status === DEV_LOOP_STATUS.APPROVAL_READY
|
|
141
|
+
|| canonicalState.status === DEV_LOOP_STATUS.MERGE_READY;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasCleanVisibleCurrentHeadPreApprovalGate(gateReviewEvidence) {
|
|
145
|
+
return gateReviewEvidence !== null
|
|
146
|
+
&& gateReviewEvidence.currentHeadSha !== null
|
|
147
|
+
&& gateReviewEvidence.preApprovalGate.visible
|
|
148
|
+
&& gateReviewEvidence.preApprovalGate.verdict === "clean"
|
|
149
|
+
&& gateReviewEvidence.preApprovalGate.headSha === gateReviewEvidence.currentHeadSha;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeState(currentState) {
|
|
153
|
+
if (!currentState || typeof currentState !== "object") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const target = normalizeTarget(currentState.target);
|
|
158
|
+
const ownership = normalizeActor(currentState.ownership);
|
|
159
|
+
const nextActor = normalizeActor(currentState.nextActor);
|
|
160
|
+
const status = typeof currentState.status === "string" ? currentState.status.trim().toLowerCase() : "";
|
|
161
|
+
const authorization =
|
|
162
|
+
typeof currentState.authorization === "string" ? currentState.authorization.trim().toLowerCase() : "";
|
|
163
|
+
|
|
164
|
+
if (!target || !ownership || !nextActor || !STATUS_SET.has(status) || !AUTHORIZATION_SET.has(authorization)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { target, ownership, nextActor, status, authorization };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeArtifactState(value) {
|
|
172
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
173
|
+
return ARTIFACT_STATE_SET.has(normalized) ? normalized : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeOptionalLoopState(value) {
|
|
177
|
+
if (value === undefined || value === null) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
181
|
+
if (normalized.length === 0 || normalized.toLowerCase() === "unknown") {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return normalized;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeAsyncRunId(value) {
|
|
188
|
+
const asString = normalizeSha(value);
|
|
189
|
+
if (asString !== null) return asString;
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeAsyncRun(value) {
|
|
194
|
+
if (value === undefined || value === null) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (!value || typeof value !== "object") {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const kind = typeof value.kind === "string" ? value.kind.trim().toLowerCase() : "";
|
|
202
|
+
if (kind !== "pi_managed_run" && kind !== "detached_process") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const hasInspectionState = value.inspectionState !== undefined && value.inspectionState !== null;
|
|
206
|
+
const inspectionState = hasInspectionState
|
|
207
|
+
? (typeof value.inspectionState === "string" ? value.inspectionState.trim().toLowerCase() : "")
|
|
208
|
+
: null;
|
|
209
|
+
if (
|
|
210
|
+
hasInspectionState
|
|
211
|
+
&& inspectionState !== "visible"
|
|
212
|
+
&& inspectionState !== "hidden"
|
|
213
|
+
&& inspectionState !== "stale"
|
|
214
|
+
&& inspectionState !== "uninspectable"
|
|
215
|
+
&& inspectionState !== "missing"
|
|
216
|
+
) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
kind,
|
|
222
|
+
runId: normalizeAsyncRunId(value.runId),
|
|
223
|
+
visible: value.visible === true,
|
|
224
|
+
inspectionState,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function normalizeIssueLinkageResolution(value) {
|
|
229
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
230
|
+
return ISSUE_LINKAGE_RESOLUTION_SET.has(normalized) ? normalized : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeIssueReadiness(value) {
|
|
234
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
235
|
+
return ISSUE_READINESS_SET.has(normalized) ? normalized : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeIssueAssignmentState(value) {
|
|
239
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
240
|
+
return ISSUE_ASSIGNMENT_STATE_SET.has(normalized) ? normalized : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeVariationMode(value) {
|
|
244
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
245
|
+
return VARIATION_MODE_SET.has(normalized) ? normalized : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeTargetPreference(value) {
|
|
249
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
250
|
+
return TARGET_PREFERENCE_SET.has(normalized) ? normalized : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function applyRetrospectiveCheckpointGate(result, checkpointState, checkpointStateProvided) {
|
|
254
|
+
if (!checkpointStateProvided) {
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return evaluateRetrospectiveGate({
|
|
259
|
+
checkpointState,
|
|
260
|
+
proposedRouting: result,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveStopClassification({ selectedGate, routeKind, canonicalState = null }) {
|
|
265
|
+
if (routeKind === DEV_LOOP_ROUTE_KIND.NEEDS_RECONCILE || selectedGate === DEV_LOOP_GATE.FAIL_CLOSED_RECONCILE) {
|
|
266
|
+
return DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.RECONCILE;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (routeKind === DEV_LOOP_ROUTE_KIND.INSPECT) {
|
|
270
|
+
return DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.INSPECT;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (routeKind === DEV_LOOP_ROUTE_KIND.WAIT) {
|
|
274
|
+
return DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.HEALTHY_WAIT;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (selectedGate === DEV_LOOP_GATE.STOP_DONE_TERMINAL || canonicalState?.status === DEV_LOOP_STATUS.DONE) {
|
|
278
|
+
return DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.TERMINAL;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (selectedGate === DEV_LOOP_GATE.WAITING_FOR_MERGE_AUTHORIZATION) {
|
|
282
|
+
return DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.AUTHORIZATION_GATED;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (selectedGate === DEV_LOOP_GATE.STOP_BLOCKED_OR_NOT_AUTHORIZED) {
|
|
286
|
+
return canonicalState?.status === DEV_LOOP_STATUS.BLOCKED
|
|
287
|
+
? DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.BLOCKED
|
|
288
|
+
: DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.AUTHORIZATION_GATED;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (routeKind === DEV_LOOP_ROUTE_KIND.STOP) {
|
|
292
|
+
return DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.BLOCKED;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.ROUTED_FOLLOWUP;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildContractTrace({
|
|
299
|
+
publicEntrypoint = PUBLIC_DEV_LOOP_ENTRYPOINT,
|
|
300
|
+
selectedGate,
|
|
301
|
+
routeKind,
|
|
302
|
+
selectedStrategy,
|
|
303
|
+
executionMode,
|
|
304
|
+
waitSemantics,
|
|
305
|
+
waitTimeoutPolicy,
|
|
306
|
+
canonicalState,
|
|
307
|
+
reason,
|
|
308
|
+
watchRequested = false,
|
|
309
|
+
boundary = null,
|
|
310
|
+
}) {
|
|
311
|
+
const effectiveTimeoutMs = waitTimeoutPolicy?.defaultTimeoutMs ?? null;
|
|
312
|
+
return {
|
|
313
|
+
publicEntrypoint,
|
|
314
|
+
decision: {
|
|
315
|
+
selectedGate,
|
|
316
|
+
routeKind,
|
|
317
|
+
selectedStrategy,
|
|
318
|
+
executionMode,
|
|
319
|
+
watchRequested,
|
|
320
|
+
contractClassification: resolveStopClassification({ selectedGate, routeKind, canonicalState }),
|
|
321
|
+
contractJustification: reason,
|
|
322
|
+
},
|
|
323
|
+
waitStrategy: {
|
|
324
|
+
selectedStrategy: routeKind === DEV_LOOP_ROUTE_KIND.WAIT ? selectedStrategy : null,
|
|
325
|
+
waitSemantics,
|
|
326
|
+
waitMode: routeKind === DEV_LOOP_ROUTE_KIND.WAIT ? "persistent_watch" : "not_applicable",
|
|
327
|
+
timeoutPolicyClassification: waitTimeoutPolicy?.classification ?? null,
|
|
328
|
+
effectiveTimeoutMs,
|
|
329
|
+
effectivePollIntervalMs: null,
|
|
330
|
+
},
|
|
331
|
+
stopReason: {
|
|
332
|
+
classification: resolveStopClassification({ selectedGate, routeKind, canonicalState }),
|
|
333
|
+
terminal: selectedGate === DEV_LOOP_GATE.STOP_DONE_TERMINAL || canonicalState?.status === DEV_LOOP_STATUS.DONE,
|
|
334
|
+
reason,
|
|
335
|
+
},
|
|
336
|
+
stateRefresh: boundary ?? null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function withContractTrace(result, { watchRequested = false, boundary = null } = {}) {
|
|
341
|
+
return {
|
|
342
|
+
...result,
|
|
343
|
+
contractTrace: buildContractTrace({
|
|
344
|
+
...result,
|
|
345
|
+
watchRequested,
|
|
346
|
+
boundary,
|
|
347
|
+
}),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function buildResult({
|
|
352
|
+
selectedGate,
|
|
353
|
+
routeKind,
|
|
354
|
+
selectedStrategy,
|
|
355
|
+
canonicalState,
|
|
356
|
+
nextAction,
|
|
357
|
+
reason,
|
|
358
|
+
executionMode = DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF,
|
|
359
|
+
waitSemantics = DEV_LOOP_WAIT_SEMANTICS.DEFAULT,
|
|
360
|
+
waitTimeoutPolicy = null,
|
|
361
|
+
issueAssignmentSeam = DEV_LOOP_ISSUE_ASSIGNMENT_SEAM.NOT_APPLICABLE,
|
|
362
|
+
watchRequested = false,
|
|
363
|
+
contractTraceBoundary = null,
|
|
364
|
+
}) {
|
|
365
|
+
return {
|
|
366
|
+
publicEntrypoint: PUBLIC_DEV_LOOP_ENTRYPOINT,
|
|
367
|
+
selectedGate,
|
|
368
|
+
routeKind,
|
|
369
|
+
selectedStrategy,
|
|
370
|
+
executionMode,
|
|
371
|
+
waitSemantics,
|
|
372
|
+
waitTimeoutPolicy,
|
|
373
|
+
canonicalState,
|
|
374
|
+
issueAssignmentSeam,
|
|
375
|
+
nextAction,
|
|
376
|
+
reason,
|
|
377
|
+
contractTrace: buildContractTrace({
|
|
378
|
+
selectedGate,
|
|
379
|
+
routeKind,
|
|
380
|
+
selectedStrategy,
|
|
381
|
+
executionMode,
|
|
382
|
+
waitSemantics,
|
|
383
|
+
waitTimeoutPolicy,
|
|
384
|
+
canonicalState,
|
|
385
|
+
reason,
|
|
386
|
+
watchRequested,
|
|
387
|
+
boundary: contractTraceBoundary,
|
|
388
|
+
}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function buildReconcile(
|
|
393
|
+
reason,
|
|
394
|
+
canonicalState = null,
|
|
395
|
+
executionMode = DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF,
|
|
396
|
+
{ watchRequested = false, contractTraceBoundary = null } = {},
|
|
397
|
+
) {
|
|
398
|
+
return buildResult({
|
|
399
|
+
selectedGate: DEV_LOOP_GATE.FAIL_CLOSED_RECONCILE,
|
|
400
|
+
routeKind: DEV_LOOP_ROUTE_KIND.NEEDS_RECONCILE,
|
|
401
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.NONE,
|
|
402
|
+
executionMode,
|
|
403
|
+
canonicalState,
|
|
404
|
+
nextAction: "Stop and reconcile the canonical current state before choosing an internal strategy.",
|
|
405
|
+
reason,
|
|
406
|
+
watchRequested,
|
|
407
|
+
contractTraceBoundary,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Post-routing validation for the `watch` variation parameter.
|
|
413
|
+
* If watch was explicitly requested, the routed result must be wait/watch-capable
|
|
414
|
+
* before watch semantics can be added.
|
|
415
|
+
* Existing stop and needs_reconcile results are preserved; only otherwise-successful
|
|
416
|
+
* non-wait routed results fail closed.
|
|
417
|
+
*/
|
|
418
|
+
function applyWatchValidation(result, watchRequested) {
|
|
419
|
+
const refreshBoundary = watchRequested
|
|
420
|
+
? {
|
|
421
|
+
boundaryKind: "post_watch_or_probe",
|
|
422
|
+
refreshRequired: true,
|
|
423
|
+
refreshReason: result.routeKind === DEV_LOOP_ROUTE_KIND.WAIT
|
|
424
|
+
? "Wait/watch boundaries are observational only; refresh authoritative state before treating a healthy wait boundary as completion or exit."
|
|
425
|
+
: "Requested watch/probe boundaries still require an authoritative state refresh before classifying the outcome as completion or re-entry.",
|
|
426
|
+
}
|
|
427
|
+
: null;
|
|
428
|
+
|
|
429
|
+
if (!watchRequested) return result;
|
|
430
|
+
if (result.routeKind === DEV_LOOP_ROUTE_KIND.WAIT) {
|
|
431
|
+
return withContractTrace(result, { watchRequested, boundary: refreshBoundary });
|
|
432
|
+
}
|
|
433
|
+
if (result.routeKind === DEV_LOOP_ROUTE_KIND.NEEDS_RECONCILE) return withContractTrace(result, { watchRequested });
|
|
434
|
+
if (result.routeKind === DEV_LOOP_ROUTE_KIND.STOP) return withContractTrace(result, { watchRequested });
|
|
435
|
+
if (result.selectedGate === DEV_LOOP_GATE.FAIL_CLOSED_RECONCILE) return withContractTrace(result, { watchRequested });
|
|
436
|
+
if (result.selectedGate === DEV_LOOP_GATE.STOP_BLOCKED_OR_NOT_AUTHORIZED) return withContractTrace(result, { watchRequested });
|
|
437
|
+
if (result.selectedGate === DEV_LOOP_GATE.STOP_DONE_TERMINAL) return withContractTrace(result, { watchRequested });
|
|
438
|
+
return buildReconcile(
|
|
439
|
+
"watch requested but the routed result is not eligible for wait/watch semantics.",
|
|
440
|
+
result.canonicalState,
|
|
441
|
+
result.executionMode,
|
|
442
|
+
{ watchRequested, contractTraceBoundary: refreshBoundary },
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function toRoutableCanonicalState(canonicalState) {
|
|
447
|
+
if (canonicalState.target.kind !== DEV_LOOP_TARGET_KIND.ISSUE || canonicalState.target.linkedPr === null) {
|
|
448
|
+
return canonicalState;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
...canonicalState,
|
|
453
|
+
target: {
|
|
454
|
+
kind: DEV_LOOP_TARGET_KIND.PR,
|
|
455
|
+
issue: canonicalState.target.issue,
|
|
456
|
+
pr: canonicalState.target.linkedPr,
|
|
457
|
+
linkedPr: null,
|
|
458
|
+
branch: null,
|
|
459
|
+
phase: null,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function selectGateForState(canonicalState) {
|
|
465
|
+
if (canonicalState.status === DEV_LOOP_STATUS.BLOCKED || canonicalState.authorization === DEV_LOOP_AUTHORIZATION.NOT_AUTHORIZED) {
|
|
466
|
+
return DEV_LOOP_GATE.STOP_BLOCKED_OR_NOT_AUTHORIZED;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (canonicalState.status === DEV_LOOP_STATUS.DONE) {
|
|
470
|
+
return DEV_LOOP_GATE.STOP_DONE_TERMINAL;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (
|
|
474
|
+
canonicalState.status === DEV_LOOP_STATUS.MERGE_READY
|
|
475
|
+
&& canonicalState.authorization === DEV_LOOP_AUTHORIZATION.NEEDS_CONFIRMATION
|
|
476
|
+
) {
|
|
477
|
+
return DEV_LOOP_GATE.WAITING_FOR_MERGE_AUTHORIZATION;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (
|
|
481
|
+
canonicalState.status === DEV_LOOP_STATUS.APPROVAL_READY ||
|
|
482
|
+
canonicalState.status === DEV_LOOP_STATUS.MERGE_READY
|
|
483
|
+
) {
|
|
484
|
+
return DEV_LOOP_GATE.FINAL_APPROVAL;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (canonicalState.status === DEV_LOOP_STATUS.WAITING) {
|
|
488
|
+
return DEV_LOOP_GATE.WAIT_WATCH;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (
|
|
492
|
+
canonicalState.target.kind === DEV_LOOP_TARGET_KIND.LOCAL_BRANCH ||
|
|
493
|
+
canonicalState.target.kind === DEV_LOOP_TARGET_KIND.LOCAL_PHASE
|
|
494
|
+
) {
|
|
495
|
+
return DEV_LOOP_GATE.LOCAL_IMPLEMENTATION;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (canonicalState.target.kind === DEV_LOOP_TARGET_KIND.ISSUE) {
|
|
499
|
+
return DEV_LOOP_GATE.ISSUE_INTAKE;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (canonicalState.target.kind === DEV_LOOP_TARGET_KIND.PR && canonicalState.ownership === DEV_LOOP_ACTOR.EXTERNAL_HUMAN) {
|
|
503
|
+
return DEV_LOOP_GATE.EXTERNAL_PR_FOLLOWUP;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (
|
|
507
|
+
canonicalState.target.kind === DEV_LOOP_TARGET_KIND.PR &&
|
|
508
|
+
(canonicalState.ownership === DEV_LOOP_ACTOR.REVIEWER || canonicalState.nextActor === DEV_LOOP_ACTOR.REVIEWER)
|
|
509
|
+
) {
|
|
510
|
+
return DEV_LOOP_GATE.REVIEWER_FIXER;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (canonicalState.target.kind === DEV_LOOP_TARGET_KIND.PR && canonicalState.ownership === DEV_LOOP_ACTOR.COPILOT) {
|
|
514
|
+
return DEV_LOOP_GATE.COPILOT_PR_FOLLOWUP;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return DEV_LOOP_GATE.FAIL_CLOSED_RECONCILE;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function isCopilotFirstIssueFlow(canonicalState) {
|
|
521
|
+
return canonicalState.target.kind === DEV_LOOP_TARGET_KIND.ISSUE && canonicalState.ownership === DEV_LOOP_ACTOR.COPILOT;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function shouldAcceptIssueAssignmentFacts({ intent, explicitTarget, explicitState }) {
|
|
525
|
+
if (explicitState) {
|
|
526
|
+
return isCopilotFirstIssueFlow(explicitState) && explicitState.target.linkedPr === null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return intent === DEV_LOOP_PUBLIC_INTENT.START_ON_ISSUE && explicitTarget?.kind === DEV_LOOP_TARGET_KIND.ISSUE;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function buildIssueClarificationStopNextAction(issueNumber) {
|
|
533
|
+
return `Issue #${issueNumber} is not ready yet; ask focused clarification questions and stop before assigning ${COPILOT_ISSUE_ASSIGNEE}.`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function buildIssueAssignmentNowNextAction(issueNumber) {
|
|
537
|
+
return `Issue #${issueNumber} is ready and still unassigned; assign ${COPILOT_ISSUE_ASSIGNEE} now before PR/bootstrap/watch follow-up.`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function buildIssueAssignedContinueNextAction(issueNumber) {
|
|
541
|
+
return `Issue #${issueNumber} is ready and already assigned to ${COPILOT_ISSUE_ASSIGNEE}; continue into PR/bootstrap/watch follow-up work.`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function resolveCopilotFirstIssueAssignmentSeam(canonicalState, issueReadiness, issueAssignmentState) {
|
|
545
|
+
if (issueReadiness === DEV_LOOP_ISSUE_READINESS.NEEDS_CLARIFICATION) {
|
|
546
|
+
return {
|
|
547
|
+
issueAssignmentSeam: DEV_LOOP_ISSUE_ASSIGNMENT_SEAM.NEEDS_REFINEMENT,
|
|
548
|
+
routeKind: DEV_LOOP_ROUTE_KIND.STOP,
|
|
549
|
+
nextAction: buildIssueClarificationStopNextAction(canonicalState.target.issue),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (issueAssignmentState === DEV_LOOP_ISSUE_ASSIGNMENT_STATE.ASSIGNED_TO_COPILOT) {
|
|
554
|
+
return {
|
|
555
|
+
issueAssignmentSeam: DEV_LOOP_ISSUE_ASSIGNMENT_SEAM.ASSIGNED_TO_COPILOT,
|
|
556
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
557
|
+
nextAction: buildIssueAssignedContinueNextAction(canonicalState.target.issue),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (canonicalState.authorization === DEV_LOOP_AUTHORIZATION.NEEDS_CONFIRMATION) {
|
|
562
|
+
return {
|
|
563
|
+
issueAssignmentSeam: DEV_LOOP_ISSUE_ASSIGNMENT_SEAM.READY_NEEDS_ASSIGNMENT_CONFIRMATION,
|
|
564
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
565
|
+
nextAction: buildIssueAssignmentConfirmationNextAction(canonicalState.target.issue),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
issueAssignmentSeam: DEV_LOOP_ISSUE_ASSIGNMENT_SEAM.READY_ASSIGN_NOW,
|
|
571
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
572
|
+
nextAction: buildIssueAssignmentNowNextAction(canonicalState.target.issue),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function routeForState(
|
|
577
|
+
canonicalState,
|
|
578
|
+
{
|
|
579
|
+
executionMode = DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF,
|
|
580
|
+
issueReadiness = null,
|
|
581
|
+
issueAssignmentState = null,
|
|
582
|
+
gateReviewEvidence = null,
|
|
583
|
+
targetPreference = null,
|
|
584
|
+
} = {},
|
|
585
|
+
) {
|
|
586
|
+
const routableCanonicalState = toRoutableCanonicalState(canonicalState);
|
|
587
|
+
const selectedGate = selectGateForState(routableCanonicalState);
|
|
588
|
+
if (
|
|
589
|
+
selectedGate === DEV_LOOP_GATE.FINAL_APPROVAL
|
|
590
|
+
&& routableCanonicalState.target.kind === DEV_LOOP_TARGET_KIND.PR
|
|
591
|
+
&& isFinalApprovalState(routableCanonicalState)
|
|
592
|
+
&& !hasCleanVisibleCurrentHeadPreApprovalGate(gateReviewEvidence)
|
|
593
|
+
) {
|
|
594
|
+
return buildReconcile(
|
|
595
|
+
"Final-approval routing requires explicit current-head `pre_approval_gate` evidence: (1) current head SHA identified, (2) a visible clean `pre_approval_gate` checkpoint verdict comment for that exact head SHA. CI green + resolved review threads + clean Copilot rereview are not sufficient substitutes. Do not suggest approval or merge without this proof; rerun the pre_approval_gate and confirm the checkpoint verdict comment before continuing.",
|
|
596
|
+
routableCanonicalState,
|
|
597
|
+
executionMode,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (selectedGate === DEV_LOOP_GATE.STOP_BLOCKED_OR_NOT_AUTHORIZED) {
|
|
602
|
+
return buildResult({
|
|
603
|
+
selectedGate,
|
|
604
|
+
routeKind: DEV_LOOP_ROUTE_KIND.STOP,
|
|
605
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.NONE,
|
|
606
|
+
executionMode,
|
|
607
|
+
canonicalState: routableCanonicalState,
|
|
608
|
+
nextAction: "Stop for a human decision or authorization before continuing the dev loop.",
|
|
609
|
+
reason: "The canonical state is blocked or not authorized for an automated state change.",
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (selectedGate === DEV_LOOP_GATE.STOP_DONE_TERMINAL) {
|
|
614
|
+
return buildResult({
|
|
615
|
+
selectedGate,
|
|
616
|
+
routeKind: DEV_LOOP_ROUTE_KIND.STOP,
|
|
617
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.NONE,
|
|
618
|
+
executionMode,
|
|
619
|
+
canonicalState: routableCanonicalState,
|
|
620
|
+
nextAction: "Report the terminal state and wait for a new work item.",
|
|
621
|
+
reason: "The canonical state is already done.",
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (selectedGate === DEV_LOOP_GATE.FINAL_APPROVAL) {
|
|
626
|
+
const approvalNextAction = routableCanonicalState.status === DEV_LOOP_STATUS.APPROVAL_READY
|
|
627
|
+
? "Run only the final approval step for the current PR; do not treat approval as merge authorization."
|
|
628
|
+
: "Merge is explicitly authorized for the current PR scope; run the final merge step.";
|
|
629
|
+
const approvalReason = routableCanonicalState.status === DEV_LOOP_STATUS.APPROVAL_READY
|
|
630
|
+
? "Approval-ready states require an explicit approval decision before any merge authorization check."
|
|
631
|
+
: "Merge-ready states with explicit merge authorization may proceed to merge.";
|
|
632
|
+
return buildResult({
|
|
633
|
+
selectedGate,
|
|
634
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
635
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.FINAL_APPROVAL,
|
|
636
|
+
executionMode,
|
|
637
|
+
canonicalState: routableCanonicalState,
|
|
638
|
+
nextAction: approvalNextAction,
|
|
639
|
+
reason: approvalReason,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (selectedGate === DEV_LOOP_GATE.WAITING_FOR_MERGE_AUTHORIZATION) {
|
|
644
|
+
return buildResult({
|
|
645
|
+
selectedGate,
|
|
646
|
+
routeKind: DEV_LOOP_ROUTE_KIND.STOP,
|
|
647
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.NONE,
|
|
648
|
+
executionMode,
|
|
649
|
+
canonicalState: routableCanonicalState,
|
|
650
|
+
nextAction: "Formal approval is complete; wait for explicit merge authorization for this PR scope before merging. If authorization wording is ambiguous, ask for an explicit merge decision.",
|
|
651
|
+
reason: "Merge-ready states must stop and wait when merge authorization is still missing.",
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (selectedGate === DEV_LOOP_GATE.WAIT_WATCH) {
|
|
656
|
+
const isDurableAuto = executionMode === DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO;
|
|
657
|
+
return buildResult({
|
|
658
|
+
selectedGate,
|
|
659
|
+
routeKind: DEV_LOOP_ROUTE_KIND.WAIT,
|
|
660
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.WAIT_WATCH,
|
|
661
|
+
executionMode,
|
|
662
|
+
waitSemantics: isDurableAuto
|
|
663
|
+
? DEV_LOOP_WAIT_SEMANTICS.AUTO_HEALTHY_WAIT
|
|
664
|
+
: DEV_LOOP_WAIT_SEMANTICS.DEFAULT,
|
|
665
|
+
waitTimeoutPolicy: isDurableAuto
|
|
666
|
+
? EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY
|
|
667
|
+
: PERSISTENT_INTERNAL_WAIT_TIMEOUT_POLICY,
|
|
668
|
+
canonicalState: routableCanonicalState,
|
|
669
|
+
nextAction: isDurableAuto
|
|
670
|
+
? "Remain in durable auto ownership while waiting on the same canonical state; do not escalate timeout/no-activity alone as attention."
|
|
671
|
+
: "Keep waiting or watching against the same canonical state instead of switching public loop names.",
|
|
672
|
+
reason: "Waiting states route to the shared wait/watch strategy.",
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (selectedGate === DEV_LOOP_GATE.LOCAL_IMPLEMENTATION) {
|
|
677
|
+
return buildResult({
|
|
678
|
+
selectedGate,
|
|
679
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
680
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.LOCAL_IMPLEMENTATION,
|
|
681
|
+
executionMode,
|
|
682
|
+
canonicalState: routableCanonicalState,
|
|
683
|
+
nextAction: "Run the local implementation strategy for the current branch or phase slice.",
|
|
684
|
+
reason: "Local branch/phase targets stay on the local implementation strategy.",
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (selectedGate === DEV_LOOP_GATE.ISSUE_INTAKE) {
|
|
689
|
+
if (targetPreference === DEV_LOOP_TARGET_PREFERENCE.PREFER_LOCAL) {
|
|
690
|
+
const localPhase = routableCanonicalState.target.issue
|
|
691
|
+
? `issue-${routableCanonicalState.target.issue}`
|
|
692
|
+
: null;
|
|
693
|
+
const localTarget = {
|
|
694
|
+
kind: DEV_LOOP_TARGET_KIND.LOCAL_PHASE,
|
|
695
|
+
issue: routableCanonicalState.target.issue,
|
|
696
|
+
pr: null,
|
|
697
|
+
linkedPr: null,
|
|
698
|
+
branch: null,
|
|
699
|
+
phase: localPhase,
|
|
700
|
+
};
|
|
701
|
+
return buildResult({
|
|
702
|
+
selectedGate: DEV_LOOP_GATE.LOCAL_IMPLEMENTATION,
|
|
703
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
704
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.LOCAL_IMPLEMENTATION,
|
|
705
|
+
executionMode,
|
|
706
|
+
canonicalState: { ...routableCanonicalState, target: localTarget },
|
|
707
|
+
nextAction: `Run the local implementation strategy for issue #${routableCanonicalState.target.issue} (tracker-backed local session).`,
|
|
708
|
+
reason: "Issue targets with `targetPreference=prefer_local` route to local implementation instead of Copilot-first issue intake.",
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
const copilotFirstIssueSeam = isCopilotFirstIssueFlow(routableCanonicalState)
|
|
712
|
+
? resolveCopilotFirstIssueAssignmentSeam(routableCanonicalState, issueReadiness, issueAssignmentState)
|
|
713
|
+
: {
|
|
714
|
+
issueAssignmentSeam: DEV_LOOP_ISSUE_ASSIGNMENT_SEAM.NOT_APPLICABLE,
|
|
715
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
716
|
+
nextAction: "Normalize the issue, confirm scope, and determine whether an existing PR already exists.",
|
|
717
|
+
};
|
|
718
|
+
return buildResult({
|
|
719
|
+
selectedGate,
|
|
720
|
+
routeKind: copilotFirstIssueSeam.routeKind,
|
|
721
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.ISSUE_INTAKE,
|
|
722
|
+
executionMode,
|
|
723
|
+
canonicalState: routableCanonicalState,
|
|
724
|
+
issueAssignmentSeam: copilotFirstIssueSeam.issueAssignmentSeam,
|
|
725
|
+
nextAction: copilotFirstIssueSeam.nextAction,
|
|
726
|
+
reason: "Issue targets without a linked PR route to issue intake before PR follow-up.",
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (selectedGate === DEV_LOOP_GATE.EXTERNAL_PR_FOLLOWUP) {
|
|
731
|
+
return buildResult({
|
|
732
|
+
selectedGate,
|
|
733
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
734
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.EXTERNAL_PR_FOLLOWUP,
|
|
735
|
+
executionMode,
|
|
736
|
+
canonicalState: routableCanonicalState,
|
|
737
|
+
nextAction: "Run the external-contributor PR follow-up strategy against the current PR state.",
|
|
738
|
+
reason: "External-human PR ownership routes to the external PR follow-up strategy.",
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (selectedGate === DEV_LOOP_GATE.REVIEWER_FIXER) {
|
|
743
|
+
return buildResult({
|
|
744
|
+
selectedGate,
|
|
745
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
746
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.REVIEWER_FIXER,
|
|
747
|
+
executionMode,
|
|
748
|
+
canonicalState: routableCanonicalState,
|
|
749
|
+
nextAction: "Run the reviewer/fixer strategy for the current PR.",
|
|
750
|
+
reason: "Reviewer-owned or reviewer-next PR states route to the reviewer/fixer strategy.",
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (selectedGate === DEV_LOOP_GATE.COPILOT_PR_FOLLOWUP) {
|
|
755
|
+
return buildResult({
|
|
756
|
+
selectedGate,
|
|
757
|
+
routeKind: DEV_LOOP_ROUTE_KIND.ROUTE,
|
|
758
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.COPILOT_PR_FOLLOWUP,
|
|
759
|
+
executionMode,
|
|
760
|
+
canonicalState: routableCanonicalState,
|
|
761
|
+
nextAction: "Run the Copilot PR follow-up strategy for the current PR; treat it as the canonical artifact for the issue and do not open a second PR.",
|
|
762
|
+
reason: "Copilot-owned PR states route to the Copilot PR follow-up strategy; an already-open linked PR must stay canonical until reconciled.",
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return buildReconcile(
|
|
767
|
+
"The canonical current state does not map cleanly to any first-slice internal strategy.",
|
|
768
|
+
routableCanonicalState,
|
|
769
|
+
executionMode,
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function buildStatusArtifactIdentity(canonicalState) {
|
|
774
|
+
return {
|
|
775
|
+
kind: canonicalState.target.kind,
|
|
776
|
+
issue: canonicalState.target.issue,
|
|
777
|
+
pr: canonicalState.target.pr,
|
|
778
|
+
branch: canonicalState.target.branch,
|
|
779
|
+
phase: canonicalState.target.phase,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function buildIssueAssignmentConfirmationNextAction(issueNumber) {
|
|
784
|
+
return `Authorize the next mutation: assign ${COPILOT_ISSUE_ASSIGNEE} to issue #${issueNumber} now?`;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function buildAuthoritativeStatusNextAction(routed) {
|
|
788
|
+
return routed?.nextAction ?? "Reconcile the current state before answering status.";
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function buildStartupResumeBundleReconcile({
|
|
792
|
+
reason,
|
|
793
|
+
canonicalState = null,
|
|
794
|
+
issueLinkageResolution = null,
|
|
795
|
+
artifactState = null,
|
|
796
|
+
loopState = null,
|
|
797
|
+
nextAction = "Stop and reconcile the authoritative startup/resume state before routing or answering status.",
|
|
798
|
+
executionMode = DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF,
|
|
799
|
+
waitSemantics = DEV_LOOP_WAIT_SEMANTICS.DEFAULT,
|
|
800
|
+
waitTimeoutPolicy = null,
|
|
801
|
+
asyncRun = null,
|
|
802
|
+
}) {
|
|
803
|
+
const result = {
|
|
804
|
+
bundleKind: DEV_LOOP_STARTUP_RESUME_BUNDLE_KIND.NEEDS_RECONCILE,
|
|
805
|
+
reason,
|
|
806
|
+
activeArtifact: canonicalState ? buildStatusArtifactIdentity(canonicalState) : null,
|
|
807
|
+
artifactState,
|
|
808
|
+
issueLinkageResolution,
|
|
809
|
+
loopState: "unknown",
|
|
810
|
+
nextAction,
|
|
811
|
+
selectedGate: DEV_LOOP_GATE.FAIL_CLOSED_RECONCILE,
|
|
812
|
+
routeKind: DEV_LOOP_ROUTE_KIND.NEEDS_RECONCILE,
|
|
813
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.NONE,
|
|
814
|
+
executionMode,
|
|
815
|
+
waitSemantics,
|
|
816
|
+
waitTimeoutPolicy,
|
|
817
|
+
asyncRun,
|
|
818
|
+
canonicalState,
|
|
819
|
+
};
|
|
820
|
+
return {
|
|
821
|
+
...result,
|
|
822
|
+
contractTrace: buildContractTrace({
|
|
823
|
+
selectedGate: result.selectedGate,
|
|
824
|
+
routeKind: result.routeKind,
|
|
825
|
+
selectedStrategy: result.selectedStrategy,
|
|
826
|
+
executionMode,
|
|
827
|
+
waitSemantics,
|
|
828
|
+
waitTimeoutPolicy,
|
|
829
|
+
canonicalState,
|
|
830
|
+
reason,
|
|
831
|
+
boundary: {
|
|
832
|
+
boundaryKind: "startup_resume_refresh",
|
|
833
|
+
refreshRequired: true,
|
|
834
|
+
refreshReason: "Startup/resume routing must record the refreshed authoritative state boundary that justified this stop or reconcile decision.",
|
|
835
|
+
...(loopState !== null ? { loopState } : {}),
|
|
836
|
+
...(artifactState !== null ? { artifactState } : {}),
|
|
837
|
+
...(issueLinkageResolution !== null ? { issueLinkageResolution } : {}),
|
|
838
|
+
},
|
|
839
|
+
}),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function normalizeIssueLinkageResolutionForBundle(canonicalState, issueLinkageResolution) {
|
|
844
|
+
if (issueLinkageResolution) {
|
|
845
|
+
return issueLinkageResolution;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (canonicalState?.target?.kind !== DEV_LOOP_TARGET_KIND.ISSUE) {
|
|
849
|
+
return DEV_LOOP_ISSUE_LINKAGE_RESOLUTION.NOT_APPLICABLE;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function applyInitialCopilotBootstrapRefreshSeam(canonicalState, issueLinkageResolution, loopState) {
|
|
856
|
+
if (loopState === PRIOR_LINKED_PR_CLOSED_UNMERGED_LOOP_STATE) {
|
|
857
|
+
if (
|
|
858
|
+
canonicalState.target.kind !== DEV_LOOP_TARGET_KIND.ISSUE
|
|
859
|
+
|| canonicalState.target.linkedPr !== null
|
|
860
|
+
|| issueLinkageResolution !== DEV_LOOP_ISSUE_LINKAGE_RESOLUTION.RESOLVED_NO_OPEN_PR
|
|
861
|
+
|| canonicalState.ownership !== DEV_LOOP_ACTOR.COPILOT
|
|
862
|
+
) {
|
|
863
|
+
return {
|
|
864
|
+
canonicalState,
|
|
865
|
+
reason:
|
|
866
|
+
"Refreshed `prior_linked_pr_closed_unmerged` state conflicts with authoritative no-open-linked-PR Copilot issue facts; reconcile before routing startup/resume state.",
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
canonicalState,
|
|
872
|
+
reason:
|
|
873
|
+
"Refreshed bootstrap state reports a prior linked PR closed unmerged; reconcile the issue instead of treating it as a healthy bootstrap wait or fresh issue-intake path.",
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (loopState !== LINKED_PR_READY_FOR_FOLLOWUP_LOOP_STATE) {
|
|
878
|
+
return { canonicalState, reason: null };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (canonicalState.target.kind === DEV_LOOP_TARGET_KIND.PR) {
|
|
882
|
+
return { canonicalState, reason: null };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (canonicalState.target.kind !== DEV_LOOP_TARGET_KIND.ISSUE) {
|
|
886
|
+
return {
|
|
887
|
+
canonicalState,
|
|
888
|
+
reason:
|
|
889
|
+
"Refreshed `linked_pr_ready_for_followup` state requires a linked PR canonical target; reconcile before routing startup/resume state.",
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (
|
|
894
|
+
issueLinkageResolution !== DEV_LOOP_ISSUE_LINKAGE_RESOLUTION.RESOLVED_LINKED_PR
|
|
895
|
+
|| canonicalState.target.linkedPr === null
|
|
896
|
+
|| canonicalState.ownership !== DEV_LOOP_ACTOR.COPILOT
|
|
897
|
+
) {
|
|
898
|
+
return {
|
|
899
|
+
canonicalState,
|
|
900
|
+
reason:
|
|
901
|
+
"Refreshed `linked_pr_ready_for_followup` state conflicts with authoritative linked PR follow-up facts; reconcile before routing startup/resume state.",
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
canonicalState: {
|
|
907
|
+
...canonicalState,
|
|
908
|
+
target: {
|
|
909
|
+
kind: DEV_LOOP_TARGET_KIND.PR,
|
|
910
|
+
issue: canonicalState.target.issue,
|
|
911
|
+
pr: canonicalState.target.linkedPr,
|
|
912
|
+
linkedPr: null,
|
|
913
|
+
branch: null,
|
|
914
|
+
phase: null,
|
|
915
|
+
},
|
|
916
|
+
status: DEV_LOOP_STATUS.ACTIVE,
|
|
917
|
+
},
|
|
918
|
+
reason: null,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function isArtifactStateCompatible(canonicalState, artifactState) {
|
|
923
|
+
if (canonicalState.target.kind !== DEV_LOOP_TARGET_KIND.PR) {
|
|
924
|
+
return artifactState === DEV_LOOP_ARTIFACT_STATE.NOT_APPLICABLE;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (canonicalState.status === DEV_LOOP_STATUS.DONE) {
|
|
928
|
+
return artifactState === DEV_LOOP_ARTIFACT_STATE.CLOSED || artifactState === DEV_LOOP_ARTIFACT_STATE.MERGED;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return artifactState === DEV_LOOP_ARTIFACT_STATE.OPEN;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function validateIssueLinkageResolution(canonicalState, issueLinkageResolution) {
|
|
935
|
+
if (canonicalState.target.kind !== DEV_LOOP_TARGET_KIND.ISSUE) {
|
|
936
|
+
return issueLinkageResolution === null
|
|
937
|
+
|| issueLinkageResolution === DEV_LOOP_ISSUE_LINKAGE_RESOLUTION.NOT_APPLICABLE;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (canonicalState.target.linkedPr !== null) {
|
|
941
|
+
return issueLinkageResolution === DEV_LOOP_ISSUE_LINKAGE_RESOLUTION.RESOLVED_LINKED_PR;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return issueLinkageResolution === DEV_LOOP_ISSUE_LINKAGE_RESOLUTION.RESOLVED_NO_OPEN_PR;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function synthesizeCanonicalStateFromShorthand(input, intent) {
|
|
948
|
+
if (intent === null) return null;
|
|
949
|
+
const explicitIssue = Number.isInteger(input.issue) && input.issue > 0 ? input.issue : null;
|
|
950
|
+
if (explicitIssue === null) return null;
|
|
951
|
+
const ISSUE_LOCAL_INTENTS = new Set([
|
|
952
|
+
DEV_LOOP_PUBLIC_INTENT.START_ISSUE_LOCALLY,
|
|
953
|
+
DEV_LOOP_PUBLIC_INTENT.START_ISSUE_LOCALLY_THEN_CONTINUE,
|
|
954
|
+
]);
|
|
955
|
+
if (ISSUE_LOCAL_INTENTS.has(intent)) {
|
|
956
|
+
const phase = `issue-${explicitIssue}`;
|
|
957
|
+
return {
|
|
958
|
+
target: { kind: DEV_LOOP_TARGET_KIND.LOCAL_PHASE, issue: explicitIssue, pr: null, linkedPr: null, branch: null, phase },
|
|
959
|
+
ownership: DEV_LOOP_ACTOR.LOCAL,
|
|
960
|
+
nextActor: DEV_LOOP_ACTOR.LOCAL,
|
|
961
|
+
status: DEV_LOOP_STATUS.ACTIVE,
|
|
962
|
+
authorization: DEV_LOOP_AUTHORIZATION.AUTHORIZED,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (intent === DEV_LOOP_PUBLIC_INTENT.START_ON_ISSUE) {
|
|
966
|
+
return {
|
|
967
|
+
target: { kind: DEV_LOOP_TARGET_KIND.ISSUE, issue: explicitIssue, pr: null, linkedPr: null, branch: null, phase: null },
|
|
968
|
+
ownership: DEV_LOOP_ACTOR.COPILOT,
|
|
969
|
+
nextActor: DEV_LOOP_ACTOR.USER,
|
|
970
|
+
status: DEV_LOOP_STATUS.ACTIVE,
|
|
971
|
+
authorization: DEV_LOOP_AUTHORIZATION.NEEDS_CONFIRMATION,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
export function resolveAuthoritativeStartupResumeBundle(input = {}) {
|
|
979
|
+
let canonicalState = normalizeState(input.currentState);
|
|
980
|
+
const intent = normalizeIntent(input.intent);
|
|
981
|
+
if (!canonicalState) {
|
|
982
|
+
canonicalState = synthesizeCanonicalStateFromShorthand(input, intent);
|
|
983
|
+
}
|
|
984
|
+
const variationMode = input.mode !== undefined ? normalizeVariationMode(input.mode) : null;
|
|
985
|
+
const requestedExecutionMode =
|
|
986
|
+
variationMode
|
|
987
|
+
?? (intent === DEV_LOOP_PUBLIC_INTENT.AUTO_CONTINUE_CURRENT
|
|
988
|
+
? DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO
|
|
989
|
+
: DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF);
|
|
990
|
+
if (!canonicalState) {
|
|
991
|
+
return buildStartupResumeBundleReconcile({
|
|
992
|
+
reason: "Authoritative startup/resume routing requires a valid canonical current state.",
|
|
993
|
+
executionMode: requestedExecutionMode,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (input.intent !== undefined && intent === null) {
|
|
998
|
+
return buildStartupResumeBundleReconcile({
|
|
999
|
+
reason: "Authoritative startup/resume routing received an invalid public dev-loop intent.",
|
|
1000
|
+
canonicalState,
|
|
1001
|
+
executionMode: requestedExecutionMode,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (input.mode !== undefined && variationMode === null) {
|
|
1006
|
+
return buildStartupResumeBundleReconcile({
|
|
1007
|
+
reason: `Authoritative startup/resume routing received an invalid execution mode value; allowed values: ${ALLOWED_MODE_VALUES_TEXT}.`,
|
|
1008
|
+
canonicalState,
|
|
1009
|
+
executionMode: requestedExecutionMode,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (
|
|
1014
|
+
intent === DEV_LOOP_PUBLIC_INTENT.AUTO_CONTINUE_CURRENT
|
|
1015
|
+
&& variationMode === DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF
|
|
1016
|
+
) {
|
|
1017
|
+
return buildStartupResumeBundleReconcile({
|
|
1018
|
+
reason: "`mode=bounded_handoff` conflicts with the `auto_continue_current` intent; `auto_continue_current` always uses durable auto execution mode.",
|
|
1019
|
+
canonicalState,
|
|
1020
|
+
executionMode: DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const effectiveMode = intent === DEV_LOOP_PUBLIC_INTENT.AUTO_CONTINUE_CURRENT
|
|
1025
|
+
? DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO
|
|
1026
|
+
: (variationMode ?? DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF);
|
|
1027
|
+
|
|
1028
|
+
const targetPreference = input.targetPreference !== undefined
|
|
1029
|
+
? normalizeTargetPreference(input.targetPreference)
|
|
1030
|
+
: null;
|
|
1031
|
+
|
|
1032
|
+
if (input.targetPreference !== undefined && targetPreference === null) {
|
|
1033
|
+
return buildStartupResumeBundleReconcile({
|
|
1034
|
+
reason: `Authoritative startup/resume routing received an invalid targetPreference value; allowed values: ${ALLOWED_TARGET_PREFERENCE_VALUES_TEXT}.`,
|
|
1035
|
+
canonicalState,
|
|
1036
|
+
executionMode: effectiveMode,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const issueLinkageResolution = normalizeIssueLinkageResolution(input.issueLinkageResolution);
|
|
1041
|
+
const issueReadiness = normalizeIssueReadiness(input.issueReadiness);
|
|
1042
|
+
const issueAssignmentState = normalizeIssueAssignmentState(input.issueAssignmentState);
|
|
1043
|
+
const gateReviewEvidence = normalizeGateReviewEvidence(input.gateReviewEvidence);
|
|
1044
|
+
const asyncRunProvided = input.asyncRun !== undefined && input.asyncRun !== null;
|
|
1045
|
+
const asyncRun = asyncRunProvided ? normalizeAsyncRun(input.asyncRun) : null;
|
|
1046
|
+
const retrospectiveCheckpointState = input.retrospectiveCheckpointState !== undefined
|
|
1047
|
+
? normalizeRetrospectiveCheckpointState(input.retrospectiveCheckpointState)
|
|
1048
|
+
: null;
|
|
1049
|
+
const retrospectiveCheckpointStateProvided =
|
|
1050
|
+
input.retrospectiveCheckpointState !== undefined && input.retrospectiveCheckpointState !== null;
|
|
1051
|
+
const issueLinkageResolutionProvided = input.issueLinkageResolution !== undefined && input.issueLinkageResolution !== null;
|
|
1052
|
+
const normalizedIssueLinkageResolution = normalizeIssueLinkageResolutionForBundle(canonicalState, issueLinkageResolution);
|
|
1053
|
+
const issueReadinessProvided = input.issueReadiness !== undefined && input.issueReadiness !== null;
|
|
1054
|
+
const issueAssignmentStateProvided = input.issueAssignmentState !== undefined && input.issueAssignmentState !== null;
|
|
1055
|
+
const loopState = normalizeOptionalLoopState(input.loopState);
|
|
1056
|
+
|
|
1057
|
+
if (asyncRunProvided && asyncRun === null) {
|
|
1058
|
+
return buildStartupResumeBundleReconcile({
|
|
1059
|
+
reason: "Authoritative startup/resume routing received an invalid async-run registration value.",
|
|
1060
|
+
canonicalState,
|
|
1061
|
+
executionMode: effectiveMode,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (issueLinkageResolutionProvided && issueLinkageResolution === null) {
|
|
1066
|
+
return buildStartupResumeBundleReconcile({
|
|
1067
|
+
reason: "Authoritative startup/resume routing received an invalid issue↔PR linkage resolution value.",
|
|
1068
|
+
canonicalState,
|
|
1069
|
+
issueLinkageResolution: null,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (issueReadinessProvided && issueReadiness === null) {
|
|
1074
|
+
return buildStartupResumeBundleReconcile({
|
|
1075
|
+
reason: "Authoritative startup/resume routing received an invalid issue readiness value.",
|
|
1076
|
+
canonicalState,
|
|
1077
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (issueAssignmentStateProvided && issueAssignmentState === null) {
|
|
1082
|
+
return buildStartupResumeBundleReconcile({
|
|
1083
|
+
reason: "Authoritative startup/resume routing received an invalid issue assignment-state value.",
|
|
1084
|
+
canonicalState,
|
|
1085
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (retrospectiveCheckpointStateProvided && retrospectiveCheckpointState === null) {
|
|
1090
|
+
return buildStartupResumeBundleReconcile({
|
|
1091
|
+
reason: "Authoritative startup/resume routing received an invalid retrospective checkpoint-state value.",
|
|
1092
|
+
canonicalState,
|
|
1093
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (loopState === null) {
|
|
1098
|
+
return buildStartupResumeBundleReconcile({
|
|
1099
|
+
reason: "Authoritative startup/resume routing requires an explicit resolved loop state before routing or answering status.",
|
|
1100
|
+
canonicalState,
|
|
1101
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1102
|
+
artifactState: normalizeArtifactState(input.artifactState),
|
|
1103
|
+
loopState,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const bootstrapRefresh = applyInitialCopilotBootstrapRefreshSeam(
|
|
1108
|
+
canonicalState,
|
|
1109
|
+
issueLinkageResolution,
|
|
1110
|
+
loopState,
|
|
1111
|
+
);
|
|
1112
|
+
if (bootstrapRefresh.reason !== null) {
|
|
1113
|
+
return buildStartupResumeBundleReconcile({
|
|
1114
|
+
reason: bootstrapRefresh.reason,
|
|
1115
|
+
canonicalState,
|
|
1116
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1117
|
+
artifactState: normalizeArtifactState(input.artifactState),
|
|
1118
|
+
loopState,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
const canonicalStateForRouting = bootstrapRefresh.canonicalState;
|
|
1122
|
+
|
|
1123
|
+
if (
|
|
1124
|
+
canonicalState.target.kind === DEV_LOOP_TARGET_KIND.ISSUE
|
|
1125
|
+
&& issueLinkageResolution === null
|
|
1126
|
+
) {
|
|
1127
|
+
return buildStartupResumeBundleReconcile({
|
|
1128
|
+
reason: "Issue targets require explicit authoritative issue↔PR linkage resolution before routing startup/resume state.",
|
|
1129
|
+
canonicalState,
|
|
1130
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1131
|
+
loopState,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (!validateIssueLinkageResolution(canonicalState, issueLinkageResolution)) {
|
|
1136
|
+
return buildStartupResumeBundleReconcile({
|
|
1137
|
+
reason: "Issue↔PR linkage resolution is incomplete or conflicts with canonical current state; reconcile before routing startup/resume state.",
|
|
1138
|
+
canonicalState,
|
|
1139
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1140
|
+
loopState,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (
|
|
1145
|
+
canonicalState.target.kind === DEV_LOOP_TARGET_KIND.ISSUE
|
|
1146
|
+
&& canonicalState.ownership === DEV_LOOP_ACTOR.COPILOT
|
|
1147
|
+
&& issueLinkageResolution === DEV_LOOP_ISSUE_LINKAGE_RESOLUTION.RESOLVED_NO_OPEN_PR
|
|
1148
|
+
) {
|
|
1149
|
+
if (!issueReadinessProvided) {
|
|
1150
|
+
return buildStartupResumeBundleReconcile({
|
|
1151
|
+
reason: "Copilot-first issue targets require explicit authoritative issue readiness before assignment/routing decisions.",
|
|
1152
|
+
canonicalState,
|
|
1153
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1154
|
+
loopState,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (!issueAssignmentStateProvided) {
|
|
1159
|
+
return buildStartupResumeBundleReconcile({
|
|
1160
|
+
reason: "Copilot-first issue targets require explicit authoritative issue assignment state before assignment/routing decisions.",
|
|
1161
|
+
canonicalState,
|
|
1162
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1163
|
+
loopState,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const routed = routeForState(canonicalStateForRouting, {
|
|
1169
|
+
executionMode: effectiveMode,
|
|
1170
|
+
issueReadiness,
|
|
1171
|
+
issueAssignmentState,
|
|
1172
|
+
gateReviewEvidence,
|
|
1173
|
+
targetPreference,
|
|
1174
|
+
});
|
|
1175
|
+
if (routed.routeKind === DEV_LOOP_ROUTE_KIND.NEEDS_RECONCILE) {
|
|
1176
|
+
return buildStartupResumeBundleReconcile({
|
|
1177
|
+
reason: routed.reason,
|
|
1178
|
+
canonicalState: routed.canonicalState,
|
|
1179
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1180
|
+
executionMode: routed.executionMode,
|
|
1181
|
+
waitSemantics: routed.waitSemantics,
|
|
1182
|
+
loopState,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const artifactState = normalizeArtifactState(input.artifactState);
|
|
1187
|
+
if (!artifactState) {
|
|
1188
|
+
return buildStartupResumeBundleReconcile({
|
|
1189
|
+
reason: "Authoritative startup/resume routing requires an explicit artifact state (open|closed|merged|not_applicable).",
|
|
1190
|
+
canonicalState: routed.canonicalState,
|
|
1191
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1192
|
+
artifactState: null,
|
|
1193
|
+
loopState,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
if (!isArtifactStateCompatible(routed.canonicalState, artifactState)) {
|
|
1198
|
+
return buildStartupResumeBundleReconcile({
|
|
1199
|
+
reason: "Canonical current state conflicts with the provided artifact state; reconcile before routing startup/resume state.",
|
|
1200
|
+
canonicalState: routed.canonicalState,
|
|
1201
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1202
|
+
artifactState,
|
|
1203
|
+
loopState,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const inspectStateIntent = intent === DEV_LOOP_PUBLIC_INTENT.INSPECT_STATE;
|
|
1208
|
+
const routedWithIntentSemantics = inspectStateIntent
|
|
1209
|
+
? {
|
|
1210
|
+
...routed,
|
|
1211
|
+
routeKind: DEV_LOOP_ROUTE_KIND.INSPECT,
|
|
1212
|
+
nextAction: "Describe the canonical state and the routed internal strategy without changing public entrypoints.",
|
|
1213
|
+
}
|
|
1214
|
+
: routed;
|
|
1215
|
+
const effectiveRouted = applyRetrospectiveCheckpointGate(
|
|
1216
|
+
routedWithIntentSemantics,
|
|
1217
|
+
retrospectiveCheckpointState,
|
|
1218
|
+
retrospectiveCheckpointStateProvided,
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
if (effectiveRouted.routeKind === DEV_LOOP_ROUTE_KIND.NEEDS_RECONCILE) {
|
|
1222
|
+
return buildStartupResumeBundleReconcile({
|
|
1223
|
+
reason: effectiveRouted.reason,
|
|
1224
|
+
canonicalState: effectiveRouted.canonicalState ?? routed.canonicalState,
|
|
1225
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1226
|
+
artifactState,
|
|
1227
|
+
nextAction: effectiveRouted.nextAction,
|
|
1228
|
+
executionMode: effectiveRouted.executionMode,
|
|
1229
|
+
waitSemantics: effectiveRouted.waitSemantics,
|
|
1230
|
+
waitTimeoutPolicy: effectiveRouted.waitTimeoutPolicy,
|
|
1231
|
+
asyncRun,
|
|
1232
|
+
loopState,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (effectiveRouted.executionMode === DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO) {
|
|
1237
|
+
const asyncRunInspectionState = asyncRun?.inspectionState;
|
|
1238
|
+
if (
|
|
1239
|
+
!asyncRunProvided
|
|
1240
|
+
|| asyncRun?.kind !== "pi_managed_run"
|
|
1241
|
+
|| asyncRun.runId === null
|
|
1242
|
+
|| asyncRun.visible !== true
|
|
1243
|
+
|| asyncRunInspectionState === "uninspectable"
|
|
1244
|
+
|| asyncRunInspectionState === "hidden"
|
|
1245
|
+
|| asyncRunInspectionState === "stale"
|
|
1246
|
+
) {
|
|
1247
|
+
return buildStartupResumeBundleReconcile({
|
|
1248
|
+
reason: asyncRun?.kind === "detached_process"
|
|
1249
|
+
? "Durable auto startup/resume requires a visible Pi-managed async run; detached local background processes do not satisfy the async-start contract."
|
|
1250
|
+
: asyncRunInspectionState === "uninspectable"
|
|
1251
|
+
? "Durable auto startup/resume requires inspectable Pi-managed async evidence; observed run is uninspectable (no child message route registered)."
|
|
1252
|
+
: asyncRunInspectionState === "hidden"
|
|
1253
|
+
? "Durable auto startup/resume requires visible Pi-managed async evidence; observed run evidence is hidden."
|
|
1254
|
+
: asyncRunInspectionState === "stale"
|
|
1255
|
+
? "Durable auto startup/resume requires fresh Pi-managed async evidence; observed run evidence is stale."
|
|
1256
|
+
: "Durable auto startup/resume requires a visible registered Pi-managed async run id before startup can be reported as successful.",
|
|
1257
|
+
canonicalState: effectiveRouted.canonicalState,
|
|
1258
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1259
|
+
artifactState,
|
|
1260
|
+
executionMode: effectiveRouted.executionMode,
|
|
1261
|
+
waitSemantics: effectiveRouted.waitSemantics,
|
|
1262
|
+
waitTimeoutPolicy: effectiveRouted.waitTimeoutPolicy,
|
|
1263
|
+
asyncRun,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return {
|
|
1269
|
+
bundleKind: DEV_LOOP_STARTUP_RESUME_BUNDLE_KIND.RESOLVED,
|
|
1270
|
+
activeArtifact: buildStatusArtifactIdentity(effectiveRouted.canonicalState),
|
|
1271
|
+
artifactState,
|
|
1272
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1273
|
+
canonicalState: effectiveRouted.canonicalState,
|
|
1274
|
+
loopState,
|
|
1275
|
+
routeKind: effectiveRouted.routeKind,
|
|
1276
|
+
selectedGate: effectiveRouted.selectedGate,
|
|
1277
|
+
selectedStrategy: effectiveRouted.selectedStrategy,
|
|
1278
|
+
executionMode: effectiveRouted.executionMode,
|
|
1279
|
+
waitSemantics: effectiveRouted.waitSemantics,
|
|
1280
|
+
waitTimeoutPolicy: effectiveRouted.waitTimeoutPolicy,
|
|
1281
|
+
asyncRun: effectiveRouted.executionMode === DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO ? asyncRun : null,
|
|
1282
|
+
issueAssignmentSeam: effectiveRouted.issueAssignmentSeam,
|
|
1283
|
+
nextAction: buildAuthoritativeStatusNextAction(effectiveRouted),
|
|
1284
|
+
reason: effectiveRouted.reason,
|
|
1285
|
+
contractTrace: buildContractTrace({
|
|
1286
|
+
selectedGate: effectiveRouted.selectedGate,
|
|
1287
|
+
routeKind: effectiveRouted.routeKind,
|
|
1288
|
+
selectedStrategy: effectiveRouted.selectedStrategy,
|
|
1289
|
+
executionMode: effectiveRouted.executionMode,
|
|
1290
|
+
waitSemantics: effectiveRouted.waitSemantics,
|
|
1291
|
+
waitTimeoutPolicy: effectiveRouted.waitTimeoutPolicy,
|
|
1292
|
+
canonicalState: effectiveRouted.canonicalState,
|
|
1293
|
+
reason: effectiveRouted.reason,
|
|
1294
|
+
boundary: {
|
|
1295
|
+
boundaryKind: "startup_resume_refresh",
|
|
1296
|
+
refreshRequired: true,
|
|
1297
|
+
refreshReason: "Startup/resume answers record the authoritative refreshed loop state that justified the routed path.",
|
|
1298
|
+
loopState,
|
|
1299
|
+
artifactState,
|
|
1300
|
+
issueLinkageResolution: normalizedIssueLinkageResolution,
|
|
1301
|
+
},
|
|
1302
|
+
}),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
const BUILT_IN_DEFAULT_TARGET_PREFERENCE = DEV_LOOP_TARGET_PREFERENCE.PREFER_GITHUB_FIRST;
|
|
1308
|
+
|
|
1309
|
+
// DEFAULT_TARGET_PREFERENCE uses the built-in default (github-first).
|
|
1310
|
+
// Config-based target preference is resolved by the startup resolver
|
|
1311
|
+
// (resolveTargetPreference in scripts/loop/resolve-dev-loop-startup.mjs)
|
|
1312
|
+
// and passed explicitly via input.targetPreference.
|
|
1313
|
+
// This module must not read the live config file at import time
|
|
1314
|
+
// because test suites run from the repo root and would pick up the
|
|
1315
|
+
// repo's own .devloops, making tests depend on live config.
|
|
1316
|
+
const DEFAULT_TARGET_PREFERENCE = BUILT_IN_DEFAULT_TARGET_PREFERENCE;
|
|
1317
|
+
|
|
1318
|
+
function buildStatusReconcile(
|
|
1319
|
+
reason,
|
|
1320
|
+
canonicalState = null,
|
|
1321
|
+
nextAction = "Stop and reconcile the authoritative active artifact and current loop state before answering status.",
|
|
1322
|
+
executionMode = DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF,
|
|
1323
|
+
waitSemantics = DEV_LOOP_WAIT_SEMANTICS.DEFAULT,
|
|
1324
|
+
waitTimeoutPolicy = null,
|
|
1325
|
+
asyncRun = null,
|
|
1326
|
+
{ artifactState = null, loopState = null, issueLinkageResolution = null } = {},
|
|
1327
|
+
) {
|
|
1328
|
+
const result = {
|
|
1329
|
+
statusKind: DEV_LOOP_STATUS_REPORT_KIND.NEEDS_RECONCILE,
|
|
1330
|
+
reason,
|
|
1331
|
+
activeArtifact: canonicalState ? buildStatusArtifactIdentity(canonicalState) : null,
|
|
1332
|
+
artifactState: null,
|
|
1333
|
+
loopState: "unknown",
|
|
1334
|
+
nextAction,
|
|
1335
|
+
selectedGate: DEV_LOOP_GATE.FAIL_CLOSED_RECONCILE,
|
|
1336
|
+
routeKind: DEV_LOOP_ROUTE_KIND.NEEDS_RECONCILE,
|
|
1337
|
+
selectedStrategy: INTERNAL_DEV_LOOP_STRATEGY.NONE,
|
|
1338
|
+
executionMode,
|
|
1339
|
+
waitSemantics,
|
|
1340
|
+
waitTimeoutPolicy,
|
|
1341
|
+
asyncRun,
|
|
1342
|
+
canonicalState,
|
|
1343
|
+
};
|
|
1344
|
+
return {
|
|
1345
|
+
...result,
|
|
1346
|
+
contractTrace: buildContractTrace({
|
|
1347
|
+
selectedGate: result.selectedGate,
|
|
1348
|
+
routeKind: result.routeKind,
|
|
1349
|
+
selectedStrategy: result.selectedStrategy,
|
|
1350
|
+
executionMode,
|
|
1351
|
+
waitSemantics,
|
|
1352
|
+
waitTimeoutPolicy,
|
|
1353
|
+
canonicalState,
|
|
1354
|
+
reason,
|
|
1355
|
+
boundary: {
|
|
1356
|
+
boundaryKind: "authoritative_status_refresh",
|
|
1357
|
+
refreshRequired: true,
|
|
1358
|
+
refreshReason: "Status answers are derived from refreshed authoritative state and must fail closed when that refresh cannot justify the stop classification.",
|
|
1359
|
+
...(loopState !== null ? { loopState } : {}),
|
|
1360
|
+
...(artifactState !== null ? { artifactState } : {}),
|
|
1361
|
+
...(issueLinkageResolution !== null ? { issueLinkageResolution } : {}),
|
|
1362
|
+
},
|
|
1363
|
+
}),
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
export function resolveAuthoritativeDevLoopStatus(input = {}) {
|
|
1368
|
+
const { intent: _ignoredIntent, ...statusInput } = input;
|
|
1369
|
+
const bundle = resolveAuthoritativeStartupResumeBundle(statusInput);
|
|
1370
|
+
if (bundle.bundleKind === DEV_LOOP_STARTUP_RESUME_BUNDLE_KIND.NEEDS_RECONCILE) {
|
|
1371
|
+
return buildStatusReconcile(
|
|
1372
|
+
bundle.reason,
|
|
1373
|
+
bundle.canonicalState,
|
|
1374
|
+
bundle.nextAction,
|
|
1375
|
+
bundle.executionMode,
|
|
1376
|
+
bundle.waitSemantics,
|
|
1377
|
+
bundle.waitTimeoutPolicy,
|
|
1378
|
+
bundle.asyncRun,
|
|
1379
|
+
{
|
|
1380
|
+
artifactState: bundle.contractTrace?.stateRefresh?.artifactState ?? bundle.artifactState,
|
|
1381
|
+
loopState: bundle.contractTrace?.stateRefresh?.loopState ?? bundle.loopState,
|
|
1382
|
+
issueLinkageResolution: bundle.contractTrace?.stateRefresh?.issueLinkageResolution ?? bundle.issueLinkageResolution,
|
|
1383
|
+
},
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const result = {
|
|
1388
|
+
statusKind: DEV_LOOP_STATUS_REPORT_KIND.RESOLVED,
|
|
1389
|
+
activeArtifact: bundle.activeArtifact,
|
|
1390
|
+
artifactState: bundle.artifactState,
|
|
1391
|
+
loopState: bundle.loopState,
|
|
1392
|
+
nextAction: bundle.nextAction,
|
|
1393
|
+
selectedGate: bundle.selectedGate,
|
|
1394
|
+
routeKind: bundle.routeKind,
|
|
1395
|
+
selectedStrategy: bundle.selectedStrategy,
|
|
1396
|
+
executionMode: bundle.executionMode,
|
|
1397
|
+
waitSemantics: bundle.waitSemantics,
|
|
1398
|
+
waitTimeoutPolicy: bundle.waitTimeoutPolicy,
|
|
1399
|
+
asyncRun: bundle.asyncRun,
|
|
1400
|
+
issueAssignmentSeam: bundle.issueAssignmentSeam,
|
|
1401
|
+
canonicalState: bundle.canonicalState,
|
|
1402
|
+
reason: bundle.reason,
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
return {
|
|
1406
|
+
...result,
|
|
1407
|
+
contractTrace: buildContractTrace({
|
|
1408
|
+
selectedGate: result.selectedGate,
|
|
1409
|
+
routeKind: result.routeKind,
|
|
1410
|
+
selectedStrategy: result.selectedStrategy,
|
|
1411
|
+
executionMode: result.executionMode,
|
|
1412
|
+
waitSemantics: result.waitSemantics,
|
|
1413
|
+
waitTimeoutPolicy: result.waitTimeoutPolicy,
|
|
1414
|
+
canonicalState: result.canonicalState,
|
|
1415
|
+
reason: result.reason,
|
|
1416
|
+
boundary: {
|
|
1417
|
+
boundaryKind: "authoritative_status_refresh",
|
|
1418
|
+
refreshRequired: true,
|
|
1419
|
+
refreshReason: "Status answers record the authoritative refreshed loop state that justified the reported state.",
|
|
1420
|
+
loopState: result.loopState,
|
|
1421
|
+
artifactState: result.artifactState,
|
|
1422
|
+
issueLinkageResolution: bundle.issueLinkageResolution,
|
|
1423
|
+
},
|
|
1424
|
+
}),
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
export function evaluatePublicDevLoopRouting(input = {}) {
|
|
1429
|
+
const intent = normalizeIntent(input.intent);
|
|
1430
|
+
const explicitTarget = normalizeTarget(input.target);
|
|
1431
|
+
const explicitState = normalizeState(input.currentState);
|
|
1432
|
+
|
|
1433
|
+
// ── Variation parameters (first-slice bounded contract) ──────────────────
|
|
1434
|
+
const variationMode = input.mode !== undefined ? normalizeVariationMode(input.mode) : null;
|
|
1435
|
+
const watchProvided = input.watch !== undefined;
|
|
1436
|
+
const watchRequested = input.watch === true;
|
|
1437
|
+
const targetPreference = input.targetPreference !== undefined
|
|
1438
|
+
? normalizeTargetPreference(input.targetPreference)
|
|
1439
|
+
: DEFAULT_TARGET_PREFERENCE;
|
|
1440
|
+
|
|
1441
|
+
// These are authoritative issue-state facts for the Copilot-first
|
|
1442
|
+
// unassigned-issue seam, not bounded public variation parameters.
|
|
1443
|
+
const issueReadiness = input.issueReadiness !== undefined ? normalizeIssueReadiness(input.issueReadiness) : null;
|
|
1444
|
+
const issueAssignmentState = input.issueAssignmentState !== undefined
|
|
1445
|
+
? normalizeIssueAssignmentState(input.issueAssignmentState)
|
|
1446
|
+
: null;
|
|
1447
|
+
const gateReviewEvidence = normalizeGateReviewEvidence(input.gateReviewEvidence);
|
|
1448
|
+
const acceptsIssueAssignmentFacts = shouldAcceptIssueAssignmentFacts({ intent, explicitTarget, explicitState });
|
|
1449
|
+
const retrospectiveCheckpointState = input.retrospectiveCheckpointState !== undefined
|
|
1450
|
+
? normalizeRetrospectiveCheckpointState(input.retrospectiveCheckpointState)
|
|
1451
|
+
: null;
|
|
1452
|
+
const retrospectiveCheckpointStateProvided =
|
|
1453
|
+
input.retrospectiveCheckpointState !== undefined && input.retrospectiveCheckpointState !== null;
|
|
1454
|
+
const requestedExecutionMode =
|
|
1455
|
+
variationMode
|
|
1456
|
+
?? (intent === DEV_LOOP_PUBLIC_INTENT.AUTO_CONTINUE_CURRENT
|
|
1457
|
+
? DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO
|
|
1458
|
+
: DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF);
|
|
1459
|
+
const buildInputReconcile = (reason, canonicalState = null, executionMode = requestedExecutionMode) => buildReconcile(
|
|
1460
|
+
reason,
|
|
1461
|
+
canonicalState,
|
|
1462
|
+
executionMode,
|
|
1463
|
+
{ watchRequested },
|
|
1464
|
+
);
|
|
1465
|
+
|
|
1466
|
+
// Fail closed on unrecognized variation parameter values
|
|
1467
|
+
if (input.mode !== undefined && variationMode === null) {
|
|
1468
|
+
return buildInputReconcile(`Unrecognized \`mode\` parameter; allowed values: ${ALLOWED_MODE_VALUES_TEXT}.`, null, requestedExecutionMode);
|
|
1469
|
+
}
|
|
1470
|
+
if (input.targetPreference !== undefined && targetPreference === null) {
|
|
1471
|
+
return buildInputReconcile(`Unrecognized \`targetPreference\` parameter; allowed values: ${ALLOWED_TARGET_PREFERENCE_VALUES_TEXT}.`, null, requestedExecutionMode);
|
|
1472
|
+
}
|
|
1473
|
+
if (watchProvided && typeof input.watch !== "boolean") {
|
|
1474
|
+
return buildInputReconcile("Unrecognized `watch` parameter; allowed values: true or false.", null, requestedExecutionMode);
|
|
1475
|
+
}
|
|
1476
|
+
if (acceptsIssueAssignmentFacts && input.issueReadiness !== undefined && issueReadiness === null) {
|
|
1477
|
+
return buildInputReconcile(
|
|
1478
|
+
`Unrecognized \`issueReadiness\` input; allowed values: ${Object.values(DEV_LOOP_ISSUE_READINESS).join(", ")}.`,
|
|
1479
|
+
null,
|
|
1480
|
+
requestedExecutionMode,
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (acceptsIssueAssignmentFacts && input.issueAssignmentState !== undefined && issueAssignmentState === null) {
|
|
1484
|
+
return buildInputReconcile(
|
|
1485
|
+
`Unrecognized \`issueAssignmentState\` input; allowed values: ${Object.values(DEV_LOOP_ISSUE_ASSIGNMENT_STATE).join(", ")}.`,
|
|
1486
|
+
null,
|
|
1487
|
+
requestedExecutionMode,
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (retrospectiveCheckpointStateProvided && retrospectiveCheckpointState === null) {
|
|
1492
|
+
return buildInputReconcile(
|
|
1493
|
+
"Unrecognized `retrospectiveCheckpointState` input; allowed values: none, complete, skipped, missing.",
|
|
1494
|
+
null,
|
|
1495
|
+
requestedExecutionMode,
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const routingOptions = {
|
|
1500
|
+
executionMode: null,
|
|
1501
|
+
issueReadiness: acceptsIssueAssignmentFacts ? issueReadiness : null,
|
|
1502
|
+
issueAssignmentState: acceptsIssueAssignmentFacts ? issueAssignmentState : null,
|
|
1503
|
+
gateReviewEvidence,
|
|
1504
|
+
targetPreference,
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
const finalizeRoutingResult = (result) => {
|
|
1508
|
+
const gated = applyRetrospectiveCheckpointGate(
|
|
1509
|
+
result,
|
|
1510
|
+
retrospectiveCheckpointState,
|
|
1511
|
+
retrospectiveCheckpointStateProvided,
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
return withContractTrace(gated, {
|
|
1515
|
+
watchRequested,
|
|
1516
|
+
boundary: gated.contractTrace?.stateRefresh ?? result.contractTrace?.stateRefresh ?? null,
|
|
1517
|
+
});
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
if (!intent) {
|
|
1521
|
+
return buildInputReconcile("The public dev-loop intent is missing or unrecognized.", null, requestedExecutionMode);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// ── Resolve effective execution mode ─────────────────────────────────────
|
|
1525
|
+
// Precedence: authoritative intent (auto_continue_current) > explicit mode > default
|
|
1526
|
+
let effectiveMode;
|
|
1527
|
+
if (intent === DEV_LOOP_PUBLIC_INTENT.AUTO_CONTINUE_CURRENT) {
|
|
1528
|
+
if (variationMode === DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF) {
|
|
1529
|
+
return buildInputReconcile(
|
|
1530
|
+
"`mode=bounded_handoff` conflicts with the `auto_continue_current` intent; `auto_continue_current` always uses durable auto execution mode.",
|
|
1531
|
+
explicitState,
|
|
1532
|
+
DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO,
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
effectiveMode = DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO;
|
|
1536
|
+
} else {
|
|
1537
|
+
effectiveMode = variationMode ?? DEV_LOOP_EXECUTION_MODE.BOUNDED_HANDOFF;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
if (variationMode === DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO && !explicitState) {
|
|
1541
|
+
return buildInputReconcile(
|
|
1542
|
+
"`mode=durable_auto` requires a valid authoritative current state.",
|
|
1543
|
+
null,
|
|
1544
|
+
DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO,
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1548
|
+
|
|
1549
|
+
if (intent === DEV_LOOP_PUBLIC_INTENT.INSPECT_STATE) {
|
|
1550
|
+
if (!explicitState) {
|
|
1551
|
+
return buildInputReconcile("`inspect_state` requires a valid canonical current state.", null, effectiveMode);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const routed = routeForState(explicitState, { ...routingOptions, executionMode: effectiveMode });
|
|
1555
|
+
return finalizeRoutingResult(applyWatchValidation({
|
|
1556
|
+
...routed,
|
|
1557
|
+
routeKind: DEV_LOOP_ROUTE_KIND.INSPECT,
|
|
1558
|
+
nextAction: "Describe the canonical state and the routed internal strategy without changing public entrypoints.",
|
|
1559
|
+
}, watchRequested));
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (intent === DEV_LOOP_PUBLIC_INTENT.START_ON_ISSUE) {
|
|
1563
|
+
if (!explicitTarget || explicitTarget.kind !== DEV_LOOP_TARGET_KIND.ISSUE) {
|
|
1564
|
+
return buildInputReconcile("`start_on_issue` requires an issue target.", null, effectiveMode);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (input.currentState !== undefined && !explicitState) {
|
|
1568
|
+
return buildInputReconcile("`start_on_issue` received an invalid canonical current state.", null, effectiveMode);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (explicitState) {
|
|
1572
|
+
if (explicitState.target.issue !== explicitTarget.issue) {
|
|
1573
|
+
return buildInputReconcile("`start_on_issue` target conflicts with the canonical current state.", explicitState, effectiveMode);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// targetPreference=prefer_local must not override authoritative linked-PR or PR state
|
|
1577
|
+
if (targetPreference === DEV_LOOP_TARGET_PREFERENCE.PREFER_LOCAL) {
|
|
1578
|
+
const isLinkedPrState =
|
|
1579
|
+
explicitState.target.kind === DEV_LOOP_TARGET_KIND.PR ||
|
|
1580
|
+
(explicitState.target.kind === DEV_LOOP_TARGET_KIND.ISSUE && explicitState.target.linkedPr !== null);
|
|
1581
|
+
if (isLinkedPrState) {
|
|
1582
|
+
return buildInputReconcile(
|
|
1583
|
+
"`targetPreference=prefer_local` conflicts with authoritative PR/linked-PR active artifact state; reconcile before overriding the routed path.",
|
|
1584
|
+
explicitState,
|
|
1585
|
+
effectiveMode,
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
return finalizeRoutingResult(applyWatchValidation(
|
|
1591
|
+
routeForState(explicitState, { ...routingOptions, executionMode: effectiveMode }),
|
|
1592
|
+
watchRequested,
|
|
1593
|
+
));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// No canonical state: steer toward local when prefer_local is requested
|
|
1597
|
+
if (targetPreference === DEV_LOOP_TARGET_PREFERENCE.PREFER_LOCAL) {
|
|
1598
|
+
return finalizeRoutingResult(applyWatchValidation(
|
|
1599
|
+
routeForState({
|
|
1600
|
+
target: {
|
|
1601
|
+
kind: DEV_LOOP_TARGET_KIND.LOCAL_PHASE,
|
|
1602
|
+
issue: explicitTarget.issue,
|
|
1603
|
+
pr: null,
|
|
1604
|
+
linkedPr: null,
|
|
1605
|
+
branch: null,
|
|
1606
|
+
phase: `issue-${explicitTarget.issue}`,
|
|
1607
|
+
},
|
|
1608
|
+
ownership: DEV_LOOP_ACTOR.LOCAL,
|
|
1609
|
+
nextActor: DEV_LOOP_ACTOR.LOCAL,
|
|
1610
|
+
status: DEV_LOOP_STATUS.ACTIVE,
|
|
1611
|
+
authorization: DEV_LOOP_AUTHORIZATION.AUTHORIZED,
|
|
1612
|
+
}, { ...routingOptions, executionMode: effectiveMode }),
|
|
1613
|
+
watchRequested,
|
|
1614
|
+
));
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return finalizeRoutingResult(applyWatchValidation(
|
|
1618
|
+
routeForState({
|
|
1619
|
+
target: explicitTarget,
|
|
1620
|
+
ownership: DEV_LOOP_ACTOR.COPILOT,
|
|
1621
|
+
nextActor: DEV_LOOP_ACTOR.USER,
|
|
1622
|
+
status: DEV_LOOP_STATUS.ACTIVE,
|
|
1623
|
+
authorization: DEV_LOOP_AUTHORIZATION.NEEDS_CONFIRMATION,
|
|
1624
|
+
}, { ...routingOptions, executionMode: effectiveMode }),
|
|
1625
|
+
watchRequested,
|
|
1626
|
+
));
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (
|
|
1630
|
+
intent === DEV_LOOP_PUBLIC_INTENT.START_ISSUE_LOCALLY ||
|
|
1631
|
+
intent === DEV_LOOP_PUBLIC_INTENT.START_ISSUE_LOCALLY_THEN_CONTINUE
|
|
1632
|
+
) {
|
|
1633
|
+
if (!explicitTarget || explicitTarget.kind !== DEV_LOOP_TARGET_KIND.ISSUE) {
|
|
1634
|
+
return buildInputReconcile("Local issue-start intents require an issue target.", null, effectiveMode);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (input.currentState !== undefined && !explicitState) {
|
|
1638
|
+
return buildInputReconcile("Local issue-start intents received an invalid canonical current state.", null, effectiveMode);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (explicitState) {
|
|
1642
|
+
if (
|
|
1643
|
+
explicitState.target.kind !== DEV_LOOP_TARGET_KIND.LOCAL_PHASE ||
|
|
1644
|
+
explicitState.target.issue !== explicitTarget.issue
|
|
1645
|
+
) {
|
|
1646
|
+
return buildInputReconcile("Local issue-start target conflicts with the canonical current state.", explicitState, effectiveMode);
|
|
1647
|
+
}
|
|
1648
|
+
return finalizeRoutingResult(applyWatchValidation(
|
|
1649
|
+
routeForState(explicitState, { ...routingOptions, executionMode: effectiveMode }),
|
|
1650
|
+
watchRequested,
|
|
1651
|
+
));
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const routed = routeForState({
|
|
1655
|
+
target: {
|
|
1656
|
+
kind: DEV_LOOP_TARGET_KIND.LOCAL_PHASE,
|
|
1657
|
+
issue: explicitTarget.issue,
|
|
1658
|
+
pr: null,
|
|
1659
|
+
linkedPr: null,
|
|
1660
|
+
branch: null,
|
|
1661
|
+
phase: `issue-${explicitTarget.issue}`,
|
|
1662
|
+
},
|
|
1663
|
+
ownership: DEV_LOOP_ACTOR.LOCAL,
|
|
1664
|
+
nextActor: DEV_LOOP_ACTOR.LOCAL,
|
|
1665
|
+
status: DEV_LOOP_STATUS.ACTIVE,
|
|
1666
|
+
authorization: DEV_LOOP_AUTHORIZATION.AUTHORIZED,
|
|
1667
|
+
}, { ...routingOptions, executionMode: effectiveMode });
|
|
1668
|
+
|
|
1669
|
+
const routedWithContinueAction = intent === DEV_LOOP_PUBLIC_INTENT.START_ISSUE_LOCALLY_THEN_CONTINUE
|
|
1670
|
+
? {
|
|
1671
|
+
...routed,
|
|
1672
|
+
nextAction:
|
|
1673
|
+
"Start with the local implementation strategy now, then re-enter the same public `dev-loop` entrypoint against the updated canonical state.",
|
|
1674
|
+
}
|
|
1675
|
+
: routed;
|
|
1676
|
+
|
|
1677
|
+
return finalizeRoutingResult(applyWatchValidation(routedWithContinueAction, watchRequested));
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (intent === DEV_LOOP_PUBLIC_INTENT.CONTINUE_ON_PR) {
|
|
1681
|
+
if (!explicitTarget || explicitTarget.kind !== DEV_LOOP_TARGET_KIND.PR) {
|
|
1682
|
+
return buildInputReconcile("`continue_on_pr` requires a PR target.", null, effectiveMode);
|
|
1683
|
+
}
|
|
1684
|
+
if (!explicitState || explicitState.target.kind !== DEV_LOOP_TARGET_KIND.PR) {
|
|
1685
|
+
return buildInputReconcile("`continue_on_pr` requires a valid canonical PR state.", explicitState, effectiveMode);
|
|
1686
|
+
}
|
|
1687
|
+
if (explicitState.target.pr !== explicitTarget.pr) {
|
|
1688
|
+
return buildInputReconcile("`continue_on_pr` target conflicts with the canonical current PR state.", explicitState, effectiveMode);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// targetPreference=prefer_local must not override an active PR artifact
|
|
1692
|
+
if (targetPreference === DEV_LOOP_TARGET_PREFERENCE.PREFER_LOCAL) {
|
|
1693
|
+
return buildInputReconcile(
|
|
1694
|
+
"`targetPreference=prefer_local` conflicts with authoritative PR/linked-PR active artifact state; reconcile before overriding the routed path.",
|
|
1695
|
+
explicitState,
|
|
1696
|
+
effectiveMode,
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
return finalizeRoutingResult(applyWatchValidation(
|
|
1701
|
+
routeForState(explicitState, { ...routingOptions, executionMode: effectiveMode }),
|
|
1702
|
+
watchRequested,
|
|
1703
|
+
));
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (intent === DEV_LOOP_PUBLIC_INTENT.CONTINUE_CURRENT) {
|
|
1707
|
+
if (!explicitState) {
|
|
1708
|
+
return buildInputReconcile("`continue_current` requires a valid canonical current state.", null, effectiveMode);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// targetPreference=prefer_local must not override an active PR artifact or linked-PR state
|
|
1712
|
+
if (targetPreference === DEV_LOOP_TARGET_PREFERENCE.PREFER_LOCAL) {
|
|
1713
|
+
const isLinkedPrState =
|
|
1714
|
+
explicitState.target.kind === DEV_LOOP_TARGET_KIND.PR ||
|
|
1715
|
+
(explicitState.target.kind === DEV_LOOP_TARGET_KIND.ISSUE && explicitState.target.linkedPr !== null);
|
|
1716
|
+
if (isLinkedPrState) {
|
|
1717
|
+
return buildInputReconcile(
|
|
1718
|
+
"`targetPreference=prefer_local` conflicts with authoritative PR/linked-PR active artifact state; reconcile before overriding the routed path.",
|
|
1719
|
+
explicitState,
|
|
1720
|
+
effectiveMode,
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
return finalizeRoutingResult(applyWatchValidation(
|
|
1726
|
+
routeForState(explicitState, { ...routingOptions, executionMode: effectiveMode }),
|
|
1727
|
+
watchRequested,
|
|
1728
|
+
));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (intent === DEV_LOOP_PUBLIC_INTENT.AUTO_CONTINUE_CURRENT) {
|
|
1732
|
+
if (!explicitState) {
|
|
1733
|
+
return buildInputReconcile(
|
|
1734
|
+
"`auto_continue_current` requires a valid canonical current state.",
|
|
1735
|
+
null,
|
|
1736
|
+
DEV_LOOP_EXECUTION_MODE.DURABLE_AUTO,
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
return finalizeRoutingResult(applyWatchValidation(
|
|
1740
|
+
routeForState(explicitState, { ...routingOptions, executionMode: effectiveMode }),
|
|
1741
|
+
watchRequested,
|
|
1742
|
+
));
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
return buildInputReconcile("The public dev-loop intent is recognized but not implemented in this first slice.", null, effectiveMode);
|
|
1746
|
+
}
|