@indigoai-us/hq-cli 5.220.0 → 5.221.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.
@@ -12,7 +12,7 @@ export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
12
12
  export { LANE_HIRE_GRANTS_FLAG };
13
13
  import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
14
14
  import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
15
- import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
15
+ import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
16
16
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
17
17
  import { type RunCompactionFn } from "../lib/lanes/compaction.js";
18
18
  import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
@@ -107,6 +107,7 @@ export declare const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-ques
107
107
  export declare const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
108
108
  export declare function parseSeniorRef(raw: string): SeniorRef;
109
109
  export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
110
+ export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
110
111
  /**
111
112
  * Spec §5.1: `hq lanes ask` from inside a lane can hit a binary or dist
112
113
  * being replaced mid-call (ENOENT / ETXTBSY / a module that no longer
@@ -14,6 +14,7 @@ import { readLocalOverride } from "@indigoai-us/hq-flags-client";
14
14
  import { Option } from "commander";
15
15
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
16
16
  import { registerLaneChecksCommand } from "./lanes-checks.js";
17
+ import { registerLaneReportCommands } from "./lanes-report.js";
17
18
  import { ensureCognitoToken } from "../utils/cognito-session.js";
18
19
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
19
20
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
@@ -43,6 +44,7 @@ import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib
43
44
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
44
45
  import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
45
46
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
47
+ import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
46
48
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
47
49
  import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
48
50
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
@@ -438,16 +440,20 @@ function readJsonFile(filePath) {
438
440
  throw err;
439
441
  }
440
442
  }
441
- function parseQuestionBody(raw) {
443
+ export function parseQuestionBody(raw) {
442
444
  if (raw === null || typeof raw !== "object" || Array.isArray(raw))
443
445
  return null;
444
446
  const obj = raw;
445
- const options = Array.isArray(obj.options)
446
- ? obj.options.filter((item) => typeof item === "string")
447
- : [];
447
+ if (typeof obj.question !== "string" || typeof obj.context !== "string" || !Array.isArray(obj.options) || obj.options.some((item) => typeof item !== "string"))
448
+ return null;
449
+ if (obj.recommended !== undefined && typeof obj.recommended !== "string")
450
+ return null;
451
+ if (obj.notes !== undefined && typeof obj.notes !== "string")
452
+ return null;
453
+ const options = obj.options.filter((item) => typeof item === "string");
448
454
  const body = {
449
- question: typeof obj.question === "string" ? obj.question : "",
450
- context: typeof obj.context === "string" ? obj.context : "",
455
+ question: obj.question,
456
+ context: obj.context,
451
457
  options,
452
458
  };
453
459
  if (typeof obj.recommended === "string")
@@ -8506,6 +8512,18 @@ export function registerLanesCommand(parent, deps = {}) {
8506
8512
  trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
8507
8513
  now: ctx.now,
8508
8514
  }, wrap);
8515
+ registerLaneReportCommands(lanes, {
8516
+ hqRoot: ctx.hqRoot,
8517
+ resolveCompany: ctx.resolveCompany,
8518
+ trackerEnabled: ctx.laneTrackerEnabled,
8519
+ trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
8520
+ now: ctx.now,
8521
+ readQuestionBody: (root, question) => {
8522
+ const body = requireQuestionBody(root, question);
8523
+ assertQuestionBodySecretSafe(body);
8524
+ return body;
8525
+ },
8526
+ }, wrap);
8509
8527
  registerOutpostPrunerCommand(lanes);
8510
8528
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
8511
8529
  noStoryOption.attributeName = () => "noStory";
@@ -0,0 +1,3 @@
1
+ /** Parse an extended ISO timestamp and return JavaScript's canonical UTC form. */
2
+ export declare function normalizeIsoTimestamp(value: unknown): string | null;
3
+ //# sourceMappingURL=timestamp.d.ts.map
@@ -0,0 +1,26 @@
1
+ /** Parse an extended ISO timestamp and return JavaScript's canonical UTC form. */
2
+ export function normalizeIsoTimestamp(value) {
3
+ if (typeof value !== "string")
4
+ return null;
5
+ const match = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.(\d+))?(Z|([+-])(\d\d):(\d\d))$/.exec(value);
6
+ if (!match)
7
+ return null;
8
+ const year = Number(match[1]);
9
+ const month = Number(match[2]);
10
+ const day = Number(match[3]);
11
+ const hour = Number(match[4]);
12
+ const minute = Number(match[5]);
13
+ const second = Number(match[6]);
14
+ const offsetHour = match[9] === undefined ? 0 : Number(match[10]);
15
+ const offsetMinute = match[9] === undefined ? 0 : Number(match[11]);
16
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
17
+ const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
18
+ if (month < 1 || month > 12 || day < 1 || daysInMonth === undefined || day > daysInMonth ||
19
+ hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59)
20
+ return null;
21
+ const parsed = Date.parse(value);
22
+ if (!Number.isFinite(parsed))
23
+ return null;
24
+ return new Date(parsed).toISOString();
25
+ }
26
+ //# sourceMappingURL=timestamp.js.map
@@ -0,0 +1,10 @@
1
+ export declare const FIRST_TRACKER_REPORT_SINCE = "1970-01-01T00:00:00.000Z";
2
+ export interface TrackerReportState {
3
+ schema_version: "hq-lanes-report-state.v1";
4
+ company: string;
5
+ last_report_at: string;
6
+ }
7
+ export declare function trackerReportStatePath(root: string, company: string): string;
8
+ export declare function readTrackerReportState(root: string, company: string): TrackerReportState | null;
9
+ export declare function writeTrackerReportState(root: string, company: string, at: string): TrackerReportState;
10
+ //# sourceMappingURL=tracker-report-state.d.ts.map
@@ -0,0 +1,124 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { LanesError } from "./errors.js";
5
+ import { normalizeIsoTimestamp } from "./timestamp.js";
6
+ export const FIRST_TRACKER_REPORT_SINCE = "1970-01-01T00:00:00.000Z";
7
+ export function trackerReportStatePath(root, company) {
8
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(company) || company === "." || company === "..") {
9
+ throw new LanesError("tracker_report_company_invalid", "Company slug must be a safe path segment", { company });
10
+ }
11
+ return path.join(root, "workspace", "lane-briefs", "tracker-report-state", `${company}.json`);
12
+ }
13
+ function stateError(error, message, file) {
14
+ throw new LanesError(error, `${message}: ${file}`, { path: file });
15
+ }
16
+ function validReportTime(value, file) {
17
+ const normalized = normalizeIsoTimestamp(value);
18
+ if (normalized === null) {
19
+ return stateError("tracker_report_state_invalid", "Report state has an invalid last_report_at", file);
20
+ }
21
+ return normalized;
22
+ }
23
+ export function readTrackerReportState(root, company) {
24
+ const file = trackerReportStatePath(root, company);
25
+ if (stateDirectory(root, false) === null)
26
+ return null;
27
+ let raw;
28
+ try {
29
+ const stat = fs.lstatSync(file);
30
+ if (!stat.isFile() || stat.isSymbolicLink()) {
31
+ return stateError("tracker_report_state_invalid", "Report state path must be a regular file", file);
32
+ }
33
+ raw = fs.readFileSync(file, "utf8");
34
+ }
35
+ catch (err) {
36
+ if (err.code === "ENOENT")
37
+ return null;
38
+ if (err instanceof LanesError)
39
+ throw err;
40
+ return stateError("tracker_report_state_unreadable", `Report state could not be read: ${err instanceof Error ? err.message : String(err)}`, file);
41
+ }
42
+ let value;
43
+ try {
44
+ value = JSON.parse(raw);
45
+ }
46
+ catch {
47
+ return stateError("tracker_report_state_malformed", "Report state contains invalid JSON", file);
48
+ }
49
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
50
+ return stateError("tracker_report_state_invalid", "Report state must be an object", file);
51
+ }
52
+ const record = value;
53
+ if (record.schema_version !== "hq-lanes-report-state.v1" || record.company !== company) {
54
+ return stateError("tracker_report_state_invalid", "Report state schema or company does not match", file);
55
+ }
56
+ return {
57
+ schema_version: "hq-lanes-report-state.v1",
58
+ company,
59
+ last_report_at: validReportTime(record.last_report_at, file),
60
+ };
61
+ }
62
+ function stateDirectory(root, create) {
63
+ let rootReal;
64
+ try {
65
+ rootReal = fs.realpathSync(root);
66
+ }
67
+ catch (err) {
68
+ return stateError("tracker_report_state_root_invalid", `HQ root could not be resolved: ${err instanceof Error ? err.message : String(err)}`, root);
69
+ }
70
+ let current = rootReal;
71
+ for (const segment of ["workspace", "lane-briefs", "tracker-report-state"]) {
72
+ current = path.join(current, segment);
73
+ let stat;
74
+ try {
75
+ stat = fs.lstatSync(current);
76
+ }
77
+ catch (err) {
78
+ if (err.code === "ENOENT" && !create)
79
+ return null;
80
+ if (err.code !== "ENOENT") {
81
+ return stateError("tracker_report_state_path_unreadable", `Report state directory could not be inspected: ${err instanceof Error ? err.message : String(err)}`, current);
82
+ }
83
+ try {
84
+ fs.mkdirSync(current, { mode: 0o700 });
85
+ }
86
+ catch (mkdirError) {
87
+ return stateError("tracker_report_state_path_unwritable", `Report state directory could not be created: ${mkdirError instanceof Error ? mkdirError.message : String(mkdirError)}`, current);
88
+ }
89
+ stat = fs.lstatSync(current);
90
+ }
91
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
92
+ return stateError("tracker_report_state_path_invalid", "Report state directory must be a real directory", current);
93
+ }
94
+ }
95
+ return current;
96
+ }
97
+ export function writeTrackerReportState(root, company, at) {
98
+ const canonicalAt = validReportTime(at, trackerReportStatePath(root, company));
99
+ const directory = stateDirectory(root, true);
100
+ if (directory === null)
101
+ return stateError("tracker_report_state_path_unavailable", "Report state directory is unavailable", trackerReportStatePath(root, company));
102
+ const file = trackerReportStatePath(root, company);
103
+ const temp = path.join(directory, `.${company}.${randomUUID()}.tmp`);
104
+ const value = { schema_version: "hq-lanes-report-state.v1", company, last_report_at: canonicalAt };
105
+ try {
106
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
107
+ fs.renameSync(temp, file);
108
+ }
109
+ catch (err) {
110
+ try {
111
+ fs.unlinkSync(temp);
112
+ }
113
+ catch (cleanupError) {
114
+ if (cleanupError.code !== "ENOENT") {
115
+ throw new LanesError("tracker_report_state_cleanup_failed", `Report state temporary file could not be removed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, { path: temp });
116
+ }
117
+ }
118
+ if (err instanceof LanesError)
119
+ throw err;
120
+ return stateError("tracker_report_state_write_failed", `Report state could not be written: ${err instanceof Error ? err.message : String(err)}`, file);
121
+ }
122
+ return value;
123
+ }
124
+ //# sourceMappingURL=tracker-report-state.js.map
@@ -0,0 +1,203 @@
1
+ import type { QuestionBody } from "./questions.js";
2
+ import type { QuestionRecord } from "./types.js";
3
+ import type { TrackerRecord } from "./tracker-store.js";
4
+ export type ReportStoryStatus = "queued" | "in_progress" | "review" | "done" | "dropped";
5
+ export type ReportAudience = "owner" | "operator" | "lane";
6
+ export type ApprovalState = "proposal" | "approved";
7
+ export interface TrackerReportRecord extends TrackerRecord {
8
+ /** Absolute path of the validated on-disk record; used only in errors. */
9
+ source_path: string;
10
+ }
11
+ export interface TrackerReportStory {
12
+ id: string;
13
+ title: string;
14
+ status: ReportStoryStatus;
15
+ references?: string[];
16
+ updated_at?: string;
17
+ source_path: string;
18
+ }
19
+ export interface TrackerReportIssue {
20
+ path: string;
21
+ field: string;
22
+ message: string;
23
+ story_id?: string;
24
+ record_id?: string;
25
+ question_id?: string;
26
+ }
27
+ export interface TrackerReportPullRequest {
28
+ number: string;
29
+ title: string;
30
+ merged_at: string;
31
+ source_path: string;
32
+ }
33
+ export interface TrackerReportRelease {
34
+ version: string;
35
+ released_at: string;
36
+ source_path: string;
37
+ }
38
+ export interface TrackerReportQuestion {
39
+ record: QuestionRecord;
40
+ /** Loaded through the existing company-checked question body reader. */
41
+ body: QuestionBody;
42
+ current_audience: ReportAudience;
43
+ source_path: string;
44
+ }
45
+ export interface TrackerReportTeamInput {
46
+ company: string;
47
+ team: string;
48
+ project_id: string;
49
+ stories: readonly TrackerReportStory[];
50
+ pull_requests: readonly TrackerReportPullRequest[];
51
+ releases: readonly TrackerReportRelease[];
52
+ blockers: readonly TrackerReportRecord[];
53
+ kpis: readonly TrackerReportRecord[];
54
+ okrs: readonly TrackerReportRecord[];
55
+ directives: readonly TrackerReportRecord[];
56
+ questions: readonly TrackerReportQuestion[];
57
+ errors?: readonly TrackerReportIssue[];
58
+ }
59
+ export interface TrackerReportInput {
60
+ company: string;
61
+ generated_at: string;
62
+ since: string;
63
+ since_source: "explicit" | "previous_report" | "first_run";
64
+ teams: readonly TrackerReportTeamInput[];
65
+ }
66
+ export interface ReportTimedStory {
67
+ id: string;
68
+ title: string;
69
+ at: string;
70
+ references: string[];
71
+ }
72
+ export interface ReportPullRequest {
73
+ number: string;
74
+ title: string;
75
+ merged_at: string;
76
+ }
77
+ export interface ReportRelease {
78
+ version: string;
79
+ released_at: string;
80
+ }
81
+ export interface ReportKpiValue {
82
+ kpi_id: string;
83
+ name: string;
84
+ at: string;
85
+ value: number;
86
+ unit: string;
87
+ source: string;
88
+ }
89
+ export interface ReportQuestion {
90
+ question_id: string;
91
+ question: string;
92
+ asked_at: string;
93
+ audience: ReportAudience;
94
+ options: string[];
95
+ recommended?: string;
96
+ answer?: {
97
+ choice: string;
98
+ answered_at: string;
99
+ answered_by: string;
100
+ };
101
+ }
102
+ export interface ReportBlocker {
103
+ id: string;
104
+ story_id: string;
105
+ what: string;
106
+ waiting_on: string;
107
+ next_step: string;
108
+ opened_at: string;
109
+ }
110
+ export interface ReportDirective {
111
+ id: string;
112
+ text: string;
113
+ date: string;
114
+ acknowledgements: Array<{
115
+ team: string;
116
+ lane_id: string;
117
+ at: string;
118
+ }>;
119
+ }
120
+ export interface TrackerOwnerTeamReport {
121
+ team: string;
122
+ project_id: string;
123
+ errors: TrackerReportIssue[];
124
+ progress: {
125
+ stories_moved_to_done: ReportTimedStory[];
126
+ pull_requests_merged: ReportPullRequest[];
127
+ releases: ReportRelease[];
128
+ kpi_values: ReportKpiValue[];
129
+ };
130
+ questions_waiting_on_owner: ReportQuestion[];
131
+ open_blockers: ReportBlocker[];
132
+ decisions: {
133
+ directives: ReportDirective[];
134
+ answered_questions: ReportQuestion[];
135
+ };
136
+ }
137
+ export interface TrackerOwnerReport {
138
+ schema_version: "hq-lanes-owner-report.v1";
139
+ company: string;
140
+ generated_at: string;
141
+ since: string;
142
+ since_source: TrackerReportInput["since_source"];
143
+ teams: TrackerOwnerTeamReport[];
144
+ }
145
+ export interface TrackerTeamKpi {
146
+ id: string;
147
+ name: string;
148
+ definition: string;
149
+ unit: string;
150
+ target: number;
151
+ direction: "up" | "down";
152
+ status: ApprovalState;
153
+ latest_value: {
154
+ at: string;
155
+ value: number;
156
+ source: string;
157
+ } | null;
158
+ recent_values: Array<{
159
+ at: string;
160
+ value: number;
161
+ source: string;
162
+ }>;
163
+ trend: "improving" | "regressing" | "flat" | "insufficient_data";
164
+ }
165
+ export interface TrackerTeamOkr {
166
+ id: string;
167
+ objective: string;
168
+ period: string;
169
+ status: ApprovalState;
170
+ key_results: Array<{
171
+ text: string;
172
+ kpi_id: string;
173
+ target: number;
174
+ }>;
175
+ }
176
+ export interface TrackerTeamPage {
177
+ schema_version: "hq-lanes-team-page.v1";
178
+ company: string;
179
+ team: string;
180
+ project_id: string;
181
+ since: string;
182
+ errors: TrackerReportIssue[];
183
+ okrs: TrackerTeamOkr[];
184
+ kpis: TrackerTeamKpi[];
185
+ work_in_progress: Array<{
186
+ id: string;
187
+ title: string;
188
+ status: ReportStoryStatus;
189
+ references: string[];
190
+ }>;
191
+ work_done_since: ReportTimedStory[];
192
+ blockers: ReportBlocker[];
193
+ questions: ReportQuestion[];
194
+ }
195
+ /** Pure report projection. The caller owns loading, tenant checks, body reads and report-time persistence. */
196
+ export declare function buildTrackerOwnerReport(input: TrackerReportInput): TrackerOwnerReport;
197
+ /** Pure team page projection for already loaded, company-checked records. */
198
+ export declare function buildTrackerTeamPage(team: TrackerReportTeamInput, company: string, sinceValue: string, generatedAtValue: string): TrackerTeamPage;
199
+ /** Human-readable owner report, rendered without editing after the command runs. */
200
+ export declare function renderTrackerOwnerReportText(report: TrackerOwnerReport): string;
201
+ /** Human-readable team page, with proposal status visible beside each objective and KPI. */
202
+ export declare function renderTrackerTeamPageText(page: TrackerTeamPage): string;
203
+ //# sourceMappingURL=tracker-report.d.ts.map