@indigoai-us/hq-cli 5.345.25 → 5.345.27
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 +15 -0
- package/README.md +15 -0
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +55 -16
- package/dist/command-catalog.generated.d.ts +83 -0
- package/dist/command-catalog.generated.js +104 -0
- package/dist/commands/agent-kit.js +4 -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/commands/meetings.js +210 -0
- package/dist/lib/agent-kit/run/heartbeat.d.ts +8 -2
- package/dist/lib/agent-kit/run/heartbeat.js +42 -25
- package/dist/lib/heartbeat-identity.d.ts +1 -1
- package/dist/lib/heartbeat-identity.js +1 -1
- 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
|
@@ -50,25 +50,60 @@ export function readLastHeartbeat(paths) {
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
export function defaultHeartbeatPost(
|
|
54
|
-
return async (token, body) => {
|
|
53
|
+
export function defaultHeartbeatPost(identity) {
|
|
54
|
+
return async (token, body, requestIdentity = identity) => {
|
|
55
55
|
const res = await vaultApiFetch({
|
|
56
56
|
token,
|
|
57
|
-
baseUrl: apiBaseUrl,
|
|
58
|
-
path: `/v1/agents/${encodeURIComponent(agentUid)}/heartbeat`,
|
|
57
|
+
baseUrl: requestIdentity.apiBaseUrl,
|
|
58
|
+
path: `/v1/agents/${encodeURIComponent(requestIdentity.agentUid)}/heartbeat`,
|
|
59
59
|
method: "POST",
|
|
60
60
|
body: body,
|
|
61
61
|
});
|
|
62
62
|
return { status: res.status, bodyText: await res.text().catch(() => "") };
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
function stopForIdentityMismatch(deps, body, now, status, configuredAgentUid, tokenUid) {
|
|
66
|
+
writeLastHeartbeat(deps.paths, {
|
|
67
|
+
at: now().toISOString(),
|
|
68
|
+
components: body.components,
|
|
69
|
+
status,
|
|
70
|
+
reason: "identity_mismatch",
|
|
71
|
+
configuredAgentUid,
|
|
72
|
+
...(tokenUid ? { tokenAgentUid: tokenUid } : {}),
|
|
73
|
+
});
|
|
74
|
+
deps.log("warn", identityMismatchMessage("heartbeat", configuredAgentUid, tokenUid));
|
|
75
|
+
(deps.reportIdentityMismatch ?? captureHeartbeatIdentityMismatch)({
|
|
76
|
+
runner: "agent-kit",
|
|
77
|
+
configuredAgentUid,
|
|
78
|
+
...(tokenUid ? { tokenAgentUid: tokenUid } : {}),
|
|
79
|
+
agentUidSource: deps.agentUidSource ?? "config_file",
|
|
80
|
+
credsSource: deps.credsSource ?? "default",
|
|
81
|
+
});
|
|
82
|
+
return { ok: false, stop: true };
|
|
83
|
+
}
|
|
65
84
|
async function beatOnceResult(deps) {
|
|
66
85
|
const now = deps.now ?? (() => new Date());
|
|
67
|
-
const post = deps.post ?? defaultHeartbeatPost(deps.agentUid, deps.apiBaseUrl);
|
|
68
86
|
const body = buildHeartbeatBody(deps.paths, now, undefined, deps.components ?? KIT_COMPONENTS);
|
|
69
87
|
try {
|
|
70
88
|
const token = await deps.getToken();
|
|
71
|
-
const
|
|
89
|
+
const tokenUid = tokenAgentUid(token);
|
|
90
|
+
let identity = { agentUid: deps.agentUid, apiBaseUrl: deps.apiBaseUrl };
|
|
91
|
+
if (tokenUid !== undefined && tokenUid !== identity.agentUid) {
|
|
92
|
+
if (deps.getConfiguredAgentIdentity) {
|
|
93
|
+
try {
|
|
94
|
+
identity = deps.getConfiguredAgentIdentity();
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
deps.log("error", `heartbeat identity refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (tokenUid !== identity.agentUid) {
|
|
101
|
+
// Status 0 means the client stopped before receiving an HTTP response.
|
|
102
|
+
return stopForIdentityMismatch(deps, body, now, 0, identity.agentUid, tokenUid);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const post = deps.post ?? defaultHeartbeatPost(identity);
|
|
106
|
+
const response = await post(token, body, identity);
|
|
72
107
|
const result = typeof response === "number" ? { status: response } : response;
|
|
73
108
|
const status = result.status;
|
|
74
109
|
const ok = status >= 200 && status < 300;
|
|
@@ -78,25 +113,7 @@ async function beatOnceResult(deps) {
|
|
|
78
113
|
else {
|
|
79
114
|
const failure = parseHeartbeatFailureBody(result.bodyText);
|
|
80
115
|
if (isHeartbeatIdentityMismatch({ status, code: failure.code, message: failure.message })) {
|
|
81
|
-
|
|
82
|
-
writeLastHeartbeat(deps.paths, {
|
|
83
|
-
at: now().toISOString(),
|
|
84
|
-
components: body.components,
|
|
85
|
-
status,
|
|
86
|
-
reason: "identity_mismatch",
|
|
87
|
-
configuredAgentUid: deps.agentUid,
|
|
88
|
-
...(tokenUid ? { tokenAgentUid: tokenUid } : {}),
|
|
89
|
-
});
|
|
90
|
-
const tokenLabel = tokenUid ?? "unknown";
|
|
91
|
-
deps.log("warn", identityMismatchMessage("heartbeat", deps.agentUid, tokenLabel));
|
|
92
|
-
(deps.reportIdentityMismatch ?? captureHeartbeatIdentityMismatch)({
|
|
93
|
-
runner: "agent-kit",
|
|
94
|
-
configuredAgentUid: deps.agentUid,
|
|
95
|
-
...(tokenUid ? { tokenAgentUid: tokenUid } : {}),
|
|
96
|
-
agentUidSource: deps.agentUidSource ?? "config_file",
|
|
97
|
-
credsSource: deps.credsSource ?? "default",
|
|
98
|
-
});
|
|
99
|
-
return { ok: false, stop: true };
|
|
116
|
+
return stopForIdentityMismatch(deps, body, now, status, identity.agentUid, tokenUid);
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
deps.log(ok ? "info" : "warn", `heartbeat ${status} ${Object.entries(body.components)
|
|
@@ -24,7 +24,7 @@ export declare function parseHeartbeatFailureBody(bodyText: string | undefined):
|
|
|
24
24
|
};
|
|
25
25
|
/** Match the server's registered code/name, never an arbitrary 403. */
|
|
26
26
|
export declare function isHeartbeatIdentityMismatch(failure: HeartbeatFailure): boolean;
|
|
27
|
-
/** Read the agent uid claim for diagnostics
|
|
27
|
+
/** Read the agent uid claim for diagnostics and a fail-closed path consistency check; it never authorizes a call. */
|
|
28
28
|
export declare function tokenAgentUid(token: string): string | undefined;
|
|
29
29
|
export declare function captureHeartbeatIdentityMismatch(context: HeartbeatIdentityMismatchContext, captureMessage?: (message: string, context: CaptureContext) => string): void;
|
|
30
30
|
//# sourceMappingURL=heartbeat-identity.d.ts.map
|
|
@@ -43,7 +43,7 @@ export function isHeartbeatIdentityMismatch(failure) {
|
|
|
43
43
|
return (message.includes("heartbeatmustbereportedbytheagentsownmachineidentity") ||
|
|
44
44
|
message.includes("agentidentitymismatcherror"));
|
|
45
45
|
}
|
|
46
|
-
/** Read the agent uid claim for diagnostics
|
|
46
|
+
/** Read the agent uid claim for diagnostics and a fail-closed path consistency check; it never authorizes a call. */
|
|
47
47
|
export function tokenAgentUid(token) {
|
|
48
48
|
const claims = peekIdToken(token);
|
|
49
49
|
return (safeAgentUid(claims["custom:entityUid"]) ??
|
|
@@ -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),
|