@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,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic mid-flight operator steering contract for active dev loops.
|
|
3
|
+
*
|
|
4
|
+
* This module provides:
|
|
5
|
+
* - STEERING_KIND: stable steering kind constants
|
|
6
|
+
* - STEERING_RESULT: acknowledgement/result constants
|
|
7
|
+
* - SAFE_POINT_CATEGORY: safe-point classification constants
|
|
8
|
+
* - normalizeSteeringEvent: validate and canonicalize a raw steering event
|
|
9
|
+
* - normalizeSteeringState: load and validate persisted steering state
|
|
10
|
+
* - createSteeringState: create a fresh steering state for a new run
|
|
11
|
+
* - classifySafePoint: map a copilot loop state to a safe-point category
|
|
12
|
+
* - submitSteering: process a steering event against current run state
|
|
13
|
+
* - promoteQueuedSteering: apply queued steering when the loop reaches a safe point
|
|
14
|
+
* - getEffectiveConstraints: get the current effective steering constraints
|
|
15
|
+
* - resolveEffectiveLoopState: get loop interpretation augmented with active steering
|
|
16
|
+
* - getSteeringStatus: get full inspection output for a run's steering state
|
|
17
|
+
*
|
|
18
|
+
* The proving target for this first implementation slice is the async Copilot
|
|
19
|
+
* review/fix loop (copilot-loop-state.mjs). Safe-point rules are defined for
|
|
20
|
+
* that loop's state set and the resolveEffectiveLoopState integration changes
|
|
21
|
+
* loop behavior when stop_at_next_safe_gate steering is active.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { normalizeRepoSlug } from "../github/repo-slug.mjs";
|
|
25
|
+
import { STATE, interpretLoopState } from "./copilot-loop-state.mjs";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Constants
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Steering kind constants. */
|
|
32
|
+
export const STEERING_KIND = Object.freeze({
|
|
33
|
+
/** A hard requirement that must be respected by subsequent steps. */
|
|
34
|
+
HARD_CONSTRAINT: "hard_constraint",
|
|
35
|
+
/** A preference that should be followed but is not required. */
|
|
36
|
+
PREFERENCE: "preference",
|
|
37
|
+
/** A clarification that does not change requirements but affects interpretation. */
|
|
38
|
+
CLARIFICATION: "clarification",
|
|
39
|
+
/** Request the loop to stop at the next safe approval/mutation gate. */
|
|
40
|
+
STOP_AT_NEXT_SAFE_GATE: "stop_at_next_safe_gate",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Acknowledgement/result constants for steering events. */
|
|
44
|
+
export const STEERING_RESULT = Object.freeze({
|
|
45
|
+
/** Steering was applied immediately to the current run state. */
|
|
46
|
+
APPLIED_NOW: "applied_now",
|
|
47
|
+
/** Steering was queued; will be applied when the loop reaches the next safe point. */
|
|
48
|
+
QUEUED_FOR_SAFE_POINT: "queued_for_safe_point",
|
|
49
|
+
/** Steering was rejected because applying it now would be unsafe. */
|
|
50
|
+
REJECTED_UNSAFE_NOW: "rejected_unsafe_now",
|
|
51
|
+
/** Steering was rejected because it is invalid, malformed, or conflicts with existing steering. */
|
|
52
|
+
REJECTED_INVALID_OR_CONFLICTING: "rejected_invalid_or_conflicting",
|
|
53
|
+
/** Steering cannot be resolved automatically and requires human decision. */
|
|
54
|
+
NEEDS_HUMAN_DECISION: "needs_human_decision",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** Safe-point category constants for loop states. */
|
|
58
|
+
export const SAFE_POINT_CATEGORY = Object.freeze({
|
|
59
|
+
/** Steering can be applied immediately. */
|
|
60
|
+
IMMEDIATE: "immediate",
|
|
61
|
+
/** Steering must be queued for the next safe point. */
|
|
62
|
+
NEXT_POINT: "next_point",
|
|
63
|
+
/** Steering is rejected because the loop is in a terminal or error state. */
|
|
64
|
+
TERMINAL: "terminal",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
68
|
+
const VALID_STEERING_KINDS = new Set(Object.values(STEERING_KIND));
|
|
69
|
+
const VALID_STEERING_RESULTS = new Set(Object.values(STEERING_RESULT));
|
|
70
|
+
const VALID_APPLY_MODES = new Set(["immediate", "next_safe_point"]);
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Safe-point classification
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Map a copilot loop state to a safe-point category.
|
|
78
|
+
*
|
|
79
|
+
* Safe-point rules for the async Copilot review/fix loop:
|
|
80
|
+
*
|
|
81
|
+
* IMMEDIATE — between steps / idle / waiting on external state.
|
|
82
|
+
* Steering can be applied right now without risk of splitting a mutation.
|
|
83
|
+
* States: pr_ready_no_feedback, waiting_for_copilot_review,
|
|
84
|
+
* waiting_for_ci, ready_to_rerequest_review
|
|
85
|
+
*
|
|
86
|
+
* NEXT_POINT — actively computing or in a non-interruptible mutation.
|
|
87
|
+
* Applying steering now could produce a half-applied or inconsistent state.
|
|
88
|
+
* Queue the event and promote it when the loop next reaches an IMMEDIATE state.
|
|
89
|
+
* States: pr_draft, unresolved_feedback_present, already_fixed_needs_reply_resolve
|
|
90
|
+
*
|
|
91
|
+
* TERMINAL — run is done, irreversibly failed, or has no active run.
|
|
92
|
+
* Steering is rejected; it would have no effect or could mask a real error.
|
|
93
|
+
* States: no_pr, done, review_request_unavailable, blocked_needs_user_decision
|
|
94
|
+
*
|
|
95
|
+
* @param {string} loopState - a copilot loop STATE value
|
|
96
|
+
* @returns {"immediate"|"next_point"|"terminal"}
|
|
97
|
+
*/
|
|
98
|
+
export function classifySafePoint(loopState) {
|
|
99
|
+
switch (loopState) {
|
|
100
|
+
// Between steps / idle
|
|
101
|
+
case STATE.PR_READY_NO_FEEDBACK:
|
|
102
|
+
case STATE.READY_TO_REREQUEST_REVIEW:
|
|
103
|
+
// Waiting on external state
|
|
104
|
+
case STATE.WAITING_FOR_COPILOT_REVIEW:
|
|
105
|
+
case STATE.WAITING_FOR_CI:
|
|
106
|
+
return SAFE_POINT_CATEGORY.IMMEDIATE;
|
|
107
|
+
|
|
108
|
+
// In a pre-ready state (not yet at a mutation gate)
|
|
109
|
+
case STATE.PR_DRAFT:
|
|
110
|
+
// Actively computing — about to apply fixes to resolve feedback
|
|
111
|
+
case STATE.UNRESOLVED_FEEDBACK_PRESENT:
|
|
112
|
+
// In the middle of a non-interruptible mutation (reply/resolve review threads)
|
|
113
|
+
case STATE.ALREADY_FIXED_NEEDS_REPLY_RESOLVE:
|
|
114
|
+
return SAFE_POINT_CATEGORY.NEXT_POINT;
|
|
115
|
+
|
|
116
|
+
// Terminal states: run is done, error, or has no active run
|
|
117
|
+
case STATE.NO_PR:
|
|
118
|
+
case STATE.DONE:
|
|
119
|
+
case STATE.REVIEW_REQUEST_UNAVAILABLE:
|
|
120
|
+
case STATE.BLOCKED_NEEDS_USER_DECISION:
|
|
121
|
+
default:
|
|
122
|
+
return SAFE_POINT_CATEGORY.TERMINAL;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Schema normalization
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Normalize a raw steering event into a validated, canonical shape.
|
|
132
|
+
*
|
|
133
|
+
* Schema:
|
|
134
|
+
* - eventId {string} — unique identifier for this event
|
|
135
|
+
* - runId {string} — target run identity
|
|
136
|
+
* - kind {"hard_constraint"|"preference"|"clarification"|"stop_at_next_safe_gate"}
|
|
137
|
+
* - directive {string} — operator payload / directive text
|
|
138
|
+
* - submittedAt {string} — ISO 8601 timestamp
|
|
139
|
+
* - seq {number} — positive integer; monotonically increasing for durable ordering
|
|
140
|
+
* - applyMode {"immediate"|"next_safe_point"} — default "immediate"
|
|
141
|
+
*
|
|
142
|
+
* @param {object} raw
|
|
143
|
+
* @returns {object} normalized event
|
|
144
|
+
* @throws {Error} if required fields are missing or invalid
|
|
145
|
+
*/
|
|
146
|
+
export function normalizeSteeringEvent(raw) {
|
|
147
|
+
if (!raw || typeof raw !== "object") {
|
|
148
|
+
throw new Error("Steering event must be a non-null object");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const eventId = typeof raw.eventId === "string" && raw.eventId.trim().length > 0
|
|
152
|
+
? raw.eventId.trim()
|
|
153
|
+
: null;
|
|
154
|
+
if (!eventId) {
|
|
155
|
+
throw new Error("Steering event requires a non-empty eventId");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const runId = typeof raw.runId === "string" && raw.runId.trim().length > 0
|
|
159
|
+
? raw.runId.trim()
|
|
160
|
+
: null;
|
|
161
|
+
if (!runId) {
|
|
162
|
+
throw new Error("Steering event requires a non-empty runId");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const kind = VALID_STEERING_KINDS.has(raw.kind) ? raw.kind : null;
|
|
166
|
+
if (!kind) {
|
|
167
|
+
throw new Error(`Steering event kind must be one of: ${[...VALID_STEERING_KINDS].join(", ")}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const directive = typeof raw.directive === "string" && raw.directive.trim().length > 0
|
|
171
|
+
? raw.directive.trim()
|
|
172
|
+
: null;
|
|
173
|
+
if (!directive) {
|
|
174
|
+
throw new Error("Steering event requires a non-empty directive");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const submittedAt = typeof raw.submittedAt === "string" && raw.submittedAt.trim().length > 0
|
|
178
|
+
? raw.submittedAt.trim()
|
|
179
|
+
: new Date().toISOString();
|
|
180
|
+
|
|
181
|
+
const seq = typeof raw.seq === "number" && Number.isFinite(raw.seq) && raw.seq > 0
|
|
182
|
+
? Math.floor(raw.seq)
|
|
183
|
+
: null;
|
|
184
|
+
if (seq === null) {
|
|
185
|
+
throw new Error("Steering event requires a positive integer seq");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const applyMode = VALID_APPLY_MODES.has(raw.applyMode) ? raw.applyMode : "immediate";
|
|
189
|
+
|
|
190
|
+
return { eventId, runId, kind, directive, submittedAt, seq, applyMode };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Normalize a raw steering state object into a validated, canonical shape.
|
|
195
|
+
*
|
|
196
|
+
* Durable steering state schema:
|
|
197
|
+
* - runId {string} — identifies the target run
|
|
198
|
+
* - schemaVersion {1} — version discriminator for future migration
|
|
199
|
+
* - events {object[]} — ordered log of all submitted events
|
|
200
|
+
* - effectiveStack {object[]} — events currently in effect
|
|
201
|
+
* - queuedEvents {object[]} — events waiting for the next safe point
|
|
202
|
+
* - resultHistory {object[]} — ordered log of all acknowledgement results
|
|
203
|
+
* - latestResult {object|null} — most recent acknowledgement result
|
|
204
|
+
* - nextSeq {number} — next expected seq value for ordering validation
|
|
205
|
+
*
|
|
206
|
+
* @param {object} raw
|
|
207
|
+
* @returns {object} normalized steering state
|
|
208
|
+
* @throws {Error} if required fields are missing
|
|
209
|
+
*/
|
|
210
|
+
function normalizeSteeringTarget(raw) {
|
|
211
|
+
if (raw === null || raw === undefined) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
if (!raw || typeof raw !== "object") {
|
|
215
|
+
throw new Error("target must be an object when present");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const repo = normalizeRepoSlug(raw.repo, {
|
|
219
|
+
errorMessage: "target.repo must be a non-empty owner/name repo slug",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const pr = typeof raw.pr === "number" && Number.isFinite(raw.pr) && raw.pr > 0
|
|
223
|
+
? Math.floor(raw.pr)
|
|
224
|
+
: null;
|
|
225
|
+
if (pr === null) {
|
|
226
|
+
throw new Error("target.pr must be a positive integer");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { repo, pr };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeResultEntry(raw, fieldName) {
|
|
233
|
+
if (!raw || typeof raw !== "object") {
|
|
234
|
+
throw new Error(`${fieldName} entry must be an object`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const eventId = typeof raw.eventId === "string" && raw.eventId.trim().length > 0
|
|
238
|
+
? raw.eventId.trim()
|
|
239
|
+
: null;
|
|
240
|
+
if (!eventId) {
|
|
241
|
+
throw new Error(`${fieldName} entry requires a non-empty eventId`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const seq = typeof raw.seq === "number" && Number.isFinite(raw.seq) && raw.seq > 0
|
|
245
|
+
? Math.floor(raw.seq)
|
|
246
|
+
: null;
|
|
247
|
+
if (seq === null) {
|
|
248
|
+
throw new Error(`${fieldName} entry requires a positive integer seq`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const result = VALID_STEERING_RESULTS.has(raw.result) ? raw.result : null;
|
|
252
|
+
if (!result) {
|
|
253
|
+
throw new Error(`${fieldName} entry result must be one of: ${[...VALID_STEERING_RESULTS].join(", ")}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const acknowledgedAt = typeof raw.acknowledgedAt === "string" && raw.acknowledgedAt.trim().length > 0
|
|
257
|
+
? raw.acknowledgedAt.trim()
|
|
258
|
+
: null;
|
|
259
|
+
if (!acknowledgedAt) {
|
|
260
|
+
throw new Error(`${fieldName} entry requires a non-empty acknowledgedAt timestamp`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let reason = null;
|
|
264
|
+
if (raw.reason !== null && raw.reason !== undefined) {
|
|
265
|
+
if (typeof raw.reason !== "string") {
|
|
266
|
+
throw new Error(`${fieldName} entry reason must be a string or null`);
|
|
267
|
+
}
|
|
268
|
+
reason = raw.reason;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { eventId, seq, result, reason, acknowledgedAt };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeEventList(rawList, fieldName) {
|
|
275
|
+
if (!Array.isArray(rawList)) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
return rawList.map((entry, index) => {
|
|
279
|
+
try {
|
|
280
|
+
return normalizeSteeringEvent(entry);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
283
|
+
throw new Error(`${fieldName}[${index}] is invalid: ${detail}`);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizeResultList(rawList, fieldName) {
|
|
289
|
+
if (!Array.isArray(rawList)) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
return rawList.map((entry, index) => {
|
|
293
|
+
try {
|
|
294
|
+
return normalizeResultEntry(entry, `${fieldName}[${index}]`);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
297
|
+
throw new Error(detail);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function normalizeSteeringState(raw) {
|
|
303
|
+
if (!raw || typeof raw !== "object") {
|
|
304
|
+
throw new Error("Steering state must be a non-null object");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const runId = typeof raw.runId === "string" && raw.runId.trim().length > 0
|
|
308
|
+
? raw.runId.trim()
|
|
309
|
+
: null;
|
|
310
|
+
if (!runId) {
|
|
311
|
+
throw new Error("Steering state requires a non-empty runId");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (raw.schemaVersion !== undefined && raw.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
315
|
+
throw new Error(`Unsupported steering state schemaVersion '${raw.schemaVersion}'; expected ${CURRENT_SCHEMA_VERSION}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const events = normalizeEventList(raw.events, "events");
|
|
319
|
+
const effectiveStack = normalizeEventList(raw.effectiveStack, "effectiveStack");
|
|
320
|
+
const queuedEvents = normalizeEventList(raw.queuedEvents, "queuedEvents");
|
|
321
|
+
const resultHistory = normalizeResultList(raw.resultHistory, "resultHistory");
|
|
322
|
+
|
|
323
|
+
let latestResult = null;
|
|
324
|
+
if (raw.latestResult !== null && raw.latestResult !== undefined) {
|
|
325
|
+
latestResult = normalizeResultEntry(raw.latestResult, "latestResult");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let target = null;
|
|
329
|
+
if (raw.target !== undefined) {
|
|
330
|
+
try {
|
|
331
|
+
target = normalizeSteeringTarget(raw.target);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
334
|
+
throw new Error(`target is invalid: ${detail}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
runId,
|
|
340
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
341
|
+
target,
|
|
342
|
+
events,
|
|
343
|
+
effectiveStack,
|
|
344
|
+
queuedEvents,
|
|
345
|
+
resultHistory,
|
|
346
|
+
latestResult,
|
|
347
|
+
nextSeq: typeof raw.nextSeq === "number" && Number.isFinite(raw.nextSeq) && raw.nextSeq > 0
|
|
348
|
+
? Math.floor(raw.nextSeq)
|
|
349
|
+
: 1,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create a fresh steering state for a new run.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} runId
|
|
357
|
+
* @param {{repo:string,pr:number}|null} [target]
|
|
358
|
+
* @returns {object}
|
|
359
|
+
*/
|
|
360
|
+
export function createSteeringState(runId, target = null) {
|
|
361
|
+
if (typeof runId !== "string" || runId.trim().length === 0) {
|
|
362
|
+
throw new Error("createSteeringState requires a non-empty runId");
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
runId: runId.trim(),
|
|
366
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
367
|
+
target: target ? normalizeSteeringTarget(target) : null,
|
|
368
|
+
events: [],
|
|
369
|
+
effectiveStack: [],
|
|
370
|
+
queuedEvents: [],
|
|
371
|
+
resultHistory: [],
|
|
372
|
+
latestResult: null,
|
|
373
|
+
nextSeq: 1,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Conflict detection
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Check if a new steering event conflicts with the existing effective stack.
|
|
383
|
+
*
|
|
384
|
+
* Conflict rules for v1:
|
|
385
|
+
* - Exact duplicate hard_constraint directives (case-insensitive) are rejected.
|
|
386
|
+
* Two hard constraints with different content are allowed (additive stacking).
|
|
387
|
+
*
|
|
388
|
+
* @param {object} event - normalized steering event
|
|
389
|
+
* @param {object[]} effectiveStack - current effective steering events
|
|
390
|
+
* @param {object[]} queuedEvents - current queued steering events
|
|
391
|
+
* @returns {string|null} conflict reason string, or null if no conflict
|
|
392
|
+
*/
|
|
393
|
+
function detectConflict(event, effectiveStack, queuedEvents = []) {
|
|
394
|
+
if (event.kind !== STEERING_KIND.HARD_CONSTRAINT) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const existingEvents = [...effectiveStack, ...queuedEvents];
|
|
399
|
+
for (const existing of existingEvents) {
|
|
400
|
+
if (
|
|
401
|
+
existing.kind === STEERING_KIND.HARD_CONSTRAINT
|
|
402
|
+
&& existing.directive.toLowerCase() === event.directive.toLowerCase()
|
|
403
|
+
) {
|
|
404
|
+
const location = effectiveStack.includes(existing) ? "effective stack" : "queued events";
|
|
405
|
+
return `Duplicate hard_constraint directive already present in ${location} (seq ${existing.seq})`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function hasStopAtNextSafeGate(events) {
|
|
412
|
+
return events.some((event) => event.kind === STEERING_KIND.STOP_AT_NEXT_SAFE_GATE);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// Steering submission
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Process a steering event against current run state and produce an
|
|
421
|
+
* acknowledgement/result plus updated steering state.
|
|
422
|
+
*
|
|
423
|
+
* This is the main entry point for operators submitting mid-flight corrections.
|
|
424
|
+
*
|
|
425
|
+
* Result semantics:
|
|
426
|
+
* - applied_now: event is immediately effective; effectiveStack is updated.
|
|
427
|
+
* - queued_for_safe_point: loop is in a non-safe state; event is queued and
|
|
428
|
+
* will be promoted by promoteQueuedSteering when the loop reaches a safe point.
|
|
429
|
+
* - rejected_unsafe_now: terminal loop state (done/unavailable/no_pr); steering
|
|
430
|
+
* would have no effect or could mask a real issue.
|
|
431
|
+
* - rejected_invalid_or_conflicting: event is malformed, has an out-of-order seq,
|
|
432
|
+
* or exactly duplicates an existing hard_constraint.
|
|
433
|
+
* - needs_human_decision: loop is in blocked_needs_user_decision; human must act
|
|
434
|
+
* before automated steering can be safely applied.
|
|
435
|
+
*
|
|
436
|
+
* @param {object} event - normalized steering event (from normalizeSteeringEvent)
|
|
437
|
+
* @param {object} steeringState - current steering state (from normalizeSteeringState)
|
|
438
|
+
* @param {string} loopState - current copilot loop state (a STATE constant value)
|
|
439
|
+
* @returns {{ steeringState: object, result: object }}
|
|
440
|
+
*/
|
|
441
|
+
export function submitSteering(event, steeringState, loopState) {
|
|
442
|
+
const acknowledgedAt = new Date().toISOString();
|
|
443
|
+
|
|
444
|
+
// Validate seq ordering
|
|
445
|
+
if (event.seq < steeringState.nextSeq) {
|
|
446
|
+
const ackResult = {
|
|
447
|
+
eventId: event.eventId,
|
|
448
|
+
seq: event.seq,
|
|
449
|
+
result: STEERING_RESULT.REJECTED_INVALID_OR_CONFLICTING,
|
|
450
|
+
reason: `Sequence number ${event.seq} is out of order; expected >= ${steeringState.nextSeq}`,
|
|
451
|
+
acknowledgedAt,
|
|
452
|
+
};
|
|
453
|
+
return {
|
|
454
|
+
steeringState: {
|
|
455
|
+
...steeringState,
|
|
456
|
+
latestResult: ackResult,
|
|
457
|
+
resultHistory: [...steeringState.resultHistory, ackResult],
|
|
458
|
+
},
|
|
459
|
+
result: ackResult,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Check for conflicts with existing effective stack
|
|
464
|
+
const conflictReason = detectConflict(event, steeringState.effectiveStack, steeringState.queuedEvents);
|
|
465
|
+
if (conflictReason) {
|
|
466
|
+
const ackResult = {
|
|
467
|
+
eventId: event.eventId,
|
|
468
|
+
seq: event.seq,
|
|
469
|
+
result: STEERING_RESULT.REJECTED_INVALID_OR_CONFLICTING,
|
|
470
|
+
reason: conflictReason,
|
|
471
|
+
acknowledgedAt,
|
|
472
|
+
};
|
|
473
|
+
return {
|
|
474
|
+
steeringState: {
|
|
475
|
+
...steeringState,
|
|
476
|
+
latestResult: ackResult,
|
|
477
|
+
resultHistory: [...steeringState.resultHistory, ackResult],
|
|
478
|
+
events: [...steeringState.events, event],
|
|
479
|
+
nextSeq: Math.max(steeringState.nextSeq, event.seq + 1),
|
|
480
|
+
},
|
|
481
|
+
result: ackResult,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const safePointCategory = classifySafePoint(loopState);
|
|
486
|
+
|
|
487
|
+
if (event.kind === STEERING_KIND.STOP_AT_NEXT_SAFE_GATE) {
|
|
488
|
+
if (hasStopAtNextSafeGate(steeringState.effectiveStack)) {
|
|
489
|
+
const ackResult = {
|
|
490
|
+
eventId: event.eventId,
|
|
491
|
+
seq: event.seq,
|
|
492
|
+
result: STEERING_RESULT.APPLIED_NOW,
|
|
493
|
+
reason: "stop_at_next_safe_gate is already effective for this run",
|
|
494
|
+
acknowledgedAt,
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
steeringState: {
|
|
498
|
+
...steeringState,
|
|
499
|
+
latestResult: ackResult,
|
|
500
|
+
resultHistory: [...steeringState.resultHistory, ackResult],
|
|
501
|
+
events: [...steeringState.events, event],
|
|
502
|
+
nextSeq: Math.max(steeringState.nextSeq, event.seq + 1),
|
|
503
|
+
},
|
|
504
|
+
result: ackResult,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (hasStopAtNextSafeGate(steeringState.queuedEvents)) {
|
|
509
|
+
const ackResult = {
|
|
510
|
+
eventId: event.eventId,
|
|
511
|
+
seq: event.seq,
|
|
512
|
+
result: STEERING_RESULT.QUEUED_FOR_SAFE_POINT,
|
|
513
|
+
reason: "stop_at_next_safe_gate is already queued for the next safe point",
|
|
514
|
+
acknowledgedAt,
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
steeringState: {
|
|
518
|
+
...steeringState,
|
|
519
|
+
latestResult: ackResult,
|
|
520
|
+
resultHistory: [...steeringState.resultHistory, ackResult],
|
|
521
|
+
events: [...steeringState.events, event],
|
|
522
|
+
nextSeq: Math.max(steeringState.nextSeq, event.seq + 1),
|
|
523
|
+
},
|
|
524
|
+
result: ackResult,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Terminal loop states: reject or route to human decision
|
|
530
|
+
if (safePointCategory === SAFE_POINT_CATEGORY.TERMINAL) {
|
|
531
|
+
let result;
|
|
532
|
+
let reason;
|
|
533
|
+
|
|
534
|
+
if (loopState === STATE.BLOCKED_NEEDS_USER_DECISION) {
|
|
535
|
+
result = STEERING_RESULT.NEEDS_HUMAN_DECISION;
|
|
536
|
+
reason = "Loop is in blocked_needs_user_decision; human decision is required before steering can be applied";
|
|
537
|
+
} else if (loopState === STATE.DONE) {
|
|
538
|
+
result = STEERING_RESULT.REJECTED_UNSAFE_NOW;
|
|
539
|
+
reason = "Loop run is already complete (done); steering has no effect";
|
|
540
|
+
} else if (loopState === STATE.REVIEW_REQUEST_UNAVAILABLE) {
|
|
541
|
+
result = STEERING_RESULT.REJECTED_UNSAFE_NOW;
|
|
542
|
+
reason = "Loop is in review_request_unavailable terminal state; steering cannot be applied";
|
|
543
|
+
} else {
|
|
544
|
+
result = STEERING_RESULT.REJECTED_UNSAFE_NOW;
|
|
545
|
+
reason = `Loop state '${loopState}' does not have an active run to steer`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const ackResult = { eventId: event.eventId, seq: event.seq, result, reason, acknowledgedAt };
|
|
549
|
+
return {
|
|
550
|
+
steeringState: {
|
|
551
|
+
...steeringState,
|
|
552
|
+
latestResult: ackResult,
|
|
553
|
+
resultHistory: [...steeringState.resultHistory, ackResult],
|
|
554
|
+
events: [...steeringState.events, event],
|
|
555
|
+
nextSeq: Math.max(steeringState.nextSeq, event.seq + 1),
|
|
556
|
+
},
|
|
557
|
+
result: ackResult,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Non-safe states or caller-requested deferred application: queue the event
|
|
562
|
+
if (safePointCategory === SAFE_POINT_CATEGORY.NEXT_POINT || event.applyMode === "next_safe_point") {
|
|
563
|
+
const ackResult = {
|
|
564
|
+
eventId: event.eventId,
|
|
565
|
+
seq: event.seq,
|
|
566
|
+
result: STEERING_RESULT.QUEUED_FOR_SAFE_POINT,
|
|
567
|
+
reason: `Loop is in '${loopState}' (not a safe point for immediate application); steering queued for next safe point`,
|
|
568
|
+
acknowledgedAt,
|
|
569
|
+
};
|
|
570
|
+
return {
|
|
571
|
+
steeringState: {
|
|
572
|
+
...steeringState,
|
|
573
|
+
latestResult: ackResult,
|
|
574
|
+
resultHistory: [...steeringState.resultHistory, ackResult],
|
|
575
|
+
events: [...steeringState.events, event],
|
|
576
|
+
queuedEvents: [...steeringState.queuedEvents, event],
|
|
577
|
+
nextSeq: Math.max(steeringState.nextSeq, event.seq + 1),
|
|
578
|
+
},
|
|
579
|
+
result: ackResult,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Safe point and immediate mode: apply now
|
|
584
|
+
const ackResult = {
|
|
585
|
+
eventId: event.eventId,
|
|
586
|
+
seq: event.seq,
|
|
587
|
+
result: STEERING_RESULT.APPLIED_NOW,
|
|
588
|
+
reason: null,
|
|
589
|
+
acknowledgedAt,
|
|
590
|
+
};
|
|
591
|
+
return {
|
|
592
|
+
steeringState: {
|
|
593
|
+
...steeringState,
|
|
594
|
+
latestResult: ackResult,
|
|
595
|
+
resultHistory: [...steeringState.resultHistory, ackResult],
|
|
596
|
+
events: [...steeringState.events, event],
|
|
597
|
+
effectiveStack: [...steeringState.effectiveStack, event],
|
|
598
|
+
nextSeq: Math.max(steeringState.nextSeq, event.seq + 1),
|
|
599
|
+
},
|
|
600
|
+
result: ackResult,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// Queued steering promotion
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Promote queued steering events to the effective stack when the loop reaches
|
|
610
|
+
* a safe point.
|
|
611
|
+
*
|
|
612
|
+
* Call this whenever the loop transitions to a new state. If the new state is
|
|
613
|
+
* an IMMEDIATE safe point and there are queued events, they are moved to the
|
|
614
|
+
* effective stack and their results updated to applied_now.
|
|
615
|
+
*
|
|
616
|
+
* @param {object} steeringState
|
|
617
|
+
* @param {string} loopState - current loop state after the transition
|
|
618
|
+
* @returns {{ steeringState: object, promoted: object[] }}
|
|
619
|
+
*/
|
|
620
|
+
export function promoteQueuedSteering(steeringState, loopState) {
|
|
621
|
+
if (steeringState.queuedEvents.length === 0) {
|
|
622
|
+
return { steeringState, promoted: [] };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const category = classifySafePoint(loopState);
|
|
626
|
+
if (category !== SAFE_POINT_CATEGORY.IMMEDIATE) {
|
|
627
|
+
return { steeringState, promoted: [] };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const promoted = [...steeringState.queuedEvents];
|
|
631
|
+
const now = new Date().toISOString();
|
|
632
|
+
|
|
633
|
+
const newResults = promoted.map((event) => ({
|
|
634
|
+
eventId: event.eventId,
|
|
635
|
+
seq: event.seq,
|
|
636
|
+
result: STEERING_RESULT.APPLIED_NOW,
|
|
637
|
+
reason: `Promoted from queue at safe point '${loopState}'`,
|
|
638
|
+
acknowledgedAt: now,
|
|
639
|
+
}));
|
|
640
|
+
|
|
641
|
+
const updatedState = {
|
|
642
|
+
...steeringState,
|
|
643
|
+
queuedEvents: [],
|
|
644
|
+
effectiveStack: [...steeringState.effectiveStack, ...promoted],
|
|
645
|
+
resultHistory: [...steeringState.resultHistory, ...newResults],
|
|
646
|
+
latestResult: newResults.length > 0 ? newResults[newResults.length - 1] : steeringState.latestResult,
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
return { steeringState: updatedState, promoted };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
// Effective constraints query
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Get the current effective steering constraints for a run.
|
|
658
|
+
*
|
|
659
|
+
* Returns a structured view over the effective stack, split by kind.
|
|
660
|
+
*
|
|
661
|
+
* @param {object} steeringState
|
|
662
|
+
* @returns {{ hardConstraints: string[], preferences: string[], clarifications: string[], stopAtNextSafeGate: boolean, unknownConstraints: object[] }}
|
|
663
|
+
*/
|
|
664
|
+
export function getEffectiveConstraints(steeringState) {
|
|
665
|
+
const hardConstraints = [];
|
|
666
|
+
const preferences = [];
|
|
667
|
+
const clarifications = [];
|
|
668
|
+
const unknownConstraints = [];
|
|
669
|
+
let stopAtNextSafeGate = false;
|
|
670
|
+
|
|
671
|
+
for (const event of steeringState.effectiveStack) {
|
|
672
|
+
switch (event.kind) {
|
|
673
|
+
case STEERING_KIND.HARD_CONSTRAINT:
|
|
674
|
+
hardConstraints.push(event.directive);
|
|
675
|
+
break;
|
|
676
|
+
case STEERING_KIND.PREFERENCE:
|
|
677
|
+
preferences.push(event.directive);
|
|
678
|
+
break;
|
|
679
|
+
case STEERING_KIND.CLARIFICATION:
|
|
680
|
+
clarifications.push(event.directive);
|
|
681
|
+
break;
|
|
682
|
+
case STEERING_KIND.STOP_AT_NEXT_SAFE_GATE:
|
|
683
|
+
stopAtNextSafeGate = true;
|
|
684
|
+
break;
|
|
685
|
+
default:
|
|
686
|
+
unknownConstraints.push({
|
|
687
|
+
kind: event.kind,
|
|
688
|
+
directive: event.directive,
|
|
689
|
+
seq: event.seq,
|
|
690
|
+
});
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return { hardConstraints, preferences, clarifications, stopAtNextSafeGate, unknownConstraints };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// Loop state augmentation with effective steering
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Get the loop interpretation augmented with any active steering directives.
|
|
704
|
+
*
|
|
705
|
+
* This is the main integration point between the steering contract and the
|
|
706
|
+
* async Copilot review/fix loop (the first proving target). Call this instead
|
|
707
|
+
* of interpretLoopState when the run may have active steering state.
|
|
708
|
+
*
|
|
709
|
+
* Behavioral change applied by this function:
|
|
710
|
+
* - When stop_at_next_safe_gate is effective and the loop is at an IMMEDIATE
|
|
711
|
+
* safe point, the nextAction is overridden to direct the loop to stop rather
|
|
712
|
+
* than continue to the next step.
|
|
713
|
+
*
|
|
714
|
+
* The steeringApplied flag lets callers know whether active steering changed
|
|
715
|
+
* the default interpretation. The effectiveConstraints field exposes all
|
|
716
|
+
* current steering for downstream use (e.g. injecting hard constraints into
|
|
717
|
+
* agent context before a fix step).
|
|
718
|
+
*
|
|
719
|
+
* @param {object} snapshot - raw or normalized loop snapshot
|
|
720
|
+
* @param {object} steeringState - current steering state for this run
|
|
721
|
+
* @returns {{ state: string, allowedTransitions: string[], nextAction: string, steeringApplied: boolean, pendingStopAtNextSafeGate: boolean, terminalStopAtNextSafeGate: boolean, effectiveConstraints: object }}
|
|
722
|
+
*/
|
|
723
|
+
export function resolveEffectiveLoopState(snapshot, steeringState) {
|
|
724
|
+
const base = interpretLoopState(snapshot);
|
|
725
|
+
const constraints = getEffectiveConstraints(steeringState);
|
|
726
|
+
const category = classifySafePoint(base.state);
|
|
727
|
+
|
|
728
|
+
const steeringApplied = constraints.stopAtNextSafeGate
|
|
729
|
+
|| constraints.hardConstraints.length > 0
|
|
730
|
+
|| constraints.preferences.length > 0
|
|
731
|
+
|| constraints.clarifications.length > 0
|
|
732
|
+
|| constraints.unknownConstraints.length > 0;
|
|
733
|
+
const pendingStopAtNextSafeGate = constraints.stopAtNextSafeGate && category === SAFE_POINT_CATEGORY.NEXT_POINT;
|
|
734
|
+
const terminalStopAtNextSafeGate = constraints.stopAtNextSafeGate && category === SAFE_POINT_CATEGORY.TERMINAL;
|
|
735
|
+
|
|
736
|
+
let nextAction = base.nextAction;
|
|
737
|
+
|
|
738
|
+
if (constraints.stopAtNextSafeGate && category === SAFE_POINT_CATEGORY.IMMEDIATE) {
|
|
739
|
+
nextAction = "Stop at this safe gate: a stop_at_next_safe_gate steering directive is active. Do not proceed to the next loop step.";
|
|
740
|
+
} else if (pendingStopAtNextSafeGate) {
|
|
741
|
+
nextAction = `Pending stop_at_next_safe_gate: stop at the next safe gate. Until then, current state remains '${base.state}' and the immediate action is: ${base.nextAction}`;
|
|
742
|
+
} else if (terminalStopAtNextSafeGate) {
|
|
743
|
+
nextAction = `stop_at_next_safe_gate is currently inactive because the loop is in terminal state '${base.state}'. Current action remains: ${base.nextAction}`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
...base,
|
|
748
|
+
nextAction,
|
|
749
|
+
steeringApplied,
|
|
750
|
+
pendingStopAtNextSafeGate,
|
|
751
|
+
terminalStopAtNextSafeGate,
|
|
752
|
+
effectiveConstraints: constraints,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
// Status/inspection output
|
|
758
|
+
// ---------------------------------------------------------------------------
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Get full inspection output for a run's steering state.
|
|
762
|
+
*
|
|
763
|
+
* Returns:
|
|
764
|
+
* - runId: the target run
|
|
765
|
+
* - schemaVersion: for migration awareness
|
|
766
|
+
* - eventCount: total events submitted
|
|
767
|
+
* - queuedCount: events waiting for the next safe point
|
|
768
|
+
* - effectiveStackCount: events currently in effect
|
|
769
|
+
* - effectiveConstraints: structured view over the effective stack
|
|
770
|
+
* - latestResult: most recent acknowledgement/result
|
|
771
|
+
* - resultHistory: all historical acknowledgements
|
|
772
|
+
* - history: all submitted events
|
|
773
|
+
* - nextSeq: next expected sequence number
|
|
774
|
+
*
|
|
775
|
+
* @param {object} steeringState
|
|
776
|
+
* @returns {object}
|
|
777
|
+
*/
|
|
778
|
+
export function getSteeringStatus(steeringState) {
|
|
779
|
+
const effectiveConstraints = getEffectiveConstraints(steeringState);
|
|
780
|
+
return {
|
|
781
|
+
runId: steeringState.runId,
|
|
782
|
+
...(steeringState.target ? { target: steeringState.target } : {}),
|
|
783
|
+
schemaVersion: steeringState.schemaVersion,
|
|
784
|
+
eventCount: steeringState.events.length,
|
|
785
|
+
queuedCount: steeringState.queuedEvents.length,
|
|
786
|
+
effectiveStackCount: steeringState.effectiveStack.length,
|
|
787
|
+
effectiveConstraints,
|
|
788
|
+
latestResult: steeringState.latestResult,
|
|
789
|
+
resultHistory: steeringState.resultHistory,
|
|
790
|
+
history: steeringState.events,
|
|
791
|
+
nextSeq: steeringState.nextSeq,
|
|
792
|
+
};
|
|
793
|
+
}
|