@indigoai-us/hq-cli 5.228.1 → 5.228.3
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 +12 -0
- package/dist/commands/lanes-checks.js +34 -15
- package/dist/lib/flag-registry-worker-mode-cache.d.ts +7 -0
- package/dist/lib/flag-registry-worker-mode-cache.js +126 -18
- package/dist/lib/flag-registry.js +16 -5
- package/dist/lib/lanes/tracker-mesh.js +29 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.228.3] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Work Mesh tracker rows use the local record fields. Missing blocker
|
|
10
|
+
`opened_at` values use `createdAt`, and invalid mesh rows identify Work Mesh
|
|
11
|
+
as their source.
|
|
12
|
+
- KPI and OKR records retain their status when read from Work Mesh, including in
|
|
13
|
+
`kpi list`, `okr list`, and `team --json` output.
|
|
14
|
+
|
|
15
|
+
## [5.228.2] — 2026-09-27
|
|
16
|
+
|
|
5
17
|
## [5.228.1] — 2026-09-27
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -435,7 +435,17 @@ function readTeamInputs(root, company, projects, lanes) {
|
|
|
435
435
|
export function pathForTrackerRecord(projectDirectory, kind, id) {
|
|
436
436
|
return path.join(projectDirectory, "tracker", kind, `${id}.json`);
|
|
437
437
|
}
|
|
438
|
-
function recordInvalid(kind, record, projectDirectory, field) {
|
|
438
|
+
function recordInvalid(kind, record, projectDirectory, field, source = "local") {
|
|
439
|
+
if (source === "Work Mesh") {
|
|
440
|
+
return fail("tracker_checks_record_invalid", `Work Mesh tracker ${kind} record ${record.id} has invalid ${field}`, {
|
|
441
|
+
source,
|
|
442
|
+
record_id: record.id,
|
|
443
|
+
kind,
|
|
444
|
+
company: record.company,
|
|
445
|
+
project_id: record.project_id,
|
|
446
|
+
field,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
439
449
|
return fail("tracker_checks_record_invalid", `Tracker ${kind} record has invalid ${field}`, {
|
|
440
450
|
kind,
|
|
441
451
|
id: record.id,
|
|
@@ -443,10 +453,18 @@ function recordInvalid(kind, record, projectDirectory, field) {
|
|
|
443
453
|
path: pathForTrackerRecord(projectDirectory, kind, record.id),
|
|
444
454
|
});
|
|
445
455
|
}
|
|
446
|
-
function requiredRecordDate(kind, record, projectDirectory, field) {
|
|
456
|
+
function requiredRecordDate(kind, record, projectDirectory, field, source = "local") {
|
|
447
457
|
const value = record[field];
|
|
448
458
|
if (typeof value !== "string" || !Number.isFinite(Date.parse(value)))
|
|
449
|
-
return recordInvalid(kind, record, projectDirectory, field);
|
|
459
|
+
return recordInvalid(kind, record, projectDirectory, field, source);
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
function requiredRecordTimestamp(kind, record, projectDirectory, field, value, source) {
|
|
463
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
464
|
+
if (source === "Work Mesh")
|
|
465
|
+
return recordInvalid(kind, record, projectDirectory, field, source);
|
|
466
|
+
return parseTimestamp(value, field, pathForTrackerRecord(projectDirectory, kind, record.id));
|
|
467
|
+
}
|
|
450
468
|
return value;
|
|
451
469
|
}
|
|
452
470
|
export function listTrackerRecords(projectDirectory, kind) {
|
|
@@ -509,6 +527,7 @@ async function readTrackerCheckRecords(root, company, projects, deps) {
|
|
|
509
527
|
const kpis = [];
|
|
510
528
|
const directives = [];
|
|
511
529
|
const useMesh = await deps.trackerMeshEnabled(company);
|
|
530
|
+
const source = useMesh ? "Work Mesh" : "local";
|
|
512
531
|
for (const project of projects) {
|
|
513
532
|
const absoluteDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
|
|
514
533
|
const store = useMesh
|
|
@@ -522,40 +541,40 @@ async function readTrackerCheckRecords(root, company, projects, deps) {
|
|
|
522
541
|
for (const record of records.blocker) {
|
|
523
542
|
if (record.company !== company.slug || record.project_id !== project.projectId)
|
|
524
543
|
continue;
|
|
525
|
-
const openedAt = requiredRecordDate("blocker", record, absoluteDirectory, "opened_at");
|
|
544
|
+
const openedAt = requiredRecordDate("blocker", record, absoluteDirectory, "opened_at", source);
|
|
526
545
|
const resolvedAt = record.resolved_at;
|
|
527
546
|
if (resolvedAt !== null && (typeof resolvedAt !== "string" || !Number.isFinite(Date.parse(resolvedAt))))
|
|
528
|
-
recordInvalid("blocker", record, absoluteDirectory, "resolved_at");
|
|
547
|
+
recordInvalid("blocker", record, absoluteDirectory, "resolved_at", source);
|
|
529
548
|
const nextStep = record.next_step;
|
|
530
549
|
if (nextStep !== undefined && nextStep !== null && typeof nextStep !== "string")
|
|
531
|
-
recordInvalid("blocker", record, absoluteDirectory, "next_step");
|
|
550
|
+
recordInvalid("blocker", record, absoluteDirectory, "next_step", source);
|
|
532
551
|
blockers.push({ id: record.id, opened_at: openedAt, resolved_at: resolvedAt, next_step: typeof nextStep === "string" ? nextStep : null });
|
|
533
552
|
}
|
|
534
553
|
for (const record of records.kpi) {
|
|
535
554
|
if (record.company !== company.slug || record.project_id !== project.projectId)
|
|
536
555
|
continue;
|
|
537
|
-
const createdAt = requiredRecordDate("kpi", record, absoluteDirectory, "created_at");
|
|
556
|
+
const createdAt = requiredRecordDate("kpi", record, absoluteDirectory, "created_at", source);
|
|
538
557
|
if (!Array.isArray(record.values))
|
|
539
|
-
recordInvalid("kpi", record, absoluteDirectory, "values");
|
|
558
|
+
recordInvalid("kpi", record, absoluteDirectory, "values", source);
|
|
540
559
|
const values = record.values.map((value) => {
|
|
541
560
|
if (!isObject(value))
|
|
542
|
-
recordInvalid("kpi", record, absoluteDirectory, "values");
|
|
543
|
-
return { at:
|
|
561
|
+
recordInvalid("kpi", record, absoluteDirectory, "values", source);
|
|
562
|
+
return { at: requiredRecordTimestamp("kpi", record, absoluteDirectory, "values[].at", value.at, source) };
|
|
544
563
|
});
|
|
545
564
|
kpis.push({ id: record.id, created_at: createdAt, values });
|
|
546
565
|
}
|
|
547
566
|
for (const record of records.directive) {
|
|
548
567
|
if (record.company !== company.slug || record.project_id !== project.projectId)
|
|
549
568
|
continue;
|
|
550
|
-
const createdAt = requiredRecordDate("directive", record, absoluteDirectory, "created_at");
|
|
569
|
+
const createdAt = requiredRecordDate("directive", record, absoluteDirectory, "created_at", source);
|
|
551
570
|
if (!Array.isArray(record.teams) || record.teams.some((team) => typeof team !== "string"))
|
|
552
|
-
recordInvalid("directive", record, absoluteDirectory, "teams");
|
|
571
|
+
recordInvalid("directive", record, absoluteDirectory, "teams", source);
|
|
553
572
|
if (!Array.isArray(record.acknowledgements))
|
|
554
|
-
recordInvalid("directive", record, absoluteDirectory, "acknowledgements");
|
|
573
|
+
recordInvalid("directive", record, absoluteDirectory, "acknowledgements", source);
|
|
555
574
|
const acknowledgements = record.acknowledgements.map((ack) => {
|
|
556
575
|
if (!isObject(ack) || typeof ack.team !== "string" || typeof ack.lane_id !== "string")
|
|
557
|
-
recordInvalid("directive", record, absoluteDirectory, "acknowledgements");
|
|
558
|
-
return { team: ack.team, lane_id: ack.lane_id, at:
|
|
576
|
+
recordInvalid("directive", record, absoluteDirectory, "acknowledgements", source);
|
|
577
|
+
return { team: ack.team, lane_id: ack.lane_id, at: requiredRecordTimestamp("directive", record, absoluteDirectory, "acknowledgements[].at", ack.at, source) };
|
|
559
578
|
});
|
|
560
579
|
directives.push({ id: record.id, created_at: createdAt, teams: record.teams, acknowledgements });
|
|
561
580
|
}
|
|
@@ -3,6 +3,13 @@ interface CachedWorkerMode {
|
|
|
3
3
|
version: number;
|
|
4
4
|
value: boolean;
|
|
5
5
|
}
|
|
6
|
+
export type FlagRegistryWorkerModeCacheOperation = "read" | "write";
|
|
7
|
+
export type FlagRegistryWorkerModeCacheStep = "mkdir" | "write_temp" | "link" | "unlink" | "prune" | "readdir" | "read";
|
|
8
|
+
/** Build bounded filesystem diagnostics without exposing the state directory. */
|
|
9
|
+
export declare function flagRegistryWorkerModeCacheFailureContext(error: unknown, operation: FlagRegistryWorkerModeCacheOperation, filePath: string, env?: NodeJS.ProcessEnv): {
|
|
10
|
+
exception: unknown;
|
|
11
|
+
tags: Record<string, string>;
|
|
12
|
+
};
|
|
6
13
|
/** Keep the last server snapshot decision for this one startup-mode flag. */
|
|
7
14
|
export declare function flagRegistryWorkerModeCachePath(stateDir: string, scopeIdentifiers: readonly string[]): string;
|
|
8
15
|
/** A missing, invalid, or inaccessible cache never enables the rollout. */
|
|
@@ -1,8 +1,116 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as os from "node:os";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
export const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
|
|
7
|
+
class FlagRegistryWorkerModeCacheStepError extends Error {
|
|
8
|
+
cacheStep;
|
|
9
|
+
originalError;
|
|
10
|
+
constructor(cacheStep, originalError) {
|
|
11
|
+
super(`Flag registry worker-mode cache failed at ${cacheStep}`);
|
|
12
|
+
this.cacheStep = cacheStep;
|
|
13
|
+
this.originalError = originalError;
|
|
14
|
+
this.name = "FlagRegistryWorkerModeCacheStepError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const KNOWN_ERRNOS = new Set([
|
|
18
|
+
"EACCES",
|
|
19
|
+
"EBUSY",
|
|
20
|
+
"EEXIST",
|
|
21
|
+
"EIO",
|
|
22
|
+
"EISDIR",
|
|
23
|
+
"EMFILE",
|
|
24
|
+
"ENFILE",
|
|
25
|
+
"ENOENT",
|
|
26
|
+
"ENOSPC",
|
|
27
|
+
"ENOTDIR",
|
|
28
|
+
"EPERM",
|
|
29
|
+
"EROFS",
|
|
30
|
+
]);
|
|
31
|
+
function withCacheFsStep(cacheStep, operation) {
|
|
32
|
+
try {
|
|
33
|
+
return operation();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof FlagRegistryWorkerModeCacheStepError)
|
|
37
|
+
throw error;
|
|
38
|
+
throw new FlagRegistryWorkerModeCacheStepError(cacheStep, error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function originalCacheError(error) {
|
|
42
|
+
return error instanceof FlagRegistryWorkerModeCacheStepError
|
|
43
|
+
? error.originalError
|
|
44
|
+
: error;
|
|
45
|
+
}
|
|
46
|
+
function cacheErrorCode(error) {
|
|
47
|
+
const original = originalCacheError(error);
|
|
48
|
+
return typeof original === "object" && original !== null && "code" in original
|
|
49
|
+
? original.code
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
function directoryHasUchg(directory) {
|
|
53
|
+
if (process.platform !== "darwin")
|
|
54
|
+
return "unknown";
|
|
55
|
+
try {
|
|
56
|
+
const rawFlags = execFileSync("/usr/bin/stat", ["-f", "%f", directory], { timeout: 1000 }).toString("utf8").trim();
|
|
57
|
+
if (!/^\d+$/.test(rawFlags))
|
|
58
|
+
return "unknown";
|
|
59
|
+
const flags = Number(rawFlags);
|
|
60
|
+
if (!Number.isSafeInteger(flags))
|
|
61
|
+
return "unknown";
|
|
62
|
+
return String((flags & 0x00000002) !== 0 || (flags & 0x00020000) !== 0);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// This bounded diagnostic is best-effort; preserve unknown and the original cache error.
|
|
66
|
+
return "unknown";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Build bounded filesystem diagnostics without exposing the state directory. */
|
|
70
|
+
export function flagRegistryWorkerModeCacheFailureContext(error, operation, filePath, env = process.env) {
|
|
71
|
+
const cacheError = error instanceof FlagRegistryWorkerModeCacheStepError
|
|
72
|
+
? error
|
|
73
|
+
: undefined;
|
|
74
|
+
const directory = path.dirname(filePath);
|
|
75
|
+
let dirExists = "unknown";
|
|
76
|
+
let stats;
|
|
77
|
+
try {
|
|
78
|
+
stats = fs.statSync(directory);
|
|
79
|
+
dirExists = "true";
|
|
80
|
+
}
|
|
81
|
+
catch (metadataError) {
|
|
82
|
+
if (cacheErrorCode(metadataError) === "ENOENT")
|
|
83
|
+
dirExists = "false";
|
|
84
|
+
// Keep inaccessible metadata unknown; diagnostics must not hide the cache error.
|
|
85
|
+
}
|
|
86
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
87
|
+
const ownerMatches = typeof uid === "number" && typeof stats?.uid === "number"
|
|
88
|
+
? String(stats.uid === uid)
|
|
89
|
+
: "unknown";
|
|
90
|
+
const dirMode = typeof stats?.mode === "number"
|
|
91
|
+
? (stats.mode & 0o777).toString(8).padStart(3, "0")
|
|
92
|
+
: "unknown";
|
|
93
|
+
const dirHasUchg = directoryHasUchg(directory);
|
|
94
|
+
const rawErrno = cacheErrorCode(error);
|
|
95
|
+
const errno = typeof rawErrno === "string" && KNOWN_ERRNOS.has(rawErrno)
|
|
96
|
+
? rawErrno
|
|
97
|
+
: "UNKNOWN";
|
|
98
|
+
const stateDirSource = env.HQ_STATE_DIR?.trim() ? "env" : "home";
|
|
99
|
+
return {
|
|
100
|
+
exception: originalCacheError(error),
|
|
101
|
+
tags: {
|
|
102
|
+
cache_op: operation,
|
|
103
|
+
cache_step: cacheError?.cacheStep ?? (operation === "read" ? "read" : "write_temp"),
|
|
104
|
+
errno,
|
|
105
|
+
state_dir_source: stateDirSource,
|
|
106
|
+
dir_exists: dirExists,
|
|
107
|
+
dir_owner_matches_uid: ownerMatches,
|
|
108
|
+
dir_mode: dirMode,
|
|
109
|
+
dir_has_uchg: dirHasUchg,
|
|
110
|
+
cache_path_kind: "<stateDir>/flags/<basename>",
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
6
114
|
/** Keep the last server snapshot decision for this one startup-mode flag. */
|
|
7
115
|
export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
|
|
8
116
|
const basename = scopeIdentifiers.length === 0
|
|
@@ -17,11 +125,11 @@ export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
|
|
|
17
125
|
export function readCachedFlagRegistryWorkerMode(filePath) {
|
|
18
126
|
let latest = readCacheRecordIfPresent(filePath);
|
|
19
127
|
const directory = path.dirname(filePath);
|
|
20
|
-
if (!fs.existsSync(directory))
|
|
128
|
+
if (!withCacheFsStep("read", () => fs.existsSync(directory)))
|
|
21
129
|
return latest;
|
|
22
130
|
const basename = path.basename(filePath);
|
|
23
131
|
const versionedPrefix = `${basename}.v`;
|
|
24
|
-
for (const entry of fs.readdirSync(directory)) {
|
|
132
|
+
for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
|
|
25
133
|
if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
|
|
26
134
|
continue;
|
|
27
135
|
const versionedPath = path.join(directory, entry);
|
|
@@ -32,7 +140,7 @@ export function readCachedFlagRegistryWorkerMode(filePath) {
|
|
|
32
140
|
return latest;
|
|
33
141
|
}
|
|
34
142
|
export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
|
|
35
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
143
|
+
withCacheFsStep("mkdir", () => fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }));
|
|
36
144
|
const existing = readCachedFlagRegistryWorkerMode(filePath);
|
|
37
145
|
if (existing && snapshot.version < existing.version)
|
|
38
146
|
return;
|
|
@@ -42,26 +150,27 @@ export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
|
|
|
42
150
|
const versionedPath = `${filePath}.v${snapshot.version}.json`;
|
|
43
151
|
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
44
152
|
try {
|
|
45
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
|
|
153
|
+
withCacheFsStep("write_temp", () => fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
|
|
46
154
|
encoding: "utf8",
|
|
47
155
|
mode: 0o600,
|
|
48
156
|
flag: "wx",
|
|
49
|
-
});
|
|
157
|
+
}));
|
|
50
158
|
try {
|
|
51
|
-
fs.linkSync(tempPath, versionedPath);
|
|
159
|
+
withCacheFsStep("link", () => fs.linkSync(tempPath, versionedPath));
|
|
52
160
|
}
|
|
53
161
|
catch (error) {
|
|
54
162
|
if (!isAlreadyExistsError(error))
|
|
55
163
|
throw error;
|
|
56
164
|
const existingVersion = readCacheRecord(versionedPath);
|
|
57
165
|
if (existingVersion.value !== snapshot.value) {
|
|
58
|
-
throw new Error("Conflicting flag-registry worker cache values share a version");
|
|
166
|
+
throw new FlagRegistryWorkerModeCacheStepError("link", new Error("Conflicting flag-registry worker cache values share a version"));
|
|
59
167
|
}
|
|
60
168
|
}
|
|
61
169
|
}
|
|
62
170
|
finally {
|
|
63
|
-
if (fs.existsSync(tempPath))
|
|
64
|
-
fs.unlinkSync(tempPath);
|
|
171
|
+
if (withCacheFsStep("unlink", () => fs.existsSync(tempPath))) {
|
|
172
|
+
withCacheFsStep("unlink", () => fs.unlinkSync(tempPath));
|
|
173
|
+
}
|
|
65
174
|
}
|
|
66
175
|
pruneOlderCacheRecords(filePath);
|
|
67
176
|
}
|
|
@@ -76,7 +185,8 @@ function isCachedWorkerMode(value) {
|
|
|
76
185
|
"value" in value && typeof value.value === "boolean";
|
|
77
186
|
}
|
|
78
187
|
function readCacheRecord(filePath) {
|
|
79
|
-
const
|
|
188
|
+
const contents = withCacheFsStep("read", () => fs.readFileSync(filePath, "utf8"));
|
|
189
|
+
const value = JSON.parse(contents);
|
|
80
190
|
if (!isCachedWorkerMode(value)) {
|
|
81
191
|
throw new Error("Cached flag-registry worker mode has an invalid shape");
|
|
82
192
|
}
|
|
@@ -97,14 +207,14 @@ function pruneOlderCacheRecords(filePath) {
|
|
|
97
207
|
const directory = path.dirname(filePath);
|
|
98
208
|
const basename = path.basename(filePath);
|
|
99
209
|
const versionedPrefix = `${basename}.v`;
|
|
100
|
-
for (const entry of fs.readdirSync(directory)) {
|
|
210
|
+
for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
|
|
101
211
|
if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
|
|
102
212
|
continue;
|
|
103
213
|
const recordPath = path.join(directory, entry);
|
|
104
214
|
const record = readCacheRecordIfPresent(recordPath);
|
|
105
215
|
if (record && record.version < latest.version) {
|
|
106
216
|
try {
|
|
107
|
-
fs.unlinkSync(recordPath);
|
|
217
|
+
withCacheFsStep("prune", () => fs.unlinkSync(recordPath));
|
|
108
218
|
}
|
|
109
219
|
catch (error) {
|
|
110
220
|
if (!isNotFoundError(error))
|
|
@@ -112,11 +222,11 @@ function pruneOlderCacheRecords(filePath) {
|
|
|
112
222
|
}
|
|
113
223
|
}
|
|
114
224
|
}
|
|
115
|
-
if (fs.existsSync(filePath)) {
|
|
225
|
+
if (withCacheFsStep("prune", () => fs.existsSync(filePath))) {
|
|
116
226
|
const legacyRecord = readCacheRecordIfPresent(filePath);
|
|
117
227
|
if (legacyRecord && legacyRecord.version < latest.version) {
|
|
118
228
|
try {
|
|
119
|
-
fs.unlinkSync(filePath);
|
|
229
|
+
withCacheFsStep("prune", () => fs.unlinkSync(filePath));
|
|
120
230
|
}
|
|
121
231
|
catch (error) {
|
|
122
232
|
if (!isNotFoundError(error))
|
|
@@ -136,11 +246,9 @@ function readCacheRecordIfPresent(filePath) {
|
|
|
136
246
|
}
|
|
137
247
|
}
|
|
138
248
|
function isAlreadyExistsError(error) {
|
|
139
|
-
return
|
|
140
|
-
"code" in error && error.code === "EEXIST";
|
|
249
|
+
return cacheErrorCode(error) === "EEXIST";
|
|
141
250
|
}
|
|
142
251
|
function isNotFoundError(error) {
|
|
143
|
-
return
|
|
144
|
-
"code" in error && error.code === "ENOENT";
|
|
252
|
+
return cacheErrorCode(error) === "ENOENT";
|
|
145
253
|
}
|
|
146
254
|
//# sourceMappingURL=flag-registry-worker-mode-cache.js.map
|
|
@@ -14,7 +14,7 @@ import { workContextRoot } from "./work-context/paths.js";
|
|
|
14
14
|
import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
|
|
15
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";
|
|
17
|
+
import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryWorkerModeCacheFailureContext, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
|
|
18
18
|
import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
|
|
19
19
|
export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
20
20
|
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
@@ -140,8 +140,13 @@ function selfContainedWorkerEnabled(company) {
|
|
|
140
140
|
return readCachedFlagRegistryWorkerMode(filePath)?.value ?? false;
|
|
141
141
|
}
|
|
142
142
|
catch (error) {
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
const failure = flagRegistryWorkerModeCacheFailureContext(error, "read", filePath);
|
|
144
|
+
Sentry.captureException(failure.exception, {
|
|
145
|
+
tags: {
|
|
146
|
+
component: "flag-registry-worker-mode-cache",
|
|
147
|
+
worker: "flag-registry",
|
|
148
|
+
...failure.tags,
|
|
149
|
+
},
|
|
145
150
|
});
|
|
146
151
|
return false;
|
|
147
152
|
}
|
|
@@ -165,8 +170,14 @@ function cacheWorkerModeFromSnapshot(snapshot, company) {
|
|
|
165
170
|
writeCachedFlagRegistryWorkerMode(flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes), { version: snapshot.version, value: decision.value });
|
|
166
171
|
}
|
|
167
172
|
catch (error) {
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
const filePath = flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes);
|
|
174
|
+
const failure = flagRegistryWorkerModeCacheFailureContext(error, "write", filePath);
|
|
175
|
+
Sentry.captureException(failure.exception, {
|
|
176
|
+
tags: {
|
|
177
|
+
component: "flag-registry-worker-mode-cache",
|
|
178
|
+
worker: "flag-registry",
|
|
179
|
+
...failure.tags,
|
|
180
|
+
},
|
|
170
181
|
});
|
|
171
182
|
}
|
|
172
183
|
}
|
|
@@ -3,6 +3,12 @@ import { meshJson, requireToken } from "../mesh/client.js";
|
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
4
|
const ROOT = "/v1/work-mesh/tracker/records";
|
|
5
5
|
const MAX_RECORD_PAGES = 20;
|
|
6
|
+
const RECORD_FIELDS = {
|
|
7
|
+
blocker: ["story_id", "what", "waiting_on", "next_step", "opened_at", "resolved_at", "resolution", "import_key", "source_path", "source_item_id"],
|
|
8
|
+
kpi: ["name", "definition", "unit", "target", "direction", "status", "approved_by", "approved_at", "values", "import_key", "source_path", "source_item_id"],
|
|
9
|
+
okr: ["objective", "period", "key_results", "status", "approved_by", "approved_at", "import_key", "source_path", "source_item_id"],
|
|
10
|
+
directive: ["text", "date", "source_url", "teams", "policy_path", "acknowledgements", "import_key", "source_path", "source_item_id"],
|
|
11
|
+
};
|
|
6
12
|
function fail(error, message, details = {}) {
|
|
7
13
|
throw new LanesError(error, message, details);
|
|
8
14
|
}
|
|
@@ -50,23 +56,30 @@ function fromMesh(value) {
|
|
|
50
56
|
}
|
|
51
57
|
function recordFromMeta(raw, company, projectId, expectedKind) {
|
|
52
58
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
53
|
-
return fail("tracker_mesh_response_invalid", "Work Mesh returned a malformed tracker record", {
|
|
59
|
+
return fail("tracker_mesh_response_invalid", "Work Mesh returned a malformed tracker record", {
|
|
60
|
+
source: "Work Mesh", kind: expectedKind, company: company.slug, project_id: projectId,
|
|
61
|
+
});
|
|
54
62
|
}
|
|
55
63
|
const meta = raw;
|
|
56
64
|
if (meta.companyUid !== company.uid || meta.projectId !== projectId || meta.kind !== expectedKind || typeof meta.threadId !== "string" || !meta.threadId) {
|
|
57
|
-
return fail("tracker_mesh_response_invalid", "Work Mesh tracker record identity did not match the requested company, project, and kind", {
|
|
65
|
+
return fail("tracker_mesh_response_invalid", "Work Mesh tracker record identity did not match the requested company, project, and kind", {
|
|
66
|
+
source: "Work Mesh",
|
|
67
|
+
...(typeof meta.threadId === "string" && meta.threadId ? { record_id: meta.threadId } : {}),
|
|
68
|
+
kind: expectedKind,
|
|
69
|
+
company: company.slug,
|
|
70
|
+
project_id: projectId,
|
|
71
|
+
});
|
|
58
72
|
}
|
|
59
73
|
const fields = fromMesh(meta);
|
|
60
74
|
const createdAt = typeof meta.createdAt === "string" ? meta.createdAt : "";
|
|
61
75
|
const updatedAt = typeof meta.lastActivityAt === "string" ? meta.lastActivityAt : createdAt;
|
|
62
76
|
const version = meta.version;
|
|
63
77
|
if (!Number.isSafeInteger(version) || version < 1 || !Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(updatedAt))) {
|
|
64
|
-
return fail("tracker_mesh_response_invalid", "Work Mesh tracker record has invalid version or timestamps", {
|
|
78
|
+
return fail("tracker_mesh_response_invalid", "Work Mesh tracker record has invalid version or timestamps", {
|
|
79
|
+
source: "Work Mesh", record_id: meta.threadId, kind: expectedKind, company: company.slug, project_id: projectId,
|
|
80
|
+
});
|
|
65
81
|
}
|
|
66
|
-
const { company_uid: _companyUid, project_id: _projectId, thread_id: _threadId, thread_status: threadStatus, ...rest } = fields;
|
|
67
82
|
const record = {
|
|
68
|
-
...rest,
|
|
69
|
-
...(typeof threadStatus === "string" && rest.status === undefined ? { status: threadStatus } : {}),
|
|
70
83
|
schema_version: "hq-lanes-tracker-record.v1",
|
|
71
84
|
kind: expectedKind,
|
|
72
85
|
id: meta.threadId,
|
|
@@ -76,6 +89,16 @@ function recordFromMeta(raw, company, projectId, expectedKind) {
|
|
|
76
89
|
created_at: createdAt,
|
|
77
90
|
updated_at: updatedAt,
|
|
78
91
|
};
|
|
92
|
+
for (const field of RECORD_FIELDS[expectedKind]) {
|
|
93
|
+
if (Object.hasOwn(fields, field))
|
|
94
|
+
record[field] = fields[field];
|
|
95
|
+
}
|
|
96
|
+
// hq-pro stores KPI and OKR status in the shared threadStatus metadata field.
|
|
97
|
+
if ((expectedKind === "kpi" || expectedKind === "okr") && !Object.hasOwn(record, "status") && Object.hasOwn(fields, "thread_status")) {
|
|
98
|
+
record.status = fields.thread_status;
|
|
99
|
+
}
|
|
100
|
+
if (expectedKind === "blocker" && !Object.hasOwn(record, "opened_at"))
|
|
101
|
+
record.opened_at = createdAt;
|
|
79
102
|
return record;
|
|
80
103
|
}
|
|
81
104
|
function identityMatches(record, kind, id, company, projectId) {
|