@indigoai-us/hq-cli 5.293.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
  3. package/dist/command-catalog.generated.d.ts +42 -24
  4. package/dist/command-catalog.generated.js +53 -30
  5. package/dist/commands/core-command-catalog.js +3 -2
  6. package/dist/commands/core-delegate-w1b.d.ts +2 -0
  7. package/dist/commands/core-delegate-w1b.js +28 -2
  8. package/dist/commands/core-delegate.d.ts +3 -1
  9. package/dist/commands/core-delegate.js +35 -5
  10. package/dist/commands/core-handoff-fast.d.ts +18 -0
  11. package/dist/commands/core-handoff-fast.js +103 -0
  12. package/dist/commands/core-handoff-open-steps.d.ts +21 -0
  13. package/dist/commands/core-handoff-open-steps.js +264 -0
  14. package/dist/commands/core-handoff.d.ts +4 -2
  15. package/dist/commands/core-handoff.js +22 -249
  16. package/dist/commands/core-jobs-native.d.ts +2 -0
  17. package/dist/commands/core-jobs-native.js +22 -11
  18. package/dist/commands/core-knowledge-prefs.d.ts +2 -0
  19. package/dist/commands/core-knowledge-prefs.js +13 -1
  20. package/dist/commands/core-ontology-native.d.ts +2 -0
  21. package/dist/commands/core-ontology-native.js +22 -2
  22. package/dist/commands/core-policy.d.ts +2 -0
  23. package/dist/commands/core-policy.js +18 -1
  24. package/dist/commands/core.js +2 -1
  25. package/dist/commands/daemon.d.ts +2 -1
  26. package/dist/commands/daemon.js +22 -7
  27. package/dist/commands/lanes-autopilot.d.ts +11 -0
  28. package/dist/commands/lanes-autopilot.js +336 -0
  29. package/dist/commands/lanes-questions.js +8 -5
  30. package/dist/commands/lanes-shared.d.ts +17 -0
  31. package/dist/commands/lanes-stories.d.ts +14 -0
  32. package/dist/commands/lanes-stories.js +308 -0
  33. package/dist/commands/lanes.js +39 -309
  34. package/dist/fast-core.js +53 -25
  35. package/dist/fast-scaffold.d.ts +15 -2
  36. package/dist/fast-scaffold.js +167 -7
  37. package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
  38. package/dist/lib/core-utils/worktree-resume.js +16 -0
  39. package/dist/lib/daemon/migrate.d.ts +8 -1
  40. package/dist/lib/daemon/migrate.js +19 -7
  41. package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
  42. package/dist/lib/lanes/autopilot-metrics.js +301 -0
  43. package/dist/lib/lanes/store.d.ts +2 -0
  44. package/dist/lib/lanes/store.js +32 -3
  45. package/dist/native-fast-route.d.ts +3 -1
  46. package/dist/native-fast-route.js +10 -8
  47. package/dist/utils/contribution-table.d.ts +1 -1
  48. package/dist/utils/fast-stream-errors.d.ts +12 -0
  49. package/dist/utils/fast-stream-errors.js +18 -0
  50. package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
  51. package/dist/utils/scaffold-fast-import-guard.js +6 -3
  52. package/package.json +1 -1
