@indigoai-us/hq-cli 5.148.0 → 5.149.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +8 -11
- package/dist/command-catalog.generated.d.ts +129 -0
- package/dist/command-catalog.generated.js +168 -0
- package/dist/commands/lanes.js +506 -23
- package/dist/commands/mesh.js +82 -11
- package/dist/lib/lanes/index.d.ts +3 -2
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/mesh-events.d.ts +8 -0
- package/dist/lib/lanes/mesh-events.js +83 -6
- package/dist/lib/lanes/outpost-dispatch.d.ts +12 -0
- package/dist/lib/lanes/outpost-dispatch.js +17 -0
- package/dist/lib/lanes/reconcile.js +39 -12
- package/dist/lib/lanes/session-relink.d.ts +25 -0
- package/dist/lib/lanes/session-relink.js +216 -0
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +48 -5
- package/dist/lib/lanes/store.js +109 -1
- package/dist/lib/lanes/stories.d.ts +17 -0
- package/dist/lib/lanes/stories.js +73 -0
- package/dist/lib/lanes/types.d.ts +20 -0
- package/dist/lib/lanes/types.js +9 -2
- package/dist/lib/lanes/watcher.js +69 -9
- package/dist/lib/mesh/live/daemon/doctor.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/doctor.js +84 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +3 -3
- package/dist/lib/mesh/live/daemon/index.js +2 -2
- package/dist/lib/mesh/live/daemon/install.d.ts +40 -0
- package/dist/lib/mesh/live/daemon/install.js +88 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +12 -1
- package/dist/lib/mesh/live/daemon/state.d.ts +4 -0
- package/dist/lib/mesh/project-board.d.ts +15 -0
- package/dist/lib/mesh/project-board.js +84 -0
- package/dist/lib/work-context/prd-sync.d.ts +5 -1
- package/dist/lib/work-context/prd-sync.js +22 -1
- package/package.json +5 -3
- package/scripts/mesh-daemon-if-stale.mjs +16 -0
package/dist/commands/mesh.js
CHANGED
|
@@ -9,11 +9,12 @@ import { resolve as resolvePath } from "node:path";
|
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { findHqRoot } from "../utils/manifest.js";
|
|
12
|
+
import { CLI_VERSION } from "../cli-version.js";
|
|
12
13
|
import chalk from "chalk";
|
|
13
14
|
import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
|
|
14
15
|
import * as readline from "node:readline/promises";
|
|
15
16
|
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
16
|
-
import { closeSupersededProjectThread, ensureProjectBoard } from "../lib/mesh/project-board.js";
|
|
17
|
+
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard } from "../lib/mesh/project-board.js";
|
|
17
18
|
import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
|
|
18
19
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
19
20
|
import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
@@ -34,7 +35,7 @@ import { generateUlid } from "../lib/mesh/live/ulid.js";
|
|
|
34
35
|
import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
|
|
35
36
|
import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
|
|
36
37
|
import { workMeshRoot } from "../lib/mesh/live/paths.js";
|
|
37
|
-
import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
|
|
38
|
+
import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, findAncestorHqRoot, resolveInstallHqRoot, detectPlatform, formatCliIdentityLines, formatDaemonDoctor, installDaemonIfStale, installDaemonService, readDaemonState, resolveAbsoluteNodeBinary, resolveRunningCliEntry, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
|
|
38
39
|
import { workContextRoot } from "../lib/work-context/paths.js";
|
|
39
40
|
import { formatMigrateConfirmation, submitSessionMigration, } from "../lib/work-context/migrate.js";
|
|
40
41
|
import { formatOrganizeList, prepareOrganizeDecision, settleOrganizeAskWithoutBind, submitOrganizeDecision, } from "../lib/work-context/organize.js";
|
|
@@ -575,6 +576,29 @@ async function runPrdSync(opts) {
|
|
|
575
576
|
...(bound?.projectId ? { project: bound.projectId } : {}),
|
|
576
577
|
touchedPaths: [resolvePath(file)],
|
|
577
578
|
};
|
|
579
|
+
if (bound?.companyUid && bound.projectId) {
|
|
580
|
+
try {
|
|
581
|
+
const source = parseProjectPlan(readFileSync(file, "utf8"), file);
|
|
582
|
+
await ensureMissingBoardStories({
|
|
583
|
+
token,
|
|
584
|
+
company: {
|
|
585
|
+
companyUid: bound.companyUid,
|
|
586
|
+
companySlug: bound.companySlug || bound.companyUid,
|
|
587
|
+
},
|
|
588
|
+
projectId: bound.projectId,
|
|
589
|
+
name: source.name || bound.projectId,
|
|
590
|
+
description: source.description ?? "",
|
|
591
|
+
stories: source.stories,
|
|
592
|
+
repos: source.repos ?? [],
|
|
593
|
+
requiredIds: plan.events.map((event) => event.taskId),
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
598
|
+
console.error(`work-mesh: prd-sync ensure failed; emitting anyway (${reason.slice(0, 160)})`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const meshRoot = workMeshHomeRoot();
|
|
578
602
|
const seqFirst = reserveSessionSeq(sessionId, plan.events.length);
|
|
579
603
|
const events = plan.events.map((event, index) => buildMeshEmitEvent({
|
|
580
604
|
eventId: generateUlid(Date.now() + index),
|
|
@@ -592,7 +616,7 @@ async function runPrdSync(opts) {
|
|
|
592
616
|
}));
|
|
593
617
|
const poster = createEmitPoster({ token, baseUrl: resolveVaultApiBase(process.env) });
|
|
594
618
|
const emitted = await emitEvents({
|
|
595
|
-
workMeshRoot:
|
|
619
|
+
workMeshRoot: meshRoot,
|
|
596
620
|
poster,
|
|
597
621
|
newEvents: events,
|
|
598
622
|
});
|
|
@@ -601,8 +625,15 @@ async function runPrdSync(opts) {
|
|
|
601
625
|
for (const line of formatEmitRejectionLines(emitted.rejections))
|
|
602
626
|
console.error(line);
|
|
603
627
|
}
|
|
604
|
-
|
|
605
|
-
|
|
628
|
+
const blocked = new Set(emitted.rejections.map((row) => row.eventId));
|
|
629
|
+
for (const pending of readEmitRetry(meshRoot)) {
|
|
630
|
+
const id = typeof pending.eventId === "string" ? pending.eventId : "";
|
|
631
|
+
if (id)
|
|
632
|
+
blocked.add(id);
|
|
633
|
+
}
|
|
634
|
+
const advanced = events.flatMap((event) => (event.eventId && event.taskId && !blocked.has(event.eventId) ? [event.taskId] : []));
|
|
635
|
+
plan.commit(advanced);
|
|
636
|
+
if (plan.taskId && advanced.includes(plan.taskId)) {
|
|
606
637
|
updateSessionState(sessionId, root, (current) => {
|
|
607
638
|
if (!current)
|
|
608
639
|
return null;
|
|
@@ -639,6 +670,12 @@ async function runDoctor(opts) {
|
|
|
639
670
|
fail("`hq mesh doctor --apply` (paced Board PUTs from local prd.json) is not in the CLI yet.\n" +
|
|
640
671
|
"Omit --apply to warm the conversation cache (directory, inbox, pair DMs).");
|
|
641
672
|
}
|
|
673
|
+
const identity = collectDaemonDoctor();
|
|
674
|
+
const identityLines = formatCliIdentityLines(identity);
|
|
675
|
+
if (!opts.json) {
|
|
676
|
+
for (const line of identityLines)
|
|
677
|
+
console.log(line);
|
|
678
|
+
}
|
|
642
679
|
const token = await requireToken();
|
|
643
680
|
const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
|
|
644
681
|
if (!principalUid) {
|
|
@@ -647,7 +684,15 @@ async function runDoctor(opts) {
|
|
|
647
684
|
const warmed = await warmMeshConversationCache(token, principalUid);
|
|
648
685
|
const needsProject = listNeedsProjectSessions(workContextHomeRoot());
|
|
649
686
|
if (opts.json) {
|
|
650
|
-
console.log(JSON.stringify({
|
|
687
|
+
console.log(JSON.stringify({
|
|
688
|
+
ok: true,
|
|
689
|
+
action: "doctor",
|
|
690
|
+
...warmed,
|
|
691
|
+
needsProject,
|
|
692
|
+
cliMismatchLine: identity.cliMismatchLine ?? null,
|
|
693
|
+
hqOnPath: identity.hqOnPath,
|
|
694
|
+
multipleHqOnPath: identity.multipleHqOnPath,
|
|
695
|
+
}, null, 2));
|
|
651
696
|
return;
|
|
652
697
|
}
|
|
653
698
|
console.log(`Work mesh cache warmed at ${warmed.cacheRoot}` +
|
|
@@ -1938,6 +1983,7 @@ export function registerMeshCommand(program) {
|
|
|
1938
1983
|
.description("Install LaunchAgent (macOS), systemd user unit (Linux), or Scheduled Task (Windows)")
|
|
1939
1984
|
.option("--system-user <user>", "Write a root-owned unit template for agent boxes (e.g. ec2-user)")
|
|
1940
1985
|
.option("--hq-root <path>", "HQ root to record for the service (else HQ_ROOT, else the nearest HQ root above cwd)")
|
|
1986
|
+
.option("--if-stale", "Reinstall and restart only when this CLI is newer than the daemon's recorded version")
|
|
1941
1987
|
.option("--json", "Print machine-readable JSON")
|
|
1942
1988
|
.action((opts) => wrap(() => runDaemonInstall(opts))());
|
|
1943
1989
|
daemon
|
|
@@ -1971,13 +2017,38 @@ async function runDaemonInstall(opts) {
|
|
|
1971
2017
|
// Verified root only: a repo's own .claude/ must never become the daemon's
|
|
1972
2018
|
// HQ root (that path is persisted as trusted and used for attribution).
|
|
1973
2019
|
const hqRoot = resolveInstallHqRoot({ explicit: opts.hqRoot });
|
|
2020
|
+
const paths = buildInstallPaths({
|
|
2021
|
+
systemUser: opts.systemUser,
|
|
2022
|
+
hqRoot: hqRoot.root,
|
|
2023
|
+
hqRootSource: hqRoot.source,
|
|
2024
|
+
hqBinary: resolveRunningCliEntry(),
|
|
2025
|
+
nodeBinary: resolveAbsoluteNodeBinary(),
|
|
2026
|
+
});
|
|
2027
|
+
if (opts.ifStale) {
|
|
2028
|
+
const stale = installDaemonIfStale({
|
|
2029
|
+
currentVersion: CLI_VERSION,
|
|
2030
|
+
platform: detectPlatform(),
|
|
2031
|
+
paths,
|
|
2032
|
+
systemUser: opts.systemUser,
|
|
2033
|
+
});
|
|
2034
|
+
if (opts.json) {
|
|
2035
|
+
console.log(JSON.stringify({
|
|
2036
|
+
ok: true,
|
|
2037
|
+
action: stale.reinstalled ? "install" : "unchanged",
|
|
2038
|
+
reinstalled: stale.reinstalled,
|
|
2039
|
+
message: stale.message,
|
|
2040
|
+
hqBinary: paths.hqBinary,
|
|
2041
|
+
nodeBinary: paths.nodeBinary,
|
|
2042
|
+
hqRoot: hqRoot.root,
|
|
2043
|
+
}, null, 2));
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
console.log(stale.message);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
1974
2049
|
const result = installDaemonService({
|
|
1975
2050
|
platform: detectPlatform(),
|
|
1976
|
-
paths
|
|
1977
|
-
systemUser: opts.systemUser,
|
|
1978
|
-
hqRoot: hqRoot.root,
|
|
1979
|
-
hqRootSource: hqRoot.source,
|
|
1980
|
-
}),
|
|
2051
|
+
paths,
|
|
1981
2052
|
systemUser: opts.systemUser,
|
|
1982
2053
|
});
|
|
1983
2054
|
if (opts.json) {
|
|
@@ -3,8 +3,9 @@
|
|
|
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 { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, 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";
|
|
7
|
-
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LaneRecord, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
6
|
+
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";
|
|
7
|
+
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
8
|
+
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
8
9
|
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";
|
|
9
10
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
10
11
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
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 { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, 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";
|
|
6
|
+
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";
|
|
7
|
+
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
7
8
|
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";
|
|
8
9
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
9
10
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
@@ -78,6 +78,14 @@ export interface CreateLaneMeshOptions {
|
|
|
78
78
|
interactive?: boolean;
|
|
79
79
|
/** Test seam. Defaults to the shared `requireToken`. */
|
|
80
80
|
token?: () => Promise<string>;
|
|
81
|
+
/** Repair a missing project view before retrying a story status patch. */
|
|
82
|
+
repairProjectView?: (input: {
|
|
83
|
+
token: string;
|
|
84
|
+
company: LaneCompanyRef;
|
|
85
|
+
projectId: string;
|
|
86
|
+
storyId: string;
|
|
87
|
+
status: string;
|
|
88
|
+
}) => Promise<void>;
|
|
81
89
|
}
|
|
82
90
|
export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
|
|
83
91
|
/** Resolve the explicit mesh attribution before falling back to the lane tenant. */
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* of opening a browser nobody is watching, and the watcher records
|
|
43
43
|
* `mesh_event_failed` and moves on. No credential file is read here.
|
|
44
44
|
*/
|
|
45
|
-
import { appendThreadEvent, clamp, ensureProjectThread, patchStoryStatus, requireToken, resolveMeshCompany, } from "../mesh/api.js";
|
|
45
|
+
import { appendThreadEvent, clamp, ensureProjectThread, getProjectView, patchStoryStatus, putProjectView, requireToken, registerMeshProject, resolveMeshCompany, } from "../mesh/api.js";
|
|
46
46
|
import { LanesError } from "./errors.js";
|
|
47
47
|
export const LANE_MESH_EVENTS = [
|
|
48
48
|
"lane_started",
|
|
@@ -163,9 +163,64 @@ export function toWireThreadEvent(event, payload) {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
function isProjectViewNotFound(error) {
|
|
167
|
+
if (!error || typeof error !== "object")
|
|
168
|
+
return false;
|
|
169
|
+
const candidate = error;
|
|
170
|
+
return (candidate.code === "PROJECT_VIEW_NOT_FOUND" ||
|
|
171
|
+
(typeof candidate.message === "string" && candidate.message.includes("PROJECT_VIEW_NOT_FOUND")));
|
|
172
|
+
}
|
|
173
|
+
function isAlreadyRegistered(error) {
|
|
174
|
+
if (!error || typeof error !== "object")
|
|
175
|
+
return false;
|
|
176
|
+
const candidate = error;
|
|
177
|
+
return (candidate.code === "PROJECT_ALREADY_REGISTERED" ||
|
|
178
|
+
(typeof candidate.message === "string" && candidate.message.includes("PROJECT_ALREADY_REGISTERED")));
|
|
179
|
+
}
|
|
180
|
+
async function repairProjectView(input) {
|
|
181
|
+
const company = { companyUid: input.company.uid, companySlug: input.company.slug };
|
|
182
|
+
await ensureProjectThread(input.token, company, input.projectId, {
|
|
183
|
+
summary: `HQ lane ${input.projectId}`,
|
|
184
|
+
});
|
|
185
|
+
let current;
|
|
186
|
+
try {
|
|
187
|
+
current = await getProjectView(input.token, input.company.uid, input.projectId);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (!isProjectViewNotFound(error))
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
const stories = [...(current?.stories ?? [])];
|
|
194
|
+
const index = stories.findIndex((story) => story.id === input.storyId);
|
|
195
|
+
const nextStory = {
|
|
196
|
+
...(index >= 0 ? stories[index] : {}),
|
|
197
|
+
id: input.storyId,
|
|
198
|
+
title: index >= 0 ? stories[index]?.title || input.storyId : input.storyId,
|
|
199
|
+
status: input.status,
|
|
200
|
+
};
|
|
201
|
+
if (index >= 0)
|
|
202
|
+
stories[index] = nextStory;
|
|
203
|
+
else
|
|
204
|
+
stories.push(nextStory);
|
|
205
|
+
await putProjectView(input.token, input.company.uid, input.projectId, {
|
|
206
|
+
name: current?.name || input.projectId,
|
|
207
|
+
description: current?.description || "HQ lane project",
|
|
208
|
+
stories,
|
|
209
|
+
repos: current?.repos ?? [],
|
|
210
|
+
...(current?.version !== undefined ? { expectedVersion: current.version } : {}),
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
await registerMeshProject(input.token, input.company.uid, input.projectId);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (!isAlreadyRegistered(error))
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
166
220
|
export function createLaneMesh(opts = {}) {
|
|
167
221
|
const interactive = opts.interactive ?? true;
|
|
168
222
|
const token = opts.token ?? (() => requireToken({ interactive }));
|
|
223
|
+
const repair = opts.repairProjectView ?? repairProjectView;
|
|
169
224
|
return {
|
|
170
225
|
async ensureProjectThread(company, projectId) {
|
|
171
226
|
const tok = await token();
|
|
@@ -182,7 +237,21 @@ export function createLaneMesh(opts = {}) {
|
|
|
182
237
|
},
|
|
183
238
|
async patchStoryStatus(company, projectId, storyId, status) {
|
|
184
239
|
const tok = await token();
|
|
185
|
-
|
|
240
|
+
try {
|
|
241
|
+
await patchStoryStatus(tok, company.uid, projectId, storyId, status);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (!isProjectViewNotFound(error))
|
|
245
|
+
throw error;
|
|
246
|
+
await repair({
|
|
247
|
+
token: tok,
|
|
248
|
+
company,
|
|
249
|
+
projectId,
|
|
250
|
+
storyId,
|
|
251
|
+
status,
|
|
252
|
+
});
|
|
253
|
+
await patchStoryStatus(tok, company.uid, projectId, storyId, status);
|
|
254
|
+
}
|
|
186
255
|
},
|
|
187
256
|
};
|
|
188
257
|
}
|
|
@@ -232,10 +301,18 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
232
301
|
return;
|
|
233
302
|
}
|
|
234
303
|
case "blocked":
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
304
|
+
{
|
|
305
|
+
const outcomes = await Promise.allSettled([
|
|
306
|
+
mesh.appendThreadEvent(company, threadId, "lane_blocked", {
|
|
307
|
+
...baseIdentity(lane),
|
|
308
|
+
blocked_reason: envelope.blocked_reason ?? "",
|
|
309
|
+
}),
|
|
310
|
+
mesh.patchStoryStatus(company, lane.project_id, lane.story_id, "in_progress"),
|
|
311
|
+
]);
|
|
312
|
+
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
313
|
+
if (failure?.status === "rejected")
|
|
314
|
+
throw failure.reason;
|
|
315
|
+
}
|
|
239
316
|
return;
|
|
240
317
|
case "ask":
|
|
241
318
|
// A mesh question event has one `questionId`; sending one event per id
|
|
@@ -26,6 +26,16 @@ export interface OutpostDispatchResult {
|
|
|
26
26
|
stdout: string;
|
|
27
27
|
stderr: string;
|
|
28
28
|
}
|
|
29
|
+
export interface OutpostLaneCommand {
|
|
30
|
+
action: "message" | "resume";
|
|
31
|
+
laneId: string;
|
|
32
|
+
text?: string;
|
|
33
|
+
project?: string;
|
|
34
|
+
story?: string;
|
|
35
|
+
account?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
compact?: boolean;
|
|
38
|
+
}
|
|
29
39
|
export interface OutpostExecPort {
|
|
30
40
|
stageExecInput: (outpostId?: string) => Promise<{
|
|
31
41
|
putUrl: string;
|
|
@@ -47,6 +57,8 @@ export interface OutpostExecPort {
|
|
|
47
57
|
export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPayload;
|
|
48
58
|
/** Same wrapping `hq outposts exec` applies: run from the box's HQ folder. */
|
|
49
59
|
export declare function buildOutpostPickupCommand(payloadUrl: string): string;
|
|
60
|
+
/** Build the same `hq outposts exec` remote command used by lane pickup. */
|
|
61
|
+
export declare function buildOutpostLaneCommand(input: OutpostLaneCommand): string;
|
|
50
62
|
export declare function stageOutpostPayload(port: Pick<OutpostExecPort, "stageExecInput">, outpostId: string, bytes: Uint8Array, put?: typeof fetch): Promise<{
|
|
51
63
|
getUrl: string;
|
|
52
64
|
}>;
|
|
@@ -42,6 +42,23 @@ export function parseOutpostPickupPayload(raw) {
|
|
|
42
42
|
export function buildOutpostPickupCommand(payloadUrl) {
|
|
43
43
|
return withRemoteHqDir(joinCommandParts(["hq", "lanes", "pickup", "--payload-url", payloadUrl]));
|
|
44
44
|
}
|
|
45
|
+
/** Build the same `hq outposts exec` remote command used by lane pickup. */
|
|
46
|
+
export function buildOutpostLaneCommand(input) {
|
|
47
|
+
const args = ["hq", "lanes", input.action, input.laneId];
|
|
48
|
+
for (const [flag, value] of [
|
|
49
|
+
["--text", input.text],
|
|
50
|
+
["--project", input.project],
|
|
51
|
+
["--story", input.story],
|
|
52
|
+
["--account", input.account],
|
|
53
|
+
["--model", input.model],
|
|
54
|
+
]) {
|
|
55
|
+
if (value !== undefined)
|
|
56
|
+
args.push(flag, value);
|
|
57
|
+
}
|
|
58
|
+
if (input.compact === false)
|
|
59
|
+
args.push("--no-compact");
|
|
60
|
+
return withRemoteHqDir(joinCommandParts(args));
|
|
61
|
+
}
|
|
45
62
|
export async function stageOutpostPayload(port, outpostId, bytes, put = fetch) {
|
|
46
63
|
const staged = await port.stageExecInput(outpostId);
|
|
47
64
|
const upload = await put(staged.putUrl, {
|
|
@@ -20,6 +20,7 @@ import { applyDefaultQuotaHold, readQuotaSignal, recordAccountQuota } from "./qu
|
|
|
20
20
|
import { answeredQuestionEntries, raiseEnvelopeQuestions } from "./questions.js";
|
|
21
21
|
import { SILENCE_MS, classifyLaneExit, isPidAlive, readLatestEnvelope, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
|
|
22
22
|
import { createLaneMesh, emitLaneDecisionEvents, laneMeshCompany } from "./mesh-events.js";
|
|
23
|
+
import { laneStories, storyStatusForEnvelope } from "./stories.js";
|
|
23
24
|
import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
|
|
24
25
|
import { providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
25
26
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
@@ -882,18 +883,44 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
882
883
|
(latest.envelope.questions?.length ?? 0) > 0) {
|
|
883
884
|
raiseEnvelopeQuestions(hqRoot, readLane(hqRoot, lane.lane_id), latest.envelope, latest.ref, nowIso);
|
|
884
885
|
}
|
|
885
|
-
const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running", (current) =>
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
886
|
+
const result = casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "running", (current) => {
|
|
887
|
+
const next = {
|
|
888
|
+
...current,
|
|
889
|
+
state: repairedClassification.state,
|
|
890
|
+
queued_reason: undefined,
|
|
891
|
+
terminal_reason: repairedClassification.reason ?? "host_lost",
|
|
892
|
+
...(providerRejection
|
|
893
|
+
? { terminal_details: providerRejectionDetails(providerRejection, current) }
|
|
894
|
+
: {}),
|
|
895
|
+
...(quota?.quota_reset_at ? { quota_reset_at: quota.quota_reset_at } : {}),
|
|
896
|
+
...(quota?.marker ? { model_quota_marker: quota.marker } : {}),
|
|
897
|
+
last_host_report_at: nowIso,
|
|
898
|
+
};
|
|
899
|
+
if (latest?.envelope) {
|
|
900
|
+
const envelope = latest.envelope;
|
|
901
|
+
next.story_status = envelope.story_status;
|
|
902
|
+
if (envelope.story_complete !== undefined) {
|
|
903
|
+
next.story_complete = envelope.story_complete;
|
|
904
|
+
next.story_complete_reason = envelope.story_complete_reason;
|
|
905
|
+
}
|
|
906
|
+
const storyStatus = envelope.decision === "blocked"
|
|
907
|
+
? "blocked"
|
|
908
|
+
: envelope.decision === "done"
|
|
909
|
+
? "review"
|
|
910
|
+
: storyStatusForEnvelope(envelope.story_status);
|
|
911
|
+
next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
|
|
912
|
+
? {
|
|
913
|
+
...story,
|
|
914
|
+
status: storyStatus,
|
|
915
|
+
updated_at: nowIso,
|
|
916
|
+
...(envelope.decision === "blocked"
|
|
917
|
+
? { blocked_reason: envelope.blocked_reason ?? "" }
|
|
918
|
+
: { blocked_reason: undefined }),
|
|
919
|
+
}
|
|
920
|
+
: story);
|
|
921
|
+
}
|
|
922
|
+
return next;
|
|
923
|
+
}, deps.dryRun);
|
|
897
924
|
if (result.changed) {
|
|
898
925
|
summary.dead_host.push({
|
|
899
926
|
id: lane.lane_id,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type SessionCopyResult } from "./move-account.js";
|
|
2
|
+
import type { LaneProvider } from "./types.js";
|
|
3
|
+
export interface EnsureSessionHomeInput {
|
|
4
|
+
provider: Exclude<LaneProvider, "grok">;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
targetHome: string;
|
|
8
|
+
registeredHomes: string[];
|
|
9
|
+
providerHomeAtSpawn?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface EnsureSessionHomeResult extends SessionCopyResult {
|
|
12
|
+
source_home: string;
|
|
13
|
+
transcript_path: string;
|
|
14
|
+
homes_searched: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Older Claude account homes can share a registered `account-N/.claude`
|
|
18
|
+
* parent even after one account is re-registered elsewhere. Discover only
|
|
19
|
+
* those sibling homes, then inspect the exact session path; credential files
|
|
20
|
+
* are never opened.
|
|
21
|
+
*/
|
|
22
|
+
export declare function discoverSiblingAccountHomes(provider: EnsureSessionHomeInput["provider"], registeredHomes: string[]): string[];
|
|
23
|
+
/** Find, copy, and verify a provider transcript in the home the next run uses. */
|
|
24
|
+
export declare function ensureSessionHome(input: EnsureSessionHomeInput): EnsureSessionHomeResult;
|
|
25
|
+
//# sourceMappingURL=session-relink.d.ts.map
|