@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.
@@ -17,7 +17,10 @@ interface WorkerLike {
17
17
  }
18
18
  export interface FlagRegistryDependencies {
19
19
  endpoint?: string;
20
- createWorker?: (filename: URL, options: WorkerOptions) => WorkerLike;
20
+ createWorker?: (filename: string | URL, options: WorkerOptions) => WorkerLike;
21
+ /** Test seam for exercising the no-source fail-open path. */
22
+ workerSource?: string;
23
+ captureWorkerFailure?: (error: Error, tags: Record<string, string>) => void;
21
24
  }
22
25
  export interface ProcessFlagCompanyContext {
23
26
  companyUid?: string;
@@ -12,8 +12,10 @@ import { readMachineCompanyBinding } from "./work-context/company.js";
12
12
  import { readSessionState } from "./work-context/state.js";
13
13
  import { workContextRoot } from "./work-context/paths.js";
14
14
  import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
15
- import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
15
+ import { loadCachedTokens, resolveDefaultHqRoot } from "../utils/cognito-session.js";
16
16
  import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
17
+ import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
18
+ import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
17
19
  export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
18
20
  const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
19
21
  const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
@@ -99,6 +101,75 @@ function processSnapshotReader(companyIdentifiers) {
99
101
  },
100
102
  };
101
103
  }
