@indigoai-us/hq-cli 5.220.0 → 5.222.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 +24 -0
- package/dist/command-catalog.generated.d.ts +55 -0
- package/dist/command-catalog.generated.js +71 -0
- package/dist/commands/core-ontology.d.ts +6 -0
- package/dist/commands/core-ontology.js +61 -5
- package/dist/commands/lanes-checks.d.ts +36 -2
- package/dist/commands/lanes-checks.js +67 -9
- package/dist/commands/lanes-report.d.ts +11 -0
- package/dist/commands/lanes-report.js +330 -0
- package/dist/commands/lanes.d.ts +2 -1
- package/dist/commands/lanes.js +24 -6
- package/dist/lib/checkpoint-ontology/aliases.d.ts +17 -0
- package/dist/lib/checkpoint-ontology/aliases.js +44 -0
- package/dist/lib/checkpoint-ontology/apply.d.ts +10 -0
- package/dist/lib/checkpoint-ontology/apply.js +61 -44
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/entity-file.js +2 -1
- package/dist/lib/checkpoint-ontology/multi-writer.d.ts +106 -0
- package/dist/lib/checkpoint-ontology/multi-writer.js +391 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +10 -0
- package/dist/lib/checkpoint-ontology/plan.js +23 -5
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +2 -1
- package/dist/lib/lanes/timestamp.d.ts +3 -0
- package/dist/lib/lanes/timestamp.js +26 -0
- package/dist/lib/lanes/tracker-report-state.d.ts +10 -0
- package/dist/lib/lanes/tracker-report-state.js +124 -0
- package/dist/lib/lanes/tracker-report.d.ts +203 -0
- package/dist/lib/lanes/tracker-report.js +522 -0
- package/package.json +1 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { LanesError } from "../lib/lanes/errors.js";
|
|
3
|
+
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
4
|
+
import { questionRecordPath } from "../lib/lanes/store.js";
|
|
5
|
+
import { normalizeIsoTimestamp } from "../lib/lanes/timestamp.js";
|
|
6
|
+
import { mapSourceStoryStatus } from "../lib/mesh/story-status.js";
|
|
7
|
+
import { buildTrackerOwnerReport, buildTrackerTeamPage, renderTrackerOwnerReportText, renderTrackerTeamPageText, } from "../lib/lanes/tracker-report.js";
|
|
8
|
+
import { FIRST_TRACKER_REPORT_SINCE, readTrackerReportState, writeTrackerReportState, } from "../lib/lanes/tracker-report-state.js";
|
|
9
|
+
import { listTrackerRecordsTolerant, loadCompanyQuestionRecords, loadProjectMappings, pathForTrackerRecord, readProject, resolveTrackerCompany, } from "./lanes-checks.js";
|
|
10
|
+
function fail(error, message, details = {}) {
|
|
11
|
+
throw new LanesError(error, message, details);
|
|
12
|
+
}
|
|
13
|
+
function canonicalStoryStatus(story, sourcePath, storyId) {
|
|
14
|
+
try {
|
|
15
|
+
const rawStatus = story.state ?? story.status;
|
|
16
|
+
if (rawStatus === "dropped")
|
|
17
|
+
return "dropped";
|
|
18
|
+
const status = mapSourceStoryStatus(story, `${sourcePath}#${storyId}`);
|
|
19
|
+
if (status !== undefined)
|
|
20
|
+
return status;
|
|
21
|
+
return story.passes === true ? "done" : "queued";
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return fail("tracker_report_story_invalid", err instanceof Error ? err.message : String(err), { path: sourcePath, story_id: storyId, field: "state" });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function recordsArray(record, field, sourcePath, storyId, errors) {
|
|
28
|
+
const value = record[field];
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return [];
|
|
31
|
+
if (!Array.isArray(value)) {
|
|
32
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: `${field} must be an array` });
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function timestampOrIssue(value, field, sourcePath, storyId, errors) {
|
|
38
|
+
const timestamp = normalizeIsoTimestamp(value);
|
|
39
|
+
if (timestamp === null) {
|
|
40
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: `${field} must be a valid timestamp` });
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return timestamp;
|
|
44
|
+
}
|
|
45
|
+
function numericPr(value, url) {
|
|
46
|
+
if (typeof value === "string" && /^[1-9]\d*$/.test(value))
|
|
47
|
+
return value;
|
|
48
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0)
|
|
49
|
+
return String(value);
|
|
50
|
+
return url.match(/\/pull\/(\d+)(?:\/|$)/)?.[1] ?? null;
|
|
51
|
+
}
|
|
52
|
+
function storyReferences(story, sourcePath, storyId, errors) {
|
|
53
|
+
const references = new Set();
|
|
54
|
+
const pullRequests = [];
|
|
55
|
+
const releases = [];
|
|
56
|
+
const addPullRequest = (value, field) => {
|
|
57
|
+
if (typeof value === "string" && value.trim()) {
|
|
58
|
+
references.add(value.trim());
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
|
|
62
|
+
references.add(`#${value}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
66
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: "Pull request entry must be a reference or object" });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const record = value;
|
|
70
|
+
const url = typeof record.url === "string" ? record.url : typeof record.html_url === "string" ? record.html_url : "";
|
|
71
|
+
const number = numericPr(record.number, url);
|
|
72
|
+
const reference = typeof record.ref === "string" ? record.ref : url || (number ? `#${number}` : "");
|
|
73
|
+
if (reference)
|
|
74
|
+
references.add(reference);
|
|
75
|
+
else
|
|
76
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: "Pull request object needs a number, URL, or ref" });
|
|
77
|
+
const mergedAt = record.merged_at ?? record.mergedAt ?? record.at;
|
|
78
|
+
if (mergedAt === undefined)
|
|
79
|
+
return;
|
|
80
|
+
if (!number) {
|
|
81
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: "Timed pull request object needs a number or pull URL" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const at = timestampOrIssue(mergedAt, "merged_at", sourcePath, storyId, errors);
|
|
85
|
+
if (at)
|
|
86
|
+
pullRequests.push({ number, title: typeof record.title === "string" && record.title.trim() ? record.title : `Pull request #${number}`, merged_at: at, source_path: sourcePath });
|
|
87
|
+
};
|
|
88
|
+
const addRelease = (value, field) => {
|
|
89
|
+
if (typeof value === "string" && value.trim()) {
|
|
90
|
+
references.add(value.trim());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
94
|
+
references.add(String(value));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
98
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: "Release or deployment entry must be a reference or object" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const record = value;
|
|
102
|
+
const version = typeof record.version === "string" ? record.version : typeof record.tag === "string" ? record.tag : "";
|
|
103
|
+
const reference = typeof record.ref === "string" ? record.ref : typeof record.url === "string" ? record.url : version;
|
|
104
|
+
if (reference)
|
|
105
|
+
references.add(reference);
|
|
106
|
+
else
|
|
107
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: "Release or deployment object needs version, tag, ref, or URL" });
|
|
108
|
+
const releasedAt = record.released_at ?? record.releasedAt ?? record.deployed_at ?? record.deployedAt ?? record.at;
|
|
109
|
+
if (releasedAt === undefined)
|
|
110
|
+
return;
|
|
111
|
+
if (!version) {
|
|
112
|
+
errors.push({ path: sourcePath, field, story_id: storyId, message: "Timed release or deployment object needs a version or tag" });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const at = timestampOrIssue(releasedAt, "released_at", sourcePath, storyId, errors);
|
|
116
|
+
if (at)
|
|
117
|
+
releases.push({ version, released_at: at, source_path: sourcePath });
|
|
118
|
+
};
|
|
119
|
+
for (const field of ["prs", "pull_requests"]) {
|
|
120
|
+
for (const value of recordsArray(story, field, sourcePath, storyId, errors))
|
|
121
|
+
addPullRequest(value, field);
|
|
122
|
+
}
|
|
123
|
+
for (const field of ["releases", "deploys"]) {
|
|
124
|
+
for (const value of recordsArray(story, field, sourcePath, storyId, errors))
|
|
125
|
+
addRelease(value, field);
|
|
126
|
+
}
|
|
127
|
+
return { references: [...references].sort(), pullRequests, releases };
|
|
128
|
+
}
|
|
129
|
+
function currentAudience(question, sourcePath) {
|
|
130
|
+
const latest = question.routing_history.at(-1);
|
|
131
|
+
if (!latest || typeof latest !== "object" || Array.isArray(latest)) {
|
|
132
|
+
return fail("tracker_report_question_route_invalid", "Question has no valid routing history for its current audience", { question_id: question.question_id, path: sourcePath });
|
|
133
|
+
}
|
|
134
|
+
const audience = latest.audience;
|
|
135
|
+
if (!audience || typeof audience !== "object" || Array.isArray(audience) || typeof audience.kind !== "string") {
|
|
136
|
+
return fail("tracker_report_question_route_invalid", "Question current audience is malformed", { question_id: question.question_id, path: sourcePath });
|
|
137
|
+
}
|
|
138
|
+
switch (audience.kind) {
|
|
139
|
+
case "user": return "owner";
|
|
140
|
+
case "session": return "operator";
|
|
141
|
+
case "lane": return "lane";
|
|
142
|
+
default: return fail("tracker_report_question_route_invalid", "Question current audience is unsupported", { question_id: question.question_id, path: sourcePath });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function trackerRecords(root, company, project, kind, errors) {
|
|
146
|
+
const projectDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
|
|
147
|
+
const loaded = listTrackerRecordsTolerant(projectDirectory, kind);
|
|
148
|
+
for (const issue of loaded.errors) {
|
|
149
|
+
errors.push({ path: issue.path, field: issue.field, message: issue.message, ...(issue.record_id ? { record_id: issue.record_id } : {}) });
|
|
150
|
+
}
|
|
151
|
+
return loaded.records
|
|
152
|
+
.filter((record) => record.company === company.slug && record.project_id === project.projectId)
|
|
153
|
+
.map((record) => ({ ...record, source_path: pathForTrackerRecord(projectDirectory, kind, record.id) }));
|
|
154
|
+
}
|
|
155
|
+
function issueFromError(err, fallbackPath, fallbackField, ids = {}) {
|
|
156
|
+
const details = err instanceof LanesError ? err.details : {};
|
|
157
|
+
return {
|
|
158
|
+
path: typeof details.path === "string" ? details.path : fallbackPath,
|
|
159
|
+
field: typeof details.field === "string" ? details.field : fallbackField,
|
|
160
|
+
message: err instanceof Error ? err.message : String(err),
|
|
161
|
+
...(typeof details.story_id === "string" ? { story_id: details.story_id } : ids.story_id ? { story_id: ids.story_id } : {}),
|
|
162
|
+
...(typeof details.id === "string" ? { record_id: details.id } : ids.record_id ? { record_id: ids.record_id } : {}),
|
|
163
|
+
...(typeof details.question_id === "string" ? { question_id: details.question_id } : ids.question_id ? { question_id: ids.question_id } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function teamInput(root, company, project, questions, deps, errors = []) {
|
|
167
|
+
const projectDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
|
|
168
|
+
const prdPath = path.join(projectDirectory, "prd.json");
|
|
169
|
+
let prd = { stories: [], mtime: 0 };
|
|
170
|
+
try {
|
|
171
|
+
prd = readProject(root, company.slug, prdPath, true);
|
|
172
|
+
for (const issue of prd.errors ?? [])
|
|
173
|
+
errors.push(issue);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
errors.push(issueFromError(err, prdPath, "project"));
|
|
177
|
+
}
|
|
178
|
+
const pullRequests = [];
|
|
179
|
+
const releases = [];
|
|
180
|
+
const stories = [];
|
|
181
|
+
for (const story of prd.stories) {
|
|
182
|
+
const id = typeof story.id === "string" ? story.id : "";
|
|
183
|
+
if (!id.trim()) {
|
|
184
|
+
errors.push({ path: prdPath, field: "id", message: "Story id must be a non-empty string" });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const title = typeof story.title === "string" ? story.title : "";
|
|
188
|
+
if (!title.trim()) {
|
|
189
|
+
errors.push({ path: prdPath, field: "title", story_id: id, message: "Story title must be a non-empty string" });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let status;
|
|
193
|
+
try {
|
|
194
|
+
status = canonicalStoryStatus(story, prdPath, id);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
errors.push(issueFromError(err, prdPath, "state", { story_id: id }));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const updatedAt = story.updated_at ?? story.updatedAt;
|
|
201
|
+
if (status === "done" && updatedAt === undefined) {
|
|
202
|
+
errors.push({ path: prdPath, field: "updated_at", story_id: id, message: "Done story is missing updated_at" });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let normalizedUpdatedAt;
|
|
206
|
+
if (updatedAt !== undefined) {
|
|
207
|
+
const normalized = normalizeIsoTimestamp(updatedAt);
|
|
208
|
+
if (normalized === null) {
|
|
209
|
+
errors.push({ path: prdPath, field: "updated_at", story_id: id, message: "updated_at must be a valid timestamp" });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
normalizedUpdatedAt = normalized;
|
|
213
|
+
}
|
|
214
|
+
const refs = storyReferences(story, prdPath, id, errors);
|
|
215
|
+
pullRequests.push(...refs.pullRequests);
|
|
216
|
+
releases.push(...refs.releases);
|
|
217
|
+
stories.push({ id, title, status, references: refs.references, ...(normalizedUpdatedAt ? { updated_at: normalizedUpdatedAt } : {}), source_path: prdPath });
|
|
218
|
+
}
|
|
219
|
+
const teamQuestions = [];
|
|
220
|
+
for (const record of questions.filter((question) => question.project_id === project.projectId)) {
|
|
221
|
+
const sourcePath = questionRecordPath(root, record.question_id);
|
|
222
|
+
try {
|
|
223
|
+
const body = deps.readQuestionBody(root, record);
|
|
224
|
+
assertQuestionBodySecretSafe(body);
|
|
225
|
+
teamQuestions.push({ record, body, current_audience: currentAudience(record, sourcePath), source_path: sourcePath });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
errors.push(issueFromError(err, sourcePath, "body", { question_id: record.question_id }));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
company: company.slug,
|
|
233
|
+
team: project.team,
|
|
234
|
+
project_id: project.projectId,
|
|
235
|
+
stories,
|
|
236
|
+
pull_requests: pullRequests,
|
|
237
|
+
releases,
|
|
238
|
+
blockers: trackerRecords(root, company, project, "blocker", errors),
|
|
239
|
+
kpis: trackerRecords(root, company, project, "kpi", errors),
|
|
240
|
+
okrs: trackerRecords(root, company, project, "okr", errors),
|
|
241
|
+
directives: trackerRecords(root, company, project, "directive", errors),
|
|
242
|
+
questions: teamQuestions,
|
|
243
|
+
errors,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function allTeamInputs(root, company, deps) {
|
|
247
|
+
const mappings = loadProjectMappings(root, company.slug);
|
|
248
|
+
return [...mappings.values()]
|
|
249
|
+
.sort((a, b) => a.team.localeCompare(b.team) || a.projectId.localeCompare(b.projectId))
|
|
250
|
+
.map((project) => {
|
|
251
|
+
const errors = [];
|
|
252
|
+
let questions = [];
|
|
253
|
+
try {
|
|
254
|
+
questions = loadCompanyQuestionRecords(root, company, project.projectId);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", project.projectId), "questions"));
|
|
258
|
+
}
|
|
259
|
+
return teamInput(root, company, project, questions, deps, errors);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function sinceForTeam(root, company) {
|
|
263
|
+
return readTrackerReportState(root, company)?.last_report_at ?? FIRST_TRACKER_REPORT_SINCE;
|
|
264
|
+
}
|
|
265
|
+
function validateTeamSlug(team) {
|
|
266
|
+
const value = team.trim();
|
|
267
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value) || value === "." || value === "..") {
|
|
268
|
+
return fail("tracker_team_invalid", "Team slug must be a safe path segment", { team });
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
export function registerLaneReportCommands(parent, deps, wrap) {
|
|
273
|
+
parent.command("report")
|
|
274
|
+
.description("Generate the owner report for tracker teams")
|
|
275
|
+
.option("--company <slug>", "Select the company by slug")
|
|
276
|
+
.option("--since <iso>", "Report activity from this ISO timestamp")
|
|
277
|
+
.option("--json", "Emit JSON instead of the owner report text")
|
|
278
|
+
.action(wrap(async (options) => {
|
|
279
|
+
const company = await resolveTrackerCompany(deps, options.company);
|
|
280
|
+
const root = deps.hqRoot();
|
|
281
|
+
const generatedAt = deps.now().toISOString();
|
|
282
|
+
const previous = options.since === undefined ? readTrackerReportState(root, company.slug) : null;
|
|
283
|
+
const since = options.since ?? previous?.last_report_at ?? FIRST_TRACKER_REPORT_SINCE;
|
|
284
|
+
const input = {
|
|
285
|
+
company: company.slug,
|
|
286
|
+
generated_at: generatedAt,
|
|
287
|
+
since,
|
|
288
|
+
since_source: options.since !== undefined ? "explicit" : previous ? "previous_report" : "first_run",
|
|
289
|
+
teams: allTeamInputs(root, company, deps),
|
|
290
|
+
};
|
|
291
|
+
const report = buildTrackerOwnerReport(input);
|
|
292
|
+
const hasErrors = report.teams.some((team) => team.errors.length > 0);
|
|
293
|
+
if (hasErrors)
|
|
294
|
+
process.exitCode = 2;
|
|
295
|
+
else
|
|
296
|
+
writeTrackerReportState(root, company.slug, generatedAt);
|
|
297
|
+
console.log(options.json ? JSON.stringify(report, null, 2) : renderTrackerOwnerReportText(report));
|
|
298
|
+
}));
|
|
299
|
+
parent.command("team")
|
|
300
|
+
.description("Show a team's OKRs, KPIs, work, blockers, and questions")
|
|
301
|
+
.argument("<slug>", "Team slug")
|
|
302
|
+
.option("--company <slug>", "Select the company by slug")
|
|
303
|
+
.option("--json", "Emit JSON instead of the team page text")
|
|
304
|
+
.action(wrap(async (slug, options) => {
|
|
305
|
+
const team = validateTeamSlug(slug);
|
|
306
|
+
const company = await resolveTrackerCompany(deps, options.company);
|
|
307
|
+
const root = deps.hqRoot();
|
|
308
|
+
const since = sinceForTeam(root, company.slug);
|
|
309
|
+
const candidates = [...loadProjectMappings(root, company.slug).values()].filter((project) => project.team === team);
|
|
310
|
+
if (candidates.length === 0)
|
|
311
|
+
fail("tracker_team_not_found", "Team was not found for this company", { team, company: company.slug });
|
|
312
|
+
if (candidates.length > 1)
|
|
313
|
+
fail("tracker_team_ambiguous", "More than one project maps to this team", { team, company: company.slug, project_ids: candidates.map((item) => item.projectId) });
|
|
314
|
+
const generatedAt = deps.now().toISOString();
|
|
315
|
+
const errors = [];
|
|
316
|
+
let questions = [];
|
|
317
|
+
try {
|
|
318
|
+
questions = loadCompanyQuestionRecords(root, company, candidates[0].projectId);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", candidates[0].projectId), "questions"));
|
|
322
|
+
}
|
|
323
|
+
const input = teamInput(root, company, candidates[0], questions, deps, errors);
|
|
324
|
+
const page = buildTrackerTeamPage(input, company.slug, since, generatedAt);
|
|
325
|
+
if (page.errors.length > 0)
|
|
326
|
+
process.exitCode = 2;
|
|
327
|
+
console.log(options.json ? JSON.stringify(page, null, 2) : renderTrackerTeamPageText(page));
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=lanes-report.js.map
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
|
12
12
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
13
13
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
14
14
|
import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
|
|
15
|
-
import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
15
|
+
import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
16
16
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
17
17
|
import { type RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
18
18
|
import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
|
|
@@ -107,6 +107,7 @@ export declare const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-ques
|
|
|
107
107
|
export declare const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
|
|
108
108
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
109
109
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
110
|
+
export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
|
|
110
111
|
/**
|
|
111
112
|
* Spec §5.1: `hq lanes ask` from inside a lane can hit a binary or dist
|
|
112
113
|
* being replaced mid-call (ENOENT / ETXTBSY / a module that no longer
|
package/dist/commands/lanes.js
CHANGED
|
@@ -14,6 +14,7 @@ import { readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
|
14
14
|
import { Option } from "commander";
|
|
15
15
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
16
16
|
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
17
|
+
import { registerLaneReportCommands } from "./lanes-report.js";
|
|
17
18
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
18
19
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
19
20
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
@@ -43,6 +44,7 @@ import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib
|
|
|
43
44
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
44
45
|
import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
45
46
|
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
47
|
+
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
46
48
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
47
49
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
48
50
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
@@ -438,16 +440,20 @@ function readJsonFile(filePath) {
|
|
|
438
440
|
throw err;
|
|
439
441
|
}
|
|
440
442
|
}
|
|
441
|
-
function parseQuestionBody(raw) {
|
|
443
|
+
export function parseQuestionBody(raw) {
|
|
442
444
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
443
445
|
return null;
|
|
444
446
|
const obj = raw;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
447
|
+
if (typeof obj.question !== "string" || typeof obj.context !== "string" || !Array.isArray(obj.options) || obj.options.some((item) => typeof item !== "string"))
|
|
448
|
+
return null;
|
|
449
|
+
if (obj.recommended !== undefined && typeof obj.recommended !== "string")
|
|
450
|
+
return null;
|
|
451
|
+
if (obj.notes !== undefined && typeof obj.notes !== "string")
|
|
452
|
+
return null;
|
|
453
|
+
const options = obj.options.filter((item) => typeof item === "string");
|
|
448
454
|
const body = {
|
|
449
|
-
question:
|
|
450
|
-
context:
|
|
455
|
+
question: obj.question,
|
|
456
|
+
context: obj.context,
|
|
451
457
|
options,
|
|
452
458
|
};
|
|
453
459
|
if (typeof obj.recommended === "string")
|
|
@@ -8506,6 +8512,18 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8506
8512
|
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
8507
8513
|
now: ctx.now,
|
|
8508
8514
|
}, wrap);
|
|
8515
|
+
registerLaneReportCommands(lanes, {
|
|
8516
|
+
hqRoot: ctx.hqRoot,
|
|
8517
|
+
resolveCompany: ctx.resolveCompany,
|
|
8518
|
+
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8519
|
+
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
8520
|
+
now: ctx.now,
|
|
8521
|
+
readQuestionBody: (root, question) => {
|
|
8522
|
+
const body = requireQuestionBody(root, question);
|
|
8523
|
+
assertQuestionBodySecretSafe(body);
|
|
8524
|
+
return body;
|
|
8525
|
+
},
|
|
8526
|
+
}, wrap);
|
|
8509
8527
|
registerOutpostPrunerCommand(lanes);
|
|
8510
8528
|
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
8511
8529
|
noStoryOption.attributeName = () => "noStory";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alias rules shared by `apply` and the multi-writer merge-back.
|
|
3
|
+
*/
|
|
4
|
+
import type { EntityIndex } from "./tree.js";
|
|
5
|
+
/** Longest canonical name or alias the writer accepts. */
|
|
6
|
+
export declare const MAX_NAME_LENGTH = 200;
|
|
7
|
+
/** The index key for a normalised name of one type. */
|
|
8
|
+
export declare function entityKey(type: string, normalized: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Aliases worth adding to the entity at `self`: new to it, and held by no
|
|
11
|
+
* other entity of any type, so an alias can never make a later match
|
|
12
|
+
* ambiguous. `own` holds the normalised names the entity already answers to.
|
|
13
|
+
*/
|
|
14
|
+
export declare function freshAliases(index: EntityIndex, type: string, self: string, own: Set<string>, names: string[]): string[];
|
|
15
|
+
/** The normalised names an indexed entity answers to, plus aliases queued this run. */
|
|
16
|
+
export declare function ownNames(index: EntityIndex, rel: string, queued: string[]): Set<string>;
|
|
17
|
+
//# sourceMappingURL=aliases.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alias rules shared by `apply` and the multi-writer merge-back.
|
|
3
|
+
*/
|
|
4
|
+
import { canonicalizeName, isRejectedName, normalizeEntityName } from "./identity.js";
|
|
5
|
+
/** Longest canonical name or alias the writer accepts. */
|
|
6
|
+
export const MAX_NAME_LENGTH = 200;
|
|
7
|
+
/** The index key for a normalised name of one type. */
|
|
8
|
+
export function entityKey(type, normalized) {
|
|
9
|
+
return `${type}\0${normalized}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Aliases worth adding to the entity at `self`: new to it, and held by no
|
|
13
|
+
* other entity of any type, so an alias can never make a later match
|
|
14
|
+
* ambiguous. `own` holds the normalised names the entity already answers to.
|
|
15
|
+
*/
|
|
16
|
+
export function freshAliases(index, type, self, own, names) {
|
|
17
|
+
const seen = new Set(own);
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const raw of names) {
|
|
20
|
+
const alias = canonicalizeName(raw);
|
|
21
|
+
if (!alias || alias.length > MAX_NAME_LENGTH)
|
|
22
|
+
continue;
|
|
23
|
+
const normalized = normalizeEntityName(alias, type);
|
|
24
|
+
if (!normalized || seen.has(normalized) || isRejectedName(normalized))
|
|
25
|
+
continue;
|
|
26
|
+
const holders = [
|
|
27
|
+
...(index.canonical.get(entityKey(type, normalized)) ?? []),
|
|
28
|
+
...(index.alias.get(entityKey(type, normalized)) ?? []),
|
|
29
|
+
];
|
|
30
|
+
if (holders.some((holder) => holder !== self))
|
|
31
|
+
continue;
|
|
32
|
+
if ([...(index.types.get(normalized) ?? [])].some((other) => other !== type))
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(normalized);
|
|
35
|
+
out.push(alias);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** The normalised names an indexed entity answers to, plus aliases queued this run. */
|
|
40
|
+
export function ownNames(index, rel, queued) {
|
|
41
|
+
const entity = index.byRel.get(rel);
|
|
42
|
+
return new Set([entity.canonical_name, ...entity.aliases, ...queued].map((name) => normalizeEntityName(name, entity.type)));
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=aliases.js.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* sections 4.3, 5 and 14). The model output is untrusted input: a malformed
|
|
6
6
|
* candidate is rejected and counted, never repaired.
|
|
7
7
|
*/
|
|
8
|
+
import { type DuplicateReport, type MergeBackOutcome } from "./multi-writer.js";
|
|
8
9
|
import { type CompanyBriefResult } from "./company-brief.js";
|
|
9
10
|
import { type WritableEntityType } from "./identity.js";
|
|
10
11
|
import { type EntityIndex } from "./tree.js";
|
|
@@ -76,6 +77,15 @@ export interface ApplyReport {
|
|
|
76
77
|
}>;
|
|
77
78
|
/** Present only when the plan ran the company brief and this apply created or updated entities. */
|
|
78
79
|
company_brief?: CompanyBriefResult;
|
|
80
|
+
/** Present only when `checkpoint.ontology.multi-writer` was on for the plan (design section 20). */
|
|
81
|
+
multi_writer?: {
|
|
82
|
+
conflicts: MergeBackOutcome[];
|
|
83
|
+
/** Entity files this apply wrote and recorded in the outbox. */
|
|
84
|
+
outbox_recorded: number;
|
|
85
|
+
outbox_reset?: true;
|
|
86
|
+
error?: string;
|
|
87
|
+
duplicates?: DuplicateReport;
|
|
88
|
+
};
|
|
79
89
|
duration_ms: number;
|
|
80
90
|
}
|
|
81
91
|
export interface ApplyOptions {
|