@indigoai-us/hq-cli 5.345.25 → 5.345.26
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 +7 -0
- package/dist/command-catalog.generated.d.ts +50 -0
- package/dist/command-catalog.generated.js +62 -0
- package/dist/commands/install-global.js +19 -4
- package/dist/commands/integrations-core.d.ts +4 -0
- package/dist/commands/integrations-core.js +15 -3
- package/dist/commands/lanes.js +180 -28
- package/dist/lib/lanes/grants.d.ts +0 -1
- package/dist/lib/lanes/grants.js +3 -6
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +2 -2
- package/dist/lib/lanes/keep-alive-audit.d.ts +5 -0
- package/dist/lib/lanes/keep-alive-audit.js +15 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/store.js +33 -0
- package/dist/lib/lanes/story-ledger.d.ts +2 -0
- package/dist/lib/lanes/story-ledger.js +33 -0
- package/dist/lib/lanes/types.d.ts +10 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/main.js +4 -1
- package/dist/utils/install-tree-torn.d.ts +7 -7
- package/dist/utils/install-tree-torn.js +10 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.26] — 2026-10-06
|
|
6
|
+
- Give users recovery guidance for malformed HTTP 200 integration gateway errors while keeping the gateway defect reportable.
|
|
7
|
+
- Fix Claude plugin setup on CLI versions that reject `--json` for marketplace add and remove. Failed Claude plugin commands show a bounded stderr excerpt with quoted and unquoted credential fields redacted.
|
|
8
|
+
- Add `hq lanes repair-company` for legacy lanes with missing company attribution, report company-less lead lanes in `hq lanes keep-alive-audit`, and inherit company attribution from company-bound seniors when creating child lanes.
|
|
9
|
+
- Resolve resume hire-carve policy from the senior record and remove the misleading alive-child helper alias.
|
|
10
|
+
- Keep install-tree recovery waiting through the bounded settle deadline when a retired npm sibling cannot be fully inspected.
|
|
11
|
+
|
|
5
12
|
## [5.345.25] — 2026-10-06
|
|
6
13
|
- Route qmd post-sync indexing and ontology README drift through isolated fast handlers; cover all rebuild-index targets in fast-route parity and benchmarks.
|
|
7
14
|
|
|
@@ -11395,6 +11395,31 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
11395
11395
|
readonly hidden: false;
|
|
11396
11396
|
}];
|
|
11397
11397
|
readonly subcommands: readonly [];
|
|
11398
|
+
}, {
|
|
11399
|
+
readonly name: "repair-company-target";
|
|
11400
|
+
readonly description: "";
|
|
11401
|
+
readonly aliases: readonly [];
|
|
11402
|
+
readonly hidden: true;
|
|
11403
|
+
readonly helpOption: true;
|
|
11404
|
+
readonly helpCommand: false;
|
|
11405
|
+
readonly usage: "[options] <lane-id>";
|
|
11406
|
+
readonly helpText: readonly [];
|
|
11407
|
+
readonly arguments: readonly [{
|
|
11408
|
+
readonly name: "lane-id";
|
|
11409
|
+
readonly required: true;
|
|
11410
|
+
readonly variadic: false;
|
|
11411
|
+
readonly description: "Lane id";
|
|
11412
|
+
}];
|
|
11413
|
+
readonly options: readonly [{
|
|
11414
|
+
readonly flags: "--company <slug>";
|
|
11415
|
+
readonly description: "Company slug to assign";
|
|
11416
|
+
readonly hidden: false;
|
|
11417
|
+
}, {
|
|
11418
|
+
readonly flags: "--json";
|
|
11419
|
+
readonly description: "JSON on stdout (always)";
|
|
11420
|
+
readonly hidden: false;
|
|
11421
|
+
}];
|
|
11422
|
+
readonly subcommands: readonly [];
|
|
11398
11423
|
}, {
|
|
11399
11424
|
readonly name: "grant-target";
|
|
11400
11425
|
readonly description: "";
|
|
@@ -12328,6 +12353,31 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
12328
12353
|
readonly hidden: false;
|
|
12329
12354
|
}];
|
|
12330
12355
|
readonly subcommands: readonly [];
|
|
12356
|
+
}, {
|
|
12357
|
+
readonly name: "repair-company";
|
|
12358
|
+
readonly description: "Set missing company attribution on a legacy lane";
|
|
12359
|
+
readonly aliases: readonly [];
|
|
12360
|
+
readonly hidden: false;
|
|
12361
|
+
readonly helpOption: true;
|
|
12362
|
+
readonly helpCommand: false;
|
|
12363
|
+
readonly usage: "[options] <lane-id>";
|
|
12364
|
+
readonly helpText: readonly [];
|
|
12365
|
+
readonly arguments: readonly [{
|
|
12366
|
+
readonly name: "lane-id";
|
|
12367
|
+
readonly required: true;
|
|
12368
|
+
readonly variadic: false;
|
|
12369
|
+
readonly description: "Lane id";
|
|
12370
|
+
}];
|
|
12371
|
+
readonly options: readonly [{
|
|
12372
|
+
readonly flags: "--company <slug>";
|
|
12373
|
+
readonly description: "Company slug to assign";
|
|
12374
|
+
readonly hidden: false;
|
|
12375
|
+
}, {
|
|
12376
|
+
readonly flags: "--json";
|
|
12377
|
+
readonly description: "JSON on stdout (always)";
|
|
12378
|
+
readonly hidden: false;
|
|
12379
|
+
}];
|
|
12380
|
+
readonly subcommands: readonly [];
|
|
12331
12381
|
}, {
|
|
12332
12382
|
readonly name: "logs";
|
|
12333
12383
|
readonly description: "Print a lane's local logs";
|
|
@@ -13843,6 +13843,37 @@ export const COMMAND_CATALOG = [
|
|
|
13843
13843
|
],
|
|
13844
13844
|
"subcommands": []
|
|
13845
13845
|
},
|
|
13846
|
+
{
|
|
13847
|
+
"name": "repair-company-target",
|
|
13848
|
+
"description": "",
|
|
13849
|
+
"aliases": [],
|
|
13850
|
+
"hidden": true,
|
|
13851
|
+
"helpOption": true,
|
|
13852
|
+
"helpCommand": false,
|
|
13853
|
+
"usage": "[options] <lane-id>",
|
|
13854
|
+
"helpText": [],
|
|
13855
|
+
"arguments": [
|
|
13856
|
+
{
|
|
13857
|
+
"name": "lane-id",
|
|
13858
|
+
"required": true,
|
|
13859
|
+
"variadic": false,
|
|
13860
|
+
"description": "Lane id"
|
|
13861
|
+
}
|
|
13862
|
+
],
|
|
13863
|
+
"options": [
|
|
13864
|
+
{
|
|
13865
|
+
"flags": "--company <slug>",
|
|
13866
|
+
"description": "Company slug to assign",
|
|
13867
|
+
"hidden": false
|
|
13868
|
+
},
|
|
13869
|
+
{
|
|
13870
|
+
"flags": "--json",
|
|
13871
|
+
"description": "JSON on stdout (always)",
|
|
13872
|
+
"hidden": false
|
|
13873
|
+
}
|
|
13874
|
+
],
|
|
13875
|
+
"subcommands": []
|
|
13876
|
+
},
|
|
13846
13877
|
{
|
|
13847
13878
|
"name": "grant-target",
|
|
13848
13879
|
"description": "",
|
|
@@ -14991,6 +15022,37 @@ export const COMMAND_CATALOG = [
|
|
|
14991
15022
|
],
|
|
14992
15023
|
"subcommands": []
|
|
14993
15024
|
},
|
|
15025
|
+
{
|
|
15026
|
+
"name": "repair-company",
|
|
15027
|
+
"description": "Set missing company attribution on a legacy lane",
|
|
15028
|
+
"aliases": [],
|
|
15029
|
+
"hidden": false,
|
|
15030
|
+
"helpOption": true,
|
|
15031
|
+
"helpCommand": false,
|
|
15032
|
+
"usage": "[options] <lane-id>",
|
|
15033
|
+
"helpText": [],
|
|
15034
|
+
"arguments": [
|
|
15035
|
+
{
|
|
15036
|
+
"name": "lane-id",
|
|
15037
|
+
"required": true,
|
|
15038
|
+
"variadic": false,
|
|
15039
|
+
"description": "Lane id"
|
|
15040
|
+
}
|
|
15041
|
+
],
|
|
15042
|
+
"options": [
|
|
15043
|
+
{
|
|
15044
|
+
"flags": "--company <slug>",
|
|
15045
|
+
"description": "Company slug to assign",
|
|
15046
|
+
"hidden": false
|
|
15047
|
+
},
|
|
15048
|
+
{
|
|
15049
|
+
"flags": "--json",
|
|
15050
|
+
"description": "JSON on stdout (always)",
|
|
15051
|
+
"hidden": false
|
|
15052
|
+
}
|
|
15053
|
+
],
|
|
15054
|
+
"subcommands": []
|
|
15055
|
+
},
|
|
14994
15056
|
{
|
|
14995
15057
|
"name": "logs",
|
|
14996
15058
|
"description": "Print a lane's local logs",
|
|
@@ -76,8 +76,23 @@ function runClaudeCli(args, home, env) {
|
|
|
76
76
|
const childEnv = { ...process.env, ...env, HOME: home, CLAUDE_CONFIG_DIR: path.join(home, '.claude') };
|
|
77
77
|
const result = spawnSync('claude', ['plugin', ...args], { encoding: 'utf8', env: childEnv, timeout: 120_000, windowsHide: true });
|
|
78
78
|
if (result.error || result.status !== 0) {
|
|
79
|
+
if (result.error?.code === 'ENOENT') {
|
|
80
|
+
throw Object.assign(new Error('Claude Code plugin command failed because the Claude Code CLI was not found. Ensure the Claude Code CLI is installed and try again.'), { expected: true });
|
|
81
|
+
}
|
|
82
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
83
|
+
let excerpt = stderr.replace(/\s+/g, ' ').trim();
|
|
84
|
+
excerpt = excerpt
|
|
85
|
+
.replace(/\bBearer\s+\S+/gi, 'Bearer [redacted]')
|
|
86
|
+
.replace(/(?:"((?:(?:access|refresh|id)[_-]?)?token|secret|password|authorization|api[_-]?key)"|'((?:(?:access|refresh|id)[_-]?)?token|secret|password|authorization|api[_-]?key)'|\b((?:(?:access|refresh|id)[_-]?)?token|secret|password|authorization|api[_-]?key)\b)\s*[:=]\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,}\]]+)/gi, (_match, doubleQuotedKey, singleQuotedKey, unquotedKey) => `${doubleQuotedKey ?? singleQuotedKey ?? unquotedKey}=[redacted]`)
|
|
87
|
+
.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, '[redacted]')
|
|
88
|
+
.replace(/\b(?:gh[pousr]_|github_pat_|sk-|xox[baprs]-)[A-Za-z0-9_-]{16,}\b/g, '[redacted]');
|
|
89
|
+
for (const value of Object.values(childEnv).filter((value) => typeof value === 'string' && value.length >= 4).sort((a, b) => b.length - a.length)) {
|
|
90
|
+
excerpt = excerpt.split(value).join('[redacted]');
|
|
91
|
+
}
|
|
92
|
+
excerpt = excerpt.slice(0, 240);
|
|
79
93
|
const cause = result.error?.message ?? `exit status ${String(result.status)}`;
|
|
80
|
-
|
|
94
|
+
const detail = excerpt ? `; stderr: ${excerpt}` : '';
|
|
95
|
+
throw Object.assign(new Error(`Claude Code plugin command failed (${cause}${detail}).`), { expected: true });
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
function removePluginSettings(doc, plugin, removePlugin, removeMarketplace, pluginSettingBefore) {
|
|
@@ -374,7 +389,7 @@ export function installGlobalClaude(opts) {
|
|
|
374
389
|
try {
|
|
375
390
|
if (marketplaceNeedsAdd) {
|
|
376
391
|
marketplaceMayNeedRollback = true;
|
|
377
|
-
runClaude(['marketplace', 'add', '--scope', 'user',
|
|
392
|
+
runClaude(['marketplace', 'add', '--scope', 'user', pluginSource.sourceDir], home, env);
|
|
378
393
|
}
|
|
379
394
|
if (pluginNeedsInstall)
|
|
380
395
|
runClaude(['install', '--scope', 'user', '--json', '-y', pluginSource.pluginId], home, env);
|
|
@@ -391,7 +406,7 @@ export function installGlobalClaude(opts) {
|
|
|
391
406
|
}
|
|
392
407
|
if (marketplaceMayNeedRollback) {
|
|
393
408
|
try {
|
|
394
|
-
runClaude(['marketplace', 'remove', '--scope', 'user',
|
|
409
|
+
runClaude(['marketplace', 'remove', '--scope', 'user', pluginSource.marketplaceName], home, env);
|
|
395
410
|
}
|
|
396
411
|
catch (rollbackError) {
|
|
397
412
|
rollbackErrors.push(`marketplace removal: ${rollbackError.message}`);
|
|
@@ -574,7 +589,7 @@ export function uninstallGlobalClaude(opts = {}) {
|
|
|
574
589
|
runClaude(['uninstall', '--scope', 'user', '--json', pluginSource.pluginId], home, env);
|
|
575
590
|
}
|
|
576
591
|
if (pluginSource && state?.plugin?.marketplaceAdded) {
|
|
577
|
-
runClaude(['marketplace', 'remove', '--scope', 'user',
|
|
592
|
+
runClaude(['marketplace', 'remove', '--scope', 'user', pluginSource.marketplaceName], home, env);
|
|
578
593
|
}
|
|
579
594
|
unregisterClaudeServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
580
595
|
if (fs.existsSync(t.settingsJson)) {
|
|
@@ -113,6 +113,7 @@ interface GatewayMessage {
|
|
|
113
113
|
error?: unknown;
|
|
114
114
|
}
|
|
115
115
|
export type GatewayErrorShape = "string_error" | "object_no_code" | "object_code" | "string_code" | "nested_code" | "empty" | "redacted_empty";
|
|
116
|
+
export type GatewayFailureCode = "invalid_envelope";
|
|
116
117
|
export declare class IntegrationsCliError extends Error {
|
|
117
118
|
/**
|
|
118
119
|
* True when the error is the caller's request/state/permission (a client 4xx
|
|
@@ -152,6 +153,8 @@ export declare class IntegrationsCliError extends Error {
|
|
|
152
153
|
readonly gatewaySubcommand?: "tools_list" | "tools_call" | "other";
|
|
153
154
|
/** Fixed vocabulary describing the shape of an upstream JSON-RPC error. */
|
|
154
155
|
readonly gatewayErrorShape?: GatewayErrorShape;
|
|
156
|
+
/** Fixed classification for malformed gateway envelopes. */
|
|
157
|
+
readonly gatewayFailureCode?: GatewayFailureCode;
|
|
155
158
|
/**
|
|
156
159
|
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
157
160
|
* refused to register HQ automatically. The provider must register the exact
|
|
@@ -168,6 +171,7 @@ export declare class IntegrationsCliError extends Error {
|
|
|
168
171
|
rpcCode?: number;
|
|
169
172
|
gatewaySubcommand?: "tools_list" | "tools_call" | "other";
|
|
170
173
|
gatewayErrorShape?: GatewayErrorShape;
|
|
174
|
+
gatewayFailureCode?: GatewayFailureCode;
|
|
171
175
|
redirectUri?: string;
|
|
172
176
|
});
|
|
173
177
|
}
|
|
@@ -59,6 +59,8 @@ export class IntegrationsCliError extends Error {
|
|
|
59
59
|
gatewaySubcommand;
|
|
60
60
|
/** Fixed vocabulary describing the shape of an upstream JSON-RPC error. */
|
|
61
61
|
gatewayErrorShape;
|
|
62
|
+
/** Fixed classification for malformed gateway envelopes. */
|
|
63
|
+
gatewayFailureCode;
|
|
62
64
|
/**
|
|
63
65
|
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
64
66
|
* refused to register HQ automatically. The provider must register the exact
|
|
@@ -83,6 +85,8 @@ export class IntegrationsCliError extends Error {
|
|
|
83
85
|
this.gatewaySubcommand = opts.gatewaySubcommand;
|
|
84
86
|
if (opts.gatewayErrorShape !== undefined)
|
|
85
87
|
this.gatewayErrorShape = opts.gatewayErrorShape;
|
|
88
|
+
if (opts.gatewayFailureCode !== undefined)
|
|
89
|
+
this.gatewayFailureCode = opts.gatewayFailureCode;
|
|
86
90
|
if (opts.redirectUri !== undefined) {
|
|
87
91
|
Object.defineProperty(this, "redirectUri", {
|
|
88
92
|
value: opts.redirectUri,
|
|
@@ -523,6 +527,15 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
523
527
|
}
|
|
524
528
|
if (terminalMessage?.error) {
|
|
525
529
|
const gatewayError = gatewayErrorDetails(terminalMessage.error);
|
|
530
|
+
const invalidEnvelope = gatewayError.rpcCode === undefined && !gatewayError.message;
|
|
531
|
+
const gatewayFailureCode = invalidEnvelope
|
|
532
|
+
? "invalid_envelope"
|
|
533
|
+
: undefined;
|
|
534
|
+
const userMessage = invalidEnvelope
|
|
535
|
+
? "Integration gateway returned an invalid response. Try again. If it keeps happening, contact support."
|
|
536
|
+
: gatewayError.message ||
|
|
537
|
+
expectedGatewayErrorFallback(gatewayError.rpcCode) ||
|
|
538
|
+
"Integration gateway returned an error.";
|
|
526
539
|
// The gateway's error text is minted UPSTREAM (hq-pro, the integration
|
|
527
540
|
// factory, and beyond it the third-party provider), so it is untrusted:
|
|
528
541
|
// scrub credentials and bound the length here, at the throw site, because
|
|
@@ -531,13 +544,12 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
531
544
|
// The chain is idempotent, so the unexpected path scrubbing again is a
|
|
532
545
|
// no-op. This preserves PR #298's user-visible diagnostic; it only makes
|
|
533
546
|
// it safe on the newly-expected path.
|
|
534
|
-
throw new IntegrationsCliError(
|
|
535
|
-
expectedGatewayErrorFallback(gatewayError.rpcCode) ||
|
|
536
|
-
"Integration gateway returned an error.", {
|
|
547
|
+
throw new IntegrationsCliError(userMessage, {
|
|
537
548
|
expected: isExpectedGatewayError(gatewayError.rpcCode),
|
|
538
549
|
status: terminalStatus,
|
|
539
550
|
gatewaySubcommand,
|
|
540
551
|
gatewayErrorShape: gatewayError.shape,
|
|
552
|
+
...(gatewayFailureCode ? { gatewayFailureCode } : {}),
|
|
541
553
|
// Carry the protocol code to the capture site as a bounded grouping
|
|
542
554
|
// discriminator (HQ-CLI collision, Sentry 7642756130). This does not
|
|
543
555
|
// alter the `expected` decision above; it only labels the error.
|
package/dist/commands/lanes.js
CHANGED
|
@@ -62,19 +62,21 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
|
|
|
62
62
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
63
63
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
64
64
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
65
|
-
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild,
|
|
65
|
+
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
66
66
|
import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
|
|
67
67
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
68
68
|
import { laneCompanyUidSource, resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
69
69
|
import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
70
70
|
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
71
71
|
import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
|
|
72
|
+
import { writeQuestionBody } from "../lib/lanes/questions.js";
|
|
72
73
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
73
74
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
74
75
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
75
76
|
import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
|
|
76
77
|
import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
77
78
|
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
79
|
+
import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
|
|
78
80
|
import { resolveManagerStatusTarget, } from "../lib/lanes/manager-status-gate.js";
|
|
79
81
|
import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "../lib/lanes/blocker-freshness.js";
|
|
80
82
|
import { resolveKrFreshnessForShow } from "../lib/lanes/kr-freshness.js";
|
|
@@ -496,7 +498,7 @@ function loadAllLanes(hqRoot) {
|
|
|
496
498
|
function laneHireFields(lane, lanes, aliveChildrenByParent, finishedChildrenDoNotCount = false) {
|
|
497
499
|
const aliveChildCount = aliveChildrenByParent
|
|
498
500
|
? aliveChildrenByParent.get(lane.lane_id) ?? 0
|
|
499
|
-
:
|
|
501
|
+
: childrenHoldingHireGrantCarve(lanes, lane.lane_id, finishedChildrenDoNotCount).length;
|
|
500
502
|
const hireGrant = lane.hire_grant ?? 0;
|
|
501
503
|
const hireSummary = `hire ${aliveChildCount}/${hireGrant}`;
|
|
502
504
|
return {
|
|
@@ -1046,9 +1048,13 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
|
|
|
1046
1048
|
const seniorLaneId = lane.senior.kind === "lane" ? lane.senior.id : undefined;
|
|
1047
1049
|
const resumeSenior = seniorLaneId
|
|
1048
1050
|
? grantContext?.hierarchy.find((entry) => entry.lane_id === seniorLaneId) ??
|
|
1049
|
-
(listLaneIds(hqRoot).includes(seniorLaneId) ? readLane(hqRoot, seniorLaneId) :
|
|
1051
|
+
(listLaneIds(hqRoot).includes(seniorLaneId) ? readLane(hqRoot, seniorLaneId) : undefined)
|
|
1050
1052
|
: lane;
|
|
1051
|
-
|
|
1053
|
+
if (resumeSenior) {
|
|
1054
|
+
finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, resumeSenior);
|
|
1055
|
+
}
|
|
1056
|
+
// A missing senior record keeps the live-only flag off, so finished
|
|
1057
|
+
// children continue to count. Never borrow the child's company for this gate.
|
|
1052
1058
|
}
|
|
1053
1059
|
const shouldCheckGrant = isTerminalLaneState(lane.state) ||
|
|
1054
1060
|
(finishedChildrenDoNotCount && isFinishedResumableHireChild(lane.state));
|
|
@@ -2134,6 +2140,16 @@ function companyBindingFromSessionMeta(meta) {
|
|
|
2134
2140
|
return { binding: "none" };
|
|
2135
2141
|
return { binding: "company", slug, uid };
|
|
2136
2142
|
}
|
|
2143
|
+
function canonicalCompanySlugForUid(hqRoot, uid, senior) {
|
|
2144
|
+
const companies = readCompaniesManifestMap(hqRoot);
|
|
2145
|
+
const matches = companies
|
|
2146
|
+
? Object.entries(companies).filter(([, entry]) => entry?.cloud_uid === uid).map(([slug]) => slug)
|
|
2147
|
+
: [];
|
|
2148
|
+
if (matches.length !== 1) {
|
|
2149
|
+
throw new LanesError("senior_company_unresolved", `Cannot resolve senior ${seniorKey(senior)} to one company slug from uid ${uid}`, { senior, company_uid: uid, matches });
|
|
2150
|
+
}
|
|
2151
|
+
return matches[0];
|
|
2152
|
+
}
|
|
2137
2153
|
function serializeSeniorCompany(seniorCompany) {
|
|
2138
2154
|
if (seniorCompany.binding === "unknown")
|
|
2139
2155
|
return null;
|
|
@@ -3064,9 +3080,17 @@ async function runCreate(ctx, opts) {
|
|
|
3064
3080
|
}
|
|
3065
3081
|
const senior = opts.senior ? parseSeniorRef(opts.senior) : defaultSenior(ctx);
|
|
3066
3082
|
assertSeniorExists(hqRoot, senior, ctx);
|
|
3067
|
-
|
|
3083
|
+
let company = companyFlag
|
|
3068
3084
|
? await ctx.resolveCompany(companyFlag)
|
|
3069
3085
|
: null;
|
|
3086
|
+
const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
|
|
3087
|
+
if (company === null && seniorCompany.binding === "company") {
|
|
3088
|
+
const seniorCompanyIdentifier = seniorCompany.slug ?? (seniorCompany.uid
|
|
3089
|
+
? canonicalCompanySlugForUid(hqRoot, seniorCompany.uid, senior)
|
|
3090
|
+
: undefined);
|
|
3091
|
+
if (seniorCompanyIdentifier)
|
|
3092
|
+
company = await ctx.resolveCompany(seniorCompanyIdentifier);
|
|
3093
|
+
}
|
|
3070
3094
|
const callerAuthorization = requestedHireGrant !== undefined || ctx.env.HQ_LANE_ID?.trim()
|
|
3071
3095
|
? callerSeniorAuthorization(ctx, senior)
|
|
3072
3096
|
: { authorized: true };
|
|
@@ -3122,7 +3146,6 @@ async function runCreate(ctx, opts) {
|
|
|
3122
3146
|
if (requestedPlacement !== undefined || defaultPlacement !== undefined) {
|
|
3123
3147
|
await assertPlacementEnabled(ctx, resolvePlacementCompany(), placementEnabled);
|
|
3124
3148
|
}
|
|
3125
|
-
const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
|
|
3126
3149
|
assertSameTenant(company, senior, seniorCompany);
|
|
3127
3150
|
const createStoryWarnings = [];
|
|
3128
3151
|
const knownAbsentStories = new Set();
|
|
@@ -4333,7 +4356,7 @@ function laneRunsHereForOperatorMutation(ctx, lane) {
|
|
|
4333
4356
|
function assertLaneRunsHereForOperatorMutation(ctx, lane, action) {
|
|
4334
4357
|
if (laneRunsHereForOperatorMutation(ctx, lane))
|
|
4335
4358
|
return;
|
|
4336
|
-
throw new LanesError("lane_remote", `Cannot ${action} lane ${lane.lane_id} from this host; its state is owned by ${lane.host.kind}
|
|
4359
|
+
throw new LanesError("lane_remote", `Cannot ${action} lane ${lane.lane_id} from this host; its state is owned by ${lane.host.kind === "outpost" ? `Outpost ${lane.host.id}` : `host ${lane.host.id}`}. Run this command on the owning host.`, { lane_id: lane.lane_id, host: lane.host, action });
|
|
4337
4360
|
}
|
|
4338
4361
|
async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior = {}) {
|
|
4339
4362
|
if (lane.host.kind !== "outpost") {
|
|
@@ -4399,9 +4422,11 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4399
4422
|
? "reparent-target"
|
|
4400
4423
|
: action === "approve"
|
|
4401
4424
|
? "approve-target"
|
|
4402
|
-
: action
|
|
4425
|
+
: action === "repair-company"
|
|
4426
|
+
? "repair-company-target"
|
|
4427
|
+
: action,
|
|
4403
4428
|
laneId: lane.lane_id,
|
|
4404
|
-
hqBinary: action === "grant" || action === "reparent"
|
|
4429
|
+
hqBinary: action === "grant" || action === "reparent" || action === "repair-company"
|
|
4405
4430
|
? outpostCliOverride(ctx, outpostId)
|
|
4406
4431
|
: undefined,
|
|
4407
4432
|
text: options.text,
|
|
@@ -4416,6 +4441,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4416
4441
|
hire: options.hire,
|
|
4417
4442
|
senior: options.senior,
|
|
4418
4443
|
reason: options.reason,
|
|
4444
|
+
company: options.company,
|
|
4419
4445
|
grantContext: contexts.grantContext,
|
|
4420
4446
|
profileRepinContext: contexts.profileRepinContext,
|
|
4421
4447
|
failedResumeContext: contexts.failedResumeContext,
|
|
@@ -4534,23 +4560,28 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4534
4560
|
: action === "approve"
|
|
4535
4561
|
? remote.checkpoint === options.checkpoint &&
|
|
4536
4562
|
remote.lane !== null && typeof remote.lane === "object" && !Array.isArray(remote.lane)
|
|
4537
|
-
: action === "
|
|
4538
|
-
? remote.lane !== null && typeof remote.lane === "object" &&
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
remote.
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
remote.
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
remote.lane
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
remote.
|
|
4563
|
+
: action === "repair-company"
|
|
4564
|
+
? remote.lane !== null && typeof remote.lane === "object" && !Array.isArray(remote.lane) &&
|
|
4565
|
+
remote.lane.company !== null &&
|
|
4566
|
+
remote.lane.company !== undefined &&
|
|
4567
|
+
remote.lane.company.slug === options.company
|
|
4568
|
+
: action === "reparent"
|
|
4569
|
+
? remote.lane !== null && typeof remote.lane === "object" &&
|
|
4570
|
+
Array.isArray(remote.changed_descendant_ids)
|
|
4571
|
+
: (((remote.delivery_status === "worker_spawned" ||
|
|
4572
|
+
remote.delivery_status === "queued_on_target_account") &&
|
|
4573
|
+
remote.lane !== undefined) ||
|
|
4574
|
+
(remote.delivery_status === "queued_on_admission" &&
|
|
4575
|
+
remote.lane !== null &&
|
|
4576
|
+
typeof remote.lane === "object" &&
|
|
4577
|
+
!Array.isArray(remote.lane) &&
|
|
4578
|
+
remote.lane.state === "queued") ||
|
|
4579
|
+
(remote.delivery_status === "queued_for_reconcile" &&
|
|
4580
|
+
remote.lane !== null &&
|
|
4581
|
+
typeof remote.lane === "object" &&
|
|
4582
|
+
!Array.isArray(remote.lane) &&
|
|
4583
|
+
remote.lane.state === "queued" &&
|
|
4584
|
+
remote.lane.queued_reason !== "worker_profile_changed")));
|
|
4554
4585
|
if (!resultConfirmed) {
|
|
4555
4586
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
|
|
4556
4587
|
lane_id: lane.lane_id,
|
|
@@ -4609,7 +4640,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4609
4640
|
printJson(response);
|
|
4610
4641
|
return response;
|
|
4611
4642
|
}
|
|
4612
|
-
if (action === "approve" || action === "reparent" || action === "resume") {
|
|
4643
|
+
if (action === "approve" || action === "reparent" || action === "resume" || action === "repair-company") {
|
|
4613
4644
|
const ownerLane = dispatchedOwnerLane(ctx, lane.lane_id, outpostId, remote.lane, behavior);
|
|
4614
4645
|
const descendants = [];
|
|
4615
4646
|
if (action === "reparent") {
|
|
@@ -6198,7 +6229,7 @@ async function runBackfillGrants(ctx, opts) {
|
|
|
6198
6229
|
const allLanes = loadAllLanes(hqRoot);
|
|
6199
6230
|
const profileIds = new Set(allLanes
|
|
6200
6231
|
.filter((lane) => !isTerminalLaneState(lane.state) &&
|
|
6201
|
-
(
|
|
6232
|
+
(childrenHoldingHireGrantCarve(allLanes, lane.lane_id).length > 0 ||
|
|
6202
6233
|
lane.worker_profile === "hq-lanes-manager"))
|
|
6203
6234
|
.map((lane) => lane.worker_profile)
|
|
6204
6235
|
.filter((id) => Boolean(id)));
|
|
@@ -7121,6 +7152,10 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
|
|
|
7121
7152
|
throw new LanesError("lane_terminal", `Cannot re-parent a lane in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
7122
7153
|
}
|
|
7123
7154
|
const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
|
|
7155
|
+
if (lane.company === null && seniorCompany.binding === "company") {
|
|
7156
|
+
const company = seniorCompany.slug ?? seniorCompany.uid ?? "<company>";
|
|
7157
|
+
throw new LanesError("lane_company_missing", `Lane ${laneId} has no company attribution. Repair it with hq lanes repair-company ${laneId} --company ${company}, then retry reparent.`, { lane_id: laneId, senior: nextSenior, senior_company: serializeSeniorCompany(seniorCompany) });
|
|
7158
|
+
}
|
|
7124
7159
|
assertSameTenant(lane.company, nextSenior, seniorCompany, { lane_id: laneId });
|
|
7125
7160
|
if (nextSenior.kind === "lane" && seniorKey(lane.senior) !== seniorKey(nextSenior)) {
|
|
7126
7161
|
const parent = readLane(hqRoot, nextSenior.id);
|
|
@@ -7239,6 +7274,108 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
|
|
|
7239
7274
|
.map((descendant) => descendant.laneId),
|
|
7240
7275
|
};
|
|
7241
7276
|
}
|
|
7277
|
+
async function runRepairCompany(ctx, laneId, opts) {
|
|
7278
|
+
const lane = readLane(ctx.hqRoot(), laneId);
|
|
7279
|
+
if (lane.host.kind === "outpost" && !laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
|
|
7280
|
+
const companySlug = opts.company?.trim();
|
|
7281
|
+
if (!companySlug)
|
|
7282
|
+
throw new LanesError("missing_company", "--company is required", {});
|
|
7283
|
+
const company = await ctx.resolveCompany(companySlug);
|
|
7284
|
+
await dispatchOutpostLaneCommand(ctx, lane, "repair-company", { company: company.slug });
|
|
7285
|
+
return;
|
|
7286
|
+
}
|
|
7287
|
+
await repairCompanyOnOwner(ctx, laneId, opts);
|
|
7288
|
+
}
|
|
7289
|
+
async function repairCompanyOnOwner(ctx, laneId, opts) {
|
|
7290
|
+
const hqRoot = ctx.hqRoot();
|
|
7291
|
+
const companySlug = opts.company?.trim();
|
|
7292
|
+
if (!companySlug) {
|
|
7293
|
+
throw new LanesError("missing_company", "--company is required", {});
|
|
7294
|
+
}
|
|
7295
|
+
const company = await ctx.resolveCompany(companySlug);
|
|
7296
|
+
const initial = readLane(hqRoot, laneId);
|
|
7297
|
+
assertLaneRunsHereForOperatorMutation(ctx, initial, "repair company for");
|
|
7298
|
+
const initialSeniorCompany = companyOfSenior(hqRoot, initial.senior, ctx);
|
|
7299
|
+
if (initialSeniorCompany.binding === "company" && !sameTenant(company, initial.senior, initialSeniorCompany)) {
|
|
7300
|
+
throw new LanesError("lane_company_conflict", `Cannot repair lane ${laneId} to ${company.slug}: its senior belongs to a different company`, { lane_id: laneId, requested_company: company.slug, senior_company: serializeSeniorCompany(initialSeniorCompany) });
|
|
7301
|
+
}
|
|
7302
|
+
if (initial.mesh_company && !sameLaneCompany(initial.mesh_company, company)) {
|
|
7303
|
+
throw new LanesError("lane_company_conflict", `Cannot repair lane ${laneId} to ${company.slug}: its mesh company is ${initial.mesh_company.slug}`, { lane_id: laneId, requested_company: company.slug, mesh_company: initial.mesh_company });
|
|
7304
|
+
}
|
|
7305
|
+
const pendingQuestionMigrations = loadAllQuestions(hqRoot)
|
|
7306
|
+
.filter((entry) => entry.lane_id === laneId && entry.status === "pending")
|
|
7307
|
+
.map((question) => {
|
|
7308
|
+
if (question.company && !sameLaneCompany(question.company, company)) {
|
|
7309
|
+
throw new LanesError("lane_company_conflict", `Cannot repair lane ${laneId}: pending question ${question.question_id} belongs to ${question.company.slug}`, { lane_id: laneId, question_id: question.question_id, question_company: question.company, requested_company: company.slug });
|
|
7310
|
+
}
|
|
7311
|
+
if (question.company && sameLaneCompany(question.company, company))
|
|
7312
|
+
return null;
|
|
7313
|
+
const oldBodyPath = questionBodyAbsPath(hqRoot, question);
|
|
7314
|
+
const body = readQuestionBody(hqRoot, question);
|
|
7315
|
+
if (!body) {
|
|
7316
|
+
throw new LanesError("question_body_missing", `Cannot repair lane ${laneId}: pending question ${question.question_id} has no readable body`, { lane_id: laneId, question_id: question.question_id });
|
|
7317
|
+
}
|
|
7318
|
+
return { question, oldBodyPath, body };
|
|
7319
|
+
})
|
|
7320
|
+
.filter((entry) => entry !== null);
|
|
7321
|
+
const at = ctx.now().toISOString();
|
|
7322
|
+
casUpdateLane(hqRoot, laneId, (current) => {
|
|
7323
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "repair company for");
|
|
7324
|
+
const seniorCompany = companyOfSenior(hqRoot, current.senior, ctx);
|
|
7325
|
+
if (seniorCompany.binding === "company" && !sameTenant(company, current.senior, seniorCompany)) {
|
|
7326
|
+
throw new LanesError("lane_company_conflict", `Cannot repair lane ${laneId} to ${company.slug}: its senior belongs to a different company`, { lane_id: laneId, requested_company: company.slug, senior_company: serializeSeniorCompany(seniorCompany) });
|
|
7327
|
+
}
|
|
7328
|
+
if (current.mesh_company && !sameLaneCompany(current.mesh_company, company)) {
|
|
7329
|
+
throw new LanesError("lane_company_conflict", `Cannot repair lane ${laneId} to ${company.slug}: its mesh company is ${current.mesh_company.slug}`, { lane_id: laneId, requested_company: company.slug, mesh_company: current.mesh_company });
|
|
7330
|
+
}
|
|
7331
|
+
if (current.company !== null) {
|
|
7332
|
+
if (sameLaneCompany(current.company, company))
|
|
7333
|
+
return current;
|
|
7334
|
+
throw new LanesError("lane_company_already_set", `Lane ${laneId} already belongs to ${current.company.slug}; company repair only fills missing attribution`, { lane_id: laneId, company: current.company.slug, requested_company: company.slug });
|
|
7335
|
+
}
|
|
7336
|
+
return {
|
|
7337
|
+
...current,
|
|
7338
|
+
company,
|
|
7339
|
+
company_history: appendCappedHistory(current.company_history ?? [], {
|
|
7340
|
+
from: null,
|
|
7341
|
+
to: company,
|
|
7342
|
+
at,
|
|
7343
|
+
by: sessionIdFromEnv(ctx.env) ?? "user",
|
|
7344
|
+
reason: "repair_company",
|
|
7345
|
+
}),
|
|
7346
|
+
};
|
|
7347
|
+
});
|
|
7348
|
+
const migratedQuestionRefs = [];
|
|
7349
|
+
for (const { question, oldBodyPath, body } of pendingQuestionMigrations) {
|
|
7350
|
+
const bodyRef = writeQuestionBody(hqRoot, {
|
|
7351
|
+
...question,
|
|
7352
|
+
company,
|
|
7353
|
+
body: { ...question.body, store: "company" },
|
|
7354
|
+
}, body);
|
|
7355
|
+
casUpdateQuestion(hqRoot, question.question_id, (current) => {
|
|
7356
|
+
if (current.lane_id !== laneId || current.status !== "pending" || current.company !== null) {
|
|
7357
|
+
throw new LanesError("question_changed_during_company_repair", `Pending question ${question.question_id} changed during company repair`, { lane_id: laneId, question_id: question.question_id });
|
|
7358
|
+
}
|
|
7359
|
+
return { ...current, company, body: bodyRef };
|
|
7360
|
+
});
|
|
7361
|
+
fs.rmSync(oldBodyPath, { force: true });
|
|
7362
|
+
migratedQuestionRefs.push({ question_id: question.question_id, project_id: question.project_id, story_id: question.story_id });
|
|
7363
|
+
}
|
|
7364
|
+
const repairedLane = readLane(hqRoot, laneId);
|
|
7365
|
+
syncLaneToProjectStoryLedger(hqRoot, repairedLane, migratedQuestionRefs);
|
|
7366
|
+
removeLaneFromProjectStoryLedger(hqRoot, initial);
|
|
7367
|
+
printJson({ ok: true, action: "repair-company", lane_id: laneId, lane: repairedLane, migrated_pending_questions: migratedQuestionRefs.length });
|
|
7368
|
+
}
|
|
7369
|
+
async function runRepairCompanyOwnerTarget(ctx, laneId, opts) {
|
|
7370
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
7371
|
+
throw new LanesError("outpost_repair_company_dispatch_unauthorized", "The Outpost company-repair target is available only to an SSM Run Command dispatch", { lane_id: laneId });
|
|
7372
|
+
}
|
|
7373
|
+
const lane = readLane(ctx.hqRoot(), laneId);
|
|
7374
|
+
if (lane.host.kind !== "outpost" || !laneIsHostedHere(ctx, lane)) {
|
|
7375
|
+
throw new LanesError("lane_remote", `Outpost repair-company target cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: lane.host });
|
|
7376
|
+
}
|
|
7377
|
+
await repairCompanyOnOwner(ctx, laneId, opts);
|
|
7378
|
+
}
|
|
7242
7379
|
const CANONICAL_LOG_STREAMS = [
|
|
7243
7380
|
"watcher.err",
|
|
7244
7381
|
"watcher.out",
|
|
@@ -8671,6 +8808,13 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8671
8808
|
.action(wrap(async (laneId, opts) => {
|
|
8672
8809
|
await runReparentOwnerTarget(ctx, laneId, opts);
|
|
8673
8810
|
}));
|
|
8811
|
+
lanes
|
|
8812
|
+
.command("repair-company-target", { hidden: true })
|
|
8813
|
+
.argument("<lane-id>", "Lane id")
|
|
8814
|
+
.requiredOption("--company <slug>", "Company slug to assign")
|
|
8815
|
+
.action(wrap(async (laneId, opts) => {
|
|
8816
|
+
await runRepairCompanyOwnerTarget(ctx, laneId, opts);
|
|
8817
|
+
}));
|
|
8674
8818
|
lanes
|
|
8675
8819
|
.command("grant-target", { hidden: true })
|
|
8676
8820
|
.argument("<lane-id>", "Lane id")
|
|
@@ -8976,6 +9120,14 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8976
9120
|
.action(wrap(async (laneId, opts) => {
|
|
8977
9121
|
await runReparent(ctx, laneId, opts);
|
|
8978
9122
|
}));
|
|
9123
|
+
lanes
|
|
9124
|
+
.command("repair-company")
|
|
9125
|
+
.description("Set missing company attribution on a legacy lane")
|
|
9126
|
+
.argument("<lane-id>", "Lane id")
|
|
9127
|
+
.requiredOption("--company <slug>", "Company slug to assign")
|
|
9128
|
+
.action(wrap(async (laneId, opts) => {
|
|
9129
|
+
await runRepairCompany(ctx, laneId, opts);
|
|
9130
|
+
}));
|
|
8979
9131
|
lanes
|
|
8980
9132
|
.command("logs")
|
|
8981
9133
|
.description("Print a lane's local logs")
|
|
@@ -3,7 +3,6 @@ import { LanesError } from "./errors.js";
|
|
|
3
3
|
export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" | "hire_grant">;
|
|
4
4
|
export declare function isFinishedResumableHireChild(state: LaneRecord["state"]): boolean;
|
|
5
5
|
export declare function laneHoldsHireGrantCarve(lane: Pick<LaneHireGrantRow, "state">, finishedChildrenDoNotCount?: boolean): boolean;
|
|
6
|
-
export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string, finishedChildrenDoNotCount?: boolean): T[];
|
|
7
6
|
/** Children whose existing hire grants occupy their senior's carve. */
|
|
8
7
|
export declare function childrenHoldingHireGrantCarve<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string, finishedChildrenDoNotCount?: boolean): T[];
|
|
9
8
|
export declare function assignedChildHireGrants(children: readonly LaneHireGrantRow[], exceptLaneId?: string): number;
|
package/dist/lib/lanes/grants.js
CHANGED
|
@@ -8,9 +8,6 @@ export function laneHoldsHireGrantCarve(lane, finishedChildrenDoNotCount = false
|
|
|
8
8
|
return !isTerminalLaneState(lane.state) &&
|
|
9
9
|
!(finishedChildrenDoNotCount && isFinishedResumableHireChild(lane.state));
|
|
10
10
|
}
|
|
11
|
-
export function aliveDirectChildren(lanes, parentLaneId, finishedChildrenDoNotCount = false) {
|
|
12
|
-
return childrenHoldingHireGrantCarve(lanes, parentLaneId, finishedChildrenDoNotCount);
|
|
13
|
-
}
|
|
14
11
|
/** Children whose existing hire grants occupy their senior's carve. */
|
|
15
12
|
export function childrenHoldingHireGrantCarve(lanes, parentLaneId, finishedChildrenDoNotCount = false) {
|
|
16
13
|
return lanes.filter((lane) => lane.senior.kind === "lane" &&
|
|
@@ -119,7 +116,7 @@ export function planHireGrantBackfill(lanes, profileMaxConcurrent) {
|
|
|
119
116
|
const candidates = lanes.filter((lane) => {
|
|
120
117
|
if (isTerminalLaneState(lane.state))
|
|
121
118
|
return false;
|
|
122
|
-
return
|
|
119
|
+
return childrenHoldingHireGrantCarve(lanes, lane.lane_id).length > 0 ||
|
|
123
120
|
lane.worker_profile === "hq-lanes-manager";
|
|
124
121
|
});
|
|
125
122
|
const candidateIds = new Set(candidates.map((lane) => lane.lane_id));
|
|
@@ -133,7 +130,7 @@ export function planHireGrantBackfill(lanes, profileMaxConcurrent) {
|
|
|
133
130
|
throw new LanesError("hire_grant_backfill_cycle", `Cannot plan hire grants for cyclic lane hierarchy at ${lane.lane_id}`, { lane_id: lane.lane_id });
|
|
134
131
|
}
|
|
135
132
|
active.add(lane.lane_id);
|
|
136
|
-
const children =
|
|
133
|
+
const children = childrenHoldingHireGrantCarve(lanes, lane.lane_id);
|
|
137
134
|
const allocated = children.reduce((sum, child) => {
|
|
138
135
|
if (child.hire_grant !== undefined)
|
|
139
136
|
return sum + child.hire_grant;
|
|
@@ -151,7 +148,7 @@ export function planHireGrantBackfill(lanes, profileMaxConcurrent) {
|
|
|
151
148
|
proposalFor(lane);
|
|
152
149
|
return candidates
|
|
153
150
|
.map((lane) => {
|
|
154
|
-
const children =
|
|
151
|
+
const children = childrenHoldingHireGrantCarve(lanes, lane.lane_id);
|
|
155
152
|
const profileMax = lane.worker_profile
|
|
156
153
|
? profileMaxConcurrent.get(lane.worker_profile) ?? null
|
|
157
154
|
: null;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants,
|
|
6
|
+
export { assignedChildHireGrants, childrenHoldingHireGrantCarve, isFinishedResumableHireChild, laneHoldsHireGrantCarve, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
7
7
|
export type { HireGrantBackfillRow } from "./grants.js";
|
|
8
8
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
9
9
|
export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
|
@@ -16,7 +16,7 @@ export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFT
|
|
|
16
16
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
17
17
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
18
18
|
export type { LanesHqRootOpts } from "./hq-root.js";
|
|
19
|
-
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
19
|
+
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
20
20
|
export type { LaneStatusSnapshot, ProjectStoryLedger, StoryLedgerWriteDeps } from "./story-ledger.js";
|
|
21
21
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
22
22
|
export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants,
|
|
6
|
+
export { assignedChildHireGrants, childrenHoldingHireGrantCarve, isFinishedResumableHireChild, laneHoldsHireGrantCarve, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
7
7
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
8
8
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
9
9
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
10
10
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
11
11
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
12
12
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
13
|
-
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
13
|
+
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
14
14
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
15
15
|
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
16
16
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
@@ -37,6 +37,11 @@ interface KeepAliveAuditError {
|
|
|
37
37
|
export interface KeepAliveAuditResult {
|
|
38
38
|
ok: true;
|
|
39
39
|
since: string;
|
|
40
|
+
companyless_lead_lanes: Array<{
|
|
41
|
+
lane: string;
|
|
42
|
+
project: string;
|
|
43
|
+
reason: "has_juniors" | "can_hire_juniors";
|
|
44
|
+
}>;
|
|
40
45
|
rows: KeepAliveAuditRow[];
|
|
41
46
|
errors: KeepAliveAuditError[];
|
|
42
47
|
summary: KeepAliveAuditSummary;
|
|
@@ -478,7 +478,19 @@ export function buildKeepAliveAudit(input) {
|
|
|
478
478
|
if (sinceMs === undefined || !Number.isFinite(sinceMs)) {
|
|
479
479
|
throw new LanesError("invalid_since", "--since must be a valid ISO date-time", { since: input.since });
|
|
480
480
|
}
|
|
481
|
-
const
|
|
481
|
+
const allLanes = readAllLanes(input.hqRoot);
|
|
482
|
+
const companylessLeadLanes = allLanes
|
|
483
|
+
.filter((lane) => lane.company === null && (input.laneId === undefined || lane.lane_id === input.laneId))
|
|
484
|
+
.map((lane) => {
|
|
485
|
+
const hasJuniors = allLanes.some((candidate) => candidate.senior.kind === "lane" && candidate.senior.id === lane.lane_id);
|
|
486
|
+
const canHireJuniors = (lane.hire_grant ?? 0) > 0;
|
|
487
|
+
if (!hasJuniors && !canHireJuniors)
|
|
488
|
+
return null;
|
|
489
|
+
return { lane: lane.lane_id, project: lane.project_id, reason: hasJuniors ? "has_juniors" : "can_hire_juniors" };
|
|
490
|
+
})
|
|
491
|
+
.filter((entry) => entry !== null)
|
|
492
|
+
.sort((a, b) => a.lane.localeCompare(b.lane));
|
|
493
|
+
const lanes = allLanes.filter((lane) => (input.laneId === undefined || lane.lane_id === input.laneId) &&
|
|
482
494
|
typeof lane.hire_grant === "number" && Number.isFinite(lane.hire_grant) && lane.hire_grant > 0);
|
|
483
495
|
const rows = [];
|
|
484
496
|
const errors = [];
|
|
@@ -544,6 +556,7 @@ export function buildKeepAliveAudit(input) {
|
|
|
544
556
|
return {
|
|
545
557
|
ok: true,
|
|
546
558
|
since: iso(sinceMs),
|
|
559
|
+
companyless_lead_lanes: companylessLeadLanes,
|
|
547
560
|
rows,
|
|
548
561
|
summary: {
|
|
549
562
|
by_verdict: byVerdict,
|
|
@@ -581,6 +594,7 @@ export function formatKeepAliveAuditText(result) {
|
|
|
581
594
|
`Since ${result.since}; ${counts}`,
|
|
582
595
|
`not_restarted=${result.summary.not_restarted}, late_restarts_after_window=${result.summary.late_restarts_after_window}, late_over_10m=${result.summary.late_over_10m}, early_wake_by_others=${result.summary.early_wake_by_others}, self_wake=${result.summary.self_wake}`,
|
|
583
596
|
`Stopped and not restarted on their own: ${result.summary.stopped_and_not_restarted_on_their_own}`,
|
|
597
|
+
...result.companyless_lead_lanes.map((lane) => `Company-less lead ${lane.lane} (${lane.project}): ${lane.reason}; repair with hq lanes repair-company ${lane.lane} --company <slug>`),
|
|
584
598
|
...result.errors.map((error) => `Error ${error.lane} (${error.error}) at ${error.path}: ${error.message}`),
|
|
585
599
|
].join("\n");
|
|
586
600
|
}
|
|
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
|
|
|
27
27
|
stderr: string;
|
|
28
28
|
}
|
|
29
29
|
export interface OutpostLaneCommand {
|
|
30
|
-
action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target";
|
|
30
|
+
action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target" | "repair-company-target";
|
|
31
31
|
laneId: string;
|
|
32
32
|
hqBinary?: string;
|
|
33
33
|
text?: string;
|
|
@@ -46,6 +46,7 @@ export interface OutpostLaneCommand {
|
|
|
46
46
|
approvalContext?: string;
|
|
47
47
|
senior?: string;
|
|
48
48
|
reason?: string;
|
|
49
|
+
company?: string;
|
|
49
50
|
compact?: boolean;
|
|
50
51
|
tail?: string;
|
|
51
52
|
json?: boolean;
|
|
@@ -210,6 +210,7 @@ export function buildOutpostLaneCommand(input) {
|
|
|
210
210
|
["--failed-resume-context", input.failedResumeContext],
|
|
211
211
|
["--senior", input.senior],
|
|
212
212
|
["--reason", input.reason],
|
|
213
|
+
["--company", input.company],
|
|
213
214
|
]) {
|
|
214
215
|
if (value !== undefined)
|
|
215
216
|
args.push(flag, value);
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -784,6 +784,25 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
784
784
|
}
|
|
785
785
|
}
|
|
786
786
|
parseOptionalCompanyField(raw, filePath);
|
|
787
|
+
if (raw.company_history !== undefined) {
|
|
788
|
+
if (!Array.isArray(raw.company_history) || raw.company_history.length > HISTORY_CAP) {
|
|
789
|
+
throw new LaneSchemaError(`company_history must be an array capped at ${HISTORY_CAP} entries`, {
|
|
790
|
+
path: filePath,
|
|
791
|
+
field: "company_history",
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
for (const [index, entry] of raw.company_history.entries()) {
|
|
795
|
+
const field = `company_history[${index}]`;
|
|
796
|
+
if (!isPlainObject(entry) || Object.keys(entry).some((key) => !["from", "to", "at", "by", "reason"].includes(key)) ||
|
|
797
|
+
entry.from !== null || entry.reason !== "repair_company" || !isPlainObject(entry.to)) {
|
|
798
|
+
throw new LaneSchemaError(`${field} must be a company repair audit entry`, { path: filePath, field });
|
|
799
|
+
}
|
|
800
|
+
requireString(entry.to.slug, `${field}.to.slug`);
|
|
801
|
+
requireString(entry.to.uid, `${field}.to.uid`);
|
|
802
|
+
requireString(entry.by, `${field}.by`);
|
|
803
|
+
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
787
806
|
parseOptionalMeshCompanyField(raw, filePath);
|
|
788
807
|
if (!isPlainObject(raw.senior)) {
|
|
789
808
|
throw new LaneSchemaError("senior must be an object", { path: filePath });
|
|
@@ -1242,9 +1261,23 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
1242
1261
|
return live;
|
|
1243
1262
|
const all = [live, ...sidecars];
|
|
1244
1263
|
const base = higherVersionRecord(live, sidecars);
|
|
1264
|
+
const attributedCompanies = all.filter((record) => record.company !== null).map((record) => record.company);
|
|
1265
|
+
const conflictingCompanies = attributedCompanies.some((company, index) => attributedCompanies.slice(index + 1).some((other) => company.uid && other.uid
|
|
1266
|
+
? company.uid !== other.uid
|
|
1267
|
+
: Boolean(company.slug && other.slug && company.slug !== other.slug)));
|
|
1268
|
+
if (conflictingCompanies) {
|
|
1269
|
+
throw new LaneSchemaError("Conflicting company attribution in lane record sidecars", { lane_id: live.lane_id });
|
|
1270
|
+
}
|
|
1271
|
+
const company = base.company ?? attributedCompanies[0] ?? null;
|
|
1245
1272
|
return {
|
|
1246
1273
|
...base,
|
|
1274
|
+
company,
|
|
1247
1275
|
senior_history: unionHistory(all.map((record) => record.senior_history), entryIdentity, base.senior_history),
|
|
1276
|
+
...(all.some((record) => record.company_history !== undefined)
|
|
1277
|
+
? {
|
|
1278
|
+
company_history: unionHistory(all.map((record) => record.company_history ?? []), entryIdentity, base.company_history ?? []),
|
|
1279
|
+
}
|
|
1280
|
+
: {}),
|
|
1248
1281
|
attribution_history: unionHistory(all.map((record) => record.attribution_history), attributionIdentity, base.attribution_history),
|
|
1249
1282
|
...(all.some((record) => record.account_history !== undefined)
|
|
1250
1283
|
? {
|
|
@@ -64,6 +64,8 @@ export declare function laneStoryLedgerEnabled(): boolean;
|
|
|
64
64
|
export declare function resolveProjectStoryLedger(hqRoot: string, projectId: string, companySlug?: string): ProjectStoryLedger;
|
|
65
65
|
export declare function validateProjectStories(hqRoot: string, projectId: string, storyIds: readonly string[], companySlug?: string): ProjectStoryLedger;
|
|
66
66
|
export declare function syncLaneToProjectStoryLedger(hqRoot: string, lane: LaneRecord, questionRefs?: readonly StoryLedgerQuestionRef[], deps?: StoryLedgerWriteDeps): void;
|
|
67
|
+
/** Remove only this lane's attribution from its former personal story ledgers. */
|
|
68
|
+
export declare function removeLaneFromProjectStoryLedger(hqRoot: string, lane: LaneRecord): void;
|
|
67
69
|
/** Read canonical ledger states for the story ids currently carried by a lane. */
|
|
68
70
|
export declare function readProjectStoryStates(hqRoot: string, projectId: string, storyIds: readonly string[], companySlug?: string): Map<string, string>;
|
|
69
71
|
export declare function buildLaneStatusSnapshot(input: {
|
|
@@ -643,6 +643,39 @@ export function syncLaneToProjectStoryLedger(hqRoot, lane, questionRefs = [], de
|
|
|
643
643
|
}, deps);
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
|
+
/** Remove only this lane's attribution from its former personal story ledgers. */
|
|
647
|
+
export function removeLaneFromProjectStoryLedger(hqRoot, lane) {
|
|
648
|
+
if (!laneStoryLedgerEnabled())
|
|
649
|
+
return;
|
|
650
|
+
const refs = [...new Map(laneStoriesForLedger(lane).map((story) => [
|
|
651
|
+
story.project_id + "\u0000" + story.story_id,
|
|
652
|
+
{ projectId: story.project_id, storyId: story.story_id },
|
|
653
|
+
])).values()];
|
|
654
|
+
for (const ref of refs) {
|
|
655
|
+
const ledger = resolveProjectStoryLedger(hqRoot, ref.projectId);
|
|
656
|
+
if (!fs.existsSync(ledger.path))
|
|
657
|
+
continue;
|
|
658
|
+
updateLedgerAtomically(ledger, (current) => {
|
|
659
|
+
const story = current.userStories.find((item) => item.id === ref.storyId);
|
|
660
|
+
if (!story)
|
|
661
|
+
return current;
|
|
662
|
+
if (Array.isArray(story.lanes)) {
|
|
663
|
+
story.lanes = story.lanes.filter((entry) => !isJsonObject(entry) || entry.lane_id !== lane.lane_id);
|
|
664
|
+
}
|
|
665
|
+
if (Array.isArray(story.pending_questions)) {
|
|
666
|
+
const pendingQuestions = story.pending_questions.filter((entry) => !isJsonObject(entry) || entry.lane_id !== lane.lane_id);
|
|
667
|
+
if (pendingQuestions.length === 0)
|
|
668
|
+
delete story.pending_questions;
|
|
669
|
+
else
|
|
670
|
+
story.pending_questions = pendingQuestions;
|
|
671
|
+
}
|
|
672
|
+
if (isJsonObject(story.lane_status) && isJsonObject(story.lane_status.saved) && Array.isArray(story.lane_status.saved.pending_questions)) {
|
|
673
|
+
story.lane_status.saved.pending_questions = story.lane_status.saved.pending_questions.filter((entry) => !isJsonObject(entry) || entry.lane_id !== lane.lane_id);
|
|
674
|
+
}
|
|
675
|
+
return current;
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
646
679
|
function storyLaneState(lane, laneStoryStatus, pendingQuestionIds) {
|
|
647
680
|
if (isTerminalLaneState(lane.state))
|
|
648
681
|
return lane.state;
|
|
@@ -308,6 +308,14 @@ export interface SeniorHistoryEntry {
|
|
|
308
308
|
by: string;
|
|
309
309
|
reason: string;
|
|
310
310
|
}
|
|
311
|
+
/** Explicit repair of a legacy lane whose company attribution was missing. */
|
|
312
|
+
export interface CompanyHistoryEntry {
|
|
313
|
+
from: null;
|
|
314
|
+
to: LaneCompanyRef;
|
|
315
|
+
at: string;
|
|
316
|
+
by: string;
|
|
317
|
+
reason: "repair_company";
|
|
318
|
+
}
|
|
311
319
|
interface LaneStopHistoryEntry {
|
|
312
320
|
at: string;
|
|
313
321
|
reason: string;
|
|
@@ -509,6 +517,8 @@ export interface LaneRecord {
|
|
|
509
517
|
owner_written_by_version?: string;
|
|
510
518
|
/** `null` is a core/personal lane (spec §6.2 / §10). Not an empty slug. */
|
|
511
519
|
company: LaneCompanyRef | null;
|
|
520
|
+
/** Bounded audit history of explicit company-attribution repairs. */
|
|
521
|
+
company_history?: CompanyHistoryEntry[];
|
|
512
522
|
/** Explicit Work Mesh attribution for a personal lane; never inferred from cwd. */
|
|
513
523
|
mesh_company?: LaneCompanyRef;
|
|
514
524
|
project_id: string;
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -224,7 +224,7 @@ export const REQUIRED_LANE_RECORD_FIELDS = [
|
|
|
224
224
|
export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spawn", "story-ledger"];
|
|
225
225
|
/** Field ownership at the durable-record mutation boundary (spec §10.1). */
|
|
226
226
|
export const LANE_FIELD_WRITERS = {
|
|
227
|
-
schema_version: ["creator"], lane_id: ["creator"], written_by_version: [], owner_written_by_version: [], company: ["creator"],
|
|
227
|
+
schema_version: ["creator"], lane_id: ["creator"], written_by_version: [], owner_written_by_version: [], company: ["creator", "command"], company_history: ["command"],
|
|
228
228
|
project_id: ["command"], story_id: ["command"], no_story_reason: ["creator", "command"], ledger_sync_error: ["story-ledger"], pending_question_ids: ["story-ledger"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
|
|
229
229
|
depth: ["command"], senior: ["command"], senior_history: ["command"],
|
|
230
230
|
hire_grant: ["creator", "command"],
|
package/dist/main.js
CHANGED
|
@@ -392,9 +392,12 @@ function integrationsGatewayCaptureContext(err) {
|
|
|
392
392
|
? String(status)
|
|
393
393
|
: status === undefined ? "unknown" : "other";
|
|
394
394
|
const rpcCode = record.rpcCode;
|
|
395
|
+
const gatewayFailureCode = record.gatewayFailureCode;
|
|
395
396
|
const codeTag = typeof rpcCode === "number" && Number.isInteger(rpcCode)
|
|
396
397
|
? GATEWAY_RPC_CODE_TAGS.get(rpcCode) ?? "other"
|
|
397
|
-
:
|
|
398
|
+
: gatewayFailureCode === "invalid_envelope"
|
|
399
|
+
? gatewayFailureCode
|
|
400
|
+
: rpcCode === undefined ? "missing" : "other";
|
|
398
401
|
const errorShape = record.gatewayErrorShape;
|
|
399
402
|
const errorShapeTag = typeof errorShape === "string" && GATEWAY_ERROR_SHAPES.has(errorShape)
|
|
400
403
|
? errorShape
|
|
@@ -288,14 +288,14 @@ type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "v
|
|
|
288
288
|
* app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
|
|
289
289
|
* reinstall look stale:
|
|
290
290
|
* - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
|
|
291
|
-
* by `deadlineMs`. If the target is still missing and
|
|
291
|
+
* by `deadlineMs`. If the target is still missing and a fully observed tree
|
|
292
292
|
* signature stays unchanged for `staleProbeMs`, the wait returns `staleTree`
|
|
293
|
-
* early. When
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
293
|
+
* early. When a named retired npm sibling's deep scan is incomplete, a stable
|
|
294
|
+
* partial signature is not enough to call the tree abandoned: the wait keeps
|
|
295
|
+
* polling until the bounded deadline, where it returns
|
|
296
|
+
* `retiredSiblingUnverified`. Any observed signature change also keeps the
|
|
297
|
+
* ordinary settle deadline in force. `deadlineMs === 0` evaluates readiness
|
|
298
|
+
* once.
|
|
299
299
|
* - WRITER time — wall time observed under a fresh foreign lock, or while a
|
|
300
300
|
* recorded foreign updater pid is alive and the tree has an in-flight signal
|
|
301
301
|
* — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
|
|
@@ -949,14 +949,14 @@ const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
|
949
949
|
* app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
|
|
950
950
|
* reinstall look stale:
|
|
951
951
|
* - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
|
|
952
|
-
* by `deadlineMs`. If the target is still missing and
|
|
952
|
+
* by `deadlineMs`. If the target is still missing and a fully observed tree
|
|
953
953
|
* signature stays unchanged for `staleProbeMs`, the wait returns `staleTree`
|
|
954
|
-
* early. When
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
954
|
+
* early. When a named retired npm sibling's deep scan is incomplete, a stable
|
|
955
|
+
* partial signature is not enough to call the tree abandoned: the wait keeps
|
|
956
|
+
* polling until the bounded deadline, where it returns
|
|
957
|
+
* `retiredSiblingUnverified`. Any observed signature change also keeps the
|
|
958
|
+
* ordinary settle deadline in force. `deadlineMs === 0` evaluates readiness
|
|
959
|
+
* once.
|
|
960
960
|
* - WRITER time — wall time observed under a fresh foreign lock, or while a
|
|
961
961
|
* recorded foreign updater pid is alive and the tree has an in-flight signal
|
|
962
962
|
* — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
|
|
@@ -1085,12 +1085,14 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
1085
1085
|
updaterPidAlive: updaterPidAlive(args.lockPath, fs, isPidAlive),
|
|
1086
1086
|
};
|
|
1087
1087
|
}
|
|
1088
|
+
const retiredSiblingUnverified = !lastActivitySnapshot.observed && lastActivitySnapshot.retiredSiblingPresent === true;
|
|
1088
1089
|
if (!ready &&
|
|
1089
1090
|
!foreignLockHeld &&
|
|
1090
1091
|
!updaterPidHeld &&
|
|
1091
1092
|
!retiredCheckFailed &&
|
|
1092
1093
|
!sawLock &&
|
|
1093
1094
|
!sawTreeChange &&
|
|
1095
|
+
!retiredSiblingUnverified &&
|
|
1094
1096
|
staleObservationStartedAt !== null &&
|
|
1095
1097
|
t - staleObservationStartedAt >= staleProbeMs) {
|
|
1096
1098
|
return {
|
|
@@ -1129,6 +1131,7 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
1129
1131
|
sawTreeChange,
|
|
1130
1132
|
staleTree: false,
|
|
1131
1133
|
retiredSiblingPresent: retiredPresent,
|
|
1134
|
+
retiredSiblingUnverified: !lastActivitySnapshot.observed && lastActivitySnapshot.retiredSiblingPresent === true,
|
|
1132
1135
|
retiredSiblingKind: retiredSiblingKind(args.packageRoot, fs),
|
|
1133
1136
|
installTargetState: installTargetState(args.target, args.packageRoot, fs),
|
|
1134
1137
|
updaterPidAlive: updaterPidAlive(args.lockPath, fs, isPidAlive),
|