@indigoai-us/hq-cli 5.197.0 → 5.198.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 -0
- package/dist/command-catalog.generated.d.ts +390 -0
- package/dist/command-catalog.generated.js +506 -0
- package/dist/commands/index-cmd.d.ts +7 -1
- package/dist/commands/index-cmd.js +46 -21
- package/dist/commands/lanes-tracker.d.ts +20 -0
- package/dist/commands/lanes-tracker.js +420 -0
- package/dist/commands/lanes.d.ts +6 -0
- package/dist/commands/lanes.js +18 -0
- package/dist/lib/bot/inflight.d.ts +12 -0
- package/dist/lib/bot/run.js +21 -2
- package/dist/lib/bot/runtime/messages-stream.d.ts +21 -1
- package/dist/lib/bot/runtime/messages-stream.js +54 -2
- package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +3 -1
- package/dist/lib/core-utils/qmd-reindex-after-sync.js +69 -40
- package/dist/lib/lanes/tracker-store.d.ts +27 -0
- package/dist/lib/lanes/tracker-store.js +277 -0
- package/dist/lib/search-index/embed-lock.d.ts +5 -4
- package/dist/lib/search-index/embed-lock.js +5 -4
- package/dist/lib/search-index/reindex-single-flight-flag.d.ts +5 -0
- package/dist/lib/search-index/reindex-single-flight-flag.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,420 @@
|
|
|
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 { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
8
|
+
import { createLocalTrackerStore } from "../lib/lanes/tracker-store.js";
|
|
9
|
+
import { peekIdToken } from "../utils/id-token.js";
|
|
10
|
+
export const LANES_TRACKER_FLAG = "lanes.tracker";
|
|
11
|
+
export const LANES_TRACKER_MESH_FLAG = "lanes.tracker-mesh";
|
|
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
|
+
if (await deps.trackerMeshEnabled(company)) {
|
|
76
|
+
fail("tracker_mesh_unavailable", "Work Mesh tracker storage is not available in this CLI version", { company: company.slug });
|
|
77
|
+
}
|
|
78
|
+
const project = validSegment(options.project, "project");
|
|
79
|
+
const ledger = resolveProjectStoryLedger(deps.hqRoot(), project, company.slug);
|
|
80
|
+
const projectCompany = readProjectCompany(ledger.path);
|
|
81
|
+
if (projectCompany !== null && projectCompany !== company.slug) {
|
|
82
|
+
fail("tracker_company_mismatch", "Project company does not match the active HQ company", {
|
|
83
|
+
active_company: company.slug,
|
|
84
|
+
project_company: projectCompany,
|
|
85
|
+
project_id: project,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const directory = path.dirname(ledger.path);
|
|
89
|
+
let realDirectory;
|
|
90
|
+
try {
|
|
91
|
+
realDirectory = fs.realpathSync(directory);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
fail("tracker_project_not_found", "Project directory was not found for this company", {
|
|
95
|
+
project_id: project,
|
|
96
|
+
filesystem_code: err.code ?? "unknown",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const expectedRoot = fs.realpathSync(path.join(deps.hqRoot(), "companies", company.slug, "projects"));
|
|
100
|
+
if (!realDirectory.startsWith(`${expectedRoot}${path.sep}`)) {
|
|
101
|
+
fail("tracker_company_mismatch", "Project directory resolves outside the active HQ company", {
|
|
102
|
+
active_company: company.slug,
|
|
103
|
+
project_id: project,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
company,
|
|
108
|
+
project,
|
|
109
|
+
store: (deps.store ?? ((dir) => createLocalTrackerStore(dir, deps.now)))(realDirectory),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function requireActor(deps) {
|
|
113
|
+
const actor = deps.actor()?.trim();
|
|
114
|
+
if (!actor)
|
|
115
|
+
fail("tracker_identity_unavailable", "Cannot record approval without a signed-in HQ identity", {});
|
|
116
|
+
return actor;
|
|
117
|
+
}
|
|
118
|
+
function numberValue(raw, field) {
|
|
119
|
+
const value = Number(raw);
|
|
120
|
+
if (!Number.isFinite(value))
|
|
121
|
+
return fail("tracker_input_invalid", `${field} must be a finite number`, { field });
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
function parseKr(raw) {
|
|
125
|
+
const match = /^(.+)=([a-zA-Z0-9][a-zA-Z0-9._-]{0,127}):(-?(?:\d+(?:\.\d*)?|\.\d+))$/.exec(raw);
|
|
126
|
+
if (!match)
|
|
127
|
+
fail("tracker_key_result_invalid", "Key result must be <text>=<kpi-id>:<target>", {});
|
|
128
|
+
return { text: match[1].trim(), kpi_id: validSegment(match[2], "kpi-id"), target: numberValue(match[3], "key-result target") };
|
|
129
|
+
}
|
|
130
|
+
function validDate(raw) {
|
|
131
|
+
let normalized;
|
|
132
|
+
try {
|
|
133
|
+
normalized = new Date(`${raw}T00:00:00.000Z`).toISOString().slice(0, 10);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
normalized = undefined;
|
|
137
|
+
}
|
|
138
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw) || normalized !== raw) {
|
|
139
|
+
return fail("tracker_date_invalid", "Date must be a valid YYYY-MM-DD value", { field: "date" });
|
|
140
|
+
}
|
|
141
|
+
return raw;
|
|
142
|
+
}
|
|
143
|
+
function validTimestamp(raw) {
|
|
144
|
+
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);
|
|
145
|
+
if (!match)
|
|
146
|
+
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
147
|
+
const [, year, month, day, hour, minute, second = "00", fraction = "", zone] = match;
|
|
148
|
+
const date = `${year}-${month}-${day}`;
|
|
149
|
+
let calendarDate;
|
|
150
|
+
try {
|
|
151
|
+
calendarDate = new Date(`${date}T00:00:00.000Z`).toISOString().slice(0, 10);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
calendarDate = undefined;
|
|
155
|
+
}
|
|
156
|
+
if (calendarDate !== date || Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59) {
|
|
157
|
+
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
158
|
+
}
|
|
159
|
+
let offsetMinutes = 0;
|
|
160
|
+
if (zone !== "Z") {
|
|
161
|
+
const offset = /^([+-])(\d{2}):(\d{2})$/.exec(zone);
|
|
162
|
+
const offsetHours = Number(offset?.[2]);
|
|
163
|
+
const offsetRemainder = Number(offset?.[3]);
|
|
164
|
+
if (!offset || offsetHours > 14 || offsetRemainder > 59 || (offsetHours === 14 && offsetRemainder !== 0)) {
|
|
165
|
+
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
166
|
+
}
|
|
167
|
+
const sign = offset[1] === "+" ? 1 : -1;
|
|
168
|
+
offsetMinutes = sign * (offsetHours * 60 + offsetRemainder);
|
|
169
|
+
}
|
|
170
|
+
const localTimeAsUtc = new Date(`${date}T${hour}:${minute}:${second}.${fraction.padEnd(3, "0")}Z`);
|
|
171
|
+
const expectedEpoch = localTimeAsUtc.getTime() - offsetMinutes * 60_000;
|
|
172
|
+
const parsed = new Date(raw);
|
|
173
|
+
if (!Number.isFinite(expectedEpoch) || !Number.isFinite(parsed.getTime()) || parsed.getTime() !== expectedEpoch) {
|
|
174
|
+
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
175
|
+
}
|
|
176
|
+
const roundTripped = new Date(parsed.toISOString());
|
|
177
|
+
if (roundTripped.getTime() !== expectedEpoch) {
|
|
178
|
+
return fail("tracker_date_invalid", "KPI --at must be an ISO 8601 timestamp with a timezone", { field: "at" });
|
|
179
|
+
}
|
|
180
|
+
return parsed;
|
|
181
|
+
}
|
|
182
|
+
function addProjectOptions(command) {
|
|
183
|
+
return command
|
|
184
|
+
.requiredOption("--project <slug>", "HQ team project slug")
|
|
185
|
+
.option("--company <slug>", "Select the company by slug");
|
|
186
|
+
}
|
|
187
|
+
function createRecord(store, kind, company, project, fields) {
|
|
188
|
+
return store.create({ kind, id: randomUUID(), company, project_id: project, ...fields });
|
|
189
|
+
}
|
|
190
|
+
export function registerLaneTrackerCommands(parent, deps, wrap) {
|
|
191
|
+
const blocker = parent.command("blocker").description("Record and manage project blockers");
|
|
192
|
+
addProjectOptions(blocker.command("open").description("Open a blocker"))
|
|
193
|
+
.requiredOption("--story <id>", "Blocked story id")
|
|
194
|
+
.requiredOption("--what <text>", "What is blocked")
|
|
195
|
+
.requiredOption("--waiting-on <text>", "Who or what is needed")
|
|
196
|
+
.requiredOption("--next-step <text>", "Next action")
|
|
197
|
+
.action(wrap(async (options) => {
|
|
198
|
+
const { company, project, store } = await target(deps, options);
|
|
199
|
+
if (!options.story.trim())
|
|
200
|
+
fail("tracker_input_invalid", "story must not be empty", { field: "story" });
|
|
201
|
+
for (const [field, value] of [["what", options.what], ["waiting_on", options.waitingOn], ["next_step", options.nextStep]])
|
|
202
|
+
checkedText(field, value);
|
|
203
|
+
const record = createRecord(store, "blocker", company.slug, project, {
|
|
204
|
+
story_id: options.story.trim(), what: options.what, waiting_on: options.waitingOn,
|
|
205
|
+
next_step: options.nextStep, opened_at: deps.now().toISOString(), resolved_at: null, resolution: null,
|
|
206
|
+
});
|
|
207
|
+
console.log(JSON.stringify({ ok: true, action: "blocker.open", record }, null, 2));
|
|
208
|
+
}));
|
|
209
|
+
addProjectOptions(blocker.command("update").description("Update a blocker").argument("<blocker-id>", "Blocker id"))
|
|
210
|
+
.option("--waiting-on <text>", "Who or what is needed")
|
|
211
|
+
.option("--next-step <text>", "Next action")
|
|
212
|
+
.action(wrap(async (id, options) => {
|
|
213
|
+
const { store } = await target(deps, options);
|
|
214
|
+
if (options.waitingOn === undefined && options.nextStep === undefined)
|
|
215
|
+
fail("tracker_update_empty", "Supply --waiting-on or --next-step", {});
|
|
216
|
+
if (options.waitingOn !== undefined)
|
|
217
|
+
checkedText("waiting_on", options.waitingOn);
|
|
218
|
+
if (options.nextStep !== undefined)
|
|
219
|
+
checkedText("next_step", options.nextStep);
|
|
220
|
+
const current = store.read("blocker", id);
|
|
221
|
+
if (!current)
|
|
222
|
+
fail("tracker_record_not_found", `Tracker blocker record ${id} was not found`, { kind: "blocker", id });
|
|
223
|
+
if (current.resolved_at !== null)
|
|
224
|
+
fail("tracker_already_resolved", "Blocker is already resolved", { id });
|
|
225
|
+
const record = store.update("blocker", id, current.version, (value) => ({
|
|
226
|
+
...value,
|
|
227
|
+
...(options.waitingOn === undefined ? {} : { waiting_on: options.waitingOn }),
|
|
228
|
+
...(options.nextStep === undefined ? {} : { next_step: options.nextStep }),
|
|
229
|
+
}));
|
|
230
|
+
console.log(JSON.stringify({ ok: true, action: "blocker.update", record }, null, 2));
|
|
231
|
+
}));
|
|
232
|
+
addProjectOptions(blocker.command("resolve").description("Resolve a blocker").argument("<blocker-id>", "Blocker id"))
|
|
233
|
+
.requiredOption("--resolution <text>", "Resolution details")
|
|
234
|
+
.action(wrap(async (id, options) => {
|
|
235
|
+
const { store } = await target(deps, options);
|
|
236
|
+
checkedText("resolution", options.resolution);
|
|
237
|
+
const current = store.read("blocker", id);
|
|
238
|
+
if (!current)
|
|
239
|
+
fail("tracker_record_not_found", `Tracker blocker record ${id} was not found`, { kind: "blocker", id });
|
|
240
|
+
if (current.resolved_at !== null)
|
|
241
|
+
fail("tracker_already_resolved", "Blocker is already resolved", { id });
|
|
242
|
+
const record = store.update("blocker", id, current.version, (value) => ({ ...value, resolved_at: deps.now().toISOString(), resolution: options.resolution }));
|
|
243
|
+
console.log(JSON.stringify({ ok: true, action: "blocker.resolve", record }, null, 2));
|
|
244
|
+
}));
|
|
245
|
+
addProjectOptions(blocker.command("list").description("List blockers")).option("--all", "Include resolved blockers")
|
|
246
|
+
.action(wrap(async (options) => {
|
|
247
|
+
const { store } = await target(deps, options);
|
|
248
|
+
const records = store.list("blocker").filter((record) => options.all || record.resolved_at === null);
|
|
249
|
+
console.log(JSON.stringify({ ok: true, action: "blocker.list", records }, null, 2));
|
|
250
|
+
}));
|
|
251
|
+
const kpi = parent.command("kpi").description("Define KPIs and record values");
|
|
252
|
+
addProjectOptions(kpi.command("define").description("Define a proposed KPI"))
|
|
253
|
+
.requiredOption("--name <text>", "KPI name")
|
|
254
|
+
.requiredOption("--definition <text>", "Definition or query")
|
|
255
|
+
.requiredOption("--unit <unit>", "Measurement unit")
|
|
256
|
+
.requiredOption("--target <number>", "Target value")
|
|
257
|
+
.requiredOption("--direction <direction>", "up | down")
|
|
258
|
+
.action(wrap(async (options) => {
|
|
259
|
+
const { company, project, store } = await target(deps, options);
|
|
260
|
+
if (options.direction !== "up" && options.direction !== "down")
|
|
261
|
+
fail("tracker_direction_invalid", "KPI direction must be up or down", { direction: options.direction });
|
|
262
|
+
checkedText("name", options.name);
|
|
263
|
+
checkedText("definition", options.definition);
|
|
264
|
+
requiredText("unit", options.unit);
|
|
265
|
+
const record = createRecord(store, "kpi", company.slug, project, {
|
|
266
|
+
name: options.name, definition: options.definition, unit: options.unit,
|
|
267
|
+
target: numberValue(options.target, "target"), direction: options.direction,
|
|
268
|
+
status: "proposed", approved_by: null, approved_at: null, values: [],
|
|
269
|
+
});
|
|
270
|
+
console.log(JSON.stringify({ ok: true, action: "kpi.define", record }, null, 2));
|
|
271
|
+
}));
|
|
272
|
+
addProjectOptions(kpi.command("record").description("Record a KPI value").argument("<kpi-id>", "KPI id"))
|
|
273
|
+
.requiredOption("--value <number>", "Observed value")
|
|
274
|
+
.requiredOption("--source <text>", "Value source")
|
|
275
|
+
.option("--at <iso>", "Observation time")
|
|
276
|
+
.action(wrap(async (id, options) => {
|
|
277
|
+
const { store } = await target(deps, options);
|
|
278
|
+
checkedText("source", options.source);
|
|
279
|
+
const value = numberValue(options.value, "value");
|
|
280
|
+
const at = options.at === undefined ? deps.now() : validTimestamp(options.at);
|
|
281
|
+
const current = store.read("kpi", id);
|
|
282
|
+
if (!current)
|
|
283
|
+
fail("tracker_record_not_found", `Tracker KPI record ${id} was not found`, { kind: "kpi", id });
|
|
284
|
+
const values = current.values;
|
|
285
|
+
if (!Array.isArray(values))
|
|
286
|
+
fail("tracker_record_invalid", "KPI values must be an array", { id });
|
|
287
|
+
const record = store.update("kpi", id, current.version, (record) => ({ ...record, values: [...values, { at: at.toISOString(), value, source: options.source }] }));
|
|
288
|
+
console.log(JSON.stringify({ ok: true, action: "kpi.record", record }, null, 2));
|
|
289
|
+
}));
|
|
290
|
+
addProjectOptions(kpi.command("approve").description("Approve a KPI proposal").argument("<kpi-id>", "KPI id"))
|
|
291
|
+
.action(wrap(async (id, options) => {
|
|
292
|
+
const { store } = await target(deps, options);
|
|
293
|
+
const current = store.read("kpi", id);
|
|
294
|
+
if (!current)
|
|
295
|
+
fail("tracker_record_not_found", `Tracker KPI record ${id} was not found`, { kind: "kpi", id });
|
|
296
|
+
const record = store.update("kpi", id, current.version, (value) => {
|
|
297
|
+
if (value.status === "approved")
|
|
298
|
+
fail("tracker_already_approved", "KPI is already approved", { id });
|
|
299
|
+
return { ...value, status: "approved", approved_by: requireActor(deps), approved_at: deps.now().toISOString() };
|
|
300
|
+
});
|
|
301
|
+
console.log(JSON.stringify({ ok: true, action: "kpi.approve", record }, null, 2));
|
|
302
|
+
}));
|
|
303
|
+
addProjectOptions(kpi.command("list").description("List KPIs")).action(wrap(async (options) => {
|
|
304
|
+
const { store } = await target(deps, options);
|
|
305
|
+
console.log(JSON.stringify({ ok: true, action: "kpi.list", records: store.list("kpi") }, null, 2));
|
|
306
|
+
}));
|
|
307
|
+
const okr = parent.command("okr").description("Set and approve OKRs");
|
|
308
|
+
addProjectOptions(okr.command("set").description("Create a proposed OKR"))
|
|
309
|
+
.requiredOption("--objective <text>", "Objective")
|
|
310
|
+
.requiredOption("--period <period>", "Planning period")
|
|
311
|
+
.requiredOption("--key-result <text=kpi-id:target>", "Key result", (value, previous = []) => previous.concat(value))
|
|
312
|
+
.action(wrap(async (options) => {
|
|
313
|
+
const { company, project, store } = await target(deps, options);
|
|
314
|
+
checkedText("objective", options.objective);
|
|
315
|
+
requiredText("period", options.period);
|
|
316
|
+
if (!Array.isArray(options.keyResult) || options.keyResult.length === 0)
|
|
317
|
+
fail("tracker_key_result_invalid", "At least one --key-result is required", {});
|
|
318
|
+
const keyResults = options.keyResult.map(parseKr);
|
|
319
|
+
for (const keyResult of keyResults)
|
|
320
|
+
checkedText("key_result", keyResult.text);
|
|
321
|
+
for (const keyResult of keyResults) {
|
|
322
|
+
if (!store.read("kpi", keyResult.kpi_id)) {
|
|
323
|
+
fail("tracker_kpi_not_found", `Referenced KPI ${keyResult.kpi_id} was not found in this project`, { kpi_id: keyResult.kpi_id, project_id: project });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const record = createRecord(store, "okr", company.slug, project, {
|
|
327
|
+
objective: options.objective, period: options.period,
|
|
328
|
+
key_results: keyResults.map((value) => ({ kr_id: randomUUID(), ...value })),
|
|
329
|
+
status: "proposed", approved_by: null, approved_at: null,
|
|
330
|
+
});
|
|
331
|
+
console.log(JSON.stringify({ ok: true, action: "okr.set", record }, null, 2));
|
|
332
|
+
}));
|
|
333
|
+
addProjectOptions(okr.command("approve").description("Approve an OKR proposal").argument("<okr-id>", "OKR id"))
|
|
334
|
+
.action(wrap(async (id, options) => {
|
|
335
|
+
const { store } = await target(deps, options);
|
|
336
|
+
const current = store.read("okr", id);
|
|
337
|
+
if (!current)
|
|
338
|
+
fail("tracker_record_not_found", `Tracker OKR record ${id} was not found`, { kind: "okr", id });
|
|
339
|
+
const record = store.update("okr", id, current.version, (value) => {
|
|
340
|
+
if (value.status === "approved")
|
|
341
|
+
fail("tracker_already_approved", "OKR is already approved", { id });
|
|
342
|
+
return { ...value, status: "approved", approved_by: requireActor(deps), approved_at: deps.now().toISOString() };
|
|
343
|
+
});
|
|
344
|
+
console.log(JSON.stringify({ ok: true, action: "okr.approve", record }, null, 2));
|
|
345
|
+
}));
|
|
346
|
+
addProjectOptions(okr.command("list").description("List OKRs")).action(wrap(async (options) => {
|
|
347
|
+
const { store } = await target(deps, options);
|
|
348
|
+
console.log(JSON.stringify({ ok: true, action: "okr.list", records: store.list("okr") }, null, 2));
|
|
349
|
+
}));
|
|
350
|
+
const directive = parent.command("directive").description("Record directives and acknowledgements");
|
|
351
|
+
addProjectOptions(directive.command("record").description("Record a directive"))
|
|
352
|
+
.requiredOption("--text <text>", "Verbatim directive")
|
|
353
|
+
.requiredOption("--date <yyyy-mm-dd>", "Directive date")
|
|
354
|
+
.requiredOption("--source-url <url>", "Source URL")
|
|
355
|
+
.requiredOption("--team <slug>", "Affected team (repeatable)", (value, previous = []) => previous.concat(value))
|
|
356
|
+
.option("--policy-path <path>", "Related policy path")
|
|
357
|
+
.action(wrap(async (options) => {
|
|
358
|
+
const { company, project, store } = await target(deps, options);
|
|
359
|
+
checkedText("text", options.text);
|
|
360
|
+
checkedText("source_url", options.sourceUrl);
|
|
361
|
+
const policyPath = options.policyPath === undefined
|
|
362
|
+
? null
|
|
363
|
+
: checkedText("policy_path", options.policyPath, MAX_POLICY_PATH_BYTES);
|
|
364
|
+
const teams = (options.team ?? []).map((team) => validSegment(team, "team"));
|
|
365
|
+
if (teams.length === 0)
|
|
366
|
+
fail("tracker_input_invalid", "At least one --team is required", { field: "team" });
|
|
367
|
+
let url;
|
|
368
|
+
try {
|
|
369
|
+
url = new URL(options.sourceUrl);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return fail("tracker_source_url_invalid", "Source URL must be an absolute URL", {});
|
|
373
|
+
}
|
|
374
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
375
|
+
fail("tracker_source_url_invalid", "Source URL must use HTTP or HTTPS", {});
|
|
376
|
+
const record = createRecord(store, "directive", company.slug, project, {
|
|
377
|
+
text: options.text, date: validDate(options.date), source_url: options.sourceUrl,
|
|
378
|
+
teams: [...new Set(teams)].sort(), policy_path: policyPath, acknowledgements: [],
|
|
379
|
+
});
|
|
380
|
+
console.log(JSON.stringify({ ok: true, action: "directive.record", record }, null, 2));
|
|
381
|
+
}));
|
|
382
|
+
addProjectOptions(directive.command("ack").description("Acknowledge a directive").argument("<directive-id>", "Directive id"))
|
|
383
|
+
.requiredOption("--team <slug>", "Acknowledging team")
|
|
384
|
+
.requiredOption("--lane <lane-id>", "Acknowledging lane id")
|
|
385
|
+
.action(wrap(async (id, options) => {
|
|
386
|
+
const { store } = await target(deps, options);
|
|
387
|
+
const team = validSegment(options.team, "team");
|
|
388
|
+
const lane = validSegment(options.lane, "lane");
|
|
389
|
+
const current = store.read("directive", id);
|
|
390
|
+
if (!current)
|
|
391
|
+
fail("tracker_record_not_found", `Tracker directive record ${id} was not found`, { kind: "directive", id });
|
|
392
|
+
const teams = current.teams;
|
|
393
|
+
const acknowledgements = current.acknowledgements;
|
|
394
|
+
if (!Array.isArray(teams) || !teams.includes(team))
|
|
395
|
+
fail("tracker_team_not_targeted", "Team is not listed on this directive", { id, team });
|
|
396
|
+
if (!Array.isArray(acknowledgements))
|
|
397
|
+
fail("tracker_record_invalid", "Directive acknowledgements must be an array", { id });
|
|
398
|
+
const record = store.update("directive", id, current.version, (value) => ({
|
|
399
|
+
...value,
|
|
400
|
+
acknowledgements: [...acknowledgements.filter((ack) => !(ack && typeof ack === "object" && ack.team === team && ack.lane_id === lane)), { team, lane_id: lane, at: deps.now().toISOString() }],
|
|
401
|
+
}));
|
|
402
|
+
console.log(JSON.stringify({ ok: true, action: "directive.ack", record }, null, 2));
|
|
403
|
+
}));
|
|
404
|
+
addProjectOptions(directive.command("list").description("List directives")).action(wrap(async (options) => {
|
|
405
|
+
const { store } = await target(deps, options);
|
|
406
|
+
console.log(JSON.stringify({ ok: true, action: "directive.list", records: store.list("directive") }, null, 2));
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
export function defaultTrackerActor() {
|
|
410
|
+
const tokens = loadCachedTokens();
|
|
411
|
+
if (!tokens)
|
|
412
|
+
return null;
|
|
413
|
+
const claims = peekIdToken(tokens.idToken);
|
|
414
|
+
const uid = claims["custom:entityUid"];
|
|
415
|
+
if (typeof uid === "string" && uid.trim())
|
|
416
|
+
return uid.trim();
|
|
417
|
+
const sub = claims.sub;
|
|
418
|
+
return typeof sub === "string" && sub.trim() ? sub.trim() : null;
|
|
419
|
+
}
|
|
420
|
+
//# sourceMappingURL=lanes-tracker.js.map
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -88,6 +88,12 @@ export interface LanesCommandDeps {
|
|
|
88
88
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
89
89
|
/** Test seam for the Claude account-balancing gate. */
|
|
90
90
|
laneClaudeAccountBalanceEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
91
|
+
/** Test seam for the hq-flags gated tracker commands. */
|
|
92
|
+
laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
93
|
+
/** The local-only PR 2 implementation refuses if mesh storage is enabled. */
|
|
94
|
+
laneTrackerMeshEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
95
|
+
/** Test seam for recording approvals against the active identity. */
|
|
96
|
+
trackerActor?: () => string | null;
|
|
91
97
|
}
|
|
92
98
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
93
99
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -17,6 +17,7 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
|
17
17
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
18
18
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
19
19
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
20
|
+
import { defaultTrackerActor, LANES_TRACKER_FLAG, LANES_TRACKER_MESH_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
|
|
20
21
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
21
22
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
22
23
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
@@ -62,6 +63,12 @@ function defaultLaneClaudeAccountBalanceEnabled(company) {
|
|
|
62
63
|
// behavior, matching the lane hire-grants gate.
|
|
63
64
|
return readLanesRegistryFlag(CLAUDE_ACCOUNT_BALANCE_FLAG, { company });
|
|
64
65
|
}
|
|
66
|
+
function defaultLaneTrackerEnabled(company) {
|
|
67
|
+
return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
|
|
68
|
+
}
|
|
69
|
+
function defaultLaneTrackerMeshEnabled(company) {
|
|
70
|
+
return readLanesRegistryFlag(LANES_TRACKER_MESH_FLAG, { company });
|
|
71
|
+
}
|
|
65
72
|
function defaultLaneHireGrantsEnabled(company) {
|
|
66
73
|
return readLanesRegistryFlag(LANE_HIRE_GRANTS_FLAG, { company });
|
|
67
74
|
}
|
|
@@ -264,6 +271,9 @@ function resolveDeps(deps = {}) {
|
|
|
264
271
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
265
272
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
266
273
|
laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
|
|
274
|
+
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
275
|
+
laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
|
|
276
|
+
trackerActor: deps.trackerActor ?? defaultTrackerActor,
|
|
267
277
|
};
|
|
268
278
|
}
|
|
269
279
|
function livenessDepsFromCtx(ctx) {
|
|
@@ -7299,6 +7309,14 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7299
7309
|
const lanes = parent
|
|
7300
7310
|
.command("lanes", { hidden: true })
|
|
7301
7311
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
7312
|
+
registerLaneTrackerCommands(lanes, {
|
|
7313
|
+
hqRoot: ctx.hqRoot,
|
|
7314
|
+
resolveCompany: ctx.resolveCompany,
|
|
7315
|
+
trackerEnabled: ctx.laneTrackerEnabled,
|
|
7316
|
+
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
7317
|
+
now: ctx.now,
|
|
7318
|
+
actor: ctx.trackerActor,
|
|
7319
|
+
}, wrap);
|
|
7302
7320
|
registerOutpostPrunerCommand(lanes);
|
|
7303
7321
|
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
7304
7322
|
noStoryOption.attributeName = () => "noStory";
|
|
@@ -93,6 +93,18 @@ export interface InflightTurn {
|
|
|
93
93
|
* RECOVERY_ATTEMPT_LIMIT and says so once (run.ts).
|
|
94
94
|
*/
|
|
95
95
|
recoveryFailures?: number;
|
|
96
|
+
/**
|
|
97
|
+
* True when the interrupted turn was a "silenceable" one: an unmentioned
|
|
98
|
+
* room message nobody asked the bot to answer. On recovery a silenceable
|
|
99
|
+
* turn with no saved reply is acked silently — a "I didn't finish that —
|
|
100
|
+
* I was restarted…" notice in a public room the bot was not addressed in
|
|
101
|
+
* is noise, and (2026-09-25) had two bots posting it in a channel where
|
|
102
|
+
* neither had been @-mentioned. A silenceable marker WITH a saved
|
|
103
|
+
* `reply` still posts: that was a real answer the model produced and only
|
|
104
|
+
* delivery failed. Absent (older marker) → recover posts the notice, as
|
|
105
|
+
* before.
|
|
106
|
+
*/
|
|
107
|
+
silenceable?: boolean;
|
|
96
108
|
}
|
|
97
109
|
export declare function botInflightPath(dir: string): string;
|
|
98
110
|
/** Every turn that was in progress, oldest first. */
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -648,10 +648,15 @@ export async function runBot(deps) {
|
|
|
648
648
|
*/
|
|
649
649
|
const runTurnWithProgress = async (args) => {
|
|
650
650
|
const { messageId } = args.marker;
|
|
651
|
-
|
|
651
|
+
const silenceable = args.silenceable === true;
|
|
652
|
+
writeInflight(dir, {
|
|
653
|
+
v: 1,
|
|
654
|
+
...args.marker,
|
|
655
|
+
startedAt: now().toISOString(),
|
|
656
|
+
...(silenceable ? { silenceable: true } : {}),
|
|
657
|
+
});
|
|
652
658
|
let running = true;
|
|
653
659
|
let posted = 0;
|
|
654
|
-
const silenceable = args.silenceable === true;
|
|
655
660
|
const makeSendFn = (fn) => async (body) => {
|
|
656
661
|
try {
|
|
657
662
|
await fn(body);
|
|
@@ -937,6 +942,20 @@ export async function runBot(deps) {
|
|
|
937
942
|
clearInflight(dir, marker.messageId);
|
|
938
943
|
return true;
|
|
939
944
|
}
|
|
945
|
+
// An unmentioned room turn (silenceable) that never produced a saved
|
|
946
|
+
// reply: the person never asked the bot to answer. Posting "I didn't
|
|
947
|
+
// finish that — I was restarted…" into that room is noise (2026-09-25:
|
|
948
|
+
// two bots posted the notice in a channel where neither was mentioned).
|
|
949
|
+
// Ack and clear silently; a saved `reply` still posts below because a
|
|
950
|
+
// real answer that only failed delivery is worth keeping.
|
|
951
|
+
if (!isKickoff &&
|
|
952
|
+
marker.silenceable === true &&
|
|
953
|
+
!marker.jobId &&
|
|
954
|
+
(marker.reply === undefined || marker.reply.trim() === "")) {
|
|
955
|
+
log("info", `recovered unmentioned room turn ${marker.messageId} silently (no saved reply); not posting a restart notice`);
|
|
956
|
+
await closeMarker(marker, isKickoff);
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
940
959
|
const parts = marker.replyParts ?? splitBody(recoveryBody(marker));
|
|
941
960
|
const from = marker.replyParts ? Math.min(Math.max(0, marker.replyPartsDelivered ?? 0), parts.length) : 0;
|
|
942
961
|
try {
|
|
@@ -31,6 +31,26 @@ export declare function messagesStreamEvents(line: string): RuntimeStreamEvent[]
|
|
|
31
31
|
* RuntimeError for an error result, or when there is nothing usable.
|
|
32
32
|
*/
|
|
33
33
|
export declare function messagesStreamResult(stdout: string, runtimeName: string): RuntimeTurnResult;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The error text of an `is_error` result event, if the stream has one.
|
|
36
|
+
*
|
|
37
|
+
* Also handles the case grok 0.2.56+ produces on some non-zero exits: the
|
|
38
|
+
* whole stdout is one pretty-printed JSON object (a `result` envelope
|
|
39
|
+
* spanning many lines) rather than NDJSON. The per-line parser sees only
|
|
40
|
+
* `{` / `"key": …` / `}` fragments and cannot recover the fields, so
|
|
41
|
+
* `stderrExcerpt(stdout)` ended up logging the tail of the pretty JSON —
|
|
42
|
+
* "…costUsdTicks…numTurns…" — instead of the actual error text
|
|
43
|
+
* (observed 2026-09-25 in sheister's log). We try a whole-stdout JSON parse
|
|
44
|
+
* as a fallback and pull the real error string out of the envelope.
|
|
45
|
+
*/
|
|
35
46
|
export declare function messagesStreamFailure(stdout: string): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Whole stdout parsed as ONE JSON object (grok 0.2.56+ pretty-printed
|
|
49
|
+
* result envelope). Pulls the error text out of the fields the runtimes
|
|
50
|
+
* actually populate: `error.message` / `error`, then `result` when
|
|
51
|
+
* `is_error` is set, then `subtype` when it isn't "success". Returns
|
|
52
|
+
* undefined when there is nothing useful to say — the caller falls back
|
|
53
|
+
* to its own default.
|
|
54
|
+
*/
|
|
55
|
+
export declare function prettyJsonFailure(stdout: string): string | undefined;
|
|
36
56
|
//# sourceMappingURL=messages-stream.d.ts.map
|
|
@@ -127,7 +127,18 @@ function grokTokenStreamResult(stdout, runtimeName) {
|
|
|
127
127
|
}
|
|
128
128
|
return saw ? { text, sessionId } : null;
|
|
129
129
|
}
|
|
130
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The error text of an `is_error` result event, if the stream has one.
|
|
132
|
+
*
|
|
133
|
+
* Also handles the case grok 0.2.56+ produces on some non-zero exits: the
|
|
134
|
+
* whole stdout is one pretty-printed JSON object (a `result` envelope
|
|
135
|
+
* spanning many lines) rather than NDJSON. The per-line parser sees only
|
|
136
|
+
* `{` / `"key": …` / `}` fragments and cannot recover the fields, so
|
|
137
|
+
* `stderrExcerpt(stdout)` ended up logging the tail of the pretty JSON —
|
|
138
|
+
* "…costUsdTicks…numTurns…" — instead of the actual error text
|
|
139
|
+
* (observed 2026-09-25 in sheister's log). We try a whole-stdout JSON parse
|
|
140
|
+
* as a fallback and pull the real error string out of the envelope.
|
|
141
|
+
*/
|
|
131
142
|
export function messagesStreamFailure(stdout) {
|
|
132
143
|
let reason;
|
|
133
144
|
for (const line of stdout.split("\n")) {
|
|
@@ -137,6 +148,47 @@ export function messagesStreamFailure(stdout) {
|
|
|
137
148
|
if (ev?.type === "error" && typeof ev.message === "string" && ev.message.trim())
|
|
138
149
|
reason = ev.message.trim();
|
|
139
150
|
}
|
|
140
|
-
|
|
151
|
+
if (reason)
|
|
152
|
+
return reason;
|
|
153
|
+
return prettyJsonFailure(stdout);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whole stdout parsed as ONE JSON object (grok 0.2.56+ pretty-printed
|
|
157
|
+
* result envelope). Pulls the error text out of the fields the runtimes
|
|
158
|
+
* actually populate: `error.message` / `error`, then `result` when
|
|
159
|
+
* `is_error` is set, then `subtype` when it isn't "success". Returns
|
|
160
|
+
* undefined when there is nothing useful to say — the caller falls back
|
|
161
|
+
* to its own default.
|
|
162
|
+
*/
|
|
163
|
+
export function prettyJsonFailure(stdout) {
|
|
164
|
+
const trimmed = stdout.trim();
|
|
165
|
+
if (!trimmed.startsWith("{"))
|
|
166
|
+
return undefined;
|
|
167
|
+
let ev = null;
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(trimmed);
|
|
170
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
171
|
+
ev = parsed;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (!ev)
|
|
177
|
+
return undefined;
|
|
178
|
+
const err = ev.error;
|
|
179
|
+
if (err && typeof err === "object" && !Array.isArray(err)) {
|
|
180
|
+
const m = err.message;
|
|
181
|
+
if (typeof m === "string" && m.trim())
|
|
182
|
+
return m.trim();
|
|
183
|
+
}
|
|
184
|
+
if (typeof err === "string" && err.trim())
|
|
185
|
+
return err.trim();
|
|
186
|
+
if (ev.is_error === true && typeof ev.result === "string" && ev.result.trim())
|
|
187
|
+
return ev.result.trim();
|
|
188
|
+
if (typeof ev.message === "string" && ev.message.trim())
|
|
189
|
+
return ev.message.trim();
|
|
190
|
+
if (typeof ev.subtype === "string" && ev.subtype.trim() && ev.subtype !== "success")
|
|
191
|
+
return ev.subtype.trim();
|
|
192
|
+
return undefined;
|
|
141
193
|
}
|
|
142
194
|
//# sourceMappingURL=messages-stream.js.map
|
|
@@ -15,12 +15,14 @@ export type QmdReindexOptions = UtilityIo & {
|
|
|
15
15
|
cwd: string;
|
|
16
16
|
}) => unknown;
|
|
17
17
|
sizeLimit?: (hqRoot: string) => unknown;
|
|
18
|
-
/** Test seams for the lock shared by every hq-cli
|
|
18
|
+
/** Test seams for the lock shared by every hq-cli QMD writer. */
|
|
19
19
|
embedLockDependencies?: EmbedLockDependencies;
|
|
20
20
|
/** Test seams for the host load gate consulted while the embed lock is held. */
|
|
21
21
|
loadGateDependencies?: LoadGateDependencies;
|
|
22
22
|
/** Test seam for the hq-flags-gated warning on qmd native startup failures. */
|
|
23
23
|
startupFailureWarningsEnabled?: () => boolean;
|
|
24
|
+
/** Test seam for the default-off shared lock around the full reindex pass. */
|
|
25
|
+
singleFlightEnabled?: () => boolean;
|
|
24
26
|
};
|
|
25
27
|
export declare function qmdReindexAfterSync(args?: string[], options?: QmdReindexOptions): number;
|
|
26
28
|
//# sourceMappingURL=qmd-reindex-after-sync.d.ts.map
|