@indigoai-us/hq-cli 5.292.0 → 5.294.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
- package/dist/command-catalog.generated.d.ts +45 -24
- package/dist/command-catalog.generated.js +57 -30
- package/dist/commands/core-command-catalog.js +3 -2
- package/dist/commands/core-delegate-w1b.d.ts +2 -0
- package/dist/commands/core-delegate-w1b.js +28 -2
- package/dist/commands/core-delegate.d.ts +3 -1
- package/dist/commands/core-delegate.js +35 -5
- package/dist/commands/core-handoff-fast.d.ts +18 -0
- package/dist/commands/core-handoff-fast.js +103 -0
- package/dist/commands/core-handoff-open-steps.d.ts +21 -0
- package/dist/commands/core-handoff-open-steps.js +264 -0
- package/dist/commands/core-handoff.d.ts +4 -2
- package/dist/commands/core-handoff.js +22 -249
- package/dist/commands/core-jobs-native.d.ts +2 -0
- package/dist/commands/core-jobs-native.js +22 -11
- package/dist/commands/core-knowledge-prefs.d.ts +2 -0
- package/dist/commands/core-knowledge-prefs.js +13 -1
- package/dist/commands/core-ontology-native.d.ts +2 -0
- package/dist/commands/core-ontology-native.js +22 -2
- package/dist/commands/core-policy.d.ts +2 -0
- package/dist/commands/core-policy.js +18 -1
- package/dist/commands/core.js +2 -1
- package/dist/commands/daemon.d.ts +2 -1
- package/dist/commands/daemon.js +22 -7
- package/dist/commands/lanes-autopilot.d.ts +11 -0
- package/dist/commands/lanes-autopilot.js +336 -0
- package/dist/commands/lanes-move.js +54 -1
- package/dist/commands/lanes-questions.js +12 -5
- package/dist/commands/lanes-shared.d.ts +17 -0
- package/dist/commands/lanes-stories.d.ts +14 -0
- package/dist/commands/lanes-stories.js +308 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +158 -314
- package/dist/fast-core.js +53 -25
- package/dist/fast-scaffold.d.ts +15 -2
- package/dist/fast-scaffold.js +167 -7
- package/dist/lib/core-utils/sentry-report.js +2 -2
- package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
- package/dist/lib/core-utils/worktree-resume.js +16 -0
- package/dist/lib/daemon/migrate.d.ts +8 -1
- package/dist/lib/daemon/migrate.js +19 -7
- package/dist/lib/daemon/run.d.ts +11 -0
- package/dist/lib/daemon/run.js +17 -1
- package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
- package/dist/lib/lanes/autopilot-metrics.js +301 -0
- package/dist/lib/lanes/autopilot.d.ts +13 -1
- package/dist/lib/lanes/autopilot.js +271 -28
- package/dist/lib/lanes/grants.d.ts +1 -1
- package/dist/lib/lanes/grants.js +7 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +5 -0
- package/dist/lib/lanes/keep-alive.js +9 -4
- package/dist/lib/lanes/questions.d.ts +2 -1
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +186 -9
- package/dist/lib/lanes/spawn.js +4 -0
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +53 -4
- package/dist/lib/lanes/types.d.ts +13 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -1
- package/dist/native-fast-route.d.ts +3 -1
- package/dist/native-fast-route.js +10 -8
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/fast-stream-errors.d.ts +12 -0
- package/dist/utils/fast-stream-errors.js +18 -0
- package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
- package/dist/utils/scaffold-fast-import-guard.js +6 -3
- package/dist/utils/self-update.d.ts +7 -0
- package/dist/utils/self-update.js +5 -1
- package/dist/utils/version-gate.d.ts +15 -0
- package/dist/utils/version-gate.js +46 -2
- package/package.json +1 -1
|
@@ -11,6 +11,9 @@ export const LEAD_AUTOPILOT_FILL_DELAY_MS = 15 * 60_000;
|
|
|
11
11
|
export const LEAD_AUTOPILOT_PROMPT_LOOP = "personal/knowledge/team-lead-operating-loop.md";
|
|
12
12
|
const LIVE_STATES = new Set(["running", "queued", "awaiting_input", "blocked"]);
|
|
13
13
|
const OPEN_STORY_STATES = new Set(["todo", "queued", "in_progress", "review", "blocked"]);
|
|
14
|
+
function isOpenProjectStory(story) {
|
|
15
|
+
return story.passes !== true && OPEN_STORY_STATES.has(story.status);
|
|
16
|
+
}
|
|
14
17
|
function isRecord(value) {
|
|
15
18
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
19
|
}
|
|
@@ -95,18 +98,25 @@ function projectStories(hqRoot, relativePath, companySlug, projectId) {
|
|
|
95
98
|
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: userStories or stories must be an array`, { path: file });
|
|
96
99
|
}
|
|
97
100
|
return rows.map((story, index) => {
|
|
98
|
-
const
|
|
101
|
+
const rawStatus = isRecord(story) ? story.status ?? story.state : undefined;
|
|
102
|
+
const status = isRecord(story) && story.passes === true ? "done" : rawStatus;
|
|
99
103
|
if (!isRecord(story) || typeof story.id !== "string" || typeof status !== "string") {
|
|
100
104
|
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: stories[${index}] needs string id and status/state`, { path: file, index });
|
|
101
105
|
}
|
|
102
106
|
const sourceRef = isRecord(story.source) ? story.source.ref : undefined;
|
|
103
107
|
const rawBrief = story.brief_path ?? story.brief_file ?? sourceRef;
|
|
104
|
-
if (story.lanes !== undefined && (!Array.isArray(story.lanes) || story.lanes.some((
|
|
105
|
-
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: stories[${index}] lanes must
|
|
108
|
+
if (story.lanes !== undefined && (!Array.isArray(story.lanes) || story.lanes.some((lane) => typeof lane === "string" ? !lane.trim() : !isRecord(lane) || typeof lane.lane_id !== "string" || !lane.lane_id.trim()))) {
|
|
109
|
+
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: stories[${index}] lanes must contain lane IDs or lane_id objects`, { path: file, index });
|
|
106
110
|
}
|
|
107
111
|
if (story.updated_at !== undefined && (typeof story.updated_at !== "string" || !Number.isFinite(Date.parse(story.updated_at)))) {
|
|
108
112
|
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: stories[${index}] updated_at must be a valid timestamp`, { path: file, index });
|
|
109
113
|
}
|
|
114
|
+
if (story.lane !== undefined && story.lane !== null && typeof story.lane !== "string") {
|
|
115
|
+
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: stories[${index}] lane must be a string`, { path: file, index });
|
|
116
|
+
}
|
|
117
|
+
if (story.external_blocker !== undefined && (typeof story.external_blocker !== "string" || !story.external_blocker.trim())) {
|
|
118
|
+
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: stories[${index}] external_blocker must be a non-empty string`, { path: file, index });
|
|
119
|
+
}
|
|
110
120
|
if (rawBrief !== undefined && (typeof rawBrief !== "string" || !rawBrief.trim())) {
|
|
111
121
|
throw new LanesError("lead_autopilot_project_invalid", `Invalid project ${file}: stories[${index}] brief path must be a non-empty string`, { path: file, index });
|
|
112
122
|
}
|
|
@@ -131,30 +141,96 @@ function projectStories(hqRoot, relativePath, companySlug, projectId) {
|
|
|
131
141
|
return {
|
|
132
142
|
id: story.id,
|
|
133
143
|
status,
|
|
144
|
+
...(story.passes === true ? { passes: true } : {}),
|
|
134
145
|
...(briefPath ? { briefPath } : {}),
|
|
135
|
-
...(Array.isArray(story.lanes) ? { laneIds: story.lanes } : {}),
|
|
146
|
+
...(Array.isArray(story.lanes) ? { laneIds: story.lanes.map((lane) => typeof lane === "string" ? lane : lane.lane_id) } : typeof story.lane === "string" && story.lane.trim() ? { laneIds: [story.lane] } : story.lane === "" ? { laneIds: [] } : {}),
|
|
136
147
|
...(typeof story.updated_at === "string" ? { updatedAt: story.updated_at } : {}),
|
|
148
|
+
...(typeof story.external_blocker === "string" ? { externalBlocker: story.external_blocker } : {}),
|
|
137
149
|
};
|
|
138
150
|
});
|
|
139
151
|
}
|
|
140
|
-
function readStatus(hqRoot, relativePath) {
|
|
141
|
-
|
|
152
|
+
function readStatus(hqRoot, relativePath, companySlug) {
|
|
153
|
+
if (path.isAbsolute(relativePath)) {
|
|
154
|
+
throw new LanesError("lead_autopilot_status_path_invalid", `Registered status path must be relative to the HQ root: ${relativePath}`, { path: relativePath });
|
|
155
|
+
}
|
|
156
|
+
const root = path.resolve(hqRoot);
|
|
157
|
+
const file = registeredFilePath(root, relativePath, "lead_autopilot_status_path_invalid");
|
|
158
|
+
const assertCompanyBoundary = (candidate) => {
|
|
159
|
+
const relative = path.relative(root, candidate).split(path.sep);
|
|
160
|
+
if (relative[0] === "companies" && relative[1] !== companySlug) {
|
|
161
|
+
throw new LanesError("lead_autopilot_tenant_mismatch", `Manager status path does not match lead company ${companySlug}`, { path: relativePath, company: companySlug });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
assertCompanyBoundary(path.resolve(root, relativePath));
|
|
165
|
+
assertCompanyBoundary(fs.realpathSync(file));
|
|
142
166
|
const status = readJsonFile(file, "lead_autopilot_status_invalid");
|
|
143
167
|
if (!isRecord(status))
|
|
144
168
|
throw new LanesError("lead_autopilot_status_invalid", `Invalid manager status ${file}: expected an object`, { path: file });
|
|
145
169
|
return status;
|
|
146
170
|
}
|
|
171
|
+
function parseOkrReports(status) {
|
|
172
|
+
const raw = status.okr_reports;
|
|
173
|
+
if (raw === undefined)
|
|
174
|
+
return [];
|
|
175
|
+
if (!Array.isArray(raw)) {
|
|
176
|
+
throw new LanesError("lead_autopilot_okr_reports_invalid", "Manager status okr_reports must be an array");
|
|
177
|
+
}
|
|
178
|
+
return raw.map((value, index) => {
|
|
179
|
+
if (!isRecord(value) || typeof value.at !== "string" || !Number.isFinite(Date.parse(value.at)) || !Array.isArray(value.kpis) || !Array.isArray(value.okrs)) {
|
|
180
|
+
throw new LanesError("lead_autopilot_okr_reports_invalid", `Manager status okr_reports[${index}] must include at, kpis, and okrs`);
|
|
181
|
+
}
|
|
182
|
+
const parseLines = (rows, kind) => rows.map((row, lineIndex) => {
|
|
183
|
+
if (!isRecord(row) || typeof row.id !== "string" || !row.id.trim()) {
|
|
184
|
+
throw new LanesError("lead_autopilot_okr_reports_invalid", `Manager status okr_reports[${index}].${kind.toLowerCase()}s[${lineIndex}] needs an id`);
|
|
185
|
+
}
|
|
186
|
+
const text = typeof row.line === "string" ? row.line : typeof row.name === "string" ? row.name : typeof row.objective === "string" ? row.objective : "";
|
|
187
|
+
if (!text.trim()) {
|
|
188
|
+
throw new LanesError("lead_autopilot_okr_reports_invalid", `Manager status okr_reports[${index}].${kind.toLowerCase()}s[${lineIndex}] needs a line, name, or objective`);
|
|
189
|
+
}
|
|
190
|
+
if ((row.value !== undefined && (typeof row.value !== "number" || !Number.isFinite(row.value))) ||
|
|
191
|
+
(row.moved !== undefined && (typeof row.moved !== "number" || !Number.isFinite(row.moved)))) {
|
|
192
|
+
throw new LanesError("lead_autopilot_okr_reports_invalid", `Manager status okr_reports[${index}].${kind.toLowerCase()}s[${lineIndex}] has invalid numeric progress`);
|
|
193
|
+
}
|
|
194
|
+
return { id: row.id, line: text, ...(typeof row.value === "number" ? { value: row.value } : {}), ...(typeof row.moved === "number" ? { moved: row.moved } : {}) };
|
|
195
|
+
});
|
|
196
|
+
return { at: value.at, kpis: parseLines(value.kpis, "KPI"), okrs: parseLines(value.okrs, "OKR") };
|
|
197
|
+
}).sort((a, b) => a.at.localeCompare(b.at));
|
|
198
|
+
}
|
|
199
|
+
function statusBlockers(status) {
|
|
200
|
+
const raw = status.blockers;
|
|
201
|
+
if (raw === undefined)
|
|
202
|
+
return [];
|
|
203
|
+
if (!Array.isArray(raw))
|
|
204
|
+
throw new LanesError("lead_autopilot_status_invalid", "Manager status blockers must be an array");
|
|
205
|
+
return raw.map((value, index) => {
|
|
206
|
+
if (!isRecord(value) || typeof value.id !== "string" || !value.id.trim()) {
|
|
207
|
+
throw new LanesError("lead_autopilot_status_invalid", `Manager status blockers[${index}] needs a non-empty id`);
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function leastMoved(lines, priorLines = []) {
|
|
213
|
+
const priorById = new Map(priorLines.map((line) => [line.id, line]));
|
|
214
|
+
return [...lines].sort((a, b) => {
|
|
215
|
+
const moved = (line) => line.moved !== undefined ? Math.abs(line.moved) :
|
|
216
|
+
line.value !== undefined && priorById.get(line.id)?.value !== undefined ? Math.abs(line.value - priorById.get(line.id).value) : 0;
|
|
217
|
+
return moved(a) - moved(b) || a.id.localeCompare(b.id);
|
|
218
|
+
})[0];
|
|
219
|
+
}
|
|
147
220
|
export function planLeadAutopilotWakes(input) {
|
|
148
221
|
const { lead, children, stories, now } = input;
|
|
149
222
|
if (input.keepAliveResumed)
|
|
150
223
|
return [];
|
|
224
|
+
const status = input.managerStatus ?? {};
|
|
225
|
+
const reports = parseOkrReports(status);
|
|
226
|
+
const blockers = statusBlockers(status);
|
|
151
227
|
const byStory = new Map(stories.map((story) => [story.id, story]));
|
|
152
228
|
const wakes = [];
|
|
153
229
|
const finishedReviews = children.flatMap((child) => {
|
|
154
230
|
if (!["done", "blocked", "failed", "interrupted"].includes(child.state))
|
|
155
231
|
return [];
|
|
156
232
|
const story = byStory.get(child.story_id);
|
|
157
|
-
if (!story || !
|
|
233
|
+
if (!story || !isOpenProjectStory(story))
|
|
158
234
|
return [];
|
|
159
235
|
const finishedAt = child.ended_at ?? child.updated_at;
|
|
160
236
|
const finishedMs = Date.parse(finishedAt);
|
|
@@ -164,7 +240,7 @@ export function planLeadAutopilotWakes(input) {
|
|
|
164
240
|
});
|
|
165
241
|
const knownChildIds = new Set(children.map((child) => child.lane_id));
|
|
166
242
|
const missingReviews = stories.flatMap((story) => {
|
|
167
|
-
if (!
|
|
243
|
+
if (!isOpenProjectStory(story) || !story.updatedAt || !story.laneIds)
|
|
168
244
|
return [];
|
|
169
245
|
const finishedMs = Date.parse(story.updatedAt);
|
|
170
246
|
if (!Number.isFinite(finishedMs) || now.getTime() - finishedMs < LEAD_AUTOPILOT_REVIEW_DELAY_MS)
|
|
@@ -187,7 +263,7 @@ export function planLeadAutopilotWakes(input) {
|
|
|
187
263
|
const liveChildren = children.filter((child) => LIVE_STATES.has(child.state));
|
|
188
264
|
const liveCount = liveChildren.length;
|
|
189
265
|
const assigned = new Set(liveChildren.flatMap((child) => [child.story_id, ...(child.stories ?? []).map((story) => story.story_id)]));
|
|
190
|
-
const readyStory = stories.find((story) => story.status === "todo" && !assigned.has(story.id));
|
|
266
|
+
const readyStory = stories.find((story) => story.status === "todo" && !story.passes && !assigned.has(story.id));
|
|
191
267
|
if (readyStory && liveCount < (lead.hire_grant ?? 0)) {
|
|
192
268
|
const fingerprint = `fill:${readyStory.id}:${liveCount}:${lead.hire_grant ?? 0}`;
|
|
193
269
|
wakes.push({
|
|
@@ -198,6 +274,84 @@ export function planLeadAutopilotWakes(input) {
|
|
|
198
274
|
prompt: `Start a worker for ready story ${readyStory.id}. You have ${liveCount} live workers against a grant of ${lead.hire_grant ?? 0}. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.`,
|
|
199
275
|
});
|
|
200
276
|
}
|
|
277
|
+
const hasFreeSlots = liveCount < (lead.hire_grant ?? 0);
|
|
278
|
+
if (hasFreeSlots && !readyStory) {
|
|
279
|
+
const latest = reports.at(-1);
|
|
280
|
+
const previous = reports.at(-2);
|
|
281
|
+
const kpiLine = latest ? leastMoved(latest.kpis, previous?.kpis) : undefined;
|
|
282
|
+
const okrLine = latest ? leastMoved(latest.okrs, previous?.okrs) : undefined;
|
|
283
|
+
const kpiText = kpiLine?.line ?? "no KPI line in the latest OKR report";
|
|
284
|
+
const okrText = okrLine?.line ?? "no OKR line in the latest OKR report";
|
|
285
|
+
const fingerprint = `plan:${latest?.at ?? "no-report"}:${kpiLine?.id ?? "no-kpi"}:${okrLine?.id ?? "no-okr"}:${stories.map((story) => `${story.id}:${story.status}`).sort().join(",")}`;
|
|
286
|
+
wakes.push({ condition: "plan_free_slots", lead_lane: lead.lane_id, story_id: "planning", fingerprint,
|
|
287
|
+
prompt: `You have ${liveCount} live workers against a grant of ${lead.hire_grant ?? 0} and no ready stories. Plan work against the least-moved KPI line: ${kpiText}; and OKR line: ${okrText}. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.` });
|
|
288
|
+
}
|
|
289
|
+
if (reports.length >= 2) {
|
|
290
|
+
const latest = reports.at(-1);
|
|
291
|
+
const prior = new Map(reports.at(-2).kpis.filter((line) => line.value !== undefined).map((line) => [line.id, line]));
|
|
292
|
+
const flat = latest.kpis.filter((line) => line.value !== undefined && prior.get(line.id)?.value === line.value);
|
|
293
|
+
if (flat.length > 0) {
|
|
294
|
+
const line = flat.map((item) => `${item.line} (${item.value})`).join("; ");
|
|
295
|
+
const fingerprint = `flat-kpi:${latest.at}:${flat.map((item) => `${item.id}:${item.value}`).join(",")}`;
|
|
296
|
+
wakes.push({ condition: "flat_kpi", lead_lane: lead.lane_id, story_id: flat.map((item) => item.id).join(","), fingerprint,
|
|
297
|
+
prompt: `KPI line(s) remained flat across the two latest OKR reports: ${line}. Re-plan the affected line(s). Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.` });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const staleBlockers = blockers.filter((blocker) => {
|
|
301
|
+
const blockerStatus = blocker.status ?? blocker.state;
|
|
302
|
+
const resolved = blockerStatus === "resolved" || blocker.resolved_at !== undefined && blocker.resolved_at !== null;
|
|
303
|
+
const owner = typeof blocker.unblock_owner === "string" ? blocker.unblock_owner : blocker.owner;
|
|
304
|
+
const due = blocker.unblock_by ?? blocker.unblock_at ?? blocker.due_at;
|
|
305
|
+
const ownerMissing = typeof owner !== "string" || !owner.trim();
|
|
306
|
+
const dueMissing = typeof due !== "string" || !Number.isFinite(Date.parse(due));
|
|
307
|
+
return resolved || ownerMissing || dueMissing;
|
|
308
|
+
});
|
|
309
|
+
if (staleBlockers.length > 0) {
|
|
310
|
+
const labels = staleBlockers.map((blocker) => `${String(blocker.id)}${typeof blocker.what === "string" ? ` (${blocker.what})` : ""}`).join("; ");
|
|
311
|
+
const fingerprint = `blockers:${staleBlockers.map((blocker) => `${String(blocker.id)}:${String(blocker.status ?? blocker.state ?? "")}:${String(blocker.resolved_at ?? "")}:${String(blocker.unblock_owner ?? blocker.owner ?? "")}:${String(blocker.unblock_by ?? blocker.unblock_at ?? blocker.due_at ?? "")}`).join(",")}`;
|
|
312
|
+
wakes.push({ condition: "blocker_record_needs_update", lead_lane: lead.lane_id, story_id: staleBlockers.map((blocker) => String(blocker.id)).join(","), fingerprint,
|
|
313
|
+
prompt: `Fix blocker records still listed as resolved or missing an unblock owner/time: ${labels}. Move resolved blockers out of blockers, and give every open blocker an owner and time. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.` });
|
|
314
|
+
}
|
|
315
|
+
if (stories.length > 0 && stories.every((story) => story.passes === true || story.status === "done" || story.status === "dropped" ||
|
|
316
|
+
((story.status === "parked" || story.status === "blocked") && Boolean(story.externalBlocker)))) {
|
|
317
|
+
const fingerprint = `project-close:${stories.map((story) => `${story.id}:${story.status}:${story.externalBlocker ?? ""}`).sort().join(",")}`;
|
|
318
|
+
wakes.push({ condition: "project_ready_to_close", lead_lane: lead.lane_id, story_id: stories.map((story) => story.id).join(","), fingerprint,
|
|
319
|
+
prompt: `Every story is done, dropped, or parked on a named external blocker. Write the final report and mark the project complete. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.` });
|
|
320
|
+
}
|
|
321
|
+
const staleLedger = stories.flatMap((story) => {
|
|
322
|
+
if (story.status !== "in_progress" || !story.updatedAt || !story.laneIds)
|
|
323
|
+
return [];
|
|
324
|
+
const updated = Date.parse(story.updatedAt);
|
|
325
|
+
if (!Number.isFinite(updated) || now.getTime() - updated <= 30 * 60_000)
|
|
326
|
+
return [];
|
|
327
|
+
return story.laneIds.flatMap((laneId) => {
|
|
328
|
+
const worker = children.find((child) => child.lane_id === laneId && child.state === "running" &&
|
|
329
|
+
(child.story_id === story.id || (child.stories ?? []).some((carried) => carried.story_id === story.id)));
|
|
330
|
+
return worker ? [{ story, worker }] : [];
|
|
331
|
+
});
|
|
332
|
+
}).sort((a, b) => a.story.id.localeCompare(b.story.id) || a.worker.lane_id.localeCompare(b.worker.lane_id));
|
|
333
|
+
if (staleLedger.length > 0) {
|
|
334
|
+
const labels = staleLedger.map(({ story, worker }) => `${story.id} (lane ${worker.lane_id}, ledger updated ${story.updatedAt})`).join("; ");
|
|
335
|
+
const fingerprint = `stale-ledger:${staleLedger.map(({ story, worker }) => `${story.id}:${worker.lane_id}:${story.updatedAt}`).join(",")}`;
|
|
336
|
+
wakes.push({ condition: "stale_worker_ledger", lead_lane: lead.lane_id, story_id: staleLedger.map(({ story }) => story.id).join(","), worker_lane: staleLedger.length === 1 ? staleLedger[0].worker.lane_id : undefined, fingerprint,
|
|
337
|
+
prompt: `A running worker has made no ledger update in over 30 minutes: ${labels}. Check the worker and update the story ledger. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.` });
|
|
338
|
+
}
|
|
339
|
+
const withoutLiveLane = stories.flatMap((story) => {
|
|
340
|
+
if (story.status !== "in_progress")
|
|
341
|
+
return [];
|
|
342
|
+
const laneIds = story.laneIds ?? [];
|
|
343
|
+
const live = laneIds.filter((laneId) => children.some((child) => child.lane_id === laneId && LIVE_STATES.has(child.state)));
|
|
344
|
+
if (live.length > 0)
|
|
345
|
+
return [];
|
|
346
|
+
const detail = laneIds.length === 0 ? "empty lane assignment" : `no live lane among ${laneIds.join(", ")}`;
|
|
347
|
+
return [{ story, laneIds, detail }];
|
|
348
|
+
}).sort((a, b) => a.story.id.localeCompare(b.story.id));
|
|
349
|
+
if (withoutLiveLane.length > 0) {
|
|
350
|
+
const labels = withoutLiveLane.map(({ story, detail }) => `${story.id} (${detail})`).join("; ");
|
|
351
|
+
const fingerprint = `missing-worker:${withoutLiveLane.map(({ story, detail }) => `${story.id}:${story.updatedAt ?? ""}:${detail}`).join(",")}`;
|
|
352
|
+
wakes.push({ condition: "story_without_live_lane", lead_lane: lead.lane_id, story_id: withoutLiveLane.map(({ story }) => story.id).join(","), fingerprint,
|
|
353
|
+
prompt: `In-progress story has no live worker lane: ${labels}. Relaunch a worker or set the story to waiting/blocked with its wake. Follow ${LEAD_AUTOPILOT_PROMPT_LOOP}.` });
|
|
354
|
+
}
|
|
201
355
|
return wakes;
|
|
202
356
|
}
|
|
203
357
|
function readState(file) {
|
|
@@ -212,7 +366,10 @@ function readState(file) {
|
|
|
212
366
|
(row.status_updated_at !== undefined && typeof row.status_updated_at !== "string") ||
|
|
213
367
|
(row.acted !== undefined && typeof row.acted !== "boolean") ||
|
|
214
368
|
(row.auto_started !== undefined && typeof row.auto_started !== "boolean") ||
|
|
215
|
-
(row.worker_lane !== undefined && typeof row.worker_lane !== "string")
|
|
369
|
+
(row.worker_lane !== undefined && typeof row.worker_lane !== "string") ||
|
|
370
|
+
(row.wake_count !== undefined && (!Number.isSafeInteger(row.wake_count) || row.wake_count < 1)) ||
|
|
371
|
+
(row.last_wake_at !== undefined && typeof row.last_wake_at !== "string") ||
|
|
372
|
+
(row.escalated_at !== undefined && typeof row.escalated_at !== "string")) {
|
|
216
373
|
throw new LanesError("lead_autopilot_state_invalid", `Invalid autopilot state entry ${key}`, { path: file, key });
|
|
217
374
|
}
|
|
218
375
|
}
|
|
@@ -224,9 +381,31 @@ function writeState(file, state) {
|
|
|
224
381
|
fs.writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
225
382
|
fs.renameSync(temp, file);
|
|
226
383
|
}
|
|
384
|
+
function recordOperatorEscalation(hqRoot, record) {
|
|
385
|
+
const file = path.join(hqRoot, "workspace", "lane-briefs", "ops-board-state.json");
|
|
386
|
+
let state = {};
|
|
387
|
+
if (fs.existsSync(file)) {
|
|
388
|
+
const value = readJsonFile(file, "lead_autopilot_escalation_record_invalid");
|
|
389
|
+
if (!isRecord(value))
|
|
390
|
+
throw new LanesError("lead_autopilot_escalation_record_invalid", `Invalid operator board state ${file}`);
|
|
391
|
+
state = value;
|
|
392
|
+
}
|
|
393
|
+
const current = state.autopilot_escalations;
|
|
394
|
+
if (current !== undefined && (!Array.isArray(current) || current.some((item) => !isRecord(item)))) {
|
|
395
|
+
throw new LanesError("lead_autopilot_escalation_record_invalid", `Invalid autopilot escalations in ${file}`);
|
|
396
|
+
}
|
|
397
|
+
const rows = (current ?? []);
|
|
398
|
+
if (rows.some((item) => item.lead_lane === record.lead_lane && item.fingerprint === record.fingerprint))
|
|
399
|
+
return;
|
|
400
|
+
const next = { ...state, autopilot_escalations: [...rows, record] };
|
|
401
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
402
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
403
|
+
fs.writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
404
|
+
fs.renameSync(temp, file);
|
|
405
|
+
}
|
|
227
406
|
async function runLeadAutopilotPassUnlocked(input) {
|
|
228
407
|
const { hqRoot, host, now } = input;
|
|
229
|
-
const result = { ok: true, host, evaluated: 0, sent: [], started: [], skipped: [] };
|
|
408
|
+
const result = { ok: true, host, evaluated: 0, sent: [], started: [], escalated: [], skipped: [] };
|
|
230
409
|
const leads = parseRegistry(hqRoot).filter((lead) => lead.host === host || (host === "local" && lead.host === input.hostname));
|
|
231
410
|
const lanes = readAllLanes(hqRoot);
|
|
232
411
|
const byId = new Map(lanes.map((lane) => [lane.lane_id, lane]));
|
|
@@ -253,7 +432,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
253
432
|
result.skipped.push({ lead_lane: lead.lane_id, reason: "flag_off" });
|
|
254
433
|
continue;
|
|
255
434
|
}
|
|
256
|
-
const status = readStatus(hqRoot, registered.status);
|
|
435
|
+
const status = readStatus(hqRoot, registered.status, lead.company.slug);
|
|
257
436
|
const statusUpdatedAt = typeof status.updated_at === "string" && Number.isFinite(Date.parse(status.updated_at))
|
|
258
437
|
? new Date(Date.parse(status.updated_at)).toISOString()
|
|
259
438
|
: undefined;
|
|
@@ -265,6 +444,7 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
265
444
|
children,
|
|
266
445
|
stories,
|
|
267
446
|
now,
|
|
447
|
+
managerStatus: status,
|
|
268
448
|
keepAliveResumed: input.keepAliveResumedLeads?.has(lead.lane_id) === true,
|
|
269
449
|
});
|
|
270
450
|
let stateChanged = false;
|
|
@@ -279,36 +459,69 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
279
459
|
const key = `${lead.lane_id}:${wake.condition}`;
|
|
280
460
|
const prior = state.sent[key];
|
|
281
461
|
if (prior?.fingerprint === wake.fingerprint) {
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
462
|
+
if (input.dryRun)
|
|
463
|
+
continue;
|
|
464
|
+
let current = prior;
|
|
465
|
+
const lastWakeAt = Date.parse(prior.last_wake_at ?? prior.at);
|
|
466
|
+
if (!Number.isFinite(lastWakeAt)) {
|
|
467
|
+
throw new LanesError("lead_autopilot_state_invalid", `Invalid wake time for ${lead.lane_id}`, { lead_lane: lead.lane_id, story_id: wake.story_id });
|
|
468
|
+
}
|
|
469
|
+
if (statusUpdatedAt === undefined || prior.status_updated_at === undefined) {
|
|
470
|
+
throw new LanesError("lead_autopilot_status_activity_unverifiable", `Cannot verify whether lead ${lead.lane_id} acted after its autopilot wake`, { lead_lane: lead.lane_id, story_id: wake.story_id });
|
|
471
|
+
}
|
|
472
|
+
if (!prior.acted && Date.parse(statusUpdatedAt) > Date.parse(prior.status_updated_at)) {
|
|
473
|
+
current = { ...prior, acted: true };
|
|
474
|
+
state.sent[key] = current;
|
|
475
|
+
stateChanged = true;
|
|
476
|
+
}
|
|
477
|
+
if (wake.condition === "fill_grant" && input.startWorker && !current.acted && !current.auto_started) {
|
|
478
|
+
const firstWakeAt = Date.parse(current.at);
|
|
479
|
+
if (!Number.isFinite(firstWakeAt)) {
|
|
285
480
|
throw new LanesError("lead_autopilot_state_invalid", `Invalid fill-grant wake time for ${lead.lane_id}`, { lead_lane: lead.lane_id, story_id: wake.story_id });
|
|
286
481
|
}
|
|
287
|
-
if (
|
|
288
|
-
throw new LanesError("lead_autopilot_status_activity_unverifiable", `Cannot verify whether lead ${lead.lane_id} acted after its fill-grant wake`, { lead_lane: lead.lane_id, story_id: wake.story_id });
|
|
289
|
-
}
|
|
290
|
-
if (Date.parse(statusUpdatedAt) > Date.parse(prior.status_updated_at)) {
|
|
291
|
-
state.sent[key] = { ...prior, acted: true };
|
|
292
|
-
stateChanged = true;
|
|
293
|
-
}
|
|
294
|
-
else if (now.getTime() - wakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
|
|
482
|
+
if (now.getTime() - firstWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
|
|
295
483
|
const story = stories.find((candidate) => candidate.id === wake.story_id && candidate.status === "todo");
|
|
296
484
|
if (!story?.briefPath) {
|
|
297
485
|
throw new LanesError("lead_autopilot_story_brief_missing", `Cannot auto-start story ${wake.story_id}: its project record has no valid brief path`, { lead_lane: lead.lane_id, story_id: wake.story_id, project_id: lead.project_id });
|
|
298
486
|
}
|
|
299
487
|
if (lead.state !== "running" && lead.state !== "queued") {
|
|
300
|
-
|
|
488
|
+
current = { ...current, acted: true };
|
|
489
|
+
state.sent[key] = current;
|
|
301
490
|
stateChanged = true;
|
|
302
491
|
result.skipped.push({ lead_lane: lead.lane_id, reason: "auto_start_lead_not_active" });
|
|
303
492
|
}
|
|
304
493
|
else {
|
|
305
494
|
const started = await input.startWorker({ lead, story });
|
|
306
|
-
|
|
495
|
+
current = { ...current, auto_started: true, worker_lane: started.lane_id };
|
|
496
|
+
state.sent[key] = current;
|
|
307
497
|
stateChanged = true;
|
|
308
498
|
result.started.push({ lead_lane: lead.lane_id, story_id: story.id, worker_lane: started.lane_id });
|
|
309
499
|
}
|
|
310
500
|
}
|
|
311
501
|
}
|
|
502
|
+
if (!current.acted && !current.auto_started && current.escalated_at === undefined && now.getTime() - lastWakeAt >= LEAD_AUTOPILOT_FILL_DELAY_MS) {
|
|
503
|
+
const wakeCount = current.wake_count ?? 1;
|
|
504
|
+
if (wakeCount < 2) {
|
|
505
|
+
appendDropboxMessage(hqRoot, lead.lane_id, {
|
|
506
|
+
at: now.toISOString(),
|
|
507
|
+
message_id: `lead-autopilot:${wake.fingerprint}:wake-2`,
|
|
508
|
+
kind: "system_notice",
|
|
509
|
+
text: wake.prompt,
|
|
510
|
+
});
|
|
511
|
+
current = { ...current, wake_count: 2, last_wake_at: now.toISOString() };
|
|
512
|
+
state.sent[key] = current;
|
|
513
|
+
stateChanged = true;
|
|
514
|
+
result.sent.push(wake);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
const escalation = { team: registered.team, lead_lane: lead.lane_id, condition: wake.condition, story_id: wake.story_id, at: now.toISOString(), fingerprint: wake.fingerprint };
|
|
518
|
+
recordOperatorEscalation(hqRoot, escalation);
|
|
519
|
+
current = { ...current, escalated_at: now.toISOString() };
|
|
520
|
+
state.sent[key] = current;
|
|
521
|
+
stateChanged = true;
|
|
522
|
+
result.escalated.push(escalation);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
312
525
|
continue;
|
|
313
526
|
}
|
|
314
527
|
if (!input.dryRun) {
|
|
@@ -322,6 +535,8 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
322
535
|
fingerprint: wake.fingerprint,
|
|
323
536
|
at: now.toISOString(),
|
|
324
537
|
...(statusUpdatedAt ? { status_updated_at: statusUpdatedAt } : {}),
|
|
538
|
+
wake_count: 1,
|
|
539
|
+
last_wake_at: now.toISOString(),
|
|
325
540
|
};
|
|
326
541
|
stateChanged = true;
|
|
327
542
|
}
|
|
@@ -332,6 +547,17 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
332
547
|
}
|
|
333
548
|
return result;
|
|
334
549
|
}
|
|
550
|
+
const LEAD_AUTOPILOT_STALE_LOCK_MAX_AGE_MS = 60 * 60_000;
|
|
551
|
+
function processStartIdentity(pid) {
|
|
552
|
+
try {
|
|
553
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
554
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
555
|
+
return fields[19] ?? null;
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
335
561
|
function reclaimStaleLock(lockPath) {
|
|
336
562
|
let before;
|
|
337
563
|
let text;
|
|
@@ -348,17 +574,34 @@ function reclaimStaleLock(lockPath) {
|
|
|
348
574
|
}
|
|
349
575
|
if (!before.isFile() || before.isSymbolicLink() || before.dev !== after.dev || before.ino !== after.ino)
|
|
350
576
|
return false;
|
|
351
|
-
const
|
|
577
|
+
const [pidText, createdAt, recordedStartIdentity] = text.split("\n");
|
|
578
|
+
const pid = Number(pidText);
|
|
352
579
|
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
353
580
|
return false;
|
|
581
|
+
let dead = false;
|
|
354
582
|
try {
|
|
355
583
|
process.kill(pid, 0);
|
|
356
|
-
|
|
584
|
+
const currentStartIdentity = processStartIdentity(pid);
|
|
585
|
+
if (recordedStartIdentity && currentStartIdentity) {
|
|
586
|
+
if (recordedStartIdentity !== currentStartIdentity)
|
|
587
|
+
dead = true;
|
|
588
|
+
else
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
const createdMs = Date.parse(createdAt ?? "");
|
|
593
|
+
if (!Number.isFinite(createdMs) || Date.now() - createdMs < LEAD_AUTOPILOT_STALE_LOCK_MAX_AGE_MS)
|
|
594
|
+
return false;
|
|
595
|
+
dead = true;
|
|
596
|
+
}
|
|
357
597
|
}
|
|
358
598
|
catch (error) {
|
|
359
599
|
if (error.code !== "ESRCH")
|
|
360
600
|
return false;
|
|
601
|
+
dead = true;
|
|
361
602
|
}
|
|
603
|
+
if (!dead)
|
|
604
|
+
return false;
|
|
362
605
|
try {
|
|
363
606
|
const current = fs.lstatSync(lockPath);
|
|
364
607
|
if (current.dev !== before.dev || current.ino !== before.ino)
|
|
@@ -381,7 +624,7 @@ export async function runLeadAutopilotPass(input) {
|
|
|
381
624
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
382
625
|
try {
|
|
383
626
|
lockFd = fs.openSync(lockPath, "wx", 0o600);
|
|
384
|
-
fs.writeFileSync(lockFd, `${process.pid}\n${input.now.toISOString()}\n`, "utf8");
|
|
627
|
+
fs.writeFileSync(lockFd, `${process.pid}\n${input.now.toISOString()}\n${processStartIdentity(process.pid) ?? ""}\n`, "utf8");
|
|
385
628
|
break;
|
|
386
629
|
}
|
|
387
630
|
catch (error) {
|
|
@@ -13,7 +13,7 @@ export interface HireGrantAdmissionSnapshot {
|
|
|
13
13
|
export declare function terminalChildResumeHireGrantError(current: LaneRecord, snapshot: HireGrantAdmissionSnapshot): LanesError | null;
|
|
14
14
|
export declare function hireGrantCapacityError(parent: LaneHireGrantRow, children: readonly LaneHireGrantRow[]): LanesError;
|
|
15
15
|
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
16
|
-
export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T, suppliedHierarchy?: readonly LaneHireGrantRow[]): T;
|
|
16
|
+
export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T, suppliedHierarchy?: readonly LaneHireGrantRow[], preferSuppliedLaneIds?: ReadonlySet<string>): T;
|
|
17
17
|
export interface HireGrantBackfillRow {
|
|
18
18
|
lane_id: string;
|
|
19
19
|
existing_grant: number | null;
|
package/dist/lib/lanes/grants.js
CHANGED
|
@@ -72,7 +72,7 @@ export function hireGrantCapacityError(parent, children) {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
75
|
-
export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarchy) {
|
|
75
|
+
export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarchy, preferSuppliedLaneIds = new Set()) {
|
|
76
76
|
return withLaneHireLocks(hqRoot, [parentLaneId], () => {
|
|
77
77
|
const ownerLanes = listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
|
|
78
78
|
let allLanes;
|
|
@@ -83,10 +83,12 @@ export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarc
|
|
|
83
83
|
}
|
|
84
84
|
else {
|
|
85
85
|
const byId = new Map(suppliedHierarchy.map((lane) => [lane.lane_id, lane]));
|
|
86
|
-
// The owner copy is authoritative for every lane it stores
|
|
87
|
-
//
|
|
88
|
-
for (const lane of ownerLanes)
|
|
89
|
-
|
|
86
|
+
// The owner copy is authoritative for every lane it stores unless the
|
|
87
|
+
// caller supplied a freshly verified owner snapshot for that lane.
|
|
88
|
+
for (const lane of ownerLanes) {
|
|
89
|
+
if (!preferSuppliedLaneIds.has(lane.lane_id))
|
|
90
|
+
byId.set(lane.lane_id, lane);
|
|
91
|
+
}
|
|
90
92
|
allLanes = [...byId.values()];
|
|
91
93
|
const suppliedParent = byId.get(parentLaneId);
|
|
92
94
|
if (!suppliedParent) {
|
|
@@ -10,7 +10,7 @@ export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
|
|
10
10
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
11
11
|
export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
|
|
12
12
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
13
|
-
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, LaneSettingsHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
13
|
+
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionPriority, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, LaneSettingsHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
14
14
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
15
15
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
16
16
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { LaneRecord, ManagerStatusGateHistoryEntry, QuestionRecord } from "./types.js";
|
|
2
2
|
export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
3
3
|
export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
4
|
+
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
|
|
5
|
+
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
|
|
6
|
+
export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS: number;
|
|
4
7
|
/** Missing or unreadable registry values retain the legacy supersession behavior. */
|
|
5
8
|
export declare const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
|
|
6
9
|
export declare const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
|
|
@@ -20,10 +23,12 @@ export interface LaneTerminalResumeResult {
|
|
|
20
23
|
export declare function queueLaneTerminalResume(current: LaneRecord, at: string, resumeText: string, options?: {
|
|
21
24
|
allowResume?: boolean;
|
|
22
25
|
mergeExisting?: boolean;
|
|
26
|
+
allowBlockedResume?: boolean;
|
|
23
27
|
}): LaneTerminalResumeResult;
|
|
24
28
|
/** Persist a manager status miss and queue its one bounded follow-up turn. */
|
|
25
29
|
export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string): LaneRecord;
|
|
26
30
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
31
|
+
export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT = "You ended blocked with no pending question. Record the event you wait on in waiting_on, re-check each blocker, and continue the next unblocked story.";
|
|
27
32
|
export declare const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
28
33
|
export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
|
|
29
34
|
/** The latest terminal envelope fields used by both watch and reconcile. */
|
|
@@ -5,6 +5,9 @@ import { dropboxInboxHasMessages } from "./dropbox.js";
|
|
|
5
5
|
import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } from "./store.js";
|
|
6
6
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
7
7
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
8
|
+
export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG = "lanes.keep-alive-resume-blocked";
|
|
9
|
+
export const LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_DEFAULT_ENABLED = false;
|
|
10
|
+
export const LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS = 15 * 60 * 1000;
|
|
8
11
|
/** Missing or unreadable registry values retain the legacy supersession behavior. */
|
|
9
12
|
export const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
|
|
10
13
|
export const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
|
|
@@ -28,16 +31,17 @@ export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
|
|
|
28
31
|
resumeQueued: true,
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
|
-
if (current.state === "blocked")
|
|
34
|
+
if (current.state === "blocked" && options.allowBlockedResume !== true)
|
|
32
35
|
return { lane: current, resumeQueued: false, resumeSkippedReason: "blocked" };
|
|
33
36
|
if (current.state === "awaiting_input")
|
|
34
37
|
return { lane: current, resumeQueued: false, resumeSkippedReason: "open_question" };
|
|
35
38
|
if (options.allowResume === false)
|
|
36
39
|
return { lane: current, resumeQueued: false, resumeSkippedReason: "miss_limit" };
|
|
37
|
-
if (current.state !== "done" && current.state !== "interrupted" && current.state !== "failed") {
|
|
40
|
+
if (current.state !== "done" && current.state !== "interrupted" && current.state !== "failed" && !(current.state === "blocked" && options.allowBlockedResume === true)) {
|
|
38
41
|
return { lane: current, resumeQueued: false };
|
|
39
42
|
}
|
|
40
|
-
|
|
43
|
+
const sessionId = current.provider_session_id ?? current.last_resume?.session_id;
|
|
44
|
+
if (!sessionId) {
|
|
41
45
|
return { lane: current, resumeQueued: false, resumeSkippedReason: "no_provider_session" };
|
|
42
46
|
}
|
|
43
47
|
return {
|
|
@@ -51,7 +55,7 @@ export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
|
|
|
51
55
|
terminal_reason: undefined,
|
|
52
56
|
terminal_details: undefined,
|
|
53
57
|
pending_resume: {
|
|
54
|
-
session_id:
|
|
58
|
+
session_id: sessionId,
|
|
55
59
|
trigger: "resume",
|
|
56
60
|
resumed_from: current.state,
|
|
57
61
|
text: resumeText,
|
|
@@ -92,6 +96,7 @@ export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText)
|
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
99
|
+
export const LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT = "You ended blocked with no pending question. Record the event you wait on in waiting_on, re-check each blocker, and continue the next unblocked story.";
|
|
95
100
|
export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
96
101
|
/** Shared done-state policy used by watch, reconcile, and the show command. */
|
|
97
102
|
export function doneKeepAliveVerdict(lane, envelope, hasQueuedWork) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* path and for envelope `decision: "ask"` ingestion. Two writers here would
|
|
4
4
|
* drift on hop, origin, raised_via, body store, and routing_history.
|
|
5
5
|
*/
|
|
6
|
-
import { type LaneEnvelope, type LaneRecord, type LaneCompanyRef, type QuestionBodyStore, type QuestionKind, type QuestionRecord, type RaisedVia } from "./types.js";
|
|
6
|
+
import { type LaneEnvelope, type LaneRecord, type LaneCompanyRef, type QuestionBodyStore, type QuestionKind, type QuestionPriority, type QuestionRecord, type RaisedVia } from "./types.js";
|
|
7
7
|
export interface QuestionBody {
|
|
8
8
|
question: string;
|
|
9
9
|
context: string;
|
|
@@ -42,6 +42,7 @@ export interface RaiseLaneQuestionInput {
|
|
|
42
42
|
at: string;
|
|
43
43
|
activeCompany?: LaneCompanyRef | null;
|
|
44
44
|
questionId?: string;
|
|
45
|
+
priority?: QuestionPriority;
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
47
48
|
* Persist one question the way `hq lanes ask` does: createQuestion, write the
|
|
@@ -172,6 +172,7 @@ export function raiseLaneQuestion(input) {
|
|
|
172
172
|
story_id: input.lane.story_id,
|
|
173
173
|
thread_id: input.lane.thread_id,
|
|
174
174
|
raised_via: input.raisedVia,
|
|
175
|
+
...(input.priority ? { priority: input.priority } : {}),
|
|
175
176
|
asked_at: input.at,
|
|
176
177
|
origin: { kind: "lane", id: input.lane.lane_id },
|
|
177
178
|
hop: 1,
|
|
@@ -191,6 +192,7 @@ export function raiseLaneQuestion(input) {
|
|
|
191
192
|
story_id: input.lane.story_id,
|
|
192
193
|
thread_id: input.lane.thread_id,
|
|
193
194
|
raised_via: input.raisedVia,
|
|
195
|
+
...(input.priority ? { priority: input.priority } : {}),
|
|
194
196
|
asked_at: input.at,
|
|
195
197
|
origin: { kind: "lane", id: input.lane.lane_id },
|
|
196
198
|
hop: 1,
|