104
+ function personSubjectForWorkerMode(idToken) {
105
+ if (typeof idToken !== "string")
106
+ return null;
107
+ const payload = idToken.split(".")[1];
108
+ if (!payload)
109
+ return null;
110
+ try {
111
+ const subject = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).sub;
112
+ return typeof subject === "string" && subject.trim() ? subject : null;
113
+ }
114
+ catch {
115
+ Sentry.captureMessage("Could not scope cached flag-registry worker mode because the cached ID token payload is invalid", { level: "warning", tags: { component: "flag-registry-worker-mode-cache" } });
116
+ return null;
117
+ }
118
+ }
119
+ /** Scope a persisted rollout value to the same identity that fetched it. */
120
+ function workerModeCacheScopes(company) {
121
+ const tokens = loadCachedTokens();
122
+ const personSubject = tokens ? personSubjectForWorkerMode(tokens.idToken) : null;
123
+ if (tokens && !personSubject)
124
+ return null;
125
+ return [
126
+ ...(company.companyUid ? [`company:${company.companyUid}`] : []),
127
+ ...(company.companySlug ? [`company-slug:${company.companySlug}`] : []),
128
+ ...(personSubject ? [`person:${personSubject}`] : []),
129
+ ];
130
+ }
131
+ function selfContainedWorkerEnabled(company) {
132
+ const localOverride = readLocalOverride(FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, process.env);
133
+ if (localOverride !== null)
134
+ return localOverride;
135
+ const scopes = workerModeCacheScopes(company);
136
+ if (!scopes)
137
+ return false;
138
+ const filePath = flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes);
139
+ try {
140
+ return readCachedFlagRegistryWorkerMode(filePath)?.value ?? false;
141
+ }
142
+ catch (error) {
143
+ Sentry.captureException(error, {
144
+ tags: { component: "flag-registry-worker-mode-cache", worker: "flag-registry" },
145
+ });
146
+ return false;
147
+ }
148
+ }
149
+ function cacheWorkerModeFromSnapshot(snapshot, company) {
150
+ // A developer's local hq-flags override is process-local and must not become
151
+ // the value selected by a future process.
152
+ if (readLocalOverride(FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, process.env) !== null)
153
+ return;
154
+ const scopes = workerModeCacheScopes(company);
155
+ if (!scopes)
156
+ return;
157
+ const decision = resolveFlag({
158
+ flagKey: FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG,
159
+ env: {},
160
+ snapshot,
161
+ companyIdentifiers: [company.companyUid, company.companySlug].filter((value) => Boolean(value)),
162
+ lookup: { fallback: false },
163
+ });
164
+ try {
165
+ writeCachedFlagRegistryWorkerMode(flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes), { version: snapshot.version, value: decision.value });
166
+ }
167
+ catch (error) {
168
+ Sentry.captureException(error, {
169
+ tags: { component: "flag-registry-worker-mode-cache", worker: "flag-registry" },
170
+ });
171
+ }
172
+ }
102
173
  /**
103
174
  * Begin one registry refresh without awaiting it or retaining the CLI process.
104
175
  *
@@ -117,23 +188,43 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
117
188
  const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
118
189
  processReader = processSnapshotReader(companyIdentifiers);
119
190
  try {
191
+ let workerSource = new URL("./flag-registry-worker.js", import.meta.url);
192
+ let workerOptions = { workerData: {
193
+ endpoint,
194
+ ...(companyUid ? { companyUid } : {}),
195
+ ...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
196
+ } };
197
+ if (selfContainedWorkerEnabled(company)) {
198
+ // The worker source is generated and embedded in this parent module at
199
+ // build time. It is already resident before company, identity, and cache
200
+ // reads can yield to an updater replacing this package tree.
201
+ const embeddedSource = dependencies.workerSource ?? FLAG_REGISTRY_WORKER_SOURCE;
202
+ if (embeddedSource.length === 0) {
203
+ (dependencies.captureWorkerFailure ?? ((error, tags) => {
204
+ Sentry.captureException(error, { tags });
205
+ }))(new Error("Self-contained flag-registry worker source is unavailable"), {
206
+ component: "flag-registry-worker-startup",
207
+ worker: "flag-registry",
208
+ flag_worker_failure: "embedded_source_unavailable",
209
+ });
210
+ return processReader;
211
+ }
212
+ workerSource = embeddedSource;
213
+ workerOptions = { ...workerOptions, eval: true };
214
+ }
120
215
  // The SDK's child-process integration captures every Worker error unless
121
216
  // its process-wide captureWorkerErrors option is disabled. Scope only this
122
217
  // worker so the reporting boundary can classify its startup errors without
123
218
  // changing capture for unrelated workers.
124
219
  const worker = Sentry.withScope((scope) => {
125
220
  scope.setTag("worker", "flag-registry");
126
- return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(new URL("./flag-registry-worker.js", import.meta.url), {
127
- workerData: {
128
- endpoint,
129
- ...(companyUid ? { companyUid } : {}),
130
- ...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
131
- },
132
- });
221
+ return (dependencies.createWorker ?? ((filename, options) => new Worker(filename, options)))(workerSource, workerOptions);
133
222
  });
134
223
  worker.on("message", (value) => {
135
- if (isFlagSnapshot(value))
224
+ if (isFlagSnapshot(value)) {
136
225
  heldSnapshot = value;
226
+ cacheWorkerModeFromSnapshot(value, company);
227
+ }
137
228
  });
138
229
  // A failed background refresh must stay invisible to an offline CLI.
139
230
  worker.on("error", () => { });
@@ -16,7 +16,15 @@ export declare function assertQuestionBodySecretSafe(body: QuestionBody): void;
16
16
  export declare function questionRecordRefusalCode(err: unknown): "secret_text_refused" | "question_company_unavailable" | null;
17
17
  export declare function parseQuestionBodyStore(value: unknown): QuestionBodyStore;
18
18
  export declare function bodyStoreForLane(lane: Pick<LaneRecord, "company" | "mesh_company">, resolvedCompany?: LaneCompanyRef): QuestionBodyStore;
19
- export declare function resolveQuestionCompany(lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company">, activeCompany?: LaneCompanyRef | null): LaneCompanyRef;
19
+ export declare function resolveQuestionCompany(lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company" | "project_id">, activeCompany?: LaneCompanyRef | null, hqRoot?: string): LaneCompanyRef;
20
+ /**
21
+ * Resolve project ownership only when exactly one manifest company contains
22
+ * the project's directory. Multiple matches are unresolved so the caller can
23
+ * continue to an explicit active-company choice without picking an owner.
24
+ */
25
+ export declare function resolveProjectQuestionCompany(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "project_id">): LaneCompanyRef | null;
26
+ /** Resolve the lane/project company before lazily consulting active HQ scope. */
27
+ export declare function resolveQuestionCompanyWithFallback(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company" | "project_id">, resolveActiveCompany?: () => Promise<LaneCompanyRef | null>): Promise<LaneCompanyRef>;
20
28
  export declare function questionBodyRef(questionId: string): string;
21
29
  export declare function sha256Text(text: string): string;
22
30
  /** Tenant-store path for a question body (spec §6.2 / §10). Never the personal vault for company prose. */
@@ -8,6 +8,7 @@ import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { LanesError } from "./errors.js";
10
10
  import { assertNoSecretText } from "./text-scan.js";
11
+ import { readCompaniesManifestMap } from "../work-context/repo-remote.js";
11
12
  import { appendCappedHistory, createQuestion, retryUpdateQuestion, } from "./store.js";
12
13
  import { QUESTION_BODY_STORES, } from "./types.js";
13
14
  export function assertQuestionBodySecretSafe(body) {
@@ -39,13 +40,91 @@ export function parseQuestionBodyStore(value) {
39
40
  export function bodyStoreForLane(lane, resolvedCompany) {
40
41
  return lane.company || lane.mesh_company || resolvedCompany ? "company" : "personal";
41
42
  }
42
- export function resolveQuestionCompany(lane, activeCompany) {
43
- const company = lane.company ?? lane.mesh_company ?? activeCompany;
43
+ export function resolveQuestionCompany(lane, activeCompany, hqRoot) {
44
+ const laneCompany = lane.company ?? lane.mesh_company;
45
+ if (laneCompany !== null && laneCompany !== undefined) {
46
+ return requireQuestionCompany(laneCompany, lane.lane_id, "lane");
47
+ }
48
+ if (hqRoot) {
49
+ const projectCompany = resolveProjectQuestionCompany(hqRoot, lane);
50
+ if (projectCompany)
51
+ return projectCompany;
52
+ }
53
+ const company = activeCompany;
44
54
  if (!company || !company.slug.trim() || !company.uid.trim()) {
45
- throw new LanesError("question_company_unavailable", `Cannot raise a question for lane ${lane.lane_id}: no lane, project, or active company could be resolved`, { lane_id: lane.lane_id });
55
+ throw new LanesError("question_company_unavailable", `Cannot raise a question for lane ${lane.lane_id}: no lane, project, or active company could be resolved`, { lane_id: lane.lane_id, resolution: "active_company_required" });
56
+ }
57
+ return company;
58
+ }
59
+ function requireQuestionCompany(company, laneId, source) {
60
+ if (!company.slug.trim() || !company.uid.trim()) {
61
+ throw new LanesError("question_company_unavailable", `Cannot resolve the ${source} company for lane ${laneId}: its slug or uid is missing`, { lane_id: laneId, resolution: `${source}_company_invalid` });
46
62
  }
47
63
  return company;
48
64
  }
65
+ /**
66
+ * Resolve project ownership only when exactly one manifest company contains
67
+ * the project's directory. Multiple matches are unresolved so the caller can
68
+ * continue to an explicit active-company choice without picking an owner.
69
+ */
70
+ export function resolveProjectQuestionCompany(hqRoot, lane) {
71
+ const projectId = lane.project_id?.trim();
72
+ if (!projectId)
73
+ return null;
74
+ if (projectId === "." || projectId === ".." || /[\\/\0]/.test(projectId)) {
75
+ throw new LanesError("question_company_unavailable", `Cannot resolve the project company for lane ${lane.lane_id}: its project id is invalid`, { lane_id: lane.lane_id, resolution: "project_id_invalid" });
76
+ }
77
+ const manifestPath = path.join(hqRoot, "companies", "manifest.yaml");
78
+ if (!fs.existsSync(manifestPath))
79
+ return null;
80
+ const companies = readCompaniesManifestMap(hqRoot);
81
+ if (!companies) {
82
+ throw new LanesError("question_company_unavailable", `Cannot resolve the project company for lane ${lane.lane_id}: companies/manifest.yaml is unreadable or malformed`, { lane_id: lane.lane_id, resolution: "company_manifest_unavailable" });
83
+ }
84
+ const matches = [];
85
+ for (const [slug, entry] of Object.entries(companies)) {
86
+ if (!entry || typeof entry !== "object" || !slug || /[\\/\0]/.test(slug))
87
+ continue;
88
+ const projectDir = path.join(hqRoot, "companies", slug, "projects", projectId);
89
+ let isDirectory = false;
90
+ try {
91
+ isDirectory = fs.statSync(projectDir).isDirectory();
92
+ }
93
+ catch (err) {
94
+ if (err.code !== "ENOENT" && err.code !== "ENOTDIR") {
95
+ throw new LanesError("question_company_unavailable", `Cannot verify project ownership for lane ${lane.lane_id}`, { lane_id: lane.lane_id, resolution: "project_directory_unavailable" });
96
+ }
97
+ }
98
+ if (!isDirectory)
99
+ continue;
100
+ const uid = typeof entry.cloud_uid === "string" ? entry.cloud_uid.trim() : "";
101
+ matches.push({ slug, uid });
102
+ }
103
+ if (matches.length > 1)
104
+ return null;
105
+ const match = matches[0];
106
+ if (!match)
107
+ return null;
108
+ if (!match.uid) {
109
+ throw new LanesError("question_company_unavailable", `Cannot resolve the project company for lane ${lane.lane_id}: its manifest company has no cloud uid`, { lane_id: lane.lane_id, resolution: "project_company_uid_missing" });
110
+ }
111
+ return requireQuestionCompany({ slug: match.slug, uid: match.uid }, lane.lane_id, "project");
112
+ }
113
+ /** Resolve the lane/project company before lazily consulting active HQ scope. */
114
+ export async function resolveQuestionCompanyWithFallback(hqRoot, lane, resolveActiveCompany) {
115
+ try {
116
+ return resolveQuestionCompany(lane, null, hqRoot);
117
+ }
118
+ catch (err) {
119
+ if (!(err instanceof LanesError) ||
120
+ err.error !== "question_company_unavailable" ||
121
+ err.details.resolution !== "active_company_required" ||
122
+ !resolveActiveCompany) {
123
+ throw err;
124
+ }
125
+ return resolveQuestionCompany(lane, await resolveActiveCompany(), hqRoot);
126
+ }
127
+ }
49
128
  export function questionBodyRef(questionId) {
50
129
  return path.posix.join("lanes", "questions", `${questionId}.json`);
51
130
  }
@@ -82,7 +161,7 @@ export function writeQuestionBody(hqRoot, question, body) {
82
161
  */
83
162
  export function raiseLaneQuestion(input) {
84
163
  assertQuestionBodySecretSafe(input.body);
85
- const company = resolveQuestionCompany(input.lane, input.activeCompany);
164
+ const company = resolveQuestionCompany(input.lane, input.activeCompany, input.hqRoot);
86
165
  const store = bodyStoreForLane(input.lane, company);
87
166
  let questionId = input.questionId;
88
167
  if (!questionId) {
@@ -282,7 +361,7 @@ export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at,
282
361
  for (const item of presentItems) {
283
362
  assertQuestionBodySecretSafe(envelopeQuestionBody(item));
284
363
  }
285
- resolveQuestionCompany(lane, activeCompany);
364
+ resolveQuestionCompany(lane, activeCompany, hqRoot);
286
365
  const raised = [];
287
366
  for (let i = 0; i < items.length; i += 1) {
288
367
  const item = items[i];
@@ -24,7 +24,7 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
24
24
  import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, } from "./keep-alive.js";
25
25
  import { providerSupportsSessionResume } from "./delivery.js";
26
26
  import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./quota.js";
27
- import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, } from "./questions.js";
27
+ import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
28
28
  import { SILENCE_MS, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
29
29
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
30
30
  import { laneStories, storyStatusForEnvelope } from "./stories.js";
@@ -1054,10 +1054,8 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1054
1054
  latest &&
1055
1055
  (latest.envelope.questions?.length ?? 0) > 0) {
1056
1056
  const questionLane = readLane(hqRoot, lane.lane_id);
1057
- const activeCompany = questionLane.company || questionLane.mesh_company
1058
- ? undefined
1059
- : await deps.resolveQuestionActiveCompany?.();
1060
1057
  try {
1058
+ const activeCompany = await resolveQuestionCompanyWithFallback(hqRoot, questionLane, deps.resolveQuestionActiveCompany);
1061
1059
  raiseEnvelopeQuestions(hqRoot, questionLane, latest.envelope, latest.ref, nowIso, undefined, activeCompany);
1062
1060
  }
1063
1061
  catch (err) {
@@ -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