@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.273.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 +25 -2
- package/dist/command-catalog.generated.d.ts +11 -536
- package/dist/command-catalog.generated.js +14 -691
- package/dist/commands/daemon.d.ts +0 -3
- package/dist/commands/daemon.js +1 -173
- package/dist/commands/lanes-checks.d.ts +0 -1
- package/dist/commands/lanes-checks.js +217 -54
- package/dist/commands/lanes-hold.js +40 -2
- package/dist/commands/lanes-message.js +98 -14
- package/dist/commands/lanes-shared.d.ts +3 -2
- package/dist/commands/lanes.d.ts +2 -4
- package/dist/commands/lanes.js +3 -23
- package/dist/commands/sync-mode.d.ts +0 -11
- package/dist/commands/sync-mode.js +0 -30
- package/dist/lib/daemon/catalog.d.ts +2 -18
- package/dist/lib/daemon/catalog.js +2 -18
- package/dist/lib/daemon/config.d.ts +0 -6
- package/dist/lib/daemon/config.js +2 -15
- package/dist/lib/daemon/requests.d.ts +0 -4
- package/dist/lib/daemon/requests.js +0 -2
- package/dist/lib/daemon/run.js +19 -78
- package/dist/lib/daemon/supervisor.d.ts +0 -8
- package/dist/lib/daemon/supervisor.js +1 -101
- package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
- package/dist/lib/lanes/keep-alive.d.ts +3 -2
- package/dist/lib/lanes/keep-alive.js +2 -2
- package/dist/lib/lanes/merge-command.d.ts +1 -0
- package/dist/lib/lanes/merge-command.js +299 -33
- package/dist/lib/lanes/orphans.d.ts +7 -0
- package/dist/lib/lanes/orphans.js +39 -4
- package/dist/lib/lanes/tracker-store.d.ts +2 -18
- package/dist/lib/lanes/tracker-store.js +1 -172
- package/dist/lib/scaffold-cli-floor.d.ts +9 -0
- package/dist/lib/scaffold-cli-floor.js +50 -0
- package/package.json +2 -2
- package/dist/commands/lanes-import.d.ts +0 -14
- package/dist/commands/lanes-import.js +0 -1210
- package/dist/commands/lanes-tracker.d.ts +0 -18
- package/dist/commands/lanes-tracker.js +0 -452
- package/dist/lib/daemon/sync-runner.d.ts +0 -58
- package/dist/lib/daemon/sync-runner.js +0 -277
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { type TrackerStore } from "../lib/lanes/tracker-store.js";
|
|
3
|
-
import type { LaneCompanyRef } from "../lib/lanes/types.js";
|
|
4
|
-
export declare const LANES_TRACKER_FLAG = "lanes.tracker";
|
|
5
|
-
export interface LaneTrackerCommandDeps {
|
|
6
|
-
hqRoot: () => string;
|
|
7
|
-
resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
|
|
8
|
-
trackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
9
|
-
now: () => Date;
|
|
10
|
-
actor: () => string | null;
|
|
11
|
-
/** Test seam; production uses the same local store used by the public commands. */
|
|
12
|
-
store?: (projectDirectory: string) => TrackerStore;
|
|
13
|
-
}
|
|
14
|
-
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
15
|
-
export declare function registerLaneTrackerCommands(parent: Command, deps: LaneTrackerCommandDeps, wrap: WrapHandler): void;
|
|
16
|
-
export declare function defaultTrackerActor(): string | null;
|
|
17
|
-
export {};
|
|
18
|
-
//# sourceMappingURL=lanes-tracker.d.ts.map
|
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
5
|
-
import { LanesError } from "../lib/lanes/errors.js";
|
|
6
|
-
import { resolveProjectStoryLedger } from "../lib/lanes/story-ledger.js";
|
|
7
|
-
import { tryReadQuestion } from "../lib/lanes/store.js";
|
|
8
|
-
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
9
|
-
import { createLocalTrackerStore } from "../lib/lanes/tracker-store.js";
|
|
10
|
-
import { peekIdToken } from "../utils/id-token.js";
|
|
11
|
-
export const LANES_TRACKER_FLAG = "lanes.tracker";
|
|
12
|
-
const MAX_POLICY_PATH_BYTES = 4096;
|
|
13
|
-
function fail(error, message, details = {}) {
|
|
14
|
-
throw new LanesError(error, message, details);
|
|
15
|
-
}
|
|
16
|
-
function validSegment(value, label) {
|
|
17
|
-
const normalized = value.trim();
|
|
18
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(normalized) || normalized === "." || normalized === "..") {
|
|
19
|
-
return fail("tracker_input_invalid", `${label} must be a safe path segment`, { field: label, value });
|
|
20
|
-
}
|
|
21
|
-
return normalized;
|
|
22
|
-
}
|
|
23
|
-
function requiredText(field, text) {
|
|
24
|
-
if (!text.trim())
|
|
25
|
-
fail("tracker_input_invalid", `${field} must not be empty`, { field });
|
|
26
|
-
return text;
|
|
27
|
-
}
|
|
28
|
-
function checkedText(field, text, maxBytes) {
|
|
29
|
-
requiredText(field, text);
|
|
30
|
-
if (maxBytes !== undefined && Buffer.byteLength(text, "utf8") > maxBytes) {
|
|
31
|
-
fail("tracker_input_too_long", `${field} exceeds the maximum length`, { field, max_length: maxBytes });
|
|
32
|
-
}
|
|
33
|
-
assertNoSecretText(field, text);
|
|
34
|
-
return text;
|
|
35
|
-
}
|
|
36
|
-
function readProjectCompany(prdPath) {
|
|
37
|
-
let parsed;
|
|
38
|
-
try {
|
|
39
|
-
parsed = JSON.parse(fs.readFileSync(prdPath, "utf8"));
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
const code = err.code;
|
|
43
|
-
if (code === "ENOENT")
|
|
44
|
-
fail("tracker_project_not_found", "Project prd.json was not found for this company", { path: prdPath });
|
|
45
|
-
fail("tracker_project_invalid", "Project prd.json must contain valid JSON", { path: prdPath });
|
|
46
|
-
}
|
|
47
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
48
|
-
return fail("tracker_project_invalid", "Project prd.json must contain a JSON object", { path: prdPath });
|
|
49
|
-
}
|
|
50
|
-
const object = parsed;
|
|
51
|
-
const metadata = object.metadata && typeof object.metadata === "object" && !Array.isArray(object.metadata)
|
|
52
|
-
? object.metadata
|
|
53
|
-
: undefined;
|
|
54
|
-
const raw = object.company ?? metadata?.company;
|
|
55
|
-
if (raw === undefined || raw === null)
|
|
56
|
-
return null;
|
|
57
|
-
if (typeof raw === "string")
|
|
58
|
-
return raw;
|
|
59
|
-
if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
60
|
-
const slug = raw.slug;
|
|
61
|
-
if (typeof slug === "string")
|
|
62
|
-
return slug;
|
|
63
|
-
}
|
|
64
|
-
return fail("tracker_project_invalid", "Project company metadata must name a company slug", { path: prdPath });
|
|
65
|
-
}
|
|
66
|
-
async function target(deps, options) {
|
|
67
|
-
const company = await deps.resolveCompany(options.company);
|
|
68
|
-
if (!company.slug)
|
|
69
|
-
fail("tracker_company_unresolved", "The active HQ company could not be resolved", {});
|
|
70
|
-
// Check the feature gate before inspecting the project path. A disabled flag
|
|
71
|
-
// reveals neither project existence nor any local tracker data.
|
|
72
|
-
if (!await deps.trackerEnabled(company)) {
|
|
73
|
-
fail("tracker_disabled", "hq lanes tracker commands require the lanes.tracker flag", { company: company.slug });
|
|
74
|
-
}
|
|
75
|
-
const project = validSegment(options.project, "project");
|
|
76
|
-
const ledger = resolveProjectStoryLedger(deps.hqRoot(), project, company.slug);
|
|
77
|
-
const projectCompany = readProjectCompany(ledger.path);
|
|
78
|
-
if (projectCompany !== null && projectCompany !== company.slug) {
|
|
79
|
-
fail("tracker_company_mismatch", "Project company does not match the active HQ company", {
|
|
80
|
-
active_company: company.slug,
|
|
81
|
-
project_company: projectCompany,
|
|
82
|
-
project_id: project,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
const directory = path.dirname(ledger.path);
|
|
86
|
-
let realDirectory;
|
|
87
|
-
try {
|
|
88
|
-
realDirectory = fs.realpathSync(directory);
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
fail("tracker_project_not_found", "Project directory was not found for this company", {
|
|
92
|
-
project_id: project,
|
|
93
|
-
filesystem_code: err.code ?? "unknown",
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
const expectedRoot = fs.realpathSync(path.join(deps.hqRoot(), "companies", company.slug, "projects"));
|
|
97
|
-
if (!realDirectory.startsWith(`${expectedRoot}${path.sep}`)) {
|
|
98
|
-
fail("tracker_company_mismatch", "Project directory resolves outside the active HQ company", {
|
|
99
|
-
active_company: company.slug,
|
|
100
|
-
project_id: project,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
company,
|
|
105
|
-
project,
|
|
106
|
-
store: deps.store?.(realDirectory) ?? createLocalTrackerStore(realDirectory, deps.now),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function requireActor(deps) {
|
|
110
|
-
const actor = deps.actor()?.trim();
|
|
111
|
-
if (!actor)
|
|
112
|
-
fail("tracker_identity_unavailable", "Cannot record approval without a signed-in HQ identity", {});
|
|
113
|
-
return actor;
|
|
114
|
-
}
|
|
115
|
-
function approvalQuestionId(deps, company, project, rawQuestionId) {
|
|
116
|
-
if (rawQuestionId === undefined)
|
|
117
|
-
return undefined;
|
|
118
|
-
const questionId = validSegment(rawQuestionId, "question-id");
|
|
119
|
-
const question = tryReadQuestion(deps.hqRoot(), questionId);
|
|
120
|
-
if (!question)
|
|
121
|
-
fail("tracker_question_not_found", `Answered owner question ${questionId} was not found`, { question_id: questionId });
|
|
122
|
-
if (question.status !== "answered" || !question.answer) {
|
|
123
|
-
fail("tracker_question_not_answered", `Owner question ${questionId} is not answered`, { question_id: questionId });
|
|
124
|
-
}
|
|
125
|
-
if (question.company?.slug !== company.slug || question.project_id !== project) {
|
|
126
|
-
fail("tracker_question_scope_mismatch", `Owner question ${questionId} does not belong to this company project`, {
|
|
127
|
-
question_id: questionId,
|
|
128
|
-
company: company.slug,
|
|
129
|
-
project_id: project,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
return questionId;
|
|
133
|
-
}
|
|
134
|
-
function numberValue(raw, field) {
|
|
135
|
-
const value = Number(raw);
|
|
136
|
-
if (!Number.isFinite(value))
|
|
137
|
-
return fail("tracker_input_invalid", `${field} must be a finite number`, { field });
|
|
138
|
-
return value;
|
|
139
|
-
}
|
|
140
|
-
function parseKr(raw) {
|
|
141
|
-
const match = /^(.+)=([a-zA-Z0-9][a-zA-Z0-9._-]{0,127}):(-?(?:\d+(?:\.\d*)?|\.\d+))$/.exec(raw);
|
|
142
|
-
if (!match)
|
|
143
|
-
fail("tracker_key_result_invalid", "Key result must be <text>=<kpi-id>:<target>", {});
|
|
144
|
-
return { text: match[1].trim(), kpi_id: validSegment(match[2], "kpi-id"), target: numberValue(match[3], "key-result target") };
|
|
145
|
-
}
|
|
146
|
-
function validDate(raw) {
|
|
147
|
-
let normalized;
|
|
148
|
-
try {
|
|
149
|
-
normalized = new Date(`${raw}T00:00:00.000Z`).toISOString().slice(0, 10);
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
normalized = undefined;
|
|
153
|
-
}
|
|
154
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw) || normalized !== raw) {
|
|
155
|
-
return fail("tracker_date_invalid", "Date must be a valid YYYY-MM-DD value", { field: "date" });
|
|
156
|
-
}
|
|
157
|
-
return raw;
|
|
158
|
-
}
|
|
159
|
-
function validTimestamp(raw) {
|
|
160
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|[+-]\d{2}:\d{2})$/.exec(raw);
|
|
161
|
-
if (!match)
|
|
162
|
-
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
163
|
-
const [, year, month, day, hour, minute, second = "00", fraction = "", zone] = match;
|
|
164
|
-
const date = `${year}-${month}-${day}`;
|
|
165
|
-
let calendarDate;
|
|
166
|
-
try {
|
|
167
|
-
calendarDate = new Date(`${date}T00:00:00.000Z`).toISOString().slice(0, 10);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
calendarDate = undefined;
|
|
171
|
-
}
|
|
172
|
-
if (calendarDate !== date || Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59) {
|
|
173
|
-
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
174
|
-
}
|
|
175
|
-
let offsetMinutes = 0;
|
|
176
|
-
if (zone !== "Z") {
|
|
177
|
-
const offset = /^([+-])(\d{2}):(\d{2})$/.exec(zone);
|
|
178
|
-
const offsetHours = Number(offset?.[2]);
|
|
179
|
-
const offsetRemainder = Number(offset?.[3]);
|
|
180
|
-
if (!offset || offsetHours > 14 || offsetRemainder > 59 || (offsetHours === 14 && offsetRemainder !== 0)) {
|
|
181
|
-
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
182
|
-
}
|
|
183
|
-
const sign = offset[1] === "+" ? 1 : -1;
|
|
184
|
-
offsetMinutes = sign * (offsetHours * 60 + offsetRemainder);
|
|
185
|
-
}
|
|
186
|
-
const localTimeAsUtc = new Date(`${date}T${hour}:${minute}:${second}.${fraction.padEnd(3, "0")}Z`);
|
|
187
|
-
const expectedEpoch = localTimeAsUtc.getTime() - offsetMinutes * 60_000;
|
|
188
|
-
const parsed = new Date(raw);
|
|
189
|
-
if (!Number.isFinite(expectedEpoch) || !Number.isFinite(parsed.getTime()) || parsed.getTime() !== expectedEpoch) {
|
|
190
|
-
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
191
|
-
}
|
|
192
|
-
const roundTripped = new Date(parsed.toISOString());
|
|
193
|
-
if (roundTripped.getTime() !== expectedEpoch) {
|
|
194
|
-
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
195
|
-
}
|
|
196
|
-
return parsed;
|
|
197
|
-
}
|
|
198
|
-
function addProjectOptions(command) {
|
|
199
|
-
return command
|
|
200
|
-
.requiredOption("--project <slug>", "HQ team project slug")
|
|
201
|
-
.option("--company <slug>", "Select the company by slug");
|
|
202
|
-
}
|
|
203
|
-
async function createRecord(store, kind, company, project, fields) {
|
|
204
|
-
return await store.create({ kind, id: randomUUID(), company, project_id: project, ...fields });
|
|
205
|
-
}
|
|
206
|
-
export function registerLaneTrackerCommands(parent, deps, wrap) {
|
|
207
|
-
const blocker = parent.command("blocker").description("Record and manage project blockers");
|
|
208
|
-
addProjectOptions(blocker.command("open").description("Open a blocker"))
|
|
209
|
-
.requiredOption("--story <id>", "Blocked story id")
|
|
210
|
-
.requiredOption("--what <text>", "What is blocked")
|
|
211
|
-
.requiredOption("--waiting-on <text>", "Who or what is needed")
|
|
212
|
-
.requiredOption("--next-step <text>", "Next action")
|
|
213
|
-
.action(wrap(async (options) => {
|
|
214
|
-
const { company, project, store } = await target(deps, options);
|
|
215
|
-
if (!options.story.trim())
|
|
216
|
-
fail("tracker_input_invalid", "story must not be empty", { field: "story" });
|
|
217
|
-
for (const [field, value] of [["what", options.what], ["waiting_on", options.waitingOn], ["next_step", options.nextStep]])
|
|
218
|
-
checkedText(field, value);
|
|
219
|
-
const record = await createRecord(store, "blocker", company.slug, project, {
|
|
220
|
-
story_id: options.story.trim(), what: options.what, waiting_on: options.waitingOn,
|
|
221
|
-
next_step: options.nextStep, opened_at: deps.now().toISOString(), resolved_at: null, resolution: null,
|
|
222
|
-
});
|
|
223
|
-
console.log(JSON.stringify({ ok: true, action: "blocker.open", record }, null, 2));
|
|
224
|
-
}));
|
|
225
|
-
addProjectOptions(blocker.command("update").description("Update a blocker").argument("<blocker-id>", "Blocker id"))
|
|
226
|
-
.option("--waiting-on <text>", "Who or what is needed")
|
|
227
|
-
.option("--next-step <text>", "Next action")
|
|
228
|
-
.action(wrap(async (id, options) => {
|
|
229
|
-
const { store } = await target(deps, options);
|
|
230
|
-
if (options.waitingOn === undefined && options.nextStep === undefined)
|
|
231
|
-
fail("tracker_update_empty", "Supply --waiting-on or --next-step", {});
|
|
232
|
-
if (options.waitingOn !== undefined)
|
|
233
|
-
checkedText("waiting_on", options.waitingOn);
|
|
234
|
-
if (options.nextStep !== undefined)
|
|
235
|
-
checkedText("next_step", options.nextStep);
|
|
236
|
-
const current = await store.read("blocker", id);
|
|
237
|
-
if (!current)
|
|
238
|
-
fail("tracker_record_not_found", `Tracker blocker record ${id} was not found`, { kind: "blocker", id });
|
|
239
|
-
if (current.resolved_at !== null)
|
|
240
|
-
fail("tracker_already_resolved", "Blocker is already resolved", { id });
|
|
241
|
-
const record = await store.update("blocker", id, current.version, (value) => ({
|
|
242
|
-
...value,
|
|
243
|
-
...(options.waitingOn === undefined ? {} : { waiting_on: options.waitingOn }),
|
|
244
|
-
...(options.nextStep === undefined ? {} : { next_step: options.nextStep }),
|
|
245
|
-
}));
|
|
246
|
-
console.log(JSON.stringify({ ok: true, action: "blocker.update", record }, null, 2));
|
|
247
|
-
}));
|
|
248
|
-
addProjectOptions(blocker.command("resolve").description("Resolve a blocker").argument("<blocker-id>", "Blocker id"))
|
|
249
|
-
.requiredOption("--resolution <text>", "Resolution details")
|
|
250
|
-
.action(wrap(async (id, options) => {
|
|
251
|
-
const { store } = await target(deps, options);
|
|
252
|
-
checkedText("resolution", options.resolution);
|
|
253
|
-
const current = await store.read("blocker", id);
|
|
254
|
-
if (!current)
|
|
255
|
-
fail("tracker_record_not_found", `Tracker blocker record ${id} was not found`, { kind: "blocker", id });
|
|
256
|
-
if (current.resolved_at !== null)
|
|
257
|
-
fail("tracker_already_resolved", "Blocker is already resolved", { id });
|
|
258
|
-
const record = await store.update("blocker", id, current.version, (value) => ({ ...value, resolved_at: deps.now().toISOString(), resolution: options.resolution }));
|
|
259
|
-
console.log(JSON.stringify({ ok: true, action: "blocker.resolve", record }, null, 2));
|
|
260
|
-
}));
|
|
261
|
-
addProjectOptions(blocker.command("list").description("List blockers")).option("--all", "Include resolved blockers")
|
|
262
|
-
.action(wrap(async (options) => {
|
|
263
|
-
const { store } = await target(deps, options);
|
|
264
|
-
const records = (await store.list("blocker")).filter((record) => options.all || record.resolved_at === null);
|
|
265
|
-
console.log(JSON.stringify({ ok: true, action: "blocker.list", records }, null, 2));
|
|
266
|
-
}));
|
|
267
|
-
const kpi = parent.command("kpi").description("Define KPIs and record values");
|
|
268
|
-
addProjectOptions(kpi.command("define").description("Define a proposed KPI"))
|
|
269
|
-
.requiredOption("--name <text>", "KPI name")
|
|
270
|
-
.requiredOption("--definition <text>", "Definition or query")
|
|
271
|
-
.requiredOption("--unit <unit>", "Measurement unit")
|
|
272
|
-
.requiredOption("--target <number>", "Target value")
|
|
273
|
-
.requiredOption("--direction <direction>", "up | down")
|
|
274
|
-
.action(wrap(async (options) => {
|
|
275
|
-
const { company, project, store } = await target(deps, options);
|
|
276
|
-
if (options.direction !== "up" && options.direction !== "down")
|
|
277
|
-
fail("tracker_direction_invalid", "KPI direction must be up or down", { direction: options.direction });
|
|
278
|
-
checkedText("name", options.name);
|
|
279
|
-
checkedText("definition", options.definition);
|
|
280
|
-
requiredText("unit", options.unit);
|
|
281
|
-
const record = await createRecord(store, "kpi", company.slug, project, {
|
|
282
|
-
name: options.name, definition: options.definition, unit: options.unit,
|
|
283
|
-
target: numberValue(options.target, "target"), direction: options.direction,
|
|
284
|
-
status: "proposed", approved_by: null, approved_at: null, values: [],
|
|
285
|
-
});
|
|
286
|
-
console.log(JSON.stringify({ ok: true, action: "kpi.define", record }, null, 2));
|
|
287
|
-
}));
|
|
288
|
-
addProjectOptions(kpi.command("record").description("Record a KPI value").argument("<kpi-id>", "KPI id"))
|
|
289
|
-
.requiredOption("--value <number>", "Observed value")
|
|
290
|
-
.requiredOption("--source <text>", "Value source")
|
|
291
|
-
.option("--at <iso>", "Observation time")
|
|
292
|
-
.action(wrap(async (id, options) => {
|
|
293
|
-
const { store } = await target(deps, options);
|
|
294
|
-
checkedText("source", options.source);
|
|
295
|
-
const value = numberValue(options.value, "value");
|
|
296
|
-
const at = options.at === undefined ? deps.now() : validTimestamp(options.at);
|
|
297
|
-
const current = await store.read("kpi", id);
|
|
298
|
-
if (!current)
|
|
299
|
-
fail("tracker_record_not_found", `Tracker KPI record ${id} was not found`, { kind: "kpi", id });
|
|
300
|
-
const values = current.values;
|
|
301
|
-
if (!Array.isArray(values))
|
|
302
|
-
fail("tracker_record_invalid", "KPI values must be an array", { id });
|
|
303
|
-
const record = await store.update("kpi", id, current.version, (record) => ({ ...record, values: [...values, { at: at.toISOString(), value, source: options.source }] }));
|
|
304
|
-
console.log(JSON.stringify({ ok: true, action: "kpi.record", record }, null, 2));
|
|
305
|
-
}));
|
|
306
|
-
addProjectOptions(kpi.command("approve").description("Approve a KPI proposal").argument("<kpi-id>", "KPI id"))
|
|
307
|
-
.option("--question <id>", "Answered owner question authorizing this approval")
|
|
308
|
-
.action(wrap(async (id, options) => {
|
|
309
|
-
const { company, project, store } = await target(deps, options);
|
|
310
|
-
const questionId = approvalQuestionId(deps, company, project, options.question);
|
|
311
|
-
const current = await store.read("kpi", id);
|
|
312
|
-
if (!current)
|
|
313
|
-
fail("tracker_record_not_found", `Tracker KPI record ${id} was not found`, { kind: "kpi", id });
|
|
314
|
-
const record = await store.update("kpi", id, current.version, (value) => {
|
|
315
|
-
if (value.status === "approved")
|
|
316
|
-
fail("tracker_already_approved", "KPI is already approved", { id });
|
|
317
|
-
return {
|
|
318
|
-
...value,
|
|
319
|
-
status: "approved",
|
|
320
|
-
approved_by: requireActor(deps),
|
|
321
|
-
approved_at: deps.now().toISOString(),
|
|
322
|
-
...(questionId ? { approval_question_id: questionId } : {}),
|
|
323
|
-
};
|
|
324
|
-
});
|
|
325
|
-
console.log(JSON.stringify({ ok: true, action: "kpi.approve", record }, null, 2));
|
|
326
|
-
}));
|
|
327
|
-
addProjectOptions(kpi.command("list").description("List KPIs")).action(wrap(async (options) => {
|
|
328
|
-
const { store } = await target(deps, options);
|
|
329
|
-
console.log(JSON.stringify({ ok: true, action: "kpi.list", records: await store.list("kpi") }, null, 2));
|
|
330
|
-
}));
|
|
331
|
-
const okr = parent.command("okr").description("Set and approve OKRs");
|
|
332
|
-
addProjectOptions(okr.command("set").description("Create a proposed OKR"))
|
|
333
|
-
.requiredOption("--objective <text>", "Objective")
|
|
334
|
-
.requiredOption("--period <period>", "Planning period")
|
|
335
|
-
.requiredOption("--key-result <text=kpi-id:target>", "Key result", (value, previous = []) => previous.concat(value))
|
|
336
|
-
.action(wrap(async (options) => {
|
|
337
|
-
const { company, project, store } = await target(deps, options);
|
|
338
|
-
checkedText("objective", options.objective);
|
|
339
|
-
requiredText("period", options.period);
|
|
340
|
-
if (!Array.isArray(options.keyResult) || options.keyResult.length === 0)
|
|
341
|
-
fail("tracker_key_result_invalid", "At least one --key-result is required", {});
|
|
342
|
-
const keyResults = options.keyResult.map(parseKr);
|
|
343
|
-
for (const keyResult of keyResults)
|
|
344
|
-
checkedText("key_result", keyResult.text);
|
|
345
|
-
for (const keyResult of keyResults) {
|
|
346
|
-
if (!await store.read("kpi", keyResult.kpi_id)) {
|
|
347
|
-
fail("tracker_kpi_not_found", `Referenced KPI ${keyResult.kpi_id} was not found in this project`, { kpi_id: keyResult.kpi_id, project_id: project });
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
const record = await createRecord(store, "okr", company.slug, project, {
|
|
351
|
-
objective: options.objective, period: options.period,
|
|
352
|
-
key_results: keyResults.map((value) => ({ kr_id: randomUUID(), ...value })),
|
|
353
|
-
status: "proposed", approved_by: null, approved_at: null,
|
|
354
|
-
});
|
|
355
|
-
console.log(JSON.stringify({ ok: true, action: "okr.set", record }, null, 2));
|
|
356
|
-
}));
|
|
357
|
-
addProjectOptions(okr.command("approve").description("Approve an OKR proposal").argument("<okr-id>", "OKR id"))
|
|
358
|
-
.option("--question <id>", "Answered owner question authorizing this approval")
|
|
359
|
-
.action(wrap(async (id, options) => {
|
|
360
|
-
const { company, project, store } = await target(deps, options);
|
|
361
|
-
const questionId = approvalQuestionId(deps, company, project, options.question);
|
|
362
|
-
const current = await store.read("okr", id);
|
|
363
|
-
if (!current)
|
|
364
|
-
fail("tracker_record_not_found", `Tracker OKR record ${id} was not found`, { kind: "okr", id });
|
|
365
|
-
const record = await store.update("okr", id, current.version, (value) => {
|
|
366
|
-
if (value.status === "approved")
|
|
367
|
-
fail("tracker_already_approved", "OKR is already approved", { id });
|
|
368
|
-
return {
|
|
369
|
-
...value,
|
|
370
|
-
status: "approved",
|
|
371
|
-
approved_by: requireActor(deps),
|
|
372
|
-
approved_at: deps.now().toISOString(),
|
|
373
|
-
...(questionId ? { approval_question_id: questionId } : {}),
|
|
374
|
-
};
|
|
375
|
-
});
|
|
376
|
-
console.log(JSON.stringify({ ok: true, action: "okr.approve", record }, null, 2));
|
|
377
|
-
}));
|
|
378
|
-
addProjectOptions(okr.command("list").description("List OKRs")).action(wrap(async (options) => {
|
|
379
|
-
const { store } = await target(deps, options);
|
|
380
|
-
console.log(JSON.stringify({ ok: true, action: "okr.list", records: await store.list("okr") }, null, 2));
|
|
381
|
-
}));
|
|
382
|
-
const directive = parent.command("directive").description("Record directives and acknowledgements");
|
|
383
|
-
addProjectOptions(directive.command("record").description("Record a directive"))
|
|
384
|
-
.requiredOption("--text <text>", "Verbatim directive")
|
|
385
|
-
.requiredOption("--date <yyyy-mm-dd>", "Directive date")
|
|
386
|
-
.requiredOption("--source-url <url>", "Source URL")
|
|
387
|
-
.requiredOption("--team <slug>", "Affected team (repeatable)", (value, previous = []) => previous.concat(value))
|
|
388
|
-
.option("--policy-path <path>", "Related policy path")
|
|
389
|
-
.action(wrap(async (options) => {
|
|
390
|
-
const { company, project, store } = await target(deps, options);
|
|
391
|
-
checkedText("text", options.text);
|
|
392
|
-
checkedText("source_url", options.sourceUrl);
|
|
393
|
-
const policyPath = options.policyPath === undefined
|
|
394
|
-
? null
|
|
395
|
-
: checkedText("policy_path", options.policyPath, MAX_POLICY_PATH_BYTES);
|
|
396
|
-
const teams = (options.team ?? []).map((team) => validSegment(team, "team"));
|
|
397
|
-
if (teams.length === 0)
|
|
398
|
-
fail("tracker_input_invalid", "At least one --team is required", { field: "team" });
|
|
399
|
-
let url;
|
|
400
|
-
try {
|
|
401
|
-
url = new URL(options.sourceUrl);
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
return fail("tracker_source_url_invalid", "Source URL must be an absolute URL", {});
|
|
405
|
-
}
|
|
406
|
-
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
407
|
-
fail("tracker_source_url_invalid", "Source URL must use HTTP or HTTPS", {});
|
|
408
|
-
const record = await createRecord(store, "directive", company.slug, project, {
|
|
409
|
-
text: options.text, date: validDate(options.date), source_url: options.sourceUrl,
|
|
410
|
-
teams: [...new Set(teams)].sort(), policy_path: policyPath, acknowledgements: [],
|
|
411
|
-
});
|
|
412
|
-
console.log(JSON.stringify({ ok: true, action: "directive.record", record }, null, 2));
|
|
413
|
-
}));
|
|
414
|
-
addProjectOptions(directive.command("ack").description("Acknowledge a directive").argument("<directive-id>", "Directive id"))
|
|
415
|
-
.requiredOption("--team <slug>", "Acknowledging team")
|
|
416
|
-
.requiredOption("--lane <lane-id>", "Acknowledging lane id")
|
|
417
|
-
.action(wrap(async (id, options) => {
|
|
418
|
-
const { store } = await target(deps, options);
|
|
419
|
-
const team = validSegment(options.team, "team");
|
|
420
|
-
const lane = validSegment(options.lane, "lane");
|
|
421
|
-
const current = await store.read("directive", id);
|
|
422
|
-
if (!current)
|
|
423
|
-
fail("tracker_record_not_found", `Tracker directive record ${id} was not found`, { kind: "directive", id });
|
|
424
|
-
const teams = current.teams;
|
|
425
|
-
const acknowledgements = current.acknowledgements;
|
|
426
|
-
if (!Array.isArray(teams) || !teams.includes(team))
|
|
427
|
-
fail("tracker_team_not_targeted", "Team is not listed on this directive", { id, team });
|
|
428
|
-
if (!Array.isArray(acknowledgements))
|
|
429
|
-
fail("tracker_record_invalid", "Directive acknowledgements must be an array", { id });
|
|
430
|
-
const record = await store.update("directive", id, current.version, (value) => ({
|
|
431
|
-
...value,
|
|
432
|
-
acknowledgements: [...acknowledgements.filter((ack) => !(ack && typeof ack === "object" && ack.team === team && ack.lane_id === lane)), { team, lane_id: lane, at: deps.now().toISOString() }],
|
|
433
|
-
}));
|
|
434
|
-
console.log(JSON.stringify({ ok: true, action: "directive.ack", record }, null, 2));
|
|
435
|
-
}));
|
|
436
|
-
addProjectOptions(directive.command("list").description("List directives")).action(wrap(async (options) => {
|
|
437
|
-
const { store } = await target(deps, options);
|
|
438
|
-
console.log(JSON.stringify({ ok: true, action: "directive.list", records: await store.list("directive") }, null, 2));
|
|
439
|
-
}));
|
|
440
|
-
}
|
|
441
|
-
export function defaultTrackerActor() {
|
|
442
|
-
const tokens = loadCachedTokens();
|
|
443
|
-
if (!tokens)
|
|
444
|
-
return null;
|
|
445
|
-
const claims = peekIdToken(tokens.idToken);
|
|
446
|
-
const uid = claims["custom:entityUid"];
|
|
447
|
-
if (typeof uid === "string" && uid.trim())
|
|
448
|
-
return uid.trim();
|
|
449
|
-
const sub = claims.sub;
|
|
450
|
-
return typeof sub === "string" && sub.trim() ? sub.trim() : null;
|
|
451
|
-
}
|
|
452
|
-
//# sourceMappingURL=lanes-tracker.js.map
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/** Local status and resource controls shared by the daemon and its sync runner. */
|
|
2
|
-
export declare const SYNC_RUNNER_OWNER = "hq-daemon";
|
|
3
|
-
export declare const SYNC_RUNNER_LEASE_CONFLICT_EXIT = 20;
|
|
4
|
-
export declare const SYNC_RUNNER_LEASE_RETRY_MS = 15000;
|
|
5
|
-
export declare const DEFAULT_SYNC_HEARTBEAT_TIMEOUT_MS = 300000;
|
|
6
|
-
export declare const DEFAULT_SYNC_RUNNER_HEAP_MB = 3584;
|
|
7
|
-
export declare const SYNC_HEARTBEAT_TIMEOUT_ENV = "HQ_DAEMON_SYNC_HEARTBEAT_TIMEOUT_SECONDS";
|
|
8
|
-
export declare const SYNC_RUNNER_HEAP_ENV = "HQ_SYNC_RUNNER_MAX_OLD_SPACE_MB";
|
|
9
|
-
export interface WatchRunnerOwnerStatus {
|
|
10
|
-
owner: string;
|
|
11
|
-
pid: number;
|
|
12
|
-
version: string;
|
|
13
|
-
startedAt: string;
|
|
14
|
-
heartbeatAt: string;
|
|
15
|
-
}
|
|
16
|
-
export interface SyncRunnerProcess {
|
|
17
|
-
pid: number;
|
|
18
|
-
commandLine: string;
|
|
19
|
-
}
|
|
20
|
-
export type SyncRunnerProcessList = {
|
|
21
|
-
ok: true;
|
|
22
|
-
processes: SyncRunnerProcess[];
|
|
23
|
-
} | {
|
|
24
|
-
ok: false;
|
|
25
|
-
error: string;
|
|
26
|
-
};
|
|
27
|
-
export interface ProcessListCommandResult {
|
|
28
|
-
status: number | null;
|
|
29
|
-
stdout: string;
|
|
30
|
-
stderr: string;
|
|
31
|
-
error?: Error;
|
|
32
|
-
}
|
|
33
|
-
export type ProcessListRunner = (command: string, args: string[]) => ProcessListCommandResult;
|
|
34
|
-
export interface SyncPassResult {
|
|
35
|
-
status: "ok" | "partial" | "failed";
|
|
36
|
-
completedAt: string;
|
|
37
|
-
companiesAttempted?: number;
|
|
38
|
-
errors: number;
|
|
39
|
-
transient: number;
|
|
40
|
-
}
|
|
41
|
-
/** The status file path used by hq-cloud's watch-owner lease. */
|
|
42
|
-
export declare function watchRunnerOwnerStatusPath(hqRoot: string, home?: string, env?: NodeJS.ProcessEnv): string;
|
|
43
|
-
/** Read the watch-owner status JSON without taking or changing its lease. */
|
|
44
|
-
export declare function readWatchRunnerOwnerStatus(statusPath: string): WatchRunnerOwnerStatus | null;
|
|
45
|
-
/** Match a live process command line to the hq-cloud watch runner executable. */
|
|
46
|
-
export declare function isSyncRunnerCommandLine(commandLine: string): boolean;
|
|
47
|
-
/** Return a clear wait reason when an unleased runner already targets this HQ root. */
|
|
48
|
-
export declare function syncRunnerUnleasedBlockReason(hqRoot: string, processes: readonly SyncRunnerProcess[], ownerPid: number | null, platform?: NodeJS.Platform, daemonRunnerPids?: ReadonlySet<number>): string | undefined;
|
|
49
|
-
/** Read complete process command lines so root matching can include trailing arguments. */
|
|
50
|
-
export declare function listSyncRunnerProcesses(platform?: NodeJS.Platform, run?: ProcessListRunner): SyncRunnerProcessList;
|
|
51
|
-
/** The watchdog follows the runner's NDJSON protocol, the same liveness signal used by the desktop app. */
|
|
52
|
-
export declare function parseSyncRunnerProtocolLine(line: string): Record<string, unknown> | null;
|
|
53
|
-
/** Summarize the runner's all-complete event for daemon status. */
|
|
54
|
-
export declare function syncPassResult(event: Record<string, unknown>, completedAt?: string): SyncPassResult | null;
|
|
55
|
-
export declare function syncHeartbeatTimeoutMs(env: NodeJS.ProcessEnv): number;
|
|
56
|
-
/** Preserve an explicit NODE_OPTIONS heap setting; otherwise apply the desktop runner's default/override. */
|
|
57
|
-
export declare function syncRunnerNodeOptions(env: NodeJS.ProcessEnv): string | undefined;
|
|
58
|
-
//# sourceMappingURL=sync-runner.d.ts.map
|