@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,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conductor routing contract: deterministic routing and handoff decisions
|
|
3
|
+
* above family-local state machines.
|
|
4
|
+
*
|
|
5
|
+
* This module provides:
|
|
6
|
+
* - ROUTING_OUTCOME: closed routing outcome taxonomy constants
|
|
7
|
+
* - LOOP_FAMILY: loop family identifier constants
|
|
8
|
+
* - SOURCE_MODE: confidence/source mode constants
|
|
9
|
+
* - ENTRYPOINT: handoff entrypoint identifier constants
|
|
10
|
+
* - STOP_REASON: stop reason code constants (for outer-loop backward compat)
|
|
11
|
+
* - evaluateConductorRouting: shared evaluator/policy entrypoint
|
|
12
|
+
*
|
|
13
|
+
* Contract guarantees:
|
|
14
|
+
* - One deterministic routing outcome per normalized input set
|
|
15
|
+
* - Ambiguous, conflicting, or insufficient inputs return `needs_reconcile`
|
|
16
|
+
* rather than a guessed handoff
|
|
17
|
+
* - The evaluator is purely functional; no I/O or side effects
|
|
18
|
+
* - Callers use evaluateConductorRouting as the single routing authority
|
|
19
|
+
*
|
|
20
|
+
* Integration boundary (see docs/conductor-routing-contract.md):
|
|
21
|
+
* - This module starts after active-run identity and ownership are already resolved
|
|
22
|
+
* - It consumes already-detected family-local lifecycle states as inputs
|
|
23
|
+
* - It derives the routing outcome directly from states; it does not take a
|
|
24
|
+
* pre-computed outer-loop action as an input
|
|
25
|
+
* - It emits routing decisions and handoff envelopes; it does not perform handoff
|
|
26
|
+
* - Ownership/idempotency rules remain in conductor-ownership.mjs (#32)
|
|
27
|
+
* - Family-local state machine semantics remain in copilot-loop-state.mjs etc. (#26)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Exported constants
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Closed routing outcome taxonomy constants.
|
|
36
|
+
*
|
|
37
|
+
* Covers all possible routing decisions for an already-targeted active run.
|
|
38
|
+
*/
|
|
39
|
+
export const ROUTING_OUTCOME = Object.freeze({
|
|
40
|
+
/** Outer-loop wait; re-enter after a bounded wait interval. No handoff needed yet. */
|
|
41
|
+
CONTINUE_CURRENT_WAIT: "continue_current_wait",
|
|
42
|
+
/** Copilot inner loop should handle the next step. */
|
|
43
|
+
HANDOFF_TO_COPILOT_LOOP: "handoff_to_copilot_loop",
|
|
44
|
+
/** Reviewer inner loop should handle the next step. */
|
|
45
|
+
HANDOFF_TO_REVIEWER_LOOP: "handoff_to_reviewer_loop",
|
|
46
|
+
/** A live owner already has control; no new handoff is needed at this cycle. */
|
|
47
|
+
STAY_WITH_CURRENT_LIVE_OWNER: "stay_with_current_live_owner",
|
|
48
|
+
/** Blocked state requiring human intervention before any loop can proceed. */
|
|
49
|
+
STOP_NEEDS_HUMAN: "stop_needs_human",
|
|
50
|
+
/** PR is merged, closed, or fully done; no further loop action is needed. */
|
|
51
|
+
DONE_TERMINAL: "done_terminal",
|
|
52
|
+
/** Ambiguous, conflicting, stale, or insufficient signals; reconcile before routing. */
|
|
53
|
+
NEEDS_RECONCILE: "needs_reconcile",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Loop family identifier constants.
|
|
58
|
+
*/
|
|
59
|
+
export const LOOP_FAMILY = Object.freeze({
|
|
60
|
+
/** Copilot review/fix inner loop. */
|
|
61
|
+
COPILOT_LOOP: "copilot_loop",
|
|
62
|
+
/** Reviewer-side inner loop. */
|
|
63
|
+
REVIEWER_LOOP: "reviewer_loop",
|
|
64
|
+
/** Outer conductor loop (wait/checkpoint). */
|
|
65
|
+
OUTER_LOOP: "outer_loop",
|
|
66
|
+
/** No loop family (terminal, blocked, or reconcile states). */
|
|
67
|
+
NONE: null,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Source/confidence mode constants for routing inputs.
|
|
72
|
+
*/
|
|
73
|
+
export const SOURCE_MODE = Object.freeze({
|
|
74
|
+
/** State derived from authoritative remote signals. */
|
|
75
|
+
AUTHORITATIVE: "authoritative",
|
|
76
|
+
/** State derived from local records only. */
|
|
77
|
+
LOCAL: "local",
|
|
78
|
+
/** State from a pre-captured snapshot (snapshot-mode testing or replay). */
|
|
79
|
+
SNAPSHOT: "snapshot",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handoff entrypoint identifier constants.
|
|
84
|
+
*
|
|
85
|
+
* These identify the specific handler/script that the conductor should invoke
|
|
86
|
+
* for each loop family, without requiring prose to restate the branch logic.
|
|
87
|
+
*/
|
|
88
|
+
export const ENTRYPOINT = Object.freeze({
|
|
89
|
+
/** copilot-pr-handoff.mjs — main copilot loop re-entry handler. */
|
|
90
|
+
COPILOT_PR_HANDOFF: "copilot_pr_handoff",
|
|
91
|
+
/** reviewer loop handler — reviewer-side inner loop re-entry. */
|
|
92
|
+
REVIEWER_LOOP_HANDLER: "reviewer_loop_handler",
|
|
93
|
+
/** outer-loop.mjs — outer wait/checkpoint re-run. */
|
|
94
|
+
OUTER_LOOP_WAIT: "outer_loop_wait",
|
|
95
|
+
/** No automated entrypoint; human intervention required. */
|
|
96
|
+
NONE: null,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Stop reason code constants for outer-loop backward-compatibility.
|
|
101
|
+
*
|
|
102
|
+
* Populated in `stopReason` on results whose `outerAction` is "stop".
|
|
103
|
+
*/
|
|
104
|
+
export const STOP_REASON = Object.freeze({
|
|
105
|
+
PR_NOT_READY: "pr_not_ready",
|
|
106
|
+
COPILOT_BLOCKED: "copilot_blocked",
|
|
107
|
+
REVIEWER_BLOCKED: "reviewer_blocked",
|
|
108
|
+
REVIEW_UNAVAILABLE: "review_unavailable",
|
|
109
|
+
OWNERSHIP_CONFLICT: "ownership_conflict",
|
|
110
|
+
UNKNOWN_STATE: "unknown_state",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Internal: state classification sets
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
// Copilot strong active states: win over reviewer wait states
|
|
118
|
+
const COPILOT_STRONG_ACTIVE = new Set([
|
|
119
|
+
"unresolved_feedback_present",
|
|
120
|
+
"already_fixed_needs_reply_resolve",
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
// Copilot weak active states: yield to reviewer wait states
|
|
124
|
+
const COPILOT_WEAK_ACTIVE = new Set([
|
|
125
|
+
"pr_ready_no_feedback",
|
|
126
|
+
"ready_to_rerequest_review",
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
// Copilot wait states owned by the orchestrator
|
|
130
|
+
const COPILOT_WAIT = new Set([
|
|
131
|
+
"waiting_for_copilot_review",
|
|
132
|
+
"waiting_for_ci",
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// Reviewer active states requiring handoff or isolation check
|
|
136
|
+
const REVIEWER_ACTIVE = new Set([
|
|
137
|
+
"review_requested",
|
|
138
|
+
"determine_review_plan",
|
|
139
|
+
"reviews_running",
|
|
140
|
+
"merge_results",
|
|
141
|
+
"draft_review_ready",
|
|
142
|
+
"draft_review_posted",
|
|
143
|
+
"waiting_for_user_submit",
|
|
144
|
+
"review_invalidated",
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// Reviewer wait states owned by the orchestrator
|
|
148
|
+
const REVIEWER_WAIT = new Set([
|
|
149
|
+
"submitted_review",
|
|
150
|
+
"waiting_for_author_followup",
|
|
151
|
+
"waiting_for_re_request",
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// Ownership state that indicates a live owner is already active
|
|
155
|
+
const OWNERSHIP_LIVE_OWNER = "live_owner";
|
|
156
|
+
|
|
157
|
+
// Ownership state that indicates duplicate local owners (must reconcile)
|
|
158
|
+
const OWNERSHIP_DUPLICATE_LOCAL_OWNERS = "duplicate_local_owners";
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Input normalization helpers
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
function normalizeTarget(target) {
|
|
165
|
+
if (!target || typeof target !== "object") {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const { repo, pr } = target;
|
|
169
|
+
if (typeof repo !== "string" || repo.trim().length === 0) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (typeof pr !== "number" || !Number.isInteger(pr) || pr <= 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return { repo: repo.trim().toLowerCase(), pr };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function describeMalformedTarget(target) {
|
|
179
|
+
if (!target || typeof target !== "object") {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const repo = typeof target.repo === "string" && target.repo.trim().length > 0
|
|
184
|
+
? target.repo.trim().toLowerCase()
|
|
185
|
+
: null;
|
|
186
|
+
const pr = typeof target.pr === "number" && Number.isInteger(target.pr) && target.pr > 0
|
|
187
|
+
? target.pr
|
|
188
|
+
: null;
|
|
189
|
+
|
|
190
|
+
return { repo, pr };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveConfidence(sourceMode) {
|
|
194
|
+
if (sourceMode === SOURCE_MODE.AUTHORITATIVE) {
|
|
195
|
+
return SOURCE_MODE.AUTHORITATIVE;
|
|
196
|
+
}
|
|
197
|
+
if (sourceMode === SOURCE_MODE.SNAPSHOT) {
|
|
198
|
+
return SOURCE_MODE.SNAPSHOT;
|
|
199
|
+
}
|
|
200
|
+
return SOURCE_MODE.LOCAL;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Handoff envelope builder
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build a machine-readable handoff envelope.
|
|
209
|
+
*
|
|
210
|
+
* @param {object} params
|
|
211
|
+
* @returns {object}
|
|
212
|
+
*/
|
|
213
|
+
function buildEnvelope({
|
|
214
|
+
targetIdentity,
|
|
215
|
+
loopFamily,
|
|
216
|
+
entrypoint,
|
|
217
|
+
reason,
|
|
218
|
+
requiredArgs = {},
|
|
219
|
+
requiresLocalIsolation = false,
|
|
220
|
+
confidence = SOURCE_MODE.LOCAL,
|
|
221
|
+
}) {
|
|
222
|
+
return {
|
|
223
|
+
targetIdentity,
|
|
224
|
+
loopFamily,
|
|
225
|
+
entrypoint,
|
|
226
|
+
reason,
|
|
227
|
+
requiredArgs,
|
|
228
|
+
requiresLocalIsolation,
|
|
229
|
+
confidence,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Internal: routing helpers
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build a stay_with_current_live_owner result when an active live owner
|
|
239
|
+
* is already handling this scope; no new handoff is needed.
|
|
240
|
+
*/
|
|
241
|
+
function stayWithLiveOwner({
|
|
242
|
+
normalizedTarget,
|
|
243
|
+
copilotState,
|
|
244
|
+
reviewerState,
|
|
245
|
+
baseArgs,
|
|
246
|
+
requiresLocalIsolation,
|
|
247
|
+
confidence,
|
|
248
|
+
}) {
|
|
249
|
+
return {
|
|
250
|
+
routingOutcome: ROUTING_OUTCOME.STAY_WITH_CURRENT_LIVE_OWNER,
|
|
251
|
+
outerAction: "continue_wait",
|
|
252
|
+
stopReason: null,
|
|
253
|
+
handoffEnvelope: buildEnvelope({
|
|
254
|
+
targetIdentity: normalizedTarget,
|
|
255
|
+
loopFamily: LOOP_FAMILY.OUTER_LOOP,
|
|
256
|
+
entrypoint: ENTRYPOINT.OUTER_LOOP_WAIT,
|
|
257
|
+
reason: `A live owner is already active for this scope; no new handoff issued: copilot_state=${copilotState}, reviewer_state=${reviewerState}`,
|
|
258
|
+
requiredArgs: baseArgs,
|
|
259
|
+
requiresLocalIsolation,
|
|
260
|
+
confidence,
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function continueCurrentWait({
|
|
266
|
+
normalizedTarget,
|
|
267
|
+
copilotState,
|
|
268
|
+
reviewerState,
|
|
269
|
+
baseArgs,
|
|
270
|
+
requiresLocalIsolation,
|
|
271
|
+
confidence,
|
|
272
|
+
}) {
|
|
273
|
+
return {
|
|
274
|
+
routingOutcome: ROUTING_OUTCOME.CONTINUE_CURRENT_WAIT,
|
|
275
|
+
outerAction: "continue_wait",
|
|
276
|
+
stopReason: null,
|
|
277
|
+
handoffEnvelope: buildEnvelope({
|
|
278
|
+
targetIdentity: normalizedTarget,
|
|
279
|
+
loopFamily: LOOP_FAMILY.OUTER_LOOP,
|
|
280
|
+
entrypoint: ENTRYPOINT.OUTER_LOOP_WAIT,
|
|
281
|
+
reason: `Outer-loop wait state: copilot_state=${copilotState}, reviewer_state=${reviewerState}`,
|
|
282
|
+
requiredArgs: baseArgs,
|
|
283
|
+
requiresLocalIsolation,
|
|
284
|
+
confidence,
|
|
285
|
+
}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Core routing policy: derive a routing outcome from normalized states.
|
|
291
|
+
*
|
|
292
|
+
* This function contains the real branch logic. Both evaluateConductorRouting
|
|
293
|
+
* (full contract with target validation) and the thin decideOuterAction adapter
|
|
294
|
+
* (target-agnostic) delegate here.
|
|
295
|
+
*
|
|
296
|
+
* Priority order (first match wins):
|
|
297
|
+
* 1. Ownership conflict (duplicate_local_owners) → needs_reconcile
|
|
298
|
+
* 2. Terminal (done) → done_terminal
|
|
299
|
+
* 3. Missing PR (no_pr) → stop_needs_human / pr_not_ready
|
|
300
|
+
* 4. Hard copilot stop (review_request_unavailable, blocked) → stop_needs_human
|
|
301
|
+
* 5. Hard reviewer stop (blocked) → stop_needs_human
|
|
302
|
+
* 6. pr_draft — live-owner check, then handoff (marking requiresLocalIsolation when needed)
|
|
303
|
+
* 7. Copilot explicit review-settle wait (waiting_for_copilot_review) → continue_current_wait
|
|
304
|
+
* 8. Reviewer active states — live-owner check, handoff (marking requiresLocalIsolation when needed)
|
|
305
|
+
* 9. Copilot strong active states — live-owner check, handoff (marking requiresLocalIsolation when needed)
|
|
306
|
+
* 10. Outer-loop wait states (copilot or reviewer)
|
|
307
|
+
* 11. Copilot weak active states (yield to reviewer wait above)
|
|
308
|
+
* 12. Fallback → needs_reconcile / unknown_state
|
|
309
|
+
*
|
|
310
|
+
* @param {object} params
|
|
311
|
+
* @param {{ repo: string, pr: number }} params.normalizedTarget
|
|
312
|
+
* @param {string} params.copilotState
|
|
313
|
+
* @param {string} params.reviewerState
|
|
314
|
+
* @param {string|undefined} params.ownershipState
|
|
315
|
+
* @param {boolean} params.requiresLocalIsolation
|
|
316
|
+
* @param {string} params.confidence
|
|
317
|
+
* @returns {{ routingOutcome: string, outerAction: string, stopReason: string|null, handoffEnvelope: object }}
|
|
318
|
+
*/
|
|
319
|
+
function routeFromStates({
|
|
320
|
+
normalizedTarget,
|
|
321
|
+
copilotState,
|
|
322
|
+
reviewerState,
|
|
323
|
+
ownershipState,
|
|
324
|
+
requiresLocalIsolation,
|
|
325
|
+
confidence,
|
|
326
|
+
}) {
|
|
327
|
+
const baseArgs = { repo: normalizedTarget.repo, pr: normalizedTarget.pr };
|
|
328
|
+
|
|
329
|
+
// 1. Ownership conflict — must reconcile before routing
|
|
330
|
+
if (ownershipState === OWNERSHIP_DUPLICATE_LOCAL_OWNERS) {
|
|
331
|
+
return {
|
|
332
|
+
routingOutcome: ROUTING_OUTCOME.NEEDS_RECONCILE,
|
|
333
|
+
outerAction: "stop",
|
|
334
|
+
stopReason: STOP_REASON.OWNERSHIP_CONFLICT,
|
|
335
|
+
handoffEnvelope: buildEnvelope({
|
|
336
|
+
targetIdentity: normalizedTarget,
|
|
337
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
338
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
339
|
+
reason: "Ownership state indicates duplicate local owners; reconcile ownership before routing",
|
|
340
|
+
requiredArgs: baseArgs,
|
|
341
|
+
requiresLocalIsolation,
|
|
342
|
+
confidence,
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 2. Terminal
|
|
348
|
+
if (copilotState === "done") {
|
|
349
|
+
return {
|
|
350
|
+
routingOutcome: ROUTING_OUTCOME.DONE_TERMINAL,
|
|
351
|
+
outerAction: "done",
|
|
352
|
+
stopReason: null,
|
|
353
|
+
handoffEnvelope: buildEnvelope({
|
|
354
|
+
targetIdentity: normalizedTarget,
|
|
355
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
356
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
357
|
+
reason: "PR is merged or closed; conductor loop is complete",
|
|
358
|
+
requiredArgs: baseArgs,
|
|
359
|
+
requiresLocalIsolation,
|
|
360
|
+
confidence,
|
|
361
|
+
}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 3. No PR
|
|
366
|
+
if (copilotState === "no_pr") {
|
|
367
|
+
return {
|
|
368
|
+
routingOutcome: ROUTING_OUTCOME.STOP_NEEDS_HUMAN,
|
|
369
|
+
outerAction: "stop",
|
|
370
|
+
stopReason: STOP_REASON.PR_NOT_READY,
|
|
371
|
+
handoffEnvelope: buildEnvelope({
|
|
372
|
+
targetIdentity: normalizedTarget,
|
|
373
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
374
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
375
|
+
reason: "No open PR exists for this scope; cannot route",
|
|
376
|
+
requiredArgs: baseArgs,
|
|
377
|
+
requiresLocalIsolation,
|
|
378
|
+
confidence,
|
|
379
|
+
}),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 4. Hard copilot stops
|
|
384
|
+
if (copilotState === "review_request_unavailable") {
|
|
385
|
+
return {
|
|
386
|
+
routingOutcome: ROUTING_OUTCOME.STOP_NEEDS_HUMAN,
|
|
387
|
+
outerAction: "stop",
|
|
388
|
+
stopReason: STOP_REASON.REVIEW_UNAVAILABLE,
|
|
389
|
+
handoffEnvelope: buildEnvelope({
|
|
390
|
+
targetIdentity: normalizedTarget,
|
|
391
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
392
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
393
|
+
reason: "Copilot review request returned unavailable; human intervention required",
|
|
394
|
+
requiredArgs: baseArgs,
|
|
395
|
+
requiresLocalIsolation,
|
|
396
|
+
confidence,
|
|
397
|
+
}),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (copilotState === "blocked_needs_user_decision") {
|
|
402
|
+
return {
|
|
403
|
+
routingOutcome: ROUTING_OUTCOME.STOP_NEEDS_HUMAN,
|
|
404
|
+
outerAction: "stop",
|
|
405
|
+
stopReason: STOP_REASON.COPILOT_BLOCKED,
|
|
406
|
+
handoffEnvelope: buildEnvelope({
|
|
407
|
+
targetIdentity: normalizedTarget,
|
|
408
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
409
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
410
|
+
reason: "Copilot loop is blocked and requires human decision",
|
|
411
|
+
requiredArgs: baseArgs,
|
|
412
|
+
requiresLocalIsolation,
|
|
413
|
+
confidence,
|
|
414
|
+
}),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 5. Hard reviewer stop
|
|
419
|
+
if (reviewerState === "blocked_needs_user_decision") {
|
|
420
|
+
return {
|
|
421
|
+
routingOutcome: ROUTING_OUTCOME.STOP_NEEDS_HUMAN,
|
|
422
|
+
outerAction: "stop",
|
|
423
|
+
stopReason: STOP_REASON.REVIEWER_BLOCKED,
|
|
424
|
+
handoffEnvelope: buildEnvelope({
|
|
425
|
+
targetIdentity: normalizedTarget,
|
|
426
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
427
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
428
|
+
reason: "Reviewer loop is blocked and requires human decision",
|
|
429
|
+
requiredArgs: baseArgs,
|
|
430
|
+
requiresLocalIsolation,
|
|
431
|
+
confidence,
|
|
432
|
+
}),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 6. pr_draft — hand off to the copilot loop; dirty/detached checkouts
|
|
437
|
+
// are surfaced via handoffEnvelope.requiresLocalIsolation so callers can
|
|
438
|
+
// re-enter from an isolated checkout/worktree instead of treating the seam
|
|
439
|
+
// as a terminal stop.
|
|
440
|
+
if (copilotState === "pr_draft") {
|
|
441
|
+
if (ownershipState === OWNERSHIP_LIVE_OWNER) {
|
|
442
|
+
return stayWithLiveOwner({ normalizedTarget, copilotState, reviewerState, baseArgs, requiresLocalIsolation, confidence });
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
routingOutcome: ROUTING_OUTCOME.HANDOFF_TO_COPILOT_LOOP,
|
|
446
|
+
outerAction: "reenter_copilot_loop",
|
|
447
|
+
stopReason: null,
|
|
448
|
+
handoffEnvelope: buildEnvelope({
|
|
449
|
+
targetIdentity: normalizedTarget,
|
|
450
|
+
loopFamily: LOOP_FAMILY.COPILOT_LOOP,
|
|
451
|
+
entrypoint: ENTRYPOINT.COPILOT_PR_HANDOFF,
|
|
452
|
+
reason: `PR is in draft state; copilot loop required: copilot_state=${copilotState}`,
|
|
453
|
+
requiredArgs: baseArgs,
|
|
454
|
+
requiresLocalIsolation,
|
|
455
|
+
confidence,
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 7. Copilot explicit review-settle wait — keep watch semantics until settled
|
|
461
|
+
if (copilotState === "waiting_for_copilot_review") {
|
|
462
|
+
return continueCurrentWait({
|
|
463
|
+
normalizedTarget,
|
|
464
|
+
copilotState,
|
|
465
|
+
reviewerState,
|
|
466
|
+
baseArgs,
|
|
467
|
+
requiresLocalIsolation,
|
|
468
|
+
confidence,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 8. Reviewer active states
|
|
473
|
+
if (REVIEWER_ACTIVE.has(reviewerState)) {
|
|
474
|
+
if (ownershipState === OWNERSHIP_LIVE_OWNER) {
|
|
475
|
+
return stayWithLiveOwner({ normalizedTarget, copilotState, reviewerState, baseArgs, requiresLocalIsolation, confidence });
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
routingOutcome: ROUTING_OUTCOME.HANDOFF_TO_REVIEWER_LOOP,
|
|
479
|
+
outerAction: "reenter_reviewer_loop",
|
|
480
|
+
stopReason: null,
|
|
481
|
+
handoffEnvelope: buildEnvelope({
|
|
482
|
+
targetIdentity: normalizedTarget,
|
|
483
|
+
loopFamily: LOOP_FAMILY.REVIEWER_LOOP,
|
|
484
|
+
entrypoint: ENTRYPOINT.REVIEWER_LOOP_HANDLER,
|
|
485
|
+
reason: `Reviewer loop requires action: reviewer_state=${reviewerState}`,
|
|
486
|
+
requiredArgs: baseArgs,
|
|
487
|
+
requiresLocalIsolation,
|
|
488
|
+
confidence,
|
|
489
|
+
}),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 9. Copilot strong active states — win over reviewer wait states
|
|
494
|
+
if (COPILOT_STRONG_ACTIVE.has(copilotState)) {
|
|
495
|
+
if (ownershipState === OWNERSHIP_LIVE_OWNER) {
|
|
496
|
+
return stayWithLiveOwner({ normalizedTarget, copilotState, reviewerState, baseArgs, requiresLocalIsolation, confidence });
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
routingOutcome: ROUTING_OUTCOME.HANDOFF_TO_COPILOT_LOOP,
|
|
500
|
+
outerAction: "reenter_copilot_loop",
|
|
501
|
+
stopReason: null,
|
|
502
|
+
handoffEnvelope: buildEnvelope({
|
|
503
|
+
targetIdentity: normalizedTarget,
|
|
504
|
+
loopFamily: LOOP_FAMILY.COPILOT_LOOP,
|
|
505
|
+
entrypoint: ENTRYPOINT.COPILOT_PR_HANDOFF,
|
|
506
|
+
reason: `Copilot loop requires action: copilot_state=${copilotState}`,
|
|
507
|
+
requiredArgs: baseArgs,
|
|
508
|
+
requiresLocalIsolation,
|
|
509
|
+
confidence,
|
|
510
|
+
}),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 10. Outer-loop wait states (checked before copilot weak active, since weak yields to reviewer wait)
|
|
515
|
+
if (COPILOT_WAIT.has(copilotState) || REVIEWER_WAIT.has(reviewerState)) {
|
|
516
|
+
return continueCurrentWait({
|
|
517
|
+
normalizedTarget,
|
|
518
|
+
copilotState,
|
|
519
|
+
reviewerState,
|
|
520
|
+
baseArgs,
|
|
521
|
+
requiresLocalIsolation,
|
|
522
|
+
confidence,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 11. Copilot weak active states (yield to reviewer wait states above)
|
|
527
|
+
if (COPILOT_WEAK_ACTIVE.has(copilotState)) {
|
|
528
|
+
if (ownershipState === OWNERSHIP_LIVE_OWNER) {
|
|
529
|
+
return stayWithLiveOwner({ normalizedTarget, copilotState, reviewerState, baseArgs, requiresLocalIsolation, confidence });
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
routingOutcome: ROUTING_OUTCOME.HANDOFF_TO_COPILOT_LOOP,
|
|
533
|
+
outerAction: "reenter_copilot_loop",
|
|
534
|
+
stopReason: null,
|
|
535
|
+
handoffEnvelope: buildEnvelope({
|
|
536
|
+
targetIdentity: normalizedTarget,
|
|
537
|
+
loopFamily: LOOP_FAMILY.COPILOT_LOOP,
|
|
538
|
+
entrypoint: ENTRYPOINT.COPILOT_PR_HANDOFF,
|
|
539
|
+
reason: `Copilot loop requires action: copilot_state=${copilotState}`,
|
|
540
|
+
requiredArgs: baseArgs,
|
|
541
|
+
requiresLocalIsolation,
|
|
542
|
+
confidence,
|
|
543
|
+
}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 11. Fallback — unrecognized state combination
|
|
548
|
+
return {
|
|
549
|
+
routingOutcome: ROUTING_OUTCOME.NEEDS_RECONCILE,
|
|
550
|
+
outerAction: "stop",
|
|
551
|
+
stopReason: STOP_REASON.UNKNOWN_STATE,
|
|
552
|
+
handoffEnvelope: buildEnvelope({
|
|
553
|
+
targetIdentity: normalizedTarget,
|
|
554
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
555
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
556
|
+
reason: `Unrecognized combined state: copilot_state=${copilotState}, reviewer_state=${reviewerState}`,
|
|
557
|
+
requiredArgs: baseArgs,
|
|
558
|
+
requiresLocalIsolation,
|
|
559
|
+
confidence,
|
|
560
|
+
}),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// Shared evaluator / policy entrypoint
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Evaluate deterministic conductor routing for an already-targeted active run.
|
|
570
|
+
*
|
|
571
|
+
* This is the single routing authority above family-local state machines.
|
|
572
|
+
* The routing outcome is derived directly from the normalized inputs (states +
|
|
573
|
+
* ownership + isolation); it does NOT take a pre-computed outer-loop action.
|
|
574
|
+
*
|
|
575
|
+
* Returns a closed routing outcome, a derived outer-loop action (for backward
|
|
576
|
+
* compat), and a machine-readable handoff envelope. Ambiguous, conflicting,
|
|
577
|
+
* or insufficient inputs return `needs_reconcile` rather than a guessed handoff.
|
|
578
|
+
*
|
|
579
|
+
* @param {object} input
|
|
580
|
+
* @param {{ repo: string, pr: number }} input.target
|
|
581
|
+
* Explicit target identity (already resolved by the caller).
|
|
582
|
+
* @param {string} [input.ownershipState]
|
|
583
|
+
* Settled ownership/idempotency classification from conductor-ownership (#32).
|
|
584
|
+
* "live_owner" → stay_with_current_live_owner (no new handoff this cycle).
|
|
585
|
+
* "duplicate_local_owners" → needs_reconcile.
|
|
586
|
+
* Other values or omission → routing continues from states.
|
|
587
|
+
* @param {string} input.copilotState
|
|
588
|
+
* Already-detected copilot loop lifecycle state (from copilot-loop-state.mjs STATE).
|
|
589
|
+
* @param {string} input.reviewerState
|
|
590
|
+
* Already-detected reviewer loop lifecycle state (from reviewer-loop-state.mjs REVIEWER_STATE).
|
|
591
|
+
* @param {string} [input.sourceMode]
|
|
592
|
+
* Source/confidence mode: "authoritative" | "local" | "snapshot".
|
|
593
|
+
* Defaults to "local".
|
|
594
|
+
* @param {boolean} [input.requiresLocalIsolation]
|
|
595
|
+
* Whether the checkout is dirty or detached; blocks states that need local execution.
|
|
596
|
+
* Defaults to false.
|
|
597
|
+
* @returns {{ routingOutcome: string, outerAction: string, stopReason: string|null, handoffEnvelope: object }}
|
|
598
|
+
*/
|
|
599
|
+
export function evaluateConductorRouting({
|
|
600
|
+
target,
|
|
601
|
+
ownershipState,
|
|
602
|
+
copilotState,
|
|
603
|
+
reviewerState,
|
|
604
|
+
sourceMode,
|
|
605
|
+
requiresLocalIsolation = false,
|
|
606
|
+
}) {
|
|
607
|
+
const confidence = resolveConfidence(sourceMode);
|
|
608
|
+
|
|
609
|
+
// --- 1. Validate target identity ---
|
|
610
|
+
const normalizedTarget = normalizeTarget(target);
|
|
611
|
+
if (!normalizedTarget) {
|
|
612
|
+
return {
|
|
613
|
+
routingOutcome: ROUTING_OUTCOME.NEEDS_RECONCILE,
|
|
614
|
+
outerAction: "stop",
|
|
615
|
+
stopReason: STOP_REASON.UNKNOWN_STATE,
|
|
616
|
+
handoffEnvelope: buildEnvelope({
|
|
617
|
+
targetIdentity: describeMalformedTarget(target),
|
|
618
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
619
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
620
|
+
reason: "Target identity is missing or malformed; cannot route without a resolved target",
|
|
621
|
+
requiresLocalIsolation,
|
|
622
|
+
confidence,
|
|
623
|
+
}),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// --- 2. Validate required state inputs ---
|
|
628
|
+
if (typeof copilotState !== "string" || copilotState.trim().length === 0) {
|
|
629
|
+
return {
|
|
630
|
+
routingOutcome: ROUTING_OUTCOME.NEEDS_RECONCILE,
|
|
631
|
+
outerAction: "stop",
|
|
632
|
+
stopReason: STOP_REASON.UNKNOWN_STATE,
|
|
633
|
+
handoffEnvelope: buildEnvelope({
|
|
634
|
+
targetIdentity: normalizedTarget,
|
|
635
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
636
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
637
|
+
reason: "Copilot state is missing or empty; cannot route without family-local state",
|
|
638
|
+
requiresLocalIsolation,
|
|
639
|
+
confidence,
|
|
640
|
+
}),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (typeof reviewerState !== "string" || reviewerState.trim().length === 0) {
|
|
645
|
+
return {
|
|
646
|
+
routingOutcome: ROUTING_OUTCOME.NEEDS_RECONCILE,
|
|
647
|
+
outerAction: "stop",
|
|
648
|
+
stopReason: STOP_REASON.UNKNOWN_STATE,
|
|
649
|
+
handoffEnvelope: buildEnvelope({
|
|
650
|
+
targetIdentity: normalizedTarget,
|
|
651
|
+
loopFamily: LOOP_FAMILY.NONE,
|
|
652
|
+
entrypoint: ENTRYPOINT.NONE,
|
|
653
|
+
reason: "Reviewer state is missing or empty; cannot route without family-local state",
|
|
654
|
+
requiresLocalIsolation,
|
|
655
|
+
confidence,
|
|
656
|
+
}),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// --- 3. Route from normalized states ---
|
|
661
|
+
return routeFromStates({
|
|
662
|
+
normalizedTarget,
|
|
663
|
+
copilotState,
|
|
664
|
+
reviewerState,
|
|
665
|
+
ownershipState,
|
|
666
|
+
requiresLocalIsolation,
|
|
667
|
+
confidence,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Deterministic outer-loop graph contract above family-local state machines.
|
|
673
|
+
*
|
|
674
|
+
* This module reuses conductor routing as the single source of truth for
|
|
675
|
+
* authoritative outer runtime states. It does not invent a separate outer
|
|
676
|
+
* taxonomy; instead it exposes routing outcomes as the outer state vocabulary,
|
|
677
|
+
* adds graph metadata (semantic Start / End), and provides a stable inspection-
|
|
678
|
+
* and viewer-friendly interpreter surface.
|
|
679
|
+
*/
|
|
680
|
+
|
|
681
|
+
export const OUTER_STATE = Object.freeze({
|
|
682
|
+
CONTINUE_CURRENT_WAIT: ROUTING_OUTCOME.CONTINUE_CURRENT_WAIT,
|
|
683
|
+
HANDOFF_TO_COPILOT_LOOP: ROUTING_OUTCOME.HANDOFF_TO_COPILOT_LOOP,
|
|
684
|
+
HANDOFF_TO_REVIEWER_LOOP: ROUTING_OUTCOME.HANDOFF_TO_REVIEWER_LOOP,
|
|
685
|
+
STAY_WITH_CURRENT_LIVE_OWNER: ROUTING_OUTCOME.STAY_WITH_CURRENT_LIVE_OWNER,
|
|
686
|
+
STOP_NEEDS_HUMAN: ROUTING_OUTCOME.STOP_NEEDS_HUMAN,
|
|
687
|
+
DONE_TERMINAL: ROUTING_OUTCOME.DONE_TERMINAL,
|
|
688
|
+
NEEDS_RECONCILE: ROUTING_OUTCOME.NEEDS_RECONCILE,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const OUTER_STATE_VALUES = Object.freeze(Object.values(OUTER_STATE));
|
|
692
|
+
const OUTER_STATE_SET = new Set(OUTER_STATE_VALUES);
|
|
693
|
+
|
|
694
|
+
export const OUTER_TERMINAL_STATES = Object.freeze([
|
|
695
|
+
OUTER_STATE.STOP_NEEDS_HUMAN,
|
|
696
|
+
OUTER_STATE.DONE_TERMINAL,
|
|
697
|
+
OUTER_STATE.NEEDS_RECONCILE,
|
|
698
|
+
]);
|
|
699
|
+
|
|
700
|
+
export const OUTER_NONTERMINAL_STATES = Object.freeze([
|
|
701
|
+
OUTER_STATE.CONTINUE_CURRENT_WAIT,
|
|
702
|
+
OUTER_STATE.HANDOFF_TO_COPILOT_LOOP,
|
|
703
|
+
OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP,
|
|
704
|
+
OUTER_STATE.STAY_WITH_CURRENT_LIVE_OWNER,
|
|
705
|
+
]);
|
|
706
|
+
|
|
707
|
+
const OUTER_TERMINAL_STATE_SET = new Set(OUTER_TERMINAL_STATES);
|
|
708
|
+
const ALL_OUTER_STATES = Object.freeze([...OUTER_STATE_VALUES]);
|
|
709
|
+
|
|
710
|
+
export const OUTER_GRAPH = Object.freeze({
|
|
711
|
+
start: Object.freeze({ id: "outer_start", label: "Start", semantic: true }),
|
|
712
|
+
end: Object.freeze({ id: "outer_end", label: "End", semantic: true }),
|
|
713
|
+
entryStates: Object.freeze([...OUTER_STATE_VALUES]),
|
|
714
|
+
terminalStates: OUTER_TERMINAL_STATES,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
export const OUTER_STATE_TO_OUTER_ACTION = Object.freeze({
|
|
718
|
+
[OUTER_STATE.CONTINUE_CURRENT_WAIT]: "continue_wait",
|
|
719
|
+
[OUTER_STATE.HANDOFF_TO_COPILOT_LOOP]: "reenter_copilot_loop",
|
|
720
|
+
[OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP]: "reenter_reviewer_loop",
|
|
721
|
+
[OUTER_STATE.STAY_WITH_CURRENT_LIVE_OWNER]: "continue_wait",
|
|
722
|
+
[OUTER_STATE.STOP_NEEDS_HUMAN]: "stop",
|
|
723
|
+
[OUTER_STATE.DONE_TERMINAL]: "done",
|
|
724
|
+
[OUTER_STATE.NEEDS_RECONCILE]: "stop",
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
export const OUTER_STATE_TO_ROUTING_OUTCOME = Object.freeze({
|
|
728
|
+
[OUTER_STATE.CONTINUE_CURRENT_WAIT]: ROUTING_OUTCOME.CONTINUE_CURRENT_WAIT,
|
|
729
|
+
[OUTER_STATE.HANDOFF_TO_COPILOT_LOOP]: ROUTING_OUTCOME.HANDOFF_TO_COPILOT_LOOP,
|
|
730
|
+
[OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP]: ROUTING_OUTCOME.HANDOFF_TO_REVIEWER_LOOP,
|
|
731
|
+
[OUTER_STATE.STAY_WITH_CURRENT_LIVE_OWNER]: ROUTING_OUTCOME.STAY_WITH_CURRENT_LIVE_OWNER,
|
|
732
|
+
[OUTER_STATE.STOP_NEEDS_HUMAN]: ROUTING_OUTCOME.STOP_NEEDS_HUMAN,
|
|
733
|
+
[OUTER_STATE.DONE_TERMINAL]: ROUTING_OUTCOME.DONE_TERMINAL,
|
|
734
|
+
[OUTER_STATE.NEEDS_RECONCILE]: ROUTING_OUTCOME.NEEDS_RECONCILE,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
export const OUTER_NEXT_ACTIONS = Object.freeze({
|
|
738
|
+
[OUTER_STATE.CONTINUE_CURRENT_WAIT]: "Remain in outer wait and re-inspect after the bounded interval.",
|
|
739
|
+
[OUTER_STATE.HANDOFF_TO_COPILOT_LOOP]: "Re-enter the Copilot loop.",
|
|
740
|
+
[OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP]: "Re-enter the reviewer loop.",
|
|
741
|
+
[OUTER_STATE.STAY_WITH_CURRENT_LIVE_OWNER]: "Do not issue a new handoff; wait because a live owner is already active.",
|
|
742
|
+
[OUTER_STATE.STOP_NEEDS_HUMAN]: "Stop and require human intervention before continuing.",
|
|
743
|
+
[OUTER_STATE.DONE_TERMINAL]: "End the orchestrator; no further automated action is needed.",
|
|
744
|
+
[OUTER_STATE.NEEDS_RECONCILE]: "Stop and reconcile conflicting or insufficient state before resuming.",
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
export const OUTER_TRANSITIONS = Object.freeze({
|
|
748
|
+
[OUTER_STATE.CONTINUE_CURRENT_WAIT]: ALL_OUTER_STATES,
|
|
749
|
+
[OUTER_STATE.HANDOFF_TO_COPILOT_LOOP]: ALL_OUTER_STATES,
|
|
750
|
+
[OUTER_STATE.HANDOFF_TO_REVIEWER_LOOP]: ALL_OUTER_STATES,
|
|
751
|
+
[OUTER_STATE.STAY_WITH_CURRENT_LIVE_OWNER]: ALL_OUTER_STATES,
|
|
752
|
+
[OUTER_STATE.STOP_NEEDS_HUMAN]: Object.freeze([]),
|
|
753
|
+
[OUTER_STATE.DONE_TERMINAL]: Object.freeze([]),
|
|
754
|
+
[OUTER_STATE.NEEDS_RECONCILE]: Object.freeze([]),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
export function getAllowedOuterTransitions(state) {
|
|
758
|
+
return Array.isArray(OUTER_TRANSITIONS[state]) ? [...OUTER_TRANSITIONS[state]] : [];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function normalizeOuterState(routingOutcome) {
|
|
762
|
+
return OUTER_STATE_SET.has(routingOutcome) ? routingOutcome : OUTER_STATE.NEEDS_RECONCILE;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function isKnownOuterState(value) {
|
|
766
|
+
return OUTER_STATE_SET.has(value);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export function interpretOuterLoopState({
|
|
770
|
+
target,
|
|
771
|
+
ownershipState,
|
|
772
|
+
copilotState,
|
|
773
|
+
reviewerState,
|
|
774
|
+
sourceMode,
|
|
775
|
+
requiresLocalIsolation = false,
|
|
776
|
+
routing = null,
|
|
777
|
+
} = {}) {
|
|
778
|
+
const effectiveRouting = routing && typeof routing === "object" && typeof routing.routingOutcome === "string"
|
|
779
|
+
? routing
|
|
780
|
+
: evaluateConductorRouting({
|
|
781
|
+
target,
|
|
782
|
+
ownershipState,
|
|
783
|
+
copilotState,
|
|
784
|
+
reviewerState,
|
|
785
|
+
sourceMode,
|
|
786
|
+
requiresLocalIsolation,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
const state = normalizeOuterState(effectiveRouting.routingOutcome);
|
|
790
|
+
const allowedTransitions = getAllowedOuterTransitions(state);
|
|
791
|
+
const nextAction = OUTER_NEXT_ACTIONS[state] ?? OUTER_NEXT_ACTIONS[OUTER_STATE.NEEDS_RECONCILE];
|
|
792
|
+
const isTerminal = OUTER_TERMINAL_STATE_SET.has(state);
|
|
793
|
+
|
|
794
|
+
if (state === OUTER_STATE.NEEDS_RECONCILE && effectiveRouting.routingOutcome !== OUTER_STATE.NEEDS_RECONCILE) {
|
|
795
|
+
return {
|
|
796
|
+
state,
|
|
797
|
+
allowedTransitions: [],
|
|
798
|
+
nextAction,
|
|
799
|
+
isTerminal,
|
|
800
|
+
routingOutcome: OUTER_STATE.NEEDS_RECONCILE,
|
|
801
|
+
outerAction: "stop",
|
|
802
|
+
stopReason: STOP_REASON.UNKNOWN_STATE,
|
|
803
|
+
handoffEnvelope: effectiveRouting.handoffEnvelope,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return {
|
|
808
|
+
state,
|
|
809
|
+
allowedTransitions,
|
|
810
|
+
nextAction,
|
|
811
|
+
isTerminal,
|
|
812
|
+
routingOutcome: effectiveRouting.routingOutcome,
|
|
813
|
+
outerAction: effectiveRouting.outerAction,
|
|
814
|
+
stopReason: effectiveRouting.stopReason,
|
|
815
|
+
handoffEnvelope: effectiveRouting.handoffEnvelope,
|
|
816
|
+
};
|
|
817
|
+
}
|