@@ -0,0 +1,336 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { laneRunsHere } from "../lib/lanes/reconcile.js";
4
+ import { listLaneReviews } from "../lib/lanes/reviews.js";
5
+ import { readAllLanes } from "../lib/lanes/store.js";
6
+ import { LANES_RUNS_REL } from "../lib/lanes/spawn.js";
7
+ import { normalizeIsoTimestamp } from "../lib/lanes/timestamp.js";
8
+ import { buildAutopilotMetricsReport, } from "../lib/lanes/autopilot-metrics.js";
9
+ import { LanesError } from "../lib/lanes/errors.js";
10
+ const OPERATOR_NUDGE_DEFECTS_REL = path.join("workspace", "lane-briefs", "operator-nudge-defects.jsonl");
11
+ const JUNIORS_REL = path.join("workspace", "lane-briefs", "juniors.json");
12
+ const DROPBOX_NAME = "dropbox";
13
+ const RESUME_SUFFIX = ".resume.jsonl";
14
+ function fail(error, message, details = {}) {
15
+ throw new LanesError(error, message, details);
16
+ }
17
+ function isObject(value) {
18
+ return value !== null && typeof value === "object" && !Array.isArray(value);
19
+ }
20
+ function safeReadText(root, file, missingOk = false) {
21
+ let stat;
22
+ try {
23
+ stat = fs.lstatSync(file);
24
+ }
25
+ catch (err) {
26
+ if (missingOk && err.code === "ENOENT")
27
+ return null;
28
+ return fail("autopilot_metrics_input_unreadable", `Could not inspect report input: ${err instanceof Error ? err.message : String(err)}`, { path: file });
29
+ }
30
+ if (!stat.isFile() || stat.isSymbolicLink()) {
31
+ return fail("autopilot_metrics_input_invalid", "Report input must be a regular file", { path: file });
32
+ }
33
+ let realRoot;
34
+ let realFile;
35
+ try {
36
+ realRoot = fs.realpathSync(root);
37
+ realFile = fs.realpathSync(file);
38
+ }
39
+ catch (err) {
40
+ return fail("autopilot_metrics_input_unreadable", `Could not resolve report input: ${err instanceof Error ? err.message : String(err)}`, { path: file });
41
+ }
42
+ if (!realFile.startsWith(`${realRoot}${path.sep}`)) {
43
+ return fail("autopilot_metrics_input_invalid", "Report input must stay under the HQ root", { path: file });
44
+ }
45
+ try {
46
+ return fs.readFileSync(file, "utf8");
47
+ }
48
+ catch (err) {
49
+ return fail("autopilot_metrics_input_unreadable", `Could not read report input: ${err instanceof Error ? err.message : String(err)}`, { path: file });
50
+ }
51
+ }
52
+ function parseJsonObject(root, file) {
53
+ const raw = safeReadText(root, file);
54
+ try {
55
+ const parsed = JSON.parse(raw ?? "");
56
+ if (isObject(parsed))
57
+ return parsed;
58
+ }
59
+ catch (err) {
60
+ return fail("autopilot_metrics_input_invalid", `Report input is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, { path: file });
61
+ }
62
+ return fail("autopilot_metrics_input_invalid", "Report input must be a JSON object", { path: file });
63
+ }
64
+ function isCurrentHostLabel(label, host) {
65
+ return label === "local" ? host.outpostId === null : label === host.outpostId || label === host.hostname;
66
+ }
67
+ function loadJuniors(root, host) {
68
+ const file = path.join(root, JUNIORS_REL);
69
+ const parsed = parseJsonObject(root, file);
70
+ if (!Array.isArray(parsed.juniors))
71
+ return fail("autopilot_metrics_input_invalid", "Team registry must contain a juniors array", { path: file });
72
+ const seen = new Set();
73
+ return parsed.juniors.flatMap((value, index) => {
74
+ if (!isObject(value) || typeof value.host !== "string" || !value.host.trim()) {
75
+ return fail("autopilot_metrics_input_invalid", "Team registry row must identify its host", { path: file, index });
76
+ }
77
+ if (!isCurrentHostLabel(value.host, host))
78
+ return [];
79
+ if (["team", "lane", "status", "prd"].some((field) => typeof value[field] !== "string" || !value[field].trim())) {
80
+ return fail("autopilot_metrics_input_invalid", "Selected team registry row must contain team, lane, status, and prd strings", { path: file, index });
81
+ }
82
+ const entry = value;
83
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(entry.team) || entry.lane.includes("/") || entry.lane.includes("\\")) {
84
+ return fail("autopilot_metrics_input_invalid", "Team registry row contains an unsafe team or lane id", { path: file, index });
85
+ }
86
+ if (seen.has(entry.lane))
87
+ return fail("autopilot_metrics_mapping_ambiguous", "A lead lane appears more than once in the selected host registry", { path: file, lane_id: entry.lane });
88
+ seen.add(entry.lane);
89
+ return [entry];
90
+ });
91
+ }
92
+ function projectPath(root, raw, registryPath) {
93
+ if (path.isAbsolute(raw))
94
+ return fail("autopilot_metrics_mapping_invalid", "Project path must be relative to the HQ root", { path: registryPath, project_path: raw });
95
+ const absolute = path.resolve(root, raw);
96
+ if (!absolute.startsWith(`${path.resolve(root)}${path.sep}`)) {
97
+ return fail("autopilot_metrics_mapping_invalid", "Project path must stay under the HQ root", { path: registryPath, project_path: raw });
98
+ }
99
+ return absolute;
100
+ }
101
+ function loadProject(root, projectId, rawPrd) {
102
+ const registryPath = path.join(root, JUNIORS_REL);
103
+ const file = projectPath(root, rawPrd, registryPath);
104
+ const parsed = parseJsonObject(root, file);
105
+ const storiesValue = parsed.userStories === undefined ? parsed.stories : parsed.userStories;
106
+ if (!Array.isArray(storiesValue))
107
+ return fail("autopilot_metrics_input_invalid", "Project must contain a userStories or stories array", { path: file });
108
+ const stories = storiesValue.map((value, index) => {
109
+ if (!isObject(value) || typeof value.id !== "string" || !value.id.trim()) {
110
+ return fail("autopilot_metrics_input_invalid", "Project story must have a non-empty id", { path: file, index });
111
+ }
112
+ return value;
113
+ });
114
+ return { project_id: projectId, stories };
115
+ }
116
+ function parseIso(value, field, file) {
117
+ const normalized = normalizeIsoTimestamp(value);
118
+ if (normalized !== null)
119
+ return normalized;
120
+ if (typeof value === "string") {
121
+ const minutePrecision = /^(\d{4}-\d\d-\d\dT\d\d:\d\d)(Z|[+-]\d\d:\d\d)$/.exec(value);
122
+ if (minutePrecision) {
123
+ const withSeconds = normalizeIsoTimestamp(`${minutePrecision[1]}:00${minutePrecision[2]}`);
124
+ if (withSeconds !== null)
125
+ return withSeconds;
126
+ }
127
+ }
128
+ return fail("autopilot_metrics_input_invalid", `${field} must be an ISO timestamp`, { path: file, field });
129
+ }
130
+ function readJsonl(root, file, missingOk = false) {
131
+ const text = safeReadText(root, file, missingOk);
132
+ if (text === null)
133
+ return [];
134
+ const records = [];
135
+ for (const [index, line] of text.split("\n").entries()) {
136
+ if (!line.trim())
137
+ continue;
138
+ let parsed;
139
+ try {
140
+ parsed = JSON.parse(line);
141
+ }
142
+ catch (err) {
143
+ return fail("autopilot_metrics_input_invalid", `Report JSONL contains malformed JSON: ${err instanceof Error ? err.message : String(err)}`, { path: file, line: index + 1 });
144
+ }
145
+ if (!isObject(parsed))
146
+ return fail("autopilot_metrics_input_invalid", "Report JSONL rows must be objects", { path: file, line: index + 1 });
147
+ records.push(parsed);
148
+ }
149
+ return records;
150
+ }
151
+ function listRegularFiles(root, directory, output, depth = 0) {
152
+ let names;
153
+ try {
154
+ const stat = fs.lstatSync(directory);
155
+ if (stat.isSymbolicLink() || !stat.isDirectory())
156
+ return fail("autopilot_metrics_input_invalid", "Report path must be a regular directory", { path: directory });
157
+ const realRoot = fs.realpathSync(root);
158
+ const realDirectory = fs.realpathSync(directory);
159
+ if (!realDirectory.startsWith(`${realRoot}${path.sep}`))
160
+ return fail("autopilot_metrics_input_invalid", "Report directory must stay under the HQ root", { path: directory });
161
+ names = fs.readdirSync(directory);
162
+ }
163
+ catch (err) {
164
+ if (err.code === "ENOENT")
165
+ return;
166
+ if (err instanceof LanesError)
167
+ throw err;
168
+ return fail("autopilot_metrics_input_unreadable", `Could not list report directory: ${err instanceof Error ? err.message : String(err)}`, { path: directory });
169
+ }
170
+ for (const name of names) {
171
+ if (!name || name.startsWith("."))
172
+ continue;
173
+ const candidate = path.join(directory, name);
174
+ let stat;
175
+ try {
176
+ stat = fs.lstatSync(candidate);
177
+ }
178
+ catch (err) {
179
+ return fail("autopilot_metrics_input_unreadable", `Could not inspect report file: ${err instanceof Error ? err.message : String(err)}`, { path: candidate });
180
+ }
181
+ if (stat.isSymbolicLink())
182
+ return fail("autopilot_metrics_input_invalid", "Report directory may not contain symbolic links", { path: candidate });
183
+ if (stat.isFile() && name.endsWith(".jsonl"))
184
+ output.push(candidate);
185
+ else if (stat.isDirectory() && depth < 2)
186
+ listRegularFiles(root, candidate, output, depth + 1);
187
+ }
188
+ }
189
+ function readLeadMessages(root, laneId) {
190
+ const dropbox = path.join(root, LANES_RUNS_REL, laneId, DROPBOX_NAME);
191
+ const allFiles = [];
192
+ for (const name of ["draining", "inflight", "delivered", "undeliverable"])
193
+ listRegularFiles(root, path.join(dropbox, name), allFiles);
194
+ const inbox = path.join(dropbox, "inbox.jsonl");
195
+ try {
196
+ const stat = fs.lstatSync(inbox);
197
+ if (stat.isSymbolicLink() || !stat.isFile())
198
+ return fail("autopilot_metrics_input_invalid", "Lead inbox must be a regular file", { path: inbox });
199
+ allFiles.push(inbox);
200
+ }
201
+ catch (err) {
202
+ if (err.code !== "ENOENT")
203
+ throw err;
204
+ }
205
+ const seen = new Set();
206
+ const nudges = [];
207
+ const resumes = [];
208
+ for (const file of new Set(allFiles)) {
209
+ const resumeFile = file.endsWith(RESUME_SUFFIX);
210
+ for (const row of readJsonl(root, file)) {
211
+ // Undated records cannot be assigned to a report day, regardless of their file.
212
+ if (typeof row.at !== "string")
213
+ continue;
214
+ const at = parseIso(row.at, "at", file);
215
+ const text = typeof row.text === "string" ? row.text : "";
216
+ const messageId = typeof row.message_id === "string" ? row.message_id : `${at}\u0000${text}`;
217
+ if (!seen.has(messageId)) {
218
+ seen.add(messageId);
219
+ if (resumeFile || row.kind !== "monitor_event")
220
+ nudges.push({ team: "", at });
221
+ }
222
+ if (resumeFile)
223
+ resumes.push({ lane_id: laneId, at });
224
+ }
225
+ }
226
+ return { nudges, resumes };
227
+ }
228
+ function loadNudgeDefects(root, selectedTeams) {
229
+ const file = path.join(root, OPERATOR_NUDGE_DEFECTS_REL);
230
+ return readJsonl(root, file, true).flatMap((row, index) => {
231
+ if (typeof row.team === "string" && !selectedTeams.has(row.team))
232
+ return [];
233
+ const defect = typeof row.defect === "string" && row.defect.trim() ? row.defect : row.why;
234
+ if (typeof row.team !== "string" || !row.team.trim() || typeof defect !== "string" || !defect.trim()) {
235
+ return fail("autopilot_metrics_input_invalid", "Nudge-defect row must contain at, team, and defect (or legacy why)", { path: file, line: index + 1 });
236
+ }
237
+ return [{ at: parseIso(row.at, "at", file), team: row.team, defect }];
238
+ });
239
+ }
240
+ function loadReviews(root) {
241
+ const listing = listLaneReviews(root);
242
+ if (listing.skipped > 0)
243
+ return fail("autopilot_metrics_review_record_invalid", "One or more lane review records could not be read", { skipped: listing.skipped });
244
+ return listing.reviews.flatMap((review) => review.history.map((entry) => ({
245
+ repo: entry.repo,
246
+ pr: entry.pr,
247
+ recorded_at: entry.recorded_at,
248
+ worker_lane: entry.worker_lane,
249
+ })));
250
+ }
251
+ function selectLeads(root, host) {
252
+ const juniors = loadJuniors(root, host);
253
+ const lanes = readAllLanes(root);
254
+ const leads = [];
255
+ const projects = [];
256
+ const laneIds = new Set();
257
+ const projectIds = new Set();
258
+ for (const entry of juniors) {
259
+ const matchingLanes = lanes.filter((candidate) => candidate.lane_id === entry.lane || candidate.lane_id.startsWith(`${entry.lane}_`));
260
+ const lane = matchingLanes.find((candidate) => laneRunsHere(candidate, host));
261
+ if (!lane) {
262
+ return fail("autopilot_metrics_lead_missing", "Registered lead lane could not be read on this host", { team: entry.team, lane_id: entry.lane });
263
+ }
264
+ if (laneIds.has(lane.lane_id))
265
+ return fail("autopilot_metrics_mapping_ambiguous", "A lead lane appears more than once in the host report", { lane_id: lane.lane_id });
266
+ const projectKey = JSON.stringify([lane.project_id, projectPath(root, entry.prd, path.join(root, JUNIORS_REL))]);
267
+ const lead = { team: entry.team, lane_id: lane.lane_id, project_id: projectKey };
268
+ leads.push(lead);
269
+ laneIds.add(lane.lane_id);
270
+ if (!projectIds.has(projectKey)) {
271
+ projects.push(loadProject(root, projectKey, entry.prd));
272
+ projectIds.add(projectKey);
273
+ }
274
+ }
275
+ return { leads, projects, laneIds };
276
+ }
277
+ function windowFromOptions(now, options) {
278
+ const until = normalizeIsoTimestamp(now.toISOString());
279
+ if (until === null)
280
+ return fail("autopilot_metrics_window_invalid", "Current time is invalid");
281
+ if (options.since !== undefined && options.days !== undefined)
282
+ return fail("autopilot_metrics_window_invalid", "Use either --since or --days, not both");
283
+ if (options.since !== undefined) {
284
+ const since = normalizeIsoTimestamp(options.since);
285
+ if (since === null || Date.parse(since) >= Date.parse(until))
286
+ return fail("autopilot_metrics_window_invalid", "--since must be an ISO timestamp before now", { since: options.since });
287
+ return { since, until };
288
+ }
289
+ const days = options.days === undefined ? 3 : Number(options.days);
290
+ if (!Number.isSafeInteger(days) || days < 1 || days > 366)
291
+ return fail("autopilot_metrics_window_invalid", "--days must be an integer from 1 to 366", { days: options.days });
292
+ return { since: new Date(Date.parse(until) - days * 24 * 60 * 60 * 1000).toISOString(), until };
293
+ }
294
+ function printJson(value) {
295
+ console.log(JSON.stringify(value));
296
+ }
297
+ export function registerLaneAutopilotReportCommand(parent, deps, wrap) {
298
+ const autopilot = parent.command("autopilot").description("Measure automated lead-lane follow-through");
299
+ autopilot.command("report")
300
+ .description("Report operator nudges, review latency, and idle slots with ready work")
301
+ .option("--since <iso>", "Include activity since this ISO timestamp")
302
+ .option("--days <n>", "Include the most recent number of days")
303
+ .option("--json", "Print JSON")
304
+ .action(wrap(async (options) => {
305
+ const root = deps.hqRoot();
306
+ const window = windowFromOptions(deps.now(), options);
307
+ const host = deps.currentHost();
308
+ const selected = selectLeads(root, host);
309
+ const lanes = readAllLanes(root);
310
+ const nudges = [];
311
+ const resumes = [];
312
+ for (const lead of selected.leads) {
313
+ const events = readLeadMessages(root, lead.lane_id);
314
+ nudges.push(...events.nudges.map((event) => ({ ...event, team: lead.team })));
315
+ resumes.push(...events.resumes);
316
+ const record = lanes.find((lane) => lane.lane_id === lead.lane_id);
317
+ const lastResume = record?.last_resume?.at;
318
+ if (lastResume && !resumes.some((resume) => resume.lane_id === lead.lane_id && resume.at === lastResume))
319
+ resumes.push({ lane_id: lead.lane_id, at: lastResume });
320
+ }
321
+ const reviewListing = loadReviews(root);
322
+ const defects = loadNudgeDefects(root, new Set(selected.leads.map((lead) => lead.team)));
323
+ const result = buildAutopilotMetricsReport({
324
+ ...window,
325
+ leads: selected.leads,
326
+ lanes: lanes,
327
+ projects: selected.projects,
328
+ reviews: reviewListing,
329
+ nudges,
330
+ nudgeDefects: defects,
331
+ seniorResumes: resumes,
332
+ });
333
+ printJson(result);
334
+ }));
335
+ }
336
+ //# sourceMappingURL=lanes-autopilot.js.map
@@ -20,7 +20,7 @@ export function resolveAnswerChoice(choice, options, freeform) {
20
20
  throw new LanesError("off_menu_choice", `Choice ${JSON.stringify(choice)} is not on the menu. Pass --freeform to allow it.`, { choice, options });
21
21
  }
22
22
  export function createLaneQuestionsHandlers(deps) {
23
- const { CAPACITY_PROBE_TIMEOUT_MS, MAX_PLACEMENT_OUTPOSTS, OUTPOST_QUESTION_LOOKUP_CONCURRENCY, assertLaneRunsHereForOperatorMutation, assertQuestionCompanyMatchesLane, casUpdateQuestion, effectiveLaneQuestionCompany, emitLaneMeshEvent, groupQuestionLanesByCompany, isQuestionAnswer, laneIsHostedHere, livenessDepsFromCtx, loadAllLanes, loadAllQuestions, mapWithConcurrency, mintMessageId, parseOutpostQuestionAudience, parseOutpostQuestionBody, parseRemoteJson, parseSeniorRef, questionCompanyForLane, questionCompanyRefMatches, questionListCompanyArg, readOutpostQuestionRecords, readQuestionBody, printJson, readSessionMeta, reconcileHostFromCtx, resolveActiveQuestionCompany, resolveAudience, sameQuestionAnswer, sameQuestionCompany, seniorOfSession, writeAnswerDeliveryFailed } = deps;
23
+ const { CAPACITY_PROBE_TIMEOUT_MS, MAX_PLACEMENT_OUTPOSTS, OUTPOST_QUESTION_LOOKUP_CONCURRENCY, assertLaneRunsHereForOperatorMutation, assertQuestionAnswerAuthorized, assertQuestionCompanyMatchesLane, casUpdateQuestion, effectiveLaneQuestionCompany, emitLaneMeshEvent, groupQuestionLanesByCompany, isQuestionAnswer, laneIsHostedHere, livenessDepsFromCtx, loadAllLanes, loadAllQuestions, mapWithConcurrency, mintMessageId, parseOutpostQuestionAudience, parseOutpostQuestionBody, parseRemoteJson, parseSeniorRef, questionCompanyForLane, questionCompanyRefMatches, questionListCompanyArg, readOutpostQuestionRecords, readQuestionBody, printJson, readSessionMeta, reconcileHostFromCtx, resolveActiveQuestionCompany, resolveAudience, sameQuestionAnswer, sameQuestionCompany, seniorOfSession, writeAnswerDeliveryFailed } = deps;
24
24
  function requireQuestionBody(hqRoot, question) {
25
25
  const body = readQuestionBody(hqRoot, question);
26
26
  if (!body) {
@@ -73,10 +73,11 @@ export function createLaneQuestionsHandlers(deps) {
73
73
  return;
74
74
  throw new LanesError("question_not_pending", `Question ${current.question_id} is ${current.status}`, { question_id: current.question_id, status: current.status });
75
75
  }
76
- async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
76
+ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options, lookupErrors = []) {
77
77
  if (lane.host.kind !== "outpost") {
78
78
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to answer its question`, { lane_id: lane.lane_id, question_id: questionId });
79
79
  }
80
+ assertQuestionAnswerAuthorized(ctx, lane);
80
81
  const outpostId = lane.host.id;
81
82
  let dispatched;
82
83
  try {
@@ -224,6 +225,7 @@ export function createLaneQuestionsHandlers(deps) {
224
225
  outpost_id: outpostId,
225
226
  outpost_command_id: dispatched.commandId,
226
227
  outpost_transport: "outposts_exec",
228
+ ...(lookupErrors.length > 0 ? { lookup_errors: lookupErrors } : {}),
227
229
  });
228
230
  }
