@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.
- package/CHANGELOG.md +14 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
- package/dist/command-catalog.generated.d.ts +42 -24
- package/dist/command-catalog.generated.js +53 -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-questions.js +8 -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.js +39 -309
- 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/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/lanes/autopilot-metrics.d.ts +99 -0
- package/dist/lib/lanes/autopilot-metrics.js +301 -0
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +32 -3
- 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/package.json +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { isTerminalLaneState } from "./types.js";
|
|
2
|
+
const MINUTE_MS = 60_000;
|
|
3
|
+
const CLOSED_STORY_STATES = new Set(["done", "dropped"]);
|
|
4
|
+
function timestamp(value) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return null;
|
|
7
|
+
const parsed = Date.parse(value);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
9
|
+
}
|
|
10
|
+
function round(value) {
|
|
11
|
+
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
12
|
+
}
|
|
13
|
+
function dayKey(at) {
|
|
14
|
+
return new Date(at).toISOString().slice(0, 10);
|
|
15
|
+
}
|
|
16
|
+
function utcDays(since, until) {
|
|
17
|
+
const days = [];
|
|
18
|
+
let current = Date.UTC(new Date(since).getUTCFullYear(), new Date(since).getUTCMonth(), new Date(since).getUTCDate());
|
|
19
|
+
const last = Date.UTC(new Date(until).getUTCFullYear(), new Date(until).getUTCMonth(), new Date(until).getUTCDate());
|
|
20
|
+
while (current <= last) {
|
|
21
|
+
days.push(new Date(current).toISOString().slice(0, 10));
|
|
22
|
+
current += 24 * 60 * MINUTE_MS;
|
|
23
|
+
}
|
|
24
|
+
return days;
|
|
25
|
+
}
|
|
26
|
+
function storyState(story) {
|
|
27
|
+
return typeof story.state === "string" ? story.state : typeof story.status === "string" ? story.status : undefined;
|
|
28
|
+
}
|
|
29
|
+
function storyPrReferences(story) {
|
|
30
|
+
const refs = [...(story.prs ?? [])];
|
|
31
|
+
if (!Array.isArray(story.lanes))
|
|
32
|
+
return refs;
|
|
33
|
+
for (const entry of story.lanes) {
|
|
34
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
35
|
+
continue;
|
|
36
|
+
const record = entry;
|
|
37
|
+
if (Array.isArray(record.prs))
|
|
38
|
+
refs.push(...record.prs);
|
|
39
|
+
if (Array.isArray(record.history)) {
|
|
40
|
+
for (const historyEntry of record.history) {
|
|
41
|
+
if (historyEntry && typeof historyEntry === "object" && !Array.isArray(historyEntry)) {
|
|
42
|
+
const prs = historyEntry.prs;
|
|
43
|
+
if (Array.isArray(prs))
|
|
44
|
+
refs.push(...prs);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return refs;
|
|
50
|
+
}
|
|
51
|
+
function prReferenceMatches(value, review) {
|
|
52
|
+
if (typeof value === "number")
|
|
53
|
+
return value === review.pr;
|
|
54
|
+
if (typeof value !== "string") {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
56
|
+
return false;
|
|
57
|
+
const record = value;
|
|
58
|
+
if (record.number === review.pr)
|
|
59
|
+
return true;
|
|
60
|
+
return [record.url, record.html_url, record.ref].some((ref) => prReferenceMatches(ref, review));
|
|
61
|
+
}
|
|
62
|
+
const text = value.trim();
|
|
63
|
+
if (text === `#${review.pr}` || text === String(review.pr))
|
|
64
|
+
return true;
|
|
65
|
+
const pull = text.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:\/|$)/i);
|
|
66
|
+
return pull !== null && pull[1]?.toLowerCase() === review.repo.toLowerCase() && Number(pull[2]) === review.pr;
|
|
67
|
+
}
|
|
68
|
+
function reviewMatchesLane(review, lane, story) {
|
|
69
|
+
if (review.worker_lane !== undefined && review.worker_lane !== null)
|
|
70
|
+
return review.worker_lane === lane.lane_id;
|
|
71
|
+
return [...(story ? storyPrReferences(story) : []), ...(lane.last_envelope?.artifacts ?? [])]
|
|
72
|
+
.some((ref) => prReferenceMatches(ref, review));
|
|
73
|
+
}
|
|
74
|
+
function matchingStory(project, storyId) {
|
|
75
|
+
return project?.stories.find((story) => story.id === storyId);
|
|
76
|
+
}
|
|
77
|
+
function stateAtHistoryEntry(entry) {
|
|
78
|
+
return typeof entry.state === "string" ? entry.state : undefined;
|
|
79
|
+
}
|
|
80
|
+
function storyLaneHistory(story, laneId) {
|
|
81
|
+
if (!story || !Array.isArray(story.lanes))
|
|
82
|
+
return [];
|
|
83
|
+
for (const value of story.lanes) {
|
|
84
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
85
|
+
continue;
|
|
86
|
+
const entry = value;
|
|
87
|
+
if (entry.lane_id === laneId && Array.isArray(entry.history)) {
|
|
88
|
+
return entry.history.filter((history) => history !== null && typeof history === "object" && !Array.isArray(history));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
function laneTerminalAt(lane) {
|
|
94
|
+
const ended = timestamp(lane.ended_at);
|
|
95
|
+
if (ended !== null)
|
|
96
|
+
return ended;
|
|
97
|
+
const envelopeAt = timestamp(lane.last_envelope?.at);
|
|
98
|
+
if (envelopeAt !== null)
|
|
99
|
+
return envelopeAt;
|
|
100
|
+
return isTerminalLaneState(lane.state) ? timestamp(lane.updated_at) : null;
|
|
101
|
+
}
|
|
102
|
+
function reviewCandidates(input, lead, lane, story) {
|
|
103
|
+
const candidates = [];
|
|
104
|
+
for (const review of input.reviews) {
|
|
105
|
+
const at = timestamp(review.recorded_at);
|
|
106
|
+
if (at !== null && reviewMatchesLane(review, lane, story))
|
|
107
|
+
candidates.push({ at, event: "review_record" });
|
|
108
|
+
}
|
|
109
|
+
for (const resume of input.seniorResumes) {
|
|
110
|
+
const at = timestamp(resume.at);
|
|
111
|
+
if (resume.lane_id === lead.lane_id && at !== null)
|
|
112
|
+
candidates.push({ at, event: "senior_resume" });
|
|
113
|
+
}
|
|
114
|
+
if (story && CLOSED_STORY_STATES.has(storyState(story) ?? "")) {
|
|
115
|
+
const at = timestamp(story.updated_at);
|
|
116
|
+
if (at !== null)
|
|
117
|
+
candidates.push({ at, event: "story_closed" });
|
|
118
|
+
}
|
|
119
|
+
return candidates;
|
|
120
|
+
}
|
|
121
|
+
function idleIntervalsForLane(lane, story, until) {
|
|
122
|
+
const created = timestamp(lane.created_at);
|
|
123
|
+
if (created === null)
|
|
124
|
+
return [];
|
|
125
|
+
const history = storyLaneHistory(story, lane.lane_id)
|
|
126
|
+
.map((entry) => ({ at: timestamp(typeof entry.at === "string" ? entry.at : undefined), state: stateAtHistoryEntry(entry), terminal: entry.terminal === true }))
|
|
127
|
+
.filter((entry) => entry.at !== null && entry.at >= created)
|
|
128
|
+
.sort((a, b) => a.at - b.at);
|
|
129
|
+
if (history.length === 0) {
|
|
130
|
+
const terminal = isTerminalLaneState(lane.state);
|
|
131
|
+
const ended = timestamp(lane.ended_at) ?? (terminal ? timestamp(lane.last_envelope?.at) ?? timestamp(lane.updated_at) : null) ?? until;
|
|
132
|
+
return ended > created ? [{ start: created, end: Math.min(ended, until) }] : [];
|
|
133
|
+
}
|
|
134
|
+
const intervals = [];
|
|
135
|
+
let active = true;
|
|
136
|
+
let start = created;
|
|
137
|
+
for (const event of history) {
|
|
138
|
+
const isTerminal = event.terminal || (event.state !== undefined && isTerminalLaneState(event.state));
|
|
139
|
+
if (isTerminal && active) {
|
|
140
|
+
if (event.at > start)
|
|
141
|
+
intervals.push({ start, end: event.at });
|
|
142
|
+
active = false;
|
|
143
|
+
}
|
|
144
|
+
else if (!isTerminal && !active) {
|
|
145
|
+
start = event.at;
|
|
146
|
+
active = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (active) {
|
|
150
|
+
const terminal = isTerminalLaneState(lane.state);
|
|
151
|
+
const ended = timestamp(lane.ended_at) ?? (terminal ? timestamp(lane.last_envelope?.at) ?? timestamp(lane.updated_at) : null) ?? until;
|
|
152
|
+
if (ended > start)
|
|
153
|
+
intervals.push({ start, end: Math.min(ended, until) });
|
|
154
|
+
}
|
|
155
|
+
return intervals;
|
|
156
|
+
}
|
|
157
|
+
function overlaps(interval, start, end) {
|
|
158
|
+
return interval.start <= start && interval.end >= end;
|
|
159
|
+
}
|
|
160
|
+
function readyStoryIntervals(story, until) {
|
|
161
|
+
const created = timestamp(story.created_at);
|
|
162
|
+
const updated = timestamp(story.updated_at);
|
|
163
|
+
const state = storyState(story);
|
|
164
|
+
if (state === "todo") {
|
|
165
|
+
const start = updated ?? created;
|
|
166
|
+
return start !== null && start < until ? [{ start, end: until }] : [];
|
|
167
|
+
}
|
|
168
|
+
if (created !== null && updated !== null && updated > created)
|
|
169
|
+
return [{ start: created, end: updated }];
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
function idleSlotMinutes(input, lead, leadLane, project, children, start, end) {
|
|
173
|
+
if (!leadLane || project === undefined)
|
|
174
|
+
return 0;
|
|
175
|
+
const grant = Math.max(0, leadLane.hire_grant ?? 0);
|
|
176
|
+
if (grant === 0)
|
|
177
|
+
return 0;
|
|
178
|
+
const readyIntervals = project.stories.flatMap((story) => readyStoryIntervals(story, end));
|
|
179
|
+
if (readyIntervals.length === 0)
|
|
180
|
+
return 0;
|
|
181
|
+
const leadStory = matchingStory(project, leadLane.story_id);
|
|
182
|
+
const leadIntervals = idleIntervalsForLane(leadLane, leadStory, end);
|
|
183
|
+
if (leadIntervals.length === 0)
|
|
184
|
+
return 0;
|
|
185
|
+
const childIntervals = children.flatMap((child) => {
|
|
186
|
+
const story = matchingStory(project, child.story_id);
|
|
187
|
+
return idleIntervalsForLane(child, story, end);
|
|
188
|
+
});
|
|
189
|
+
const boundaries = new Set([start, end]);
|
|
190
|
+
for (const interval of [...childIntervals, ...readyIntervals, ...leadIntervals]) {
|
|
191
|
+
if (interval.start > start && interval.start < end)
|
|
192
|
+
boundaries.add(interval.start);
|
|
193
|
+
if (interval.end > start && interval.end < end)
|
|
194
|
+
boundaries.add(interval.end);
|
|
195
|
+
}
|
|
196
|
+
const sorted = [...boundaries].sort((a, b) => a - b);
|
|
197
|
+
let total = 0;
|
|
198
|
+
for (let index = 0; index < sorted.length - 1; index += 1) {
|
|
199
|
+
const segmentStart = sorted[index];
|
|
200
|
+
const segmentEnd = sorted[index + 1];
|
|
201
|
+
if (segmentStart === undefined || segmentEnd === undefined || segmentEnd <= segmentStart)
|
|
202
|
+
continue;
|
|
203
|
+
if (!readyIntervals.some((interval) => overlaps(interval, segmentStart, segmentEnd)))
|
|
204
|
+
continue;
|
|
205
|
+
if (!leadIntervals.some((interval) => overlaps(interval, segmentStart, segmentEnd)))
|
|
206
|
+
continue;
|
|
207
|
+
const live = childIntervals.filter((interval) => overlaps(interval, segmentStart, segmentEnd)).length;
|
|
208
|
+
total += Math.max(0, grant - live) * (segmentEnd - segmentStart) / MINUTE_MS;
|
|
209
|
+
}
|
|
210
|
+
return round(total);
|
|
211
|
+
}
|
|
212
|
+
export function buildAutopilotMetricsReport(input) {
|
|
213
|
+
const since = timestamp(input.since);
|
|
214
|
+
const until = timestamp(input.until);
|
|
215
|
+
if (since === null || until === null || since >= until) {
|
|
216
|
+
throw new Error("autopilot_metrics_window_invalid: since and until must be valid timestamps with since before until");
|
|
217
|
+
}
|
|
218
|
+
const days = utcDays(since, until);
|
|
219
|
+
const rows = new Map();
|
|
220
|
+
for (const lead of input.leads) {
|
|
221
|
+
for (const day of days) {
|
|
222
|
+
rows.set(`${lead.team}\u0000${day}`, {
|
|
223
|
+
team: lead.team,
|
|
224
|
+
day,
|
|
225
|
+
operator_nudges: 0,
|
|
226
|
+
reviewed_lanes: 0,
|
|
227
|
+
review_latency_minutes_total: 0,
|
|
228
|
+
review_latency_minutes_average: null,
|
|
229
|
+
idle_slot_minutes_ready_work: 0,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const nudge of input.nudges) {
|
|
234
|
+
const at = timestamp(nudge.at);
|
|
235
|
+
if (at === null || at < since || at > until)
|
|
236
|
+
continue;
|
|
237
|
+
const row = rows.get(`${nudge.team}\u0000${dayKey(at)}`);
|
|
238
|
+
if (row)
|
|
239
|
+
row.operator_nudges += 1;
|
|
240
|
+
}
|
|
241
|
+
const projectById = new Map(input.projects.map((project) => [project.project_id, project]));
|
|
242
|
+
const reviewLatencies = [];
|
|
243
|
+
for (const lead of input.leads) {
|
|
244
|
+
const leadLane = input.lanes.find((lane) => lane.lane_id === lead.lane_id);
|
|
245
|
+
const project = projectById.get(lead.project_id);
|
|
246
|
+
const children = input.lanes.filter((lane) => lane.lane_id !== lead.lane_id && lane.senior.kind === "lane" && lane.senior.id === lead.lane_id);
|
|
247
|
+
for (const lane of children) {
|
|
248
|
+
const finishedAt = laneTerminalAt(lane);
|
|
249
|
+
if (finishedAt === null || finishedAt < since || finishedAt > until)
|
|
250
|
+
continue;
|
|
251
|
+
const story = matchingStory(project, lane.story_id);
|
|
252
|
+
const first = reviewCandidates(input, lead, lane, story)
|
|
253
|
+
.filter((event) => event.at >= finishedAt && event.at <= until)
|
|
254
|
+
.sort((a, b) => a.at - b.at)[0];
|
|
255
|
+
if (!first)
|
|
256
|
+
continue;
|
|
257
|
+
reviewLatencies.push({
|
|
258
|
+
team: lead.team,
|
|
259
|
+
lane_id: lane.lane_id,
|
|
260
|
+
story_id: lane.story_id,
|
|
261
|
+
finished_at: new Date(finishedAt).toISOString(),
|
|
262
|
+
reviewed_at: new Date(first.at).toISOString(),
|
|
263
|
+
minutes: round((first.at - finishedAt) / MINUTE_MS),
|
|
264
|
+
end_event: first.event,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
for (const day of days) {
|
|
268
|
+
const dayStart = Math.max(since, Date.parse(`${day}T00:00:00.000Z`));
|
|
269
|
+
const dayEnd = Math.min(until, Date.parse(`${day}T00:00:00.000Z`) + 24 * 60 * MINUTE_MS);
|
|
270
|
+
const idle = idleSlotMinutes(input, lead, leadLane, project, children, dayStart, dayEnd);
|
|
271
|
+
const row = rows.get(`${lead.team}\u0000${day}`);
|
|
272
|
+
if (row)
|
|
273
|
+
row.idle_slot_minutes_ready_work = round(row.idle_slot_minutes_ready_work + idle);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const latency of reviewLatencies) {
|
|
277
|
+
const row = rows.get(`${latency.team}\u0000${dayKey(Date.parse(latency.finished_at))}`);
|
|
278
|
+
if (!row)
|
|
279
|
+
continue;
|
|
280
|
+
row.reviewed_lanes += 1;
|
|
281
|
+
row.review_latency_minutes_total = round(row.review_latency_minutes_total + latency.minutes);
|
|
282
|
+
row.review_latency_minutes_average = round(row.review_latency_minutes_total / row.reviewed_lanes);
|
|
283
|
+
}
|
|
284
|
+
const selectedTeams = new Set(input.leads.map((lead) => lead.team));
|
|
285
|
+
const nudgeDefects = input.nudgeDefects
|
|
286
|
+
.filter((defect) => {
|
|
287
|
+
if (!selectedTeams.has(defect.team))
|
|
288
|
+
return false;
|
|
289
|
+
const at = timestamp(defect.at);
|
|
290
|
+
return at !== null && at >= since && at <= until;
|
|
291
|
+
})
|
|
292
|
+
.map((defect) => ({ ...defect, wake: null }));
|
|
293
|
+
return {
|
|
294
|
+
since: new Date(since).toISOString(),
|
|
295
|
+
until: new Date(until).toISOString(),
|
|
296
|
+
days: [...rows.values()].sort((a, b) => a.day.localeCompare(b.day) || a.team.localeCompare(b.team)),
|
|
297
|
+
review_latencies: reviewLatencies.sort((a, b) => a.finished_at.localeCompare(b.finished_at) || a.lane_id.localeCompare(b.lane_id)),
|
|
298
|
+
nudge_defects: nudgeDefects,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=autopilot-metrics.js.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* compare-and-set on `version`. Histories are capped at 50 at the append site.
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
|
+
import { type OutpostIdentity } from "./admission-host.js";
|
|
10
11
|
import { type AttributionHistoryEntry, type LaneRecord, type LaneWriter, type QuestionRecord, type RoutingHistoryEntry, type SeniorHistoryEntry } from "./types.js";
|
|
11
12
|
export declare const LANES_STORE_REL: string;
|
|
12
13
|
export declare const LANE_RECORDS_DIR = "lanes";
|
|
@@ -15,6 +16,7 @@ export declare const PENDING_INDEX_DIR: string;
|
|
|
15
16
|
export interface StoreDeps {
|
|
16
17
|
now: () => Date;
|
|
17
18
|
hostname: () => string;
|
|
19
|
+
outpostIdentity: () => OutpostIdentity;
|
|
18
20
|
mkdirSync: typeof fs.mkdirSync;
|
|
19
21
|
readFileSync: typeof fs.readFileSync;
|
|
20
22
|
writeFileSync: typeof fs.writeFileSync;
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -14,6 +14,7 @@ import semver from "semver";
|
|
|
14
14
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
15
15
|
import { generateUlid } from "../mesh/live/ulid.js";
|
|
16
16
|
import { resolveLanesHqRoot } from "./hq-root.js";
|
|
17
|
+
import { readOutpostIdentity } from "./admission-host.js";
|
|
17
18
|
import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
|
|
18
19
|
import { HISTORY_CAP, HOST_KINDS, LANE_STORY_STATUSES, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isQueuedReason, isLaneState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
|
|
19
20
|
import { laneStories } from "./stories.js";
|
|
@@ -29,6 +30,7 @@ function defaultStoreDeps() {
|
|
|
29
30
|
return {
|
|
30
31
|
now: () => new Date(),
|
|
31
32
|
hostname: () => os.hostname(),
|
|
33
|
+
outpostIdentity: () => readOutpostIdentity(),
|
|
32
34
|
mkdirSync: fs.mkdirSync.bind(fs),
|
|
33
35
|
readFileSync: fs.readFileSync.bind(fs),
|
|
34
36
|
writeFileSync: fs.writeFileSync.bind(fs),
|
|
@@ -1238,9 +1240,36 @@ export function readAllLanes(hqRoot, deps = defaultStoreDeps()) {
|
|
|
1238
1240
|
...deps,
|
|
1239
1241
|
conflictSidecarPathsFor: (recordPath) => sidecarsByRecord.get(path.basename(recordPath)) ?? [],
|
|
1240
1242
|
};
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1243
|
+
const records = [];
|
|
1244
|
+
for (const laneId of laneIds) {
|
|
1245
|
+
try {
|
|
1246
|
+
records.push(readLane(hqRoot, laneId, indexedDeps));
|
|
1247
|
+
}
|
|
1248
|
+
catch (err) {
|
|
1249
|
+
if (!(err instanceof LaneSchemaError))
|
|
1250
|
+
throw err;
|
|
1251
|
+
const filePath = laneRecordPath(hqRoot, laneId);
|
|
1252
|
+
const raw = readJsonFile(filePath, indexedDeps);
|
|
1253
|
+
const host = raw !== null && typeof raw === "object" && !Array.isArray(raw)
|
|
1254
|
+
? raw.host
|
|
1255
|
+
: undefined;
|
|
1256
|
+
const hostRecord = host !== null && typeof host === "object" && !Array.isArray(host)
|
|
1257
|
+
? host
|
|
1258
|
+
: undefined;
|
|
1259
|
+
const outpostId = hostRecord?.kind === "outpost" &&
|
|
1260
|
+
typeof hostRecord.id === "string" && hostRecord.id.trim() !== ""
|
|
1261
|
+
? hostRecord.id
|
|
1262
|
+
: undefined;
|
|
1263
|
+
const identity = indexedDeps.outpostIdentity();
|
|
1264
|
+
const isRemoteMirror = outpostId !== undefined && (identity.kind === "not_outpost" ||
|
|
1265
|
+
(identity.kind === "id" && identity.id !== outpostId));
|
|
1266
|
+
if (!isRemoteMirror)
|
|
1267
|
+
throw err;
|
|
1268
|
+
const field = typeof err.details.field === "string" ? err.details.field : "unknown_field";
|
|
1269
|
+
process.stderr.write(`lanes store: skipping malformed mirrored lane ${laneId} field ${field}: ${err.message}\n`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return records.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) ||
|
|
1244
1273
|
a.lane_id.localeCompare(b.lane_id));
|
|
1245
1274
|
}
|
|
1246
1275
|
export function tryReadQuestion(hqRoot, questionId, deps = defaultStoreDeps()) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Default-off flag check for native entrypoint routes. */
|
|
2
2
|
export declare function nativeFastRouteEnabled(argv: readonly string[], env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
3
3
|
/** Reads a default-off fast-route gate from its local override or fresh snapshot. */
|
|
4
|
-
export declare function fastFlagEnabled(flagKey: string, env?: NodeJS.ProcessEnv
|
|
4
|
+
export declare function fastFlagEnabled(flagKey: string, env?: NodeJS.ProcessEnv, options?: {
|
|
5
|
+
numericEnvironmentOverride?: boolean;
|
|
6
|
+
}): Promise<boolean>;
|
|
5
7
|
//# sourceMappingURL=native-fast-route.d.ts.map
|
|
@@ -8,8 +8,8 @@ export async function nativeFastRouteEnabled(argv, env = process.env) {
|
|
|
8
8
|
return fastFlagEnabled(parsed.entry.flagKey, env);
|
|
9
9
|
}
|
|
10
10
|
/** Reads a default-off fast-route gate from its local override or fresh snapshot. */
|
|
11
|
-
export async function fastFlagEnabled(flagKey, env = process.env) {
|
|
12
|
-
const override = nativeRouteOverride(flagKey, env);
|
|
11
|
+
export async function fastFlagEnabled(flagKey, env = process.env, options = {}) {
|
|
12
|
+
const override = nativeRouteOverride(flagKey, env, options.numericEnvironmentOverride !== false);
|
|
13
13
|
if (override !== null)
|
|
14
14
|
return override;
|
|
15
15
|
try {
|
|
@@ -29,15 +29,17 @@ export async function fastFlagEnabled(flagKey, env = process.env) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/** The route's documented CLI override also accepts the conventional 1/0. */
|
|
32
|
-
function nativeRouteOverride(flagKey, env) {
|
|
32
|
+
function nativeRouteOverride(flagKey, env, numericEnvironmentOverride) {
|
|
33
33
|
const override = readLocalOverride(flagKey, env);
|
|
34
34
|
if (override !== null)
|
|
35
35
|
return override;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
if (numericEnvironmentOverride) {
|
|
37
|
+
const raw = env[localOverrideEnvVar(flagKey)];
|
|
38
|
+
if (raw === "1")
|
|
39
|
+
return true;
|
|
40
|
+
if (raw === "0")
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
41
43
|
return null;
|
|
42
44
|
}
|
|
43
45
|
//# sourceMappingURL=native-fast-route.js.map
|
|
@@ -99,5 +99,5 @@ export declare const CONTRIBUTION_KEYS: ContributionKey[];
|
|
|
99
99
|
*/
|
|
100
100
|
export declare function payloadFor(key: ContributionKey, item: string): string;
|
|
101
101
|
/** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
|
|
102
|
-
export declare const SYMLINK_KEYS: ("workers" | "mcp" | "commands" | "
|
|
102
|
+
export declare const SYMLINK_KEYS: ("workers" | "mcp" | "commands" | "skills" | "knowledge" | "hooks" | "policies" | "scripts")[];
|
|
103
103
|
//# sourceMappingURL=contribution-table.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type ErrorEventStream = {
|
|
2
|
+
on: (event: "error", listener: (err: unknown) => void) => unknown;
|
|
3
|
+
};
|
|
4
|
+
export type FastStreamErrorDependencies = {
|
|
5
|
+
stdout: ErrorEventStream;
|
|
6
|
+
stderr: ErrorEventStream;
|
|
7
|
+
exit: (code: number) => void;
|
|
8
|
+
};
|
|
9
|
+
/** Match the full CLI's clean handling when a pipe consumer closes early. */
|
|
10
|
+
export declare function installFastPathStreamErrorHandlers(deps?: FastStreamErrorDependencies): void;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=fast-stream-errors.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isEpipe } from "./epipe.js";
|
|
2
|
+
/** Match the full CLI's clean handling when a pipe consumer closes early. */
|
|
3
|
+
export function installFastPathStreamErrorHandlers(deps = {
|
|
4
|
+
stdout: process.stdout,
|
|
5
|
+
stderr: process.stderr,
|
|
6
|
+
exit: (code) => process.exit(code),
|
|
7
|
+
}) {
|
|
8
|
+
const handle = (err) => {
|
|
9
|
+
if (isEpipe(err)) {
|
|
10
|
+
deps.exit(0);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
throw err;
|
|
14
|
+
};
|
|
15
|
+
deps.stdout.on("error", handle);
|
|
16
|
+
deps.stderr.on("error", handle);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=fast-stream-errors.js.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare function forbiddenFastPathModules(loadedModules: readonly string[], distDirectory: string): string[];
|
|
2
|
-
export declare function assertFastPathStaysCommandGraphFree(loadedModules: readonly string[], distDirectory: string): void;
|
|
1
|
+
export declare function forbiddenFastPathModules(loadedModules: readonly string[], distDirectory: string, allowedCommandModules?: readonly string[]): string[];
|
|
2
|
+
export declare function assertFastPathStaysCommandGraphFree(loadedModules: readonly string[], distDirectory: string, allowedCommandModules?: readonly string[]): void;
|
|
3
3
|
//# sourceMappingURL=scaffold-fast-import-guard.d.ts.map
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
export function forbiddenFastPathModules(loadedModules, distDirectory) {
|
|
3
|
+
export function forbiddenFastPathModules(loadedModules, distDirectory, allowedCommandModules = []) {
|
|
4
4
|
const dist = path.resolve(distDirectory);
|
|
5
5
|
const main = path.join(dist, "main.js");
|
|
6
6
|
const commands = `${path.join(dist, "commands")}${path.sep}`;
|
|
7
7
|
const flagRegistry = path.join(dist, "lib", "flag-registry.js");
|
|
8
8
|
const coreCommand = path.join(dist, "commands", "core.js");
|
|
9
|
+
const allowedCommands = new Set(allowedCommandModules.map((module) => path.resolve(module)));
|
|
9
10
|
return [...new Set(loadedModules.map((loaded) => {
|
|
10
11
|
if (loaded.startsWith("file:")) {
|
|
11
12
|
try {
|
|
@@ -22,10 +23,12 @@ export function forbiddenFastPathModules(loadedModules, distDirectory) {
|
|
|
22
23
|
path.resolve(loaded) === flagRegistry ||
|
|
23
24
|
path.resolve(loaded) === coreCommand ||
|
|
24
25
|
path.resolve(loaded).includes(`${path.sep}node_modules${path.sep}@sentry${path.sep}node${path.sep}`))
|
|
26
|
+
.filter((loaded) => !allowedCommands.has(path.resolve(loaded)) ||
|
|
27
|
+
path.resolve(loaded) === main || path.resolve(loaded) === coreCommand)
|
|
25
28
|
.sort();
|
|
26
29
|
}
|
|
27
|
-
export function assertFastPathStaysCommandGraphFree(loadedModules, distDirectory) {
|
|
28
|
-
const forbidden = forbiddenFastPathModules(loadedModules, distDirectory);
|
|
30
|
+
export function assertFastPathStaysCommandGraphFree(loadedModules, distDirectory, allowedCommandModules = []) {
|
|
31
|
+
const forbidden = forbiddenFastPathModules(loadedModules, distDirectory, allowedCommandModules);
|
|
29
32
|
if (forbidden.length > 0) {
|
|
30
33
|
throw new Error(`fast core loaded forbidden command modules:\n${forbidden.join("\n")}`);
|
|
31
34
|
}
|