@indigoai-us/hq-cli 5.219.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.221.0] — 2026-09-26
6
+
7
+ ### Added
8
+
9
+ - `hq lanes report` prints the company-scoped owner report and `hq lanes team
10
+ <slug>` shows OKRs, KPI trends, work, blockers, and questions. Successful
11
+ reports save their per-company report time for the next report and team page.
12
+ PR, release, and deployment references on stories may be strings, integers,
13
+ or objects. Only timestamped objects count as activity. Invalid rows appear
14
+ under their team's errors, set exit status 2, and leave the saved report time
15
+ unchanged.
16
+
17
+ ## [5.220.0] — 2026-09-26
18
+
19
+ ### Fixed
20
+
21
+ - Lane questions now resolve their company from the lane's project before using
22
+ the active HQ company. Project ownership is used only when unique; otherwise
23
+ active-company selection applies without choosing a project owner. Legacy
24
+ questions can be answered with `--company <slug>` when lane or project
25
+ ownership cannot be resolved.
26
+
27
+ - CLI updates no longer interrupt background flag refreshes when the
28
+ self-contained worker rollout is enabled.
29
+
5
30
  ## [5.219.0] — 2026-09-26
6
31
 
7
32
  ### Added
@@ -7257,6 +7257,43 @@ export declare const COMMAND_CATALOG: readonly [{
7257
7257
  readonly description: "JSON on stdout (always)";
7258
7258
  }];
7259
7259
  readonly subcommands: readonly [];
7260
+ }, {
7261
+ readonly name: "report";
7262
+ readonly description: "Generate the owner report for tracker teams";
7263
+ readonly aliases: readonly [];
7264
+ readonly hidden: false;
7265
+ readonly usage: "[options]";
7266
+ readonly arguments: readonly [];
7267
+ readonly options: readonly [{
7268
+ readonly flags: "--company <slug>";
7269
+ readonly description: "Select the company by slug";
7270
+ }, {
7271
+ readonly flags: "--since <iso>";
7272
+ readonly description: "Report activity from this ISO timestamp";
7273
+ }, {
7274
+ readonly flags: "--json";
7275
+ readonly description: "Emit JSON instead of the owner report text";
7276
+ }];
7277
+ readonly subcommands: readonly [];
7278
+ }, {
7279
+ readonly name: "team";
7280
+ readonly description: "Show a team's OKRs, KPIs, work, blockers, and questions";
7281
+ readonly aliases: readonly [];
7282
+ readonly hidden: false;
7283
+ readonly usage: "[options] <slug>";
7284
+ readonly arguments: readonly [{
7285
+ readonly name: "slug";
7286
+ readonly required: true;
7287
+ readonly variadic: false;
7288
+ }];
7289
+ readonly options: readonly [{
7290
+ readonly flags: "--company <slug>";
7291
+ readonly description: "Select the company by slug";
7292
+ }, {
7293
+ readonly flags: "--json";
7294
+ readonly description: "Emit JSON instead of the team page text";
7295
+ }];
7296
+ readonly subcommands: readonly [];
7260
7297
  }, {
7261
7298
  readonly name: "outpost-pruner";
7262
7299
  readonly description: "Inspect, install, and run safe outpost worktree pruning";
@@ -9381,6 +9381,54 @@ export const COMMAND_CATALOG = [
9381
9381
  ],
9382
9382
  "subcommands": []
9383
9383
  },