229
231
  async function readOutpostQuestionDetail(ctx, owner) {
@@ -280,6 +282,7 @@ export function createLaneQuestionsHandlers(deps) {
280
282
  }
281
283
  async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
282
284
  const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
285
+ !laneIsHostedHere(ctx, lane) &&
283
286
  (!companyFilter ||
284
287
  effectiveLaneQuestionCompany(lane, hqRoot) === null ||
285
288
  questionCompanyRefMatches(effectiveLaneQuestionCompany(lane, hqRoot), companyFilter)));
@@ -584,13 +587,13 @@ export function createLaneQuestionsHandlers(deps) {
584
587
  throw err;
585
588
  const company = explicitCompany ?? await resolveActiveQuestionCompany(ctx, "Cannot look up this lane question because the active HQ company could not be resolved; re-run with --company <slug>");
586
589
  const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
587
- if (lookup.errors.length > 0) {
590
+ const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
591
+ if (matches.length === 0 && lookup.errors.length > 0) {
588
592
  throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
589
593
  question_id: questionId,
590
594
  hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
591
595
  });
592
596
  }
593
- const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
594
597
  if (matches.length === 0) {
595
598
  const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
596
599
  const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
@@ -620,7 +623,7 @@ export function createLaneQuestionsHandlers(deps) {
620
623
  await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
621
624
  ...opts,
622
625
  company: questionListCompanyArg(ownerCompany),
623
- });
626
+ }, lookup.errors);
624
627
  return;
