33"use strict" ;
44
55const fs = require ( "fs" ) ;
6+ const path = require ( "path" ) ;
67
78const DEFAULT_TIMEOUT_MINUTES = 30 ;
89const DEFAULT_TIME_WARNING_MINUTES = 5 ;
910const DEFAULT_TIME_CRITICAL_MINUTES = 2 ;
1011const DEFAULT_RUN_WARNING_REMAINING = 2 ;
1112const DEFAULT_RUN_CRITICAL_REMAINING = 1 ;
1213const DEFAULT_STATE_PATH = "/tmp/gh-aw/copilot-steering-state.json" ;
14+ const MS_PER_MINUTE = 60000 ;
1315
1416/**
1517 * @typedef {{
@@ -77,7 +79,11 @@ function loadState(statePath) {
7779 return null ;
7880 }
7981 const raw = fs . readFileSync ( statePath , "utf8" ) ;
80- return /** @type {SteeringState } */ JSON . parse ( raw ) ;
82+ const parsed = JSON . parse ( raw ) ;
83+ if ( ! isValidSteeringState ( parsed ) ) {
84+ return null ;
85+ }
86+ return parsed ;
8187 } catch {
8288 return null ;
8389 }
@@ -88,9 +94,29 @@ function loadState(statePath) {
8894 * @param {SteeringState } state
8995 */
9096function saveState ( statePath , state ) {
97+ fs . mkdirSync ( path . dirname ( statePath ) , { recursive : true } ) ;
9198 fs . writeFileSync ( statePath , JSON . stringify ( state ) , "utf8" ) ;
9299}
93100
101+ /**
102+ * @param {unknown } value
103+ * @returns {value is SteeringState }
104+ */
105+ function isValidSteeringState ( value ) {
106+ if ( ! value || typeof value !== "object" ) {
107+ return false ;
108+ }
109+ const candidate = /** @type {Record<string, unknown> } */ value ;
110+ return (
111+ typeof candidate . startedAtMs === "number" &&
112+ Number . isFinite ( candidate . startedAtMs ) &&
113+ typeof candidate . turns === "number" &&
114+ Number . isFinite ( candidate . turns ) &&
115+ typeof candidate . warningInjected === "boolean" &&
116+ typeof candidate . criticalInjected === "boolean"
117+ ) ;
118+ }
119+
94120/**
95121 * @param {unknown } value
96122 * @returns {number }
@@ -140,7 +166,7 @@ function buildBudgetSummary(remainingMinutes, remainingRuns) {
140166 */
141167function computeSteeringDecision ( state , config , timestamp ) {
142168 const nextState = { ...state , turns : state . turns + 1 } ;
143- const elapsedMinutes = ( timestamp - nextState . startedAtMs ) / 60000 ;
169+ const elapsedMinutes = ( timestamp - nextState . startedAtMs ) / MS_PER_MINUTE ;
144170 const remainingMinutes = Number . isFinite ( config . timeoutMinutes ) ? config . timeoutMinutes - elapsedMinutes : null ;
145171 const remainingRuns = config . maxRuns > 0 ? config . maxRuns - nextState . turns : null ;
146172
0 commit comments