@indigoai-us/hq-cli 5.293.0 → 5.295.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 +39 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
- package/dist/command-catalog.generated.d.ts +54 -33
- package/dist/command-catalog.generated.js +68 -42
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +76 -1
- 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/doctor.js +0 -74
- package/dist/commands/lanes-autopilot.d.ts +11 -0
- package/dist/commands/lanes-autopilot.js +336 -0
- package/dist/commands/lanes-message.js +23 -5
- package/dist/commands/lanes-questions.js +8 -5
- package/dist/commands/lanes-resume.js +0 -3
- package/dist/commands/lanes-shared.d.ts +18 -2
- package/dist/commands/lanes-stories.d.ts +14 -0
- package/dist/commands/lanes-stories.js +308 -0
- package/dist/commands/lanes.d.ts +1 -9
- package/dist/commands/lanes.js +216 -481
- package/dist/fast-core.js +53 -25
- package/dist/fast-scaffold.d.ts +15 -7
- package/dist/fast-scaffold.js +167 -17
- package/dist/index.js +1 -10
- package/dist/lib/core-utils/sentry-report.d.ts +0 -19
- package/dist/lib/core-utils/sentry-report.js +8 -61
- 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/doctor/checks/claude-wiring.js +9 -0
- package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
- package/dist/lib/lanes/autopilot-metrics.js +301 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
- package/dist/lib/lanes/manager-status-gate.js +2 -43
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
- package/dist/lib/lanes/outpost-dispatch.js +7 -1
- package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
- package/dist/lib/lanes/outpost-hire-cache.js +0 -1
- package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
- package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
- package/dist/lib/lanes/reconcile.d.ts +2 -3
- package/dist/lib/lanes/reconcile.js +44 -38
- package/dist/lib/lanes/spawn.d.ts +0 -2
- package/dist/lib/lanes/spawn.js +18 -5
- package/dist/lib/lanes/store.d.ts +2 -0
- package/dist/lib/lanes/store.js +54 -3
- package/dist/lib/lanes/types.d.ts +18 -1
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +4 -3
- package/dist/lib/lanes/watcher.js +102 -34
- package/dist/lib/mesh/live/daemon/run.js +18 -1
- package/dist/native-fast-route.d.ts +3 -1
- package/dist/native-fast-route.js +10 -8
- package/dist/utils/cognito-session.d.ts +6 -0
- package/dist/utils/cognito-session.js +9 -0
- 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.js +31 -0
- package/dist/utils/version-gate.js +22 -3
- package/package.json +1 -1
- package/dist/fast-lanes-list-route.d.ts +0 -8
- package/dist/fast-lanes-list-route.js +0 -32
- package/dist/fast-lanes-list.d.ts +0 -3
- package/dist/fast-lanes-list.js +0 -224
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
- package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
- package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
- package/dist/lib/doctor/client-health-sentry.js +0 -286
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
- package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
- package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
- package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface AutopilotMetricsLead {
|
|
2
|
+
team: string;
|
|
3
|
+
lane_id: string;
|
|
4
|
+
project_id: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AutopilotMetricsLane {
|
|
7
|
+
lane_id: string;
|
|
8
|
+
project_id: string;
|
|
9
|
+
story_id: string;
|
|
10
|
+
senior: {
|
|
11
|
+
kind: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
};
|
|
14
|
+
state: string;
|
|
15
|
+
created_at: string;
|
|
16
|
+
updated_at?: string;
|
|
17
|
+
ended_at?: string;
|
|
18
|
+
hire_grant?: number;
|
|
19
|
+
last_resume?: {
|
|
20
|
+
at?: string;
|
|
21
|
+
};
|
|
22
|
+
last_envelope?: {
|
|
23
|
+
at?: string;
|
|
24
|
+
artifacts?: unknown[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface AutopilotMetricsStory {
|
|
28
|
+
id: string;
|
|
29
|
+
state?: string;
|
|
30
|
+
status?: string;
|
|
31
|
+
created_at?: string;
|
|
32
|
+
updated_at?: string;
|
|
33
|
+
prs?: unknown[];
|
|
34
|
+
lanes?: unknown[];
|
|
35
|
+
}
|
|
36
|
+
export interface AutopilotMetricsProject {
|
|
37
|
+
project_id: string;
|
|
38
|
+
stories: readonly AutopilotMetricsStory[];
|
|
39
|
+
}
|
|
40
|
+
export interface AutopilotMetricsNudge {
|
|
41
|
+
team: string;
|
|
42
|
+
at: string;
|
|
43
|
+
}
|
|
44
|
+
export interface AutopilotMetricsReview {
|
|
45
|
+
repo: string;
|
|
46
|
+
pr: number;
|
|
47
|
+
recorded_at: string;
|
|
48
|
+
worker_lane?: string | null;
|
|
49
|
+
}
|
|
50
|
+
export interface AutopilotMetricsResume {
|
|
51
|
+
lane_id: string;
|
|
52
|
+
at: string;
|
|
53
|
+
}
|
|
54
|
+
export interface AutopilotMetricsDefect {
|
|
55
|
+
at: string;
|
|
56
|
+
team: string;
|
|
57
|
+
defect: string;
|
|
58
|
+
}
|
|
59
|
+
export interface BuildAutopilotMetricsReportInput {
|
|
60
|
+
since: string;
|
|
61
|
+
until: string;
|
|
62
|
+
leads: readonly AutopilotMetricsLead[];
|
|
63
|
+
lanes: readonly AutopilotMetricsLane[];
|
|
64
|
+
projects: readonly AutopilotMetricsProject[];
|
|
65
|
+
reviews: readonly AutopilotMetricsReview[];
|
|
66
|
+
nudges: readonly AutopilotMetricsNudge[];
|
|
67
|
+
nudgeDefects: readonly AutopilotMetricsDefect[];
|
|
68
|
+
seniorResumes: readonly AutopilotMetricsResume[];
|
|
69
|
+
}
|
|
70
|
+
export interface AutopilotMetricsDay {
|
|
71
|
+
team: string;
|
|
72
|
+
day: string;
|
|
73
|
+
operator_nudges: number;
|
|
74
|
+
reviewed_lanes: number;
|
|
75
|
+
review_latency_minutes_total: number;
|
|
76
|
+
review_latency_minutes_average: number | null;
|
|
77
|
+
idle_slot_minutes_ready_work: number;
|
|
78
|
+
}
|
|
79
|
+
export interface AutopilotReviewLatency {
|
|
80
|
+
team: string;
|
|
81
|
+
lane_id: string;
|
|
82
|
+
story_id: string;
|
|
83
|
+
finished_at: string;
|
|
84
|
+
reviewed_at: string;
|
|
85
|
+
minutes: number;
|
|
86
|
+
end_event: "review_record" | "senior_resume" | "story_closed";
|
|
87
|
+
}
|
|
88
|
+
export interface AutopilotNudgeDefect extends AutopilotMetricsDefect {
|
|
89
|
+
wake: null;
|
|
90
|
+
}
|
|
91
|
+
export interface AutopilotMetricsReport {
|
|
92
|
+
since: string;
|
|
93
|
+
until: string;
|
|
94
|
+
days: AutopilotMetricsDay[];
|
|
95
|
+
review_latencies: AutopilotReviewLatency[];
|
|
96
|
+
nudge_defects: AutopilotNudgeDefect[];
|
|
97
|
+
}
|
|
98
|
+
export declare function buildAutopilotMetricsReport(input: BuildAutopilotMetricsReportInput): AutopilotMetricsReport;
|
|
99
|
+
//# sourceMappingURL=autopilot-metrics.d.ts.map
|
|
@@ -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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** hq-flags keys used by lane list and placement behavior. */
|
|
2
2
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
|
+
export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
3
5
|
//# sourceMappingURL=flag-keys.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** hq-flags keys used by lane list and placement behavior. */
|
|
2
2
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
|
+
export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
3
5
|
//# sourceMappingURL=flag-keys.js.map
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
2
1
|
import type { LaneRecord, ManagerStatusGate } from "./types.js";
|
|
3
|
-
export declare const MANAGER_STATUS_GATE_FLAG = "lanes.manager-status-gate";
|
|
4
2
|
export interface ManagerStatusTarget {
|
|
5
3
|
manager: boolean;
|
|
6
4
|
identified_by?: "story_id" | "worker_profile" | "juniors";
|
|
@@ -11,23 +9,8 @@ export interface StatusSnapshot {
|
|
|
11
9
|
mtime: number | null;
|
|
12
10
|
updatedAt: string | null;
|
|
13
11
|
}
|
|
14
|
-
export interface ManagerStatusGateResolution {
|
|
15
|
-
enabled: boolean;
|
|
16
|
-
target?: ManagerStatusTarget;
|
|
17
|
-
error?: string;
|
|
18
|
-
}
|
|
19
12
|
/** Resolve manager identity and the single declared status file, if present. */
|
|
20
13
|
export declare function resolveManagerStatusTarget(root: string, lane: LaneRecord): ManagerStatusTarget;
|
|
21
|
-
export declare function resolveManagerStatusGateForLane(root: string, lane: LaneRecord, options?: {
|
|
22
|
-
env?: NodeJS.ProcessEnv;
|
|
23
|
-
readFlag?: typeof readLanesRegistryFlag;
|
|
24
|
-
onError?: (error: unknown) => void;
|
|
25
|
-
}): Promise<ManagerStatusGateResolution>;
|
|
26
|
-
export declare function managerStatusGateEnabledForLane(root: string, lane: LaneRecord, options?: {
|
|
27
|
-
env?: NodeJS.ProcessEnv;
|
|
28
|
-
readFlag?: typeof readLanesRegistryFlag;
|
|
29
|
-
onError?: (error: unknown) => void;
|
|
30
|
-
}): Promise<boolean>;
|
|
31
14
|
export declare function readStatusSnapshot(filePath: string): StatusSnapshot;
|
|
32
15
|
export declare function managerStatusGateHeartbeat(root: string, lane: LaneRecord, turnId: string, turnStartedAt: string): ManagerStatusGate | undefined;
|
|
33
16
|
export declare function managerStatusWasWritten(gate: ManagerStatusGate, snapshot: StatusSnapshot): boolean;
|
|
@@ -2,9 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { checkCompanyPath } from "./checked-company-path.js";
|
|
4
4
|
import { LanesError } from "./errors.js";
|
|
5
|
-
import {
|
|
6
|
-
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
7
|
-
export const MANAGER_STATUS_GATE_FLAG = "lanes.manager-status-gate";
|
|
5
|
+
import { resolveLaneCompany } from "./lane-company.js";
|
|
8
6
|
function isObject(value) {
|
|
9
7
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
8
|
}
|
|
@@ -49,14 +47,6 @@ function matchesLane(rowLane, laneId) {
|
|
|
49
47
|
const bareLaneId = laneId.split("_", 1)[0] ?? "";
|
|
50
48
|
return row === laneId || row === bareLaneId;
|
|
51
49
|
}
|
|
52
|
-
function managerStatusGateErrorCode(err) {
|
|
53
|
-
return err instanceof LanesError ? err.error : "manager_status_gate_unavailable";
|
|
54
|
-
}
|
|
55
|
-
function logManagerStatusGateFailure(lane, err) {
|
|
56
|
-
const code = managerStatusGateErrorCode(err);
|
|
57
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
-
process.stderr.write(`hq lanes: manager status gate disabled for ${lane.lane_id}: ${code}: ${message}\n`);
|
|
59
|
-
}
|
|
60
50
|
/** Resolve manager identity and the single declared status file, if present. */
|
|
61
51
|
export function resolveManagerStatusTarget(root, lane) {
|
|
62
52
|
const directIdentity = lane.story_id === "MANAGER"
|
|
@@ -93,38 +83,6 @@ export function resolveManagerStatusTarget(root, lane) {
|
|
|
93
83
|
statusPath,
|
|
94
84
|
};
|
|
95
85
|
}
|
|
96
|
-
export async function resolveManagerStatusGateForLane(root, lane, options = {}) {
|
|
97
|
-
const reportError = options.onError ?? ((err) => logManagerStatusGateFailure(lane, err));
|
|
98
|
-
let enabled;
|
|
99
|
-
try {
|
|
100
|
-
const resolution = resolveLaneCompany(root, lane);
|
|
101
|
-
const company = resolution.company ?? undefined;
|
|
102
|
-
enabled = await (options.readFlag ?? readLanesRegistryFlag)(MANAGER_STATUS_GATE_FLAG, {
|
|
103
|
-
company,
|
|
104
|
-
...(company ? {
|
|
105
|
-
companyUidSource: laneCompanyUidSource(resolution.source),
|
|
106
|
-
} : {}),
|
|
107
|
-
env: options.env,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
reportError(err);
|
|
112
|
-
return { enabled: false, error: managerStatusGateErrorCode(err) };
|
|
113
|
-
}
|
|
114
|
-
if (!enabled)
|
|
115
|
-
return { enabled: false };
|
|
116
|
-
try {
|
|
117
|
-
const target = resolveManagerStatusTarget(root, lane);
|
|
118
|
-
return { enabled: target.manager && target.statusPath !== undefined, target };
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
reportError(err);
|
|
122
|
-
return { enabled: false, error: managerStatusGateErrorCode(err) };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
export async function managerStatusGateEnabledForLane(root, lane, options = {}) {
|
|
126
|
-
return (await resolveManagerStatusGateForLane(root, lane, options)).enabled;
|
|
127
|
-
}
|
|
128
86
|
export function readStatusSnapshot(filePath) {
|
|
129
87
|
let stat;
|
|
130
88
|
try {
|
|
@@ -153,6 +111,7 @@ export function readStatusSnapshot(filePath) {
|
|
|
153
111
|
return { mtime: stat.mtimeMs, updatedAt };
|
|
154
112
|
}
|
|
155
113
|
export function managerStatusGateHeartbeat(root, lane, turnId, turnStartedAt) {
|
|
114
|
+
resolveLaneCompany(root, lane);
|
|
156
115
|
const target = resolveManagerStatusTarget(root, lane);
|
|
157
116
|
if (!target.manager || target.statusPath === undefined)
|
|
158
117
|
return undefined;
|
|
@@ -190,7 +190,13 @@ export function buildOutpostLaneCommand(input) {
|
|
|
190
190
|
args.push("--tail", input.tail);
|
|
191
191
|
if (input.action === "logs" && input.json === true)
|
|
192
192
|
args.push("--json");
|
|
193
|
-
|
|
193
|
+
const callerLaneId = input.callerLaneId?.trim();
|
|
194
|
+
const callerSessionId = input.callerSessionId?.trim();
|
|
195
|
+
const callerEnv = [
|
|
196
|
+
callerLaneId ? `HQ_LANE_ID=${callerLaneId}` : undefined,
|
|
197
|
+
callerSessionId ? `HQ_SESSION_ID=${callerSessionId}` : undefined,
|
|
198
|
+
].filter((value) => value !== undefined);
|
|
199
|
+
return withRemoteHqDir(joinCommandParts(callerEnv.length > 0 ? ["env", ...callerEnv, ...args] : args));
|
|
194
200
|
}
|
|
195
201
|
/** True when a remote CLI exited after rejecting the forwarded effort flag. */
|
|
196
202
|
export function outpostRejectedEffortOption(result) {
|
|
@@ -2,7 +2,6 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
export const OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS = 60_000;
|
|
5
|
-
export const LANE_OUTPOST_SNAPSHOT_CACHE_FLAG = "lanes.outpost-snapshot-cache";
|
|
6
5
|
const CACHE_RELATIVE_PATH = path.join("workspace", "lanes-runs", ".cache", "outpost-hire-summaries.json");
|
|
7
6
|
export function outpostHireSummaryCachePath(hqRoot) {
|
|
8
7
|
return path.join(hqRoot, CACHE_RELATIVE_PATH);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Deduplicates identical remote snapshot queries within one command invocation. */
|
|
2
|
+
export declare class OutpostSnapshotInvocationMemo {
|
|
3
|
+
private readonly requests;
|
|
4
|
+
getOrCreate<T>(outpostId: string, query: unknown, fetch: () => Promise<T>): Promise<T>;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=outpost-snapshot-memo.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Deduplicates identical remote snapshot queries within one command invocation. */
|
|
2
|
+
export class OutpostSnapshotInvocationMemo {
|
|
3
|
+
requests = new Map();
|
|
4
|
+
getOrCreate(outpostId, query, fetch) {
|
|
5
|
+
const key = JSON.stringify([outpostId, query]);
|
|
6
|
+
const existing = this.requests.get(key);
|
|
7
|
+
if (existing)
|
|
8
|
+
return existing;
|
|
9
|
+
const request = fetch();
|
|
10
|
+
this.requests.set(key, request);
|
|
11
|
+
return request;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=outpost-snapshot-memo.js.map
|
|
@@ -11,7 +11,7 @@ import { type ThrottleDeps, type CpuBusyObservation } from "./throttle.js";
|
|
|
11
11
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
12
12
|
import { type RunawayProcessInfo } from "./runaway-tool-guard.js";
|
|
13
13
|
import { type KeepAliveBlockedReason } from "./keep-alive.js";
|
|
14
|
-
import type { LaneCompanyRef, LaneRecord, LivenessState, QueuedReason, QuestionRecord, SeniorRef } from "./types.js";
|
|
14
|
+
import type { LaneCompanyRef, LaneAccountSelection, LaneRecord, LivenessState, QueuedReason, QuestionRecord, SeniorRef } from "./types.js";
|
|
15
15
|
import { type LaneMesh } from "./mesh-events.js";
|
|
16
16
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
17
17
|
export declare const QUESTION_AGE_MS: number;
|
|
@@ -86,8 +86,6 @@ export interface ReconcileDeps {
|
|
|
86
86
|
skipAdmissionWithoutCpuSample?: boolean;
|
|
87
87
|
/** Flag reader seam used to assert rollout reads happen before admission locks. */
|
|
88
88
|
readRegistryFlag?: typeof readLanesRegistryFlag;
|
|
89
|
-
/** Pre-read manager gate decisions, scoped to each lane for this pass. */
|
|
90
|
-
managerStatusGateByLane?: ReadonlyMap<string, boolean>;
|
|
91
89
|
/** Active-company fallback for questions raised from an unscoped lane. */
|
|
92
90
|
resolveQuestionActiveCompany?: () => Promise<LaneCompanyRef>;
|
|
93
91
|
/** Resume queued messages after this pass wins a local dead-worker CAS. */
|
|
@@ -102,6 +100,7 @@ export interface ReconcileDeps {
|
|
|
102
100
|
prepareKeepAliveClaudeResume?: (lane: LaneRecord) => Promise<{
|
|
103
101
|
lane: LaneRecord;
|
|
104
102
|
selectionFailure?: string;
|
|
103
|
+
accountSelection?: LaneAccountSelection;
|
|
105
104
|
}>;
|
|
106
105
|
/** Process-table snapshot seam for the opt-in runaway-search guard. */
|
|
107
106
|
runawayToolProcessLister?: () => readonly RunawayProcessInfo[];
|