625
628
  }
626
629
  await yieldEventLoop();
@@ -376,6 +376,7 @@ export interface LaneQuestionsHandlerDeps {
376
376
  MAX_PLACEMENT_OUTPOSTS: number;
377
377
  OUTPOST_QUESTION_LOOKUP_CONCURRENCY: number;
378
378
  assertLaneRunsHereForOperatorMutation(ctx: Ctx, lane: LaneRecord, action: string): void;
379
+ assertQuestionAnswerAuthorized(ctx: Ctx, lane: LaneRecord): void;
379
380
  assertQuestionCompanyMatchesLane(question: QuestionRecord, lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company" | "project_id">, code: string, hqRoot?: string): void;
380
381
  casUpdateQuestion(hqRoot: string, questionId: string, updater: (current: QuestionRecord) => QuestionRecord): QuestionRecord;
381
382
  effectiveLaneQuestionCompany(lane: Pick<LaneRecord, "company" | "mesh_company" | "project_id" | "lane_id">, hqRoot?: string): LaneCompanyRef | null;
@@ -407,4 +408,20 @@ export interface LaneQuestionsHandlerDeps {
407
408
  seniorOfSession(hqRoot: string, sessionId: string, ctx: Ctx): SeniorRef;
408
409
  writeAnswerDeliveryFailed(hqRoot: string, laneId: string, questionId: string, err: unknown, at: string): void;
409
410
  }
411
+ export interface LaneStoryHandlerDeps {
412
+ assertLaneRunsHereForOperatorMutation(ctx: Ctx, lane: LaneRecord, action: string): void;
413
+ casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
414
+ combinedStoryWarning(lane: LaneRecord, additional?: readonly string[]): string | null;
415
+ emitLaneMeshEvent(ctx: Ctx, company: LaneCompanyRef | null | undefined, threadId: string | undefined, event: LaneMeshEvent, payload: Record<string, unknown>): Promise<void>;
416
+ ensureLaneThreadRecord(ctx: Ctx, lane: LaneRecord, projectId?: string, storyId?: string, laneStatus?: string): Promise<LaneRecord>;
417
+ printJson(value: unknown): void;
418
+ readLane(hqRoot: string, laneId: string): LaneRecord;
419
+ storyBoardCheck(ctx: Ctx, company: LaneCompanyRef, projectId: string, storyIds: readonly string[]): Promise<{
420
+ warning?: string;
421
+ absentStoryIds: ReadonlySet<string>;
422
+ }>;
423
+ storyNotOnBoardWarning(err: unknown): string | undefined;
424
+ updateLane(hqRoot: string, laneId: string, expectedVersion: number, source: "command", updater: (current: LaneRecord) => LaneRecord): LaneRecord;
425
+ validateProjectStories(hqRoot: string, projectId: string, storyIds: string[], companySlug?: string): void;
426
+ }
410
427
  //# sourceMappingURL=lanes-shared.d.ts.map
@@ -0,0 +1,14 @@
1
+ import type { Ctx, LaneStoryHandlerDeps } from "./lanes-shared.js";
2
+ export interface StoryCommandOptions {
3
+ project?: string;
4
+ reason?: string;
5
+ verified?: boolean;
6
+ }
7
+ export declare function createLaneStoryHandlers(deps: LaneStoryHandlerDeps): {
8
+ runStoryList: (ctx: Ctx, laneId: string) => Promise<void>;
9
+ runStoryAdd: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
10
+ runStoryStart: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
11
+ runStoryBlock: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
12
+ runStoryDone: (ctx: Ctx, laneId: string, storyId: string, opts: StoryCommandOptions) => Promise<void>;
13
+ };
14
+ //# sourceMappingURL=lanes-stories.d.ts.map