@indigoai-us/hq-cli 5.217.0 → 5.219.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 +28 -0
- package/dist/command-catalog.generated.d.ts +18 -0
- package/dist/command-catalog.generated.js +23 -0
- package/dist/commands/lanes-checks.d.ts +13 -0
- package/dist/commands/lanes-checks.js +558 -0
- package/dist/commands/lanes.js +160 -44
- package/dist/lib/lanes/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +30 -0
- package/dist/lib/lanes/questions.d.ts +13 -7
- package/dist/lib/lanes/questions.js +78 -18
- package/dist/lib/lanes/reconcile.d.ts +8 -1
- package/dist/lib/lanes/reconcile.js +51 -5
- package/dist/lib/lanes/spawn.js +28 -16
- package/dist/lib/lanes/store.js +15 -1
- package/dist/lib/lanes/tracker-checks.d.ts +77 -0
- package/dist/lib/lanes/tracker-checks.js +134 -0
- package/dist/lib/lanes/types.d.ts +15 -0
- package/dist/lib/lanes/watcher-main.js +17 -1
- package/dist/lib/lanes/watcher.d.ts +4 -1
- package/dist/lib/lanes/watcher.js +30 -7
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import { readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
14
14
|
import { Option } from "commander";
|
|
15
15
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
16
|
+
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
16
17
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
17
18
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
18
19
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
@@ -33,6 +34,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
33
34
|
import { resolveActiveMembershipCompany } from "../lib/mesh/api.js";
|
|
34
35
|
import { openMeshTransport, requireToken } from "../lib/mesh/client.js";
|
|
35
36
|
import { CompanySelectionError } from "../utils/company-selection-error.js";
|
|
37
|
+
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
36
38
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
37
39
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
38
40
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
@@ -40,7 +42,7 @@ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_
|
|
|
40
42
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
41
43
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
42
44
|
import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
43
|
-
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
45
|
+
import { questionBodyAbsPath, resolveQuestionCompany, } from "../lib/lanes/questions.js";
|
|
44
46
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
45
47
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
46
48
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
@@ -1579,6 +1581,40 @@ function sameQuestionCompany(a, b) {
|
|
|
1579
1581
|
const bSlug = b.slug.trim();
|
|
1580
1582
|
return aSlug !== "" && aSlug === bSlug;
|
|
1581
1583
|
}
|
|
1584
|
+
function effectiveLaneQuestionCompany(lane) {
|
|
1585
|
+
return lane.company ?? lane.mesh_company ?? null;
|
|
1586
|
+
}
|
|
1587
|
+
function questionCompanyForLane(question, lane) {
|
|
1588
|
+
return question.company ?? effectiveLaneQuestionCompany(lane);
|
|
1589
|
+
}
|
|
1590
|
+
async function resolveActiveQuestionCompany(ctx) {
|
|
1591
|
+
try {
|
|
1592
|
+
return await ctx.resolveCompany();
|
|
1593
|
+
}
|
|
1594
|
+
catch (err) {
|
|
1595
|
+
if (!(err instanceof CompanySelectionError))
|
|
1596
|
+
throw err;
|
|
1597
|
+
throw new LanesError("question_company_unavailable", "Cannot access lane questions because the active HQ company could not be resolved", {});
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function assertQuestionCompanyMatchesLane(question, lane, code) {
|
|
1601
|
+
const laneCompany = effectiveLaneQuestionCompany(lane);
|
|
1602
|
+
if (question.company != null && laneCompany !== null && !sameQuestionCompany(question.company, laneCompany)) {
|
|
1603
|
+
throw new LanesError(code, `Question ${question.question_id} does not match lane ${lane.lane_id}'s company`, { question_id: question.question_id, lane_id: lane.lane_id });
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function assertQuestionCompanyAccess(question, lane, requestedCompany) {
|
|
1607
|
+
assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch");
|
|
1608
|
+
const questionCompany = questionCompanyForLane(question, lane);
|
|
1609
|
+
if (questionCompany !== null && !sameQuestionCompany(questionCompany, requestedCompany)) {
|
|
1610
|
+
throw new LanesError("question_company_mismatch", `Question ${question.question_id} does not belong to the requested or active company`, { question_id: question.question_id, lane_id: lane.lane_id });
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function assertQuestionCompanyResolvedForAnswer(question, lane) {
|
|
1614
|
+
if (questionCompanyForLane(question, lane) !== null)
|
|
1615
|
+
return;
|
|
1616
|
+
throw new LanesError("question_company_unavailable", `Cannot answer legacy question ${question.question_id}: its company cannot be resolved from the question or lane`, { question_id: question.question_id, lane_id: lane.lane_id });
|
|
1617
|
+
}
|
|
1582
1618
|
function questionCompanyRefMatches(recordCompany, filter) {
|
|
1583
1619
|
return recordCompany !== null &&
|
|
1584
1620
|
recordCompany !== undefined &&
|
|
@@ -3543,9 +3579,11 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3543
3579
|
});
|
|
3544
3580
|
}
|
|
3545
3581
|
const ownerAnswer = ownerQuestion.answer;
|
|
3582
|
+
const ownerLaneCompany = effectiveLaneQuestionCompany(lane);
|
|
3546
3583
|
if (!isQuestionAnswer(ownerAnswer) ||
|
|
3547
3584
|
ownerQuestion.lane_id !== lane.lane_id ||
|
|
3548
|
-
|
|
3585
|
+
(ownerQuestion.company !== null && ownerLaneCompany !== null &&
|
|
3586
|
+
!sameQuestionCompany(ownerQuestion.company, ownerLaneCompany))) {
|
|
3549
3587
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return the confirmed answer for question ${questionId}`, {
|
|
3550
3588
|
lane_id: lane.lane_id,
|
|
3551
3589
|
question_id: questionId,
|
|
@@ -3557,14 +3595,31 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3557
3595
|
if (current.lane_id !== lane.lane_id) {
|
|
3558
3596
|
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different lane than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3559
3597
|
}
|
|
3560
|
-
if (
|
|
3598
|
+
if (current.company !== null &&
|
|
3599
|
+
effectiveLaneQuestionCompany(lane) !== null &&
|
|
3600
|
+
!sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane))) {
|
|
3561
3601
|
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3562
3602
|
}
|
|
3603
|
+
if (current.company !== null &&
|
|
3604
|
+
ownerQuestion.company !== null &&
|
|
3605
|
+
!sameQuestionCompany(current.company, ownerQuestion.company)) {
|
|
3606
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} has a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3607
|
+
}
|
|
3563
3608
|
if (current.status === "pending") {
|
|
3564
|
-
return {
|
|
3609
|
+
return {
|
|
3610
|
+
...current,
|
|
3611
|
+
company: current.company ?? ownerQuestion.company,
|
|
3612
|
+
status: "answered",
|
|
3613
|
+
answer: ownerAnswer,
|
|
3614
|
+
delivered_at: ownerQuestion.delivered_at ?? current.delivered_at ?? null,
|
|
3615
|
+
};
|
|
3565
3616
|
}
|
|
3566
3617
|
if (current.status === "answered" && sameQuestionAnswer(current.answer, ownerAnswer)) {
|
|
3567
|
-
|
|
3618
|
+
const deliveredAt = ownerQuestion.delivered_at ?? current.delivered_at ?? null;
|
|
3619
|
+
const company = current.company ?? ownerQuestion.company;
|
|
3620
|
+
return deliveredAt === current.delivered_at && company === current.company
|
|
3621
|
+
? current
|
|
3622
|
+
: { ...current, company, delivered_at: deliveredAt };
|
|
3568
3623
|
}
|
|
3569
3624
|
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} is ${current.status} or has a different answer than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3570
3625
|
});
|
|
@@ -3576,7 +3631,9 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3576
3631
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
3577
3632
|
throw err;
|
|
3578
3633
|
const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
|
|
3579
|
-
|
|
3634
|
+
const primaryCompany = effectiveLaneQuestionCompany(primaryLane);
|
|
3635
|
+
const ownerCompany = effectiveLaneQuestionCompany(lane);
|
|
3636
|
+
if (primaryCompany !== null && ownerCompany !== null && !sameQuestionCompany(primaryCompany, ownerCompany)) {
|
|
3580
3637
|
throw new LanesError("outpost_question_sync_conflict", `Primary lane ${lane.lane_id} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3581
3638
|
}
|
|
3582
3639
|
try {
|
|
@@ -7035,6 +7092,10 @@ async function runAsk(ctx, opts) {
|
|
|
7035
7092
|
}
|
|
7036
7093
|
const lane = readLane(hqRoot, laneId);
|
|
7037
7094
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
|
|
7095
|
+
const activeCompany = lane.company || lane.mesh_company
|
|
7096
|
+
? undefined
|
|
7097
|
+
: await resolveActiveQuestionCompany(ctx);
|
|
7098
|
+
const questionCompany = resolveQuestionCompany(lane, activeCompany);
|
|
7038
7099
|
const optionList = Array.isArray(opts.option)
|
|
7039
7100
|
? opts.option
|
|
7040
7101
|
: opts.option
|
|
@@ -7059,6 +7120,7 @@ async function runAsk(ctx, opts) {
|
|
|
7059
7120
|
body,
|
|
7060
7121
|
raisedVia: "cli",
|
|
7061
7122
|
at,
|
|
7123
|
+
activeCompany: questionCompany,
|
|
7062
7124
|
questionId,
|
|
7063
7125
|
});
|
|
7064
7126
|
questionId = withBody.question_id;
|
|
@@ -7084,6 +7146,8 @@ async function runAsk(ctx, opts) {
|
|
|
7084
7146
|
return;
|
|
7085
7147
|
}
|
|
7086
7148
|
catch (err) {
|
|
7149
|
+
if (isLanesError(err) && err.error === "secret_text_refused")
|
|
7150
|
+
throw err;
|
|
7087
7151
|
lastErr = err;
|
|
7088
7152
|
process.stderr.write(`hq lanes ask: attempt ${i + 1}/${attempts} failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
7089
7153
|
if (i === attempts - 1)
|
|
@@ -7113,14 +7177,15 @@ async function runAsk(ctx, opts) {
|
|
|
7113
7177
|
});
|
|
7114
7178
|
}
|
|
7115
7179
|
const OUTPOST_QUESTION_LOOKUP_CONCURRENCY = 4;
|
|
7116
|
-
function groupQuestionLanesByCompany(lanes) {
|
|
7180
|
+
function groupQuestionLanesByCompany(lanes, fallbackCompany) {
|
|
7117
7181
|
const groups = [];
|
|
7118
7182
|
for (const lane of lanes) {
|
|
7119
|
-
const
|
|
7183
|
+
const company = effectiveLaneQuestionCompany(lane) ?? fallbackCompany ?? null;
|
|
7184
|
+
const existing = groups.find((group) => sameQuestionCompany(group.company, company));
|
|
7120
7185
|
if (existing)
|
|
7121
7186
|
existing.lanes.push(lane);
|
|
7122
7187
|
else
|
|
7123
|
-
groups.push({ company
|
|
7188
|
+
groups.push({ company, lanes: [lane] });
|
|
7124
7189
|
}
|
|
7125
7190
|
return groups;
|
|
7126
7191
|
}
|
|
@@ -7265,7 +7330,7 @@ async function readOutpostQuestionDetail(ctx, owner) {
|
|
|
7265
7330
|
}
|
|
7266
7331
|
if (question.question_id !== owner.question.question_id ||
|
|
7267
7332
|
question.lane_id !== owner.lane.lane_id ||
|
|
7268
|
-
!sameQuestionCompany(question.company, owner.lane
|
|
7333
|
+
!sameQuestionCompany(question.company, effectiveLaneQuestionCompany(owner.lane))) {
|
|
7269
7334
|
throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} returned question ${owner.question.question_id} for a different lane or company`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
7270
7335
|
}
|
|
7271
7336
|
const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
|
|
@@ -7274,7 +7339,9 @@ async function readOutpostQuestionDetail(ctx, owner) {
|
|
|
7274
7339
|
}
|
|
7275
7340
|
async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
7276
7341
|
const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
|
|
7277
|
-
(!companyFilter ||
|
|
7342
|
+
(!companyFilter ||
|
|
7343
|
+
effectiveLaneQuestionCompany(lane) === null ||
|
|
7344
|
+
questionCompanyRefMatches(effectiveLaneQuestionCompany(lane), companyFilter)));
|
|
7278
7345
|
const lanesByOutpost = new Map();
|
|
7279
7346
|
for (const lane of hostedLanes) {
|
|
7280
7347
|
if (lane.host.kind !== "outpost")
|
|
@@ -7287,7 +7354,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7287
7354
|
const errors = [];
|
|
7288
7355
|
const eligibleGroups = [];
|
|
7289
7356
|
for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
7290
|
-
for (const group of groupQuestionLanesByCompany(lanes)) {
|
|
7357
|
+
for (const group of groupQuestionLanesByCompany(lanes, companyFilter)) {
|
|
7291
7358
|
if (group.company === null) {
|
|
7292
7359
|
if (await ctx.laneOutpostQuestionVisibilityEnabled(null)) {
|
|
7293
7360
|
errors.push({
|
|
@@ -7346,7 +7413,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7346
7413
|
const lane = localLanesById.get(question.lane_id);
|
|
7347
7414
|
if (!lane)
|
|
7348
7415
|
continue;
|
|
7349
|
-
if (!sameQuestionCompany(question.company, lane
|
|
7416
|
+
if (!sameQuestionCompany(question.company, effectiveLaneQuestionCompany(lane))) {
|
|
7350
7417
|
throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} question ${question.question_id} does not match its primary lane company`, { outpost_id: outpostId, question_id: question.question_id });
|
|
7351
7418
|
}
|
|
7352
7419
|
if (seenQuestionIds.has(question.question_id)) {
|
|
@@ -7379,14 +7446,42 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7379
7446
|
errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()],
|
|
7380
7447
|
};
|
|
7381
7448
|
}
|
|
7449
|
+
function syncPrimaryQuestionDelivery(hqRoot, owner) {
|
|
7450
|
+
const deliveredAt = owner.question.delivered_at;
|
|
7451
|
+
if (!deliveredAt)
|
|
7452
|
+
return;
|
|
7453
|
+
try {
|
|
7454
|
+
casUpdateQuestion(hqRoot, owner.question.question_id, (current) => {
|
|
7455
|
+
if (current.lane_id !== owner.lane.lane_id) {
|
|
7456
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${current.question_id} belongs to a different lane than its Outpost owner`, { lane_id: owner.lane.lane_id, question_id: current.question_id });
|
|
7457
|
+
}
|
|
7458
|
+
if (current.company !== null &&
|
|
7459
|
+
owner.question.company !== null &&
|
|
7460
|
+
!sameQuestionCompany(current.company, owner.question.company)) {
|
|
7461
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${current.question_id} belongs to a different company than its Outpost owner`, { lane_id: owner.lane.lane_id, question_id: current.question_id });
|
|
7462
|
+
}
|
|
7463
|
+
const company = current.company ?? owner.question.company;
|
|
7464
|
+
const nextDeliveredAt = current.delivered_at ?? deliveredAt;
|
|
7465
|
+
return company === current.company && nextDeliveredAt === current.delivered_at
|
|
7466
|
+
? current
|
|
7467
|
+
: { ...current, company, delivered_at: nextDeliveredAt };
|
|
7468
|
+
});
|
|
7469
|
+
}
|
|
7470
|
+
catch (err) {
|
|
7471
|
+
if (isLanesError(err) && err.error === "question_not_found")
|
|
7472
|
+
return;
|
|
7473
|
+
throw err;
|
|
7474
|
+
}
|
|
7475
|
+
}
|
|
7382
7476
|
async function runQuestionsList(ctx, opts) {
|
|
7383
7477
|
const hqRoot = ctx.hqRoot();
|
|
7384
7478
|
const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
|
|
7385
7479
|
const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
const localLanes =
|
|
7480
|
+
const company = opts.company
|
|
7481
|
+
? await ctx.resolveCompany(opts.company)
|
|
7482
|
+
: await resolveActiveQuestionCompany(ctx);
|
|
7483
|
+
const localLanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
|
|
7484
|
+
const targeted = opts.from !== undefined || opts.project !== undefined || opts.story !== undefined;
|
|
7390
7485
|
const localQuestions = loadAllQuestions(hqRoot)
|
|
7391
7486
|
.filter((q) => {
|
|
7392
7487
|
if (status && q.status !== status)
|
|
@@ -7397,16 +7492,15 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7397
7492
|
return false;
|
|
7398
7493
|
if (opts.story && q.story_id !== opts.story)
|
|
7399
7494
|
return false;
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
if (!lane || !questionCompanyRefMatches(lane.company, company) || !sameQuestionCompany(q.company, lane.company)) {
|
|
7408
|
-
return false;
|
|
7495
|
+
const lane = localLanes.get(q.lane_id);
|
|
7496
|
+
if (lane)
|
|
7497
|
+
assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch");
|
|
7498
|
+
const questionCompany = lane ? questionCompanyForLane(q, lane) : q.company;
|
|
7499
|
+
if (questionCompany !== null && !sameQuestionCompany(questionCompany, company)) {
|
|
7500
|
+
if (targeted) {
|
|
7501
|
+
throw new LanesError("question_company_mismatch", `Question ${q.question_id} does not belong to the requested or active company`, { question_id: q.question_id, lane_id: q.lane_id });
|
|
7409
7502
|
}
|
|
7503
|
+
return false;
|
|
7410
7504
|
}
|
|
7411
7505
|
if (forFilter) {
|
|
7412
7506
|
const audience = resolveAudience(hqRoot, q, ctx);
|
|
@@ -7418,6 +7512,10 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7418
7512
|
const outpostLoad = opts.allHosts
|
|
7419
7513
|
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
|
|
7420
7514
|
: { questions: [], errors: [] };
|
|
7515
|
+
for (const owner of outpostLoad.questions) {
|
|
7516
|
+
assertQuestionCompanyAccess(owner.question, owner.lane, company);
|
|
7517
|
+
syncPrimaryQuestionDelivery(hqRoot, owner);
|
|
7518
|
+
}
|
|
7421
7519
|
const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
|
|
7422
7520
|
const questions = [
|
|
7423
7521
|
...localQuestions
|
|
@@ -7430,7 +7528,7 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7430
7528
|
};
|
|
7431
7529
|
if (!opts.allHosts)
|
|
7432
7530
|
return row;
|
|
7433
|
-
const lane = localLanes
|
|
7531
|
+
const lane = localLanes.get(q.lane_id);
|
|
7434
7532
|
const host = lane?.host.kind === "outpost" ? `outpost:${lane.host.id}` : "local";
|
|
7435
7533
|
return { ...row, host };
|
|
7436
7534
|
}),
|
|
@@ -7444,11 +7542,7 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7444
7542
|
return false;
|
|
7445
7543
|
if (opts.story && q.story_id !== opts.story)
|
|
7446
7544
|
return false;
|
|
7447
|
-
|
|
7448
|
-
!questionCompanyRefMatches(lane.company, company) ||
|
|
7449
|
-
!sameQuestionCompany(q.company, lane.company))) {
|
|
7450
|
-
return false;
|
|
7451
|
-
}
|
|
7545
|
+
assertQuestionCompanyAccess(q, lane, company);
|
|
7452
7546
|
if (forFilter) {
|
|
7453
7547
|
if (!audienceMatches(audience, forFilter))
|
|
7454
7548
|
return false;
|
|
@@ -7518,7 +7612,15 @@ async function runQuestionsShow(ctx, questionId) {
|
|
|
7518
7612
|
}
|
|
7519
7613
|
async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
7520
7614
|
const hqRoot = ctx.hqRoot();
|
|
7521
|
-
|
|
7615
|
+
if (opts.choice !== undefined)
|
|
7616
|
+
assertNoSecretText("answer choice", opts.choice);
|
|
7617
|
+
if (opts.freeform !== undefined)
|
|
7618
|
+
assertNoSecretText("freeform answer", opts.freeform);
|
|
7619
|
+
if (opts.notes !== undefined)
|
|
7620
|
+
assertNoSecretText("answer notes", opts.notes);
|
|
7621
|
+
const company = opts.company
|
|
7622
|
+
? await ctx.resolveCompany(opts.company)
|
|
7623
|
+
: await resolveActiveQuestionCompany(ctx);
|
|
7522
7624
|
let question;
|
|
7523
7625
|
try {
|
|
7524
7626
|
question = readQuestion(hqRoot, questionId);
|
|
@@ -7535,6 +7637,17 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7535
7637
|
}
|
|
7536
7638
|
const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
|
|
7537
7639
|
if (matches.length === 0) {
|
|
7640
|
+
const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
7641
|
+
const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
|
|
7642
|
+
if (otherCompanyMatch) {
|
|
7643
|
+
assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company);
|
|
7644
|
+
}
|
|
7645
|
+
if (allCompanyLookup.errors.length > 0) {
|
|
7646
|
+
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
7647
|
+
question_id: questionId,
|
|
7648
|
+
hosts: allCompanyLookup.errors.map(({ host, error }) => ({ host, error })),
|
|
7649
|
+
});
|
|
7650
|
+
}
|
|
7538
7651
|
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7539
7652
|
}
|
|
7540
7653
|
if (matches.length > 1) {
|
|
@@ -7544,29 +7657,24 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7544
7657
|
if (!owner) {
|
|
7545
7658
|
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7546
7659
|
}
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
}
|
|
7660
|
+
assertQuestionCompanyAccess(owner.question, owner.lane, company);
|
|
7661
|
+
assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane);
|
|
7550
7662
|
throwIfQuestionNotPending(owner.question);
|
|
7551
7663
|
await yieldEventLoop();
|
|
7552
7664
|
await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
|
|
7553
7665
|
...opts,
|
|
7554
|
-
company: questionListCompanyArg(owner.lane
|
|
7666
|
+
company: questionListCompanyArg(effectiveLaneQuestionCompany(owner.lane)),
|
|
7555
7667
|
});
|
|
7556
7668
|
return;
|
|
7557
7669
|
}
|
|
7558
7670
|
await yieldEventLoop();
|
|
7559
7671
|
const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
!questionCompanyRefMatches(askingLane.company, company) ||
|
|
7563
|
-
!sameQuestionCompany(question.company, askingLane.company))) {
|
|
7564
|
-
throw new LanesError("question_company_mismatch", `Question ${questionId} does not belong to the requested company`, { question_id: questionId, lane_id: askingLane.lane_id });
|
|
7565
|
-
}
|
|
7672
|
+
assertQuestionCompanyAccess(question, askingLane, company);
|
|
7673
|
+
assertQuestionCompanyResolvedForAnswer(question, askingLane);
|
|
7566
7674
|
if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
|
|
7567
7675
|
await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
|
|
7568
7676
|
...opts,
|
|
7569
|
-
|
|
7677
|
+
company: questionListCompanyArg(effectiveLaneQuestionCompany(askingLane)),
|
|
7570
7678
|
});
|
|
7571
7679
|
return;
|
|
7572
7680
|
}
|
|
@@ -8303,6 +8411,7 @@ async function runReconcile(ctx, opts) {
|
|
|
8303
8411
|
throttleDeps: ctx.throttleDeps,
|
|
8304
8412
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
8305
8413
|
presenceConnected: ctx.presenceConnected,
|
|
8414
|
+
resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
|
|
8306
8415
|
mesh: ctx.mesh,
|
|
8307
8416
|
env: ctx.env,
|
|
8308
8417
|
});
|
|
@@ -8383,6 +8492,13 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8383
8492
|
now: ctx.now,
|
|
8384
8493
|
actor: ctx.trackerActor,
|
|
8385
8494
|
}, wrap);
|
|
8495
|
+
registerLaneChecksCommand(lanes, {
|
|
8496
|
+
hqRoot: ctx.hqRoot,
|
|
8497
|
+
resolveCompany: ctx.resolveCompany,
|
|
8498
|
+
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8499
|
+
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
8500
|
+
now: ctx.now,
|
|
8501
|
+
}, wrap);
|
|
8386
8502
|
registerOutpostPrunerCommand(lanes);
|
|
8387
8503
|
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
8388
8504
|
noStoryOption.attributeName = () => "noStory";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* that resolve to the same home are refused (identity-keyed-cap rule).
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
|
-
import { type AccountConfig, type LaneProvider, type LanesConfig, type LivenessWindows, type ProviderConfig } from "./types.js";
|
|
10
|
+
import { type AccountConfig, type LaneProvider, type LanesConfig, type LivenessWindows, type ProviderConfig, type TrackerChecksConfig } from "./types.js";
|
|
11
11
|
export declare const LANES_CONFIG_REL: string;
|
|
12
12
|
export declare const DEFAULT_MAX_QUEUED = 3;
|
|
13
13
|
export declare const DEFAULT_CPU_BUSY_PAUSE = 0.85;
|
|
@@ -39,6 +39,7 @@ export declare const DEFAULT_TURN_START_LOST_MS: number;
|
|
|
39
39
|
export declare const DEFAULT_SILENT_LOST_MS: number;
|
|
40
40
|
/** Unanswered session questions move to the next audience after 10 minutes. */
|
|
41
41
|
export declare const DEFAULT_SESSION_QUESTION_TIMEOUT_SECS: number;
|
|
42
|
+
export declare const DEFAULT_TRACKER_CHECKS: TrackerChecksConfig;
|
|
42
43
|
export declare const DEFAULT_LIVENESS_WINDOWS: LivenessWindows;
|
|
43
44
|
export declare const DEFAULT_PROVIDER_MAX_RUNNING: Record<LaneProvider, number>;
|
|
44
45
|
export declare const DEFAULT_ACCOUNT_HOMES: Record<LaneProvider, string>;
|
package/dist/lib/lanes/config.js
CHANGED
|
@@ -43,6 +43,15 @@ export const DEFAULT_TURN_START_LOST_MS = 2 * 60 * 60 * 1000;
|
|
|
43
43
|
export const DEFAULT_SILENT_LOST_MS = 12 * 60 * 60 * 1000;
|
|
44
44
|
/** Unanswered session questions move to the next audience after 10 minutes. */
|
|
45
45
|
export const DEFAULT_SESSION_QUESTION_TIMEOUT_SECS = 10 * 60;
|
|
46
|
+
export const DEFAULT_TRACKER_CHECKS = {
|
|
47
|
+
owner_question_surface_ms: 10 * 60 * 1000,
|
|
48
|
+
open_question_age_ms: 2 * 60 * 60 * 1000,
|
|
49
|
+
status_stale_ms: 90 * 60 * 1000,
|
|
50
|
+
task_list_stale_ms: 120 * 60 * 1000,
|
|
51
|
+
blocker_max_age_ms: 24 * 60 * 60 * 1000,
|
|
52
|
+
kpi_value_max_age_ms: 24 * 60 * 60 * 1000,
|
|
53
|
+
directive_ack_timeout_ms: 30 * 60 * 1000,
|
|
54
|
+
};
|
|
46
55
|
export const DEFAULT_LIVENESS_WINDOWS = {
|
|
47
56
|
live_within_ms: DEFAULT_LIVE_WITHIN_MS,
|
|
48
57
|
turn_start_lost_ms: DEFAULT_TURN_START_LOST_MS,
|
|
@@ -362,6 +371,25 @@ function parseLiveness(raw) {
|
|
|
362
371
|
silent_lost_ms: positiveInt(obj.silent_lost_ms, "liveness.silent_lost_ms", DEFAULT_SILENT_LOST_MS),
|
|
363
372
|
};
|
|
364
373
|
}
|
|
374
|
+
function parseTrackerChecks(raw) {
|
|
375
|
+
const tracker = raw === undefined ? {} : raw;
|
|
376
|
+
if (!isPlainObject(tracker)) {
|
|
377
|
+
throw new LanesConfigError("invalid_config", "tracker must be an object");
|
|
378
|
+
}
|
|
379
|
+
const checks = tracker.checks === undefined ? {} : tracker.checks;
|
|
380
|
+
if (!isPlainObject(checks)) {
|
|
381
|
+
throw new LanesConfigError("invalid_config", "tracker.checks must be an object");
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
owner_question_surface_ms: positiveInt(checks.owner_question_surface_ms, "tracker.checks.owner_question_surface_ms", DEFAULT_TRACKER_CHECKS.owner_question_surface_ms),
|
|
385
|
+
open_question_age_ms: positiveInt(checks.open_question_age_ms, "tracker.checks.open_question_age_ms", DEFAULT_TRACKER_CHECKS.open_question_age_ms),
|
|
386
|
+
status_stale_ms: positiveInt(checks.status_stale_ms, "tracker.checks.status_stale_ms", DEFAULT_TRACKER_CHECKS.status_stale_ms),
|
|
387
|
+
task_list_stale_ms: positiveInt(checks.task_list_stale_ms, "tracker.checks.task_list_stale_ms", DEFAULT_TRACKER_CHECKS.task_list_stale_ms),
|
|
388
|
+
blocker_max_age_ms: positiveInt(checks.blocker_max_age_ms, "tracker.checks.blocker_max_age_ms", DEFAULT_TRACKER_CHECKS.blocker_max_age_ms),
|
|
389
|
+
kpi_value_max_age_ms: positiveInt(checks.kpi_value_max_age_ms, "tracker.checks.kpi_value_max_age_ms", DEFAULT_TRACKER_CHECKS.kpi_value_max_age_ms),
|
|
390
|
+
directive_ack_timeout_ms: positiveInt(checks.directive_ack_timeout_ms, "tracker.checks.directive_ack_timeout_ms", DEFAULT_TRACKER_CHECKS.directive_ack_timeout_ms),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
365
393
|
export function documentedDefaultConfig(deps = defaultDeps()) {
|
|
366
394
|
const providers = {};
|
|
367
395
|
for (const provider of LANE_PROVIDERS) {
|
|
@@ -384,6 +412,7 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
|
|
|
384
412
|
min_available_mem_mb: DEFAULT_MIN_AVAILABLE_MEM_MB,
|
|
385
413
|
},
|
|
386
414
|
liveness: { ...DEFAULT_LIVENESS_WINDOWS },
|
|
415
|
+
tracker: { checks: { ...DEFAULT_TRACKER_CHECKS } },
|
|
387
416
|
session_question_timeout_secs: DEFAULT_SESSION_QUESTION_TIMEOUT_SECS,
|
|
388
417
|
};
|
|
389
418
|
}
|
|
@@ -425,6 +454,7 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
|
|
|
425
454
|
providers,
|
|
426
455
|
throttle: parseThrottle(raw.throttle),
|
|
427
456
|
liveness: parseLiveness(raw.liveness),
|
|
457
|
+
tracker: { checks: parseTrackerChecks(raw.tracker) },
|
|
428
458
|
session_question_timeout_secs: positiveInt(raw.session_question_timeout_secs, "session_question_timeout_secs", DEFAULT_SESSION_QUESTION_TIMEOUT_SECS),
|
|
429
459
|
};
|
|
430
460
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* path and for envelope `decision: "ask"` ingestion. Two writers here would
|
|
4
4
|
* drift on hop, origin, raised_via, body store, and routing_history.
|
|
5
5
|
*/
|
|
6
|
-
import { type LaneEnvelope, type LaneRecord, type QuestionBodyStore, type QuestionKind, type QuestionRecord, type RaisedVia } from "./types.js";
|
|
6
|
+
import { type LaneEnvelope, type LaneRecord, type LaneCompanyRef, type QuestionBodyStore, type QuestionKind, type QuestionRecord, type RaisedVia } from "./types.js";
|
|
7
7
|
export interface QuestionBody {
|
|
8
8
|
question: string;
|
|
9
9
|
context: string;
|
|
@@ -11,8 +11,12 @@ export interface QuestionBody {
|
|
|
11
11
|
recommended?: string;
|
|
12
12
|
notes?: string;
|
|
13
13
|
}
|
|
14
|
+
export declare function assertQuestionBodySecretSafe(body: QuestionBody): void;
|
|
15
|
+
/** Errors that make an envelope ask unsafe or impossible to persist. */
|
|
16
|
+
export declare function questionRecordRefusalCode(err: unknown): "secret_text_refused" | "question_company_unavailable" | null;
|
|
14
17
|
export declare function parseQuestionBodyStore(value: unknown): QuestionBodyStore;
|
|
15
|
-
export declare function bodyStoreForLane(lane: Pick<LaneRecord, "company"
|
|
18
|
+
export declare function bodyStoreForLane(lane: Pick<LaneRecord, "company" | "mesh_company">, resolvedCompany?: LaneCompanyRef): QuestionBodyStore;
|
|
19
|
+
export declare function resolveQuestionCompany(lane: Pick<LaneRecord, "lane_id" | "company" | "mesh_company">, activeCompany?: LaneCompanyRef | null): LaneCompanyRef;
|
|
16
20
|
export declare function questionBodyRef(questionId: string): string;
|
|
17
21
|
export declare function sha256Text(text: string): string;
|
|
18
22
|
/** Tenant-store path for a question body (spec §6.2 / §10). Never the personal vault for company prose. */
|
|
@@ -28,6 +32,7 @@ export interface RaiseLaneQuestionInput {
|
|
|
28
32
|
body: QuestionBody;
|
|
29
33
|
raisedVia: RaisedVia;
|
|
30
34
|
at: string;
|
|
35
|
+
activeCompany?: LaneCompanyRef | null;
|
|
31
36
|
questionId?: string;
|
|
32
37
|
}
|
|
33
38
|
/**
|
|
@@ -43,15 +48,15 @@ export declare function raiseLaneQuestion(input: RaiseLaneQuestionInput): Questi
|
|
|
43
48
|
*/
|
|
44
49
|
export declare function classifyQuestionKind(options: readonly string[]): QuestionKind | undefined;
|
|
45
50
|
/** The store a lane's question bodies live in, derived from the lane and never the reverse. */
|
|
46
|
-
export declare function questionBodyPathForLane(hqRoot: string, lane: Pick<LaneRecord, "company">, questionId: string): string;
|
|
51
|
+
export declare function questionBodyPathForLane(hqRoot: string, lane: Pick<LaneRecord, "company" | "mesh_company">, questionId: string): string;
|
|
47
52
|
/**
|
|
48
53
|
* Free-text notes the operator attached with `questions answer --notes`.
|
|
49
54
|
* Read from the lane's own store (spec §6.5): the company store for a company
|
|
50
55
|
* lane, the personal store otherwise. Returns undefined when the answer has
|
|
51
|
-
* no notes_ref or the body carries no notes
|
|
52
|
-
*
|
|
56
|
+
* no notes_ref or the body carries no notes. An unavailable or malformed body
|
|
57
|
+
* fails the resume so the lane cannot continue without its answer notes.
|
|
53
58
|
*/
|
|
54
|
-
export declare function readAnswerNotes(hqRoot: string, lane: Pick<LaneRecord, "company">, question: Pick<QuestionRecord, "question_id" | "answer">): string | undefined;
|
|
59
|
+
export declare function readAnswerNotes(hqRoot: string, lane: Pick<LaneRecord, "company" | "mesh_company">, question: Pick<QuestionRecord, "question_id" | "company" | "body" | "answer">): string | undefined;
|
|
55
60
|
export interface AnsweredQuestionEntry {
|
|
56
61
|
question_id: string;
|
|
57
62
|
choice: string | undefined;
|
|
@@ -66,10 +71,11 @@ export interface AnsweredQuestionEntry {
|
|
|
66
71
|
* directory on the executing host.
|
|
67
72
|
*/
|
|
68
73
|
export declare function answeredQuestionEntries(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "company">, questions: QuestionRecord[]): AnsweredQuestionEntry[];
|
|
74
|
+
export declare function markAnsweredQuestionsDelivered(hqRoot: string, laneId: string, questionIds: readonly string[], deliveredAt: string): QuestionRecord[];
|
|
69
75
|
export declare function envelopeQuestionId(laneId: string, envelopeRef: string, index: number): string;
|
|
70
76
|
/**
|
|
71
77
|
* Spec §6.1: each envelope questions[] entry becomes one queue record, via
|
|
72
78
|
* raiseLaneQuestion (the same function the CLI ask path uses).
|
|
73
79
|
*/
|
|
74
|
-
export declare function raiseEnvelopeQuestions(hqRoot: string, lane: LaneRecord, envelope: LaneEnvelope, envelopeRef: string, at: string, questionIndexes?: readonly number[]): QuestionRecord[];
|
|
80
|
+
export declare function raiseEnvelopeQuestions(hqRoot: string, lane: LaneRecord, envelope: LaneEnvelope, envelopeRef: string, at: string, questionIndexes?: readonly number[], activeCompany?: LaneCompanyRef | null): QuestionRecord[];
|
|
75
81
|
//# sourceMappingURL=questions.d.ts.map
|