9384
+ {
9385
+ "name": "report",
9386
+ "description": "Generate the owner report for tracker teams",
9387
+ "aliases": [],
9388
+ "hidden": false,
9389
+ "usage": "[options]",
9390
+ "arguments": [],
9391
+ "options": [
9392
+ {
9393
+ "flags": "--company <slug>",
9394
+ "description": "Select the company by slug"
9395
+ },
9396
+ {
9397
+ "flags": "--since <iso>",
9398
+ "description": "Report activity from this ISO timestamp"
9399
+ },
9400
+ {
9401
+ "flags": "--json",
9402
+ "description": "Emit JSON instead of the owner report text"
9403
+ }
9404
+ ],
9405
+ "subcommands": []
9406
+ },
9407
+ {
9408
+ "name": "team",
9409
+ "description": "Show a team's OKRs, KPIs, work, blockers, and questions",
9410
+ "aliases": [],
9411
+ "hidden": false,
9412
+ "usage": "[options] <slug>",
9413
+ "arguments": [
9414
+ {
9415
+ "name": "slug",
9416
+ "required": true,
9417
+ "variadic": false
9418
+ }
9419
+ ],
9420
+ "options": [
9421
+ {
9422
+ "flags": "--company <slug>",
9423
+ "description": "Select the company by slug"
9424
+ },
9425
+ {
9426
+ "flags": "--json",
9427
+ "description": "Emit JSON instead of the team page text"
9428
+ }
9429
+ ],
9430
+ "subcommands": []
9431
+ },
9384
9432
  {
9385
9433
  "name": "outpost-pruner",
9386
9434
  "description": "Inspect, install, and run safe outpost worktree pruning",
@@ -1,6 +1,7 @@
1
1
  import type { Command } from "commander";
2
- import type { LaneCompanyRef } from "../lib/lanes/types.js";
3
- interface LaneChecksDeps {
2
+ import { type TrackerKind, type TrackerRecord } from "../lib/lanes/tracker-store.js";
3
+ import type { LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
4
+ export interface LaneChecksDeps {
4
5
  hqRoot: () => string;
5
6
  resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
6
7
  trackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
@@ -8,6 +9,39 @@ interface LaneChecksDeps {
8
9
  now: () => Date;
9
10
  }
10
11
  type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
12
+ export interface ProjectMapping {
13
+ projectId: string;
14
+ team: string;
15
+ leadLaneId?: string;
16
+ statusPath: string;
17
+ }
18
+ export interface ProjectReadResult {
19
+ stories: Array<Record<string, unknown>>;
20
+ mtime: number;
21
+ errors?: Array<{
22
+ path: string;
23
+ field: string;
24
+ message: string;
25
+ story_id?: string;
26
+ }>;
27
+ }
28
+ export declare function loadProjectMappings(root: string, company: string, requestedProject?: string): Map<string, ProjectMapping>;
29
+ export declare function readProject(root: string, company: string, prdPath: string, tolerantStories?: boolean): ProjectReadResult;
30
+ export declare function loadCompanyQuestionRecords(root: string, company: LaneCompanyRef, projectId?: string): QuestionRecord[];
31
+ export declare function pathForTrackerRecord(projectDirectory: string, kind: TrackerKind, id: string): string;
32
+ export declare function listTrackerRecords(projectDirectory: string, kind: TrackerKind): TrackerRecord[];
33
+ export interface TolerantTrackerReadIssue {
34
+ path: string;
35
+ field: string;
36
+ message: string;
37
+ record_id?: string;
38
+ }
39
+ /** Report loader that preserves valid records while making every unreadable file visible. */
40
+ export declare function listTrackerRecordsTolerant(projectDirectory: string, kind: TrackerKind): {
41
+ records: TrackerRecord[];
42
+ errors: TolerantTrackerReadIssue[];
43
+ };
44
+ export declare function resolveTrackerCompany(deps: LaneChecksDeps, requested?: string): Promise<LaneCompanyRef>;
11
45
  export declare function registerLaneChecksCommand(parent: Command, deps: LaneChecksDeps, wrap: WrapHandler): void;
12
46
  export {};
13
47
  //# sourceMappingURL=lanes-checks.d.ts.map
@@ -171,7 +171,7 @@ function projectHasTrackerRecords(root, company, projectId) {
171
171
  }
172
172
  return false;
173
173
  }
174
- function loadProjectMappings(root, company, requestedProject) {
174
+ export function loadProjectMappings(root, company, requestedProject) {
175
175
  const registryPath = path.join(root, "workspace", "lane-briefs", "juniors.json");
176
176
  const registry = readJsonObject(registryPath, "empty");
177
177
  const mappings = new Map();
@@ -215,7 +215,7 @@ function loadProjectMappings(root, company, requestedProject) {
215
215
  }
216
216
  return mappings;
217
217
  }
218
- function readProject(root, company, prdPath) {
218
+ export function readProject(root, company, prdPath, tolerantStories = false) {
219
219
  checkCompanyPath(root, company, path.relative(root, prdPath), prdPath);
220
220
  const parsed = readJsonObject(prdPath);
221
221
  if (parsed === null)
@@ -236,17 +236,26 @@ function readProject(root, company, prdPath) {
236
236
  const rawStories = parsed.userStories === undefined ? parsed.stories : parsed.userStories;
237
237
  if (!Array.isArray(rawStories))
238
238
  return fail("tracker_checks_input_invalid", "Project task list must contain a userStories or stories array", { path: prdPath });
239
- const stories = rawStories.map((story, index) => {
239
+ const errors = [];
240
+ const stories = rawStories.flatMap((story, index) => {
240
241
  if (!isObject(story) || typeof story.id !== "string" || story.id.trim() === "") {
242
+ if (tolerantStories) {
243
+ errors.push({ path: prdPath, field: `userStories[${index}]`, message: "Project task list contains a story without an id" });
244
+ return [];
245
+ }
241
246
  return fail("tracker_checks_input_invalid", "Project task list contains a story without an id", { path: prdPath, index });
242
247
  }
243
248
  if (story.prs !== undefined && !Array.isArray(story.prs)) {
249
+ if (tolerantStories) {
250
+ errors.push({ path: prdPath, field: "prs", message: "Project story prs must be an array", story_id: story.id });
251
+ return [{ ...story, prs: [] }];
252
+ }
244
253
  return fail("tracker_checks_input_invalid", "Project story prs must be an array", { path: prdPath, story_id: story.id });
245
254
  }
246
- return story;
255
+ return [story];
247
256
  });
248
257
  try {
249
- return { stories, mtime: fs.statSync(prdPath).mtimeMs };
258
+ return { stories, mtime: fs.statSync(prdPath).mtimeMs, ...(tolerantStories ? { errors } : {}) };
250
259
  }
251
260
  catch (err) {
252
261
  return fail("tracker_checks_input_unreadable", `Project task list could not be stat'ed: ${err instanceof Error ? err.message : String(err)}`, { path: prdPath });
@@ -352,7 +361,7 @@ function questionMetadataMatchesScope(record, company, projectId) {
352
361
  return false;
353
362
  return true;
354
363
  }
355
- function readCompanyQuestions(root, company, projectId) {
364
+ export function loadCompanyQuestionRecords(root, company, projectId) {
356
365
  const records = listQuestionIds(root)
357
366
  .flatMap((id) => {
358
367
  const file = questionRecordPath(root, id);
@@ -365,6 +374,10 @@ function readCompanyQuestions(root, company, projectId) {
365
374
  return question.company?.slug === company.slug && question.company.uid === company.uid &&
366
375
  (projectId === undefined || question.project_id === projectId) ? [question] : [];
367
376
  });
377
+ return records;
378
+ }
379
+ function readCompanyQuestions(root, company, projectId) {
380
+ const records = loadCompanyQuestionRecords(root, company, projectId);
368
381
  const surfacedAt = questionSurfacingState(root, records.map((question) => ({
369
382
  id: question.question_id,
370
383
  status: question.status,
@@ -418,7 +431,7 @@ function readTeamInputs(root, company, projects, lanes) {
418
431
  }
419
432
  return inputs;
420
433
  }
421
- function pathForTrackerRecord(projectDirectory, kind, id) {
434
+ export function pathForTrackerRecord(projectDirectory, kind, id) {
422
435
  return path.join(projectDirectory, "tracker", kind, `${id}.json`);
423
436
  }
424
437
  function recordInvalid(kind, record, projectDirectory, field) {
@@ -435,7 +448,7 @@ function requiredRecordDate(kind, record, projectDirectory, field) {
435
448
  return recordInvalid(kind, record, projectDirectory, field);
436
449
  return value;
437
450
  }
438
- function listTrackerRecords(projectDirectory, kind) {
451
+ export function listTrackerRecords(projectDirectory, kind) {
439
452
  try {
440
453
  return createLocalTrackerStore(projectDirectory).list(kind);
441
454
  }
@@ -445,6 +458,51 @@ function listTrackerRecords(projectDirectory, kind) {
445
458
  return fail("tracker_checks_store_unreadable", `Tracker ${kind} records could not be read: ${err instanceof Error ? err.message : String(err)}`, { path: path.join(projectDirectory, "tracker", kind) });
446
459
  }
447
460
  }
461
+ /** Report loader that preserves valid records while making every unreadable file visible. */
462
+ export function listTrackerRecordsTolerant(projectDirectory, kind) {
463
+ const root = path.join(projectDirectory, "tracker");
464
+ const directory = path.join(projectDirectory, "tracker", kind);
465
+ let names;
466
+ try {
467
+ const rootStat = fs.lstatSync(root);
468
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
469
+ return { records: [], errors: [{ path: root, field: "directory", message: "Tracker storage path must be a regular directory" }] };
470
+ }
471
+ const stat = fs.lstatSync(directory);
472
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
473
+ return { records: [], errors: [{ path: directory, field: "directory", message: "Tracker record path must be a regular directory" }] };
474
+ }
475
+ names = fs.readdirSync(directory).filter((name) => name.endsWith(".json")).sort();
476
+ }
477
+ catch (err) {
478
+ if (err.code === "ENOENT")
479
+ return { records: [], errors: [] };
480
+ return { records: [], errors: [{ path: directory, field: "directory", message: `Tracker records could not be listed: ${err instanceof Error ? err.message : String(err)}` }] };
481
+ }
482
+ const store = createLocalTrackerStore(projectDirectory);
483
+ const records = [];
484
+ const errors = [];
485
+ for (const name of names) {
486
+ const id = name.slice(0, -5);
487
+ const file = path.join(directory, name);
488
+ try {
489
+ const record = store.read(kind, id);
490
+ if (record !== null)
491
+ records.push(record);
492
+ }
493
+ catch (err) {
494
+ const details = isLanesError(err) ? err.details : {};
495
+ errors.push({
496
+ path: typeof details.path === "string" ? details.path : file,
497
+ field: typeof details.field === "string" ? details.field : "record",
498
+ message: err instanceof Error ? err.message : String(err),
499
+ record_id: typeof details.id === "string" ? details.id : id,
500
+ });
501
+ }
502
+ }
503
+ records.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.id.localeCompare(b.id));
504
+ return { records, errors };
505
+ }
448
506
  function readTrackerCheckRecords(root, company, projects) {
449
507
  const blockers = [];
450
508
  const kpis = [];
@@ -499,7 +557,7 @@ function readTrackerCheckRecords(root, company, projects) {
499
557
  }
500
558
  return { blockers, kpis, directives };
501
559
  }
502
- async function resolveTrackerCompany(deps, requested) {
560
+ export async function resolveTrackerCompany(deps, requested) {
503
561
  const company = await deps.resolveCompany(requested);
504
562
  if (!company.slug)
505
563
  fail("tracker_company_unresolved", "The active HQ company could not be resolved", {});
@@ -0,0 +1,11 @@
1
+ import type { Command } from "commander";
2
+ import { type QuestionBody } from "../lib/lanes/questions.js";
3
+ import type { QuestionRecord } from "../lib/lanes/types.js";
4
+ import { type LaneChecksDeps } from "./lanes-checks.js";
5
+ type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
6
+ export interface LaneReportsDeps extends LaneChecksDeps {
7
+ readQuestionBody: (root: string, question: QuestionRecord) => QuestionBody;
8
+ }
9
+ export declare function registerLaneReportCommands(parent: Command, deps: LaneReportsDeps, wrap: WrapHandler): void;
10
+ export {};
11
+ //# sourceMappingURL=lanes-report.d.ts.map
@@ -0,0 +1,330 @@
1
+ import * as path from "node:path";
2
+ import { LanesError } from "../lib/lanes/errors.js";
3
+ import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
4
+ import { questionRecordPath } from "../lib/lanes/store.js";
5
+ import { normalizeIsoTimestamp } from "../lib/lanes/timestamp.js";
6
+ import { mapSourceStoryStatus } from "../lib/mesh/story-status.js";
7
+ import { buildTrackerOwnerReport, buildTrackerTeamPage, renderTrackerOwnerReportText, renderTrackerTeamPageText, } from "../lib/lanes/tracker-report.js";
8
+ import { FIRST_TRACKER_REPORT_SINCE, readTrackerReportState, writeTrackerReportState, } from "../lib/lanes/tracker-report-state.js";
9
+ import { listTrackerRecordsTolerant, loadCompanyQuestionRecords, loadProjectMappings, pathForTrackerRecord, readProject, resolveTrackerCompany, } from "./lanes-checks.js";
10
+ function fail(error, message, details = {}) {
11
+ throw new LanesError(error, message, details);
12
+ }
13
+ function canonicalStoryStatus(story, sourcePath, storyId) {
14
+ try {
15
+ const rawStatus = story.state ?? story.status;
16
+ if (rawStatus === "dropped")
17
+ return "dropped";
18
+ const status = mapSourceStoryStatus(story, `${sourcePath}#${storyId}`);
19
+ if (status !== undefined)
20
+ return status;
21
+ return story.passes === true ? "done" : "queued";
22
+ }
23
+ catch (err) {
24
+ return fail("tracker_report_story_invalid", err instanceof Error ? err.message : String(err), { path: sourcePath, story_id: storyId, field: "state" });
25
+ }
26
+ }
27
+ function recordsArray(record, field, sourcePath, storyId, errors) {
28
+ const value = record[field];
29
+ if (value === undefined)
30
+ return [];
31
+ if (!Array.isArray(value)) {
32
+ errors.push({ path: sourcePath, field, story_id: storyId, message: `${field} must be an array` });
33
+ return [];
34
+ }
35
+ return value;
36
+ }
37
+ function timestampOrIssue(value, field, sourcePath, storyId, errors) {
38
+ const timestamp = normalizeIsoTimestamp(value);
39
+ if (timestamp === null) {
40
+ errors.push({ path: sourcePath, field, story_id: storyId, message: `${field} must be a valid timestamp` });
41
+ return null;
42
+ }
43
+ return timestamp;
44
+ }
45
+ function numericPr(value, url) {
46
+ if (typeof value === "string" && /^[1-9]\d*$/.test(value))
47
+ return value;
48
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0)
49
+ return String(value);
50
+ return url.match(/\/pull\/(\d+)(?:\/|$)/)?.[1] ?? null;
51
+ }
52
+ function storyReferences(story, sourcePath, storyId, errors) {
53
+ const references = new Set();
54
+ const pullRequests = [];
55
+ const releases = [];
56
+ const addPullRequest = (value, field) => {
57
+ if (typeof value === "string" && value.trim()) {
58
+ references.add(value.trim());
59
+ return;
60
+ }
61
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
62
+ references.add(`#${value}`);
63
+ return;
64
+ }
65
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
66
+ errors.push({ path: sourcePath, field, story_id: storyId, message: "Pull request entry must be a reference or object" });
67
+ return;
68
+ }
69
+ const record = value;
70
+ const url = typeof record.url === "string" ? record.url : typeof record.html_url === "string" ? record.html_url : "";
71
+ const number = numericPr(record.number, url);
72
+ const reference = typeof record.ref === "string" ? record.ref : url || (number ? `#${number}` : "");
73
+ if (reference)
74
+ references.add(reference);
75
+ else
76
+ errors.push({ path: sourcePath, field, story_id: storyId, message: "Pull request object needs a number, URL, or ref" });
77
+ const mergedAt = record.merged_at ?? record.mergedAt ?? record.at;
78
+ if (mergedAt === undefined)
79
+ return;
80
+ if (!number) {
81
+ errors.push({ path: sourcePath, field, story_id: storyId, message: "Timed pull request object needs a number or pull URL" });
82
+ return;
83
+ }
84
+ const at = timestampOrIssue(mergedAt, "merged_at", sourcePath, storyId, errors);
85
+ if (at)
86
+ pullRequests.push({ number, title: typeof record.title === "string" && record.title.trim() ? record.title : `Pull request #${number}`, merged_at: at, source_path: sourcePath });
87
+ };
88
+ const addRelease = (value, field) => {
89
+ if (typeof value === "string" && value.trim()) {
90
+ references.add(value.trim());
91
+ return;
92
+ }
93
+ if (typeof value === "number" && Number.isSafeInteger(value)) {
94
+ references.add(String(value));
95
+ return;
96
+ }
97
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
98
+ errors.push({ path: sourcePath, field, story_id: storyId, message: "Release or deployment entry must be a reference or object" });
99
+ return;
100
+ }
101
+ const record = value;
102
+ const version = typeof record.version === "string" ? record.version : typeof record.tag === "string" ? record.tag : "";
103
+ const reference = typeof record.ref === "string" ? record.ref : typeof record.url === "string" ? record.url : version;
104
+ if (reference)
105
+ references.add(reference);
106
+ else
107
+ errors.push({ path: sourcePath, field, story_id: storyId, message: "Release or deployment object needs version, tag, ref, or URL" });
108
+ const releasedAt = record.released_at ?? record.releasedAt ?? record.deployed_at ?? record.deployedAt ?? record.at;
109
+ if (releasedAt === undefined)
110
+ return;
111
+ if (!version) {
112
+ errors.push({ path: sourcePath, field, story_id: storyId, message: "Timed release or deployment object needs a version or tag" });
113
+ return;
114
+ }
115
+ const at = timestampOrIssue(releasedAt, "released_at", sourcePath, storyId, errors);
116
+ if (at)
117
+ releases.push({ version, released_at: at, source_path: sourcePath });
118
+ };
119
+ for (const field of ["prs", "pull_requests"]) {
120
+ for (const value of recordsArray(story, field, sourcePath, storyId, errors))
121
+ addPullRequest(value, field);
122
+ }
123
+ for (const field of ["releases", "deploys"]) {
124
+ for (const value of recordsArray(story, field, sourcePath, storyId, errors))
125
+ addRelease(value, field);
126
+ }
127
+ return { references: [...references].sort(), pullRequests, releases };
128
+ }
129
+ function currentAudience(question, sourcePath) {
130
+ const latest = question.routing_history.at(-1);
131
+ if (!latest || typeof latest !== "object" || Array.isArray(latest)) {
132
+ return fail("tracker_report_question_route_invalid", "Question has no valid routing history for its current audience", { question_id: question.question_id, path: sourcePath });
133
+ }
134
+ const audience = latest.audience;
135
+ if (!audience || typeof audience !== "object" || Array.isArray(audience) || typeof audience.kind !== "string") {
136
+ return fail("tracker_report_question_route_invalid", "Question current audience is malformed", { question_id: question.question_id, path: sourcePath });
137
+ }
138
+ switch (audience.kind) {
139
+ case "user": return "owner";
140
+ case "session": return "operator";
141
+ case "lane": return "lane";
142
+ default: return fail("tracker_report_question_route_invalid", "Question current audience is unsupported", { question_id: question.question_id, path: sourcePath });
143
+ }
144
+ }
145
+ function trackerRecords(root, company, project, kind, errors) {
146
+ const projectDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
147
+ const loaded = listTrackerRecordsTolerant(projectDirectory, kind);
148
+ for (const issue of loaded.errors) {
149
+ errors.push({ path: issue.path, field: issue.field, message: issue.message, ...(issue.record_id ? { record_id: issue.record_id } : {}) });
150
+ }
151
+ return loaded.records
152
+ .filter((record) => record.company === company.slug && record.project_id === project.projectId)
153
+ .map((record) => ({ ...record, source_path: pathForTrackerRecord(projectDirectory, kind, record.id) }));
154
+ }
155
+ function issueFromError(err, fallbackPath, fallbackField, ids = {}) {
156
+ const details = err instanceof LanesError ? err.details : {};
157
+ return {
158
+ path: typeof details.path === "string" ? details.path : fallbackPath,
159
+ field: typeof details.field === "string" ? details.field : fallbackField,
160
+ message: err instanceof Error ? err.message : String(err),
161
+ ...(typeof details.story_id === "string" ? { story_id: details.story_id } : ids.story_id ? { story_id: ids.story_id } : {}),
162
+ ...(typeof details.id === "string" ? { record_id: details.id } : ids.record_id ? { record_id: ids.record_id } : {}),
163
+ ...(typeof details.question_id === "string" ? { question_id: details.question_id } : ids.question_id ? { question_id: ids.question_id } : {}),
164
+ };
165
+ }
166
+ function teamInput(root, company, project, questions, deps, errors = []) {
167
+ const projectDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
168
+ const prdPath = path.join(projectDirectory, "prd.json");
169
+ let prd = { stories: [], mtime: 0 };
170
+ try {
171
+ prd = readProject(root, company.slug, prdPath, true);
172
+ for (const issue of prd.errors ?? [])
173
+ errors.push(issue);
174
+ }
175
+ catch (err) {
176
+ errors.push(issueFromError(err, prdPath, "project"));
177
+ }
178
+ const pullRequests = [];
179
+ const releases = [];
180
+ const stories = [];
181
+ for (const story of prd.stories) {
182
+ const id = typeof story.id === "string" ? story.id : "";
183
+ if (!id.trim()) {
184
+ errors.push({ path: prdPath, field: "id", message: "Story id must be a non-empty string" });
185
+ continue;
186
+ }
187
+ const title = typeof story.title === "string" ? story.title : "";
188
+ if (!title.trim()) {
189
+ errors.push({ path: prdPath, field: "title", story_id: id, message: "Story title must be a non-empty string" });
190
+ continue;
191
+ }
192
+ let status;
193
+ try {
194
+ status = canonicalStoryStatus(story, prdPath, id);
195
+ }
196
+ catch (err) {
197
+ errors.push(issueFromError(err, prdPath, "state", { story_id: id }));
198
+ continue;
199
+ }
200
+ const updatedAt = story.updated_at ?? story.updatedAt;
201
+ if (status === "done" && updatedAt === undefined) {
202
+ errors.push({ path: prdPath, field: "updated_at", story_id: id, message: "Done story is missing updated_at" });
203
+ continue;
204
+ }
205
+ let normalizedUpdatedAt;
206
+ if (updatedAt !== undefined) {
207
+ const normalized = normalizeIsoTimestamp(updatedAt);
208
+ if (normalized === null) {
209
+ errors.push({ path: prdPath, field: "updated_at", story_id: id, message: "updated_at must be a valid timestamp" });
210
+ continue;
211
+ }
212
+ normalizedUpdatedAt = normalized;
213
+ }
214
+ const refs = storyReferences(story, prdPath, id, errors);
215
+ pullRequests.push(...refs.pullRequests);
216
+ releases.push(...refs.releases);
217
+ stories.push({ id, title, status, references: refs.references, ...(normalizedUpdatedAt ? { updated_at: normalizedUpdatedAt } : {}), source_path: prdPath });
218
+ }
219
+ const teamQuestions = [];
220
+ for (const record of questions.filter((question) => question.project_id === project.projectId)) {
221
+ const sourcePath = questionRecordPath(root, record.question_id);
222
+ try {
223
+ const body = deps.readQuestionBody(root, record);
224
+ assertQuestionBodySecretSafe(body);
225
+ teamQuestions.push({ record, body, current_audience: currentAudience(record, sourcePath), source_path: sourcePath });
226
+ }
227
+ catch (err) {
228
+ errors.push(issueFromError(err, sourcePath, "body", { question_id: record.question_id }));
229
+ }
230
+ }
231
+ return {
232
+ company: company.slug,
233
+ team: project.team,
234
+ project_id: project.projectId,
235
+ stories,
236
+ pull_requests: pullRequests,
237
+ releases,
238
+ blockers: trackerRecords(root, company, project, "blocker", errors),
239
+ kpis: trackerRecords(root, company, project, "kpi", errors),
240
+ okrs: trackerRecords(root, company, project, "okr", errors),
241
+ directives: trackerRecords(root, company, project, "directive", errors),
242
+ questions: teamQuestions,
243
+ errors,
244
+ };
245
+ }
246
+ function allTeamInputs(root, company, deps) {
247
+ const mappings = loadProjectMappings(root, company.slug);
248
+ return [...mappings.values()]
249
+ .sort((a, b) => a.team.localeCompare(b.team) || a.projectId.localeCompare(b.projectId))
250
+ .map((project) => {
251
+ const errors = [];
252
+ let questions = [];
253
+ try {
254
+ questions = loadCompanyQuestionRecords(root, company, project.projectId);
255
+ }
256
+ catch (err) {
257
+ errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", project.projectId), "questions"));
258
+ }
259
+ return teamInput(root, company, project, questions, deps, errors);
260
+ });
261
+ }
262
+ function sinceForTeam(root, company) {
263
+ return readTrackerReportState(root, company)?.last_report_at ?? FIRST_TRACKER_REPORT_SINCE;
264
+ }
265
+ function validateTeamSlug(team) {
266
+ const value = team.trim();
267
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value) || value === "." || value === "..") {
268
+ return fail("tracker_team_invalid", "Team slug must be a safe path segment", { team });
269
+ }
270
+ return value;
271
+ }
272
+ export function registerLaneReportCommands(parent, deps, wrap) {
273
+ parent.command("report")
274
+ .description("Generate the owner report for tracker teams")
275
+ .option("--company <slug>", "Select the company by slug")
276
+ .option("--since <iso>", "Report activity from this ISO timestamp")
277
+ .option("--json", "Emit JSON instead of the owner report text")
278
+ .action(wrap(async (options) => {
279
+ const company = await resolveTrackerCompany(deps, options.company);
280
+ const root = deps.hqRoot();
281
+ const generatedAt = deps.now().toISOString();
282
+ const previous = options.since === undefined ? readTrackerReportState(root, company.slug) : null;
283
+ const since = options.since ?? previous?.last_report_at ?? FIRST_TRACKER_REPORT_SINCE;
284
+ const input = {
285
+ company: company.slug,
286
+ generated_at: generatedAt,
287
+ since,
288
+ since_source: options.since !== undefined ? "explicit" : previous ? "previous_report" : "first_run",
289
+ teams: allTeamInputs(root, company, deps),
290
+ };
291
+ const report = buildTrackerOwnerReport(input);
292
+ const hasErrors = report.teams.some((team) => team.errors.length > 0);
293
+ if (hasErrors)
294
+ process.exitCode = 2;
295
+ else
296
+ writeTrackerReportState(root, company.slug, generatedAt);
297
+ console.log(options.json ? JSON.stringify(report, null, 2) : renderTrackerOwnerReportText(report));
298
+ }));
299
+ parent.command("team")
300
+ .description("Show a team's OKRs, KPIs, work, blockers, and questions")
301
+ .argument("<slug>", "Team slug")
302
+ .option("--company <slug>", "Select the company by slug")
303
+ .option("--json", "Emit JSON instead of the team page text")
304
+ .action(wrap(async (slug, options) => {
305
+ const team = validateTeamSlug(slug);
306
+ const company = await resolveTrackerCompany(deps, options.company);
307
+ const root = deps.hqRoot();
308
+ const since = sinceForTeam(root, company.slug);
309
+ const candidates = [...loadProjectMappings(root, company.slug).values()].filter((project) => project.team === team);
310
+ if (candidates.length === 0)
311
+ fail("tracker_team_not_found", "Team was not found for this company", { team, company: company.slug });
312
+ if (candidates.length > 1)
313
+ fail("tracker_team_ambiguous", "More than one project maps to this team", { team, company: company.slug, project_ids: candidates.map((item) => item.projectId) });
314
+ const generatedAt = deps.now().toISOString();
315
+ const errors = [];
316
+ let questions = [];
317
+ try {
318
+ questions = loadCompanyQuestionRecords(root, company, candidates[0].projectId);
319
+ }
320
+ catch (err) {
321
+ errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", candidates[0].projectId), "questions"));
322
+ }
323
+ const input = teamInput(root, company, candidates[0], questions, deps, errors);
324
+ const page = buildTrackerTeamPage(input, company.slug, since, generatedAt);
325
+ if (page.errors.length > 0)
326
+ process.exitCode = 2;
327
+ console.log(options.json ? JSON.stringify(page, null, 2) : renderTrackerTeamPageText(page));
328
+ }));
329
+ }
330
+ //# sourceMappingURL=lanes-report.js.map