@indigoai-us/hq-cli 5.345.160 → 5.345.162
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 +36 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +262 -5
- package/dist/command-catalog.generated.js +322 -5
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-inbox.js +6 -2
- package/dist/commands/auth.js +6 -5
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-loop.js +7 -7
- package/dist/commands/lanes-questions.js +54 -4
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/lanes.js +2 -1
- package/dist/commands/meetings.d.ts +11 -0
- package/dist/commands/meetings.js +41 -11
- package/dist/commands/sync-held.d.ts +87 -0
- package/dist/commands/sync-held.js +191 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3830 -2812
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- package/dist/lib/agent-kit/inbox-state.d.ts +5 -4
- package/dist/lib/agent-kit/inbox-state.js +24 -3
- package/dist/lib/bot/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/loop.d.ts +17 -0
- package/dist/lib/lanes/loop.js +235 -5
- package/dist/lib/lanes/questions.d.ts +1 -0
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
|
@@ -7,7 +7,7 @@ import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
|
|
|
7
7
|
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
8
8
|
import { raiseLaneQuestion } from "../lib/lanes/questions.js";
|
|
9
9
|
import { isTerminalLaneState } from "../lib/lanes/types.js";
|
|
10
|
-
import { clearLoopControllerPid, loopAdmissionPath, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
|
|
10
|
+
import { HANDOFF_HOLD_CHOICES, clearLoopControllerPid, loopAdmissionPath, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
|
|
11
11
|
import { laneStories } from "../lib/lanes/stories.js";
|
|
12
12
|
import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
13
13
|
import { LANE_LOOP_HANDOFF_REPAIR_FLAG } from "../lib/lanes/flag-keys.js";
|
|
@@ -255,7 +255,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
255
255
|
signal.removeEventListener("abort", abortPhase);
|
|
256
256
|
}
|
|
257
257
|
},
|
|
258
|
-
raiseQuestion: ({ envelope, envelopeId, attempt, error, questionId }) => {
|
|
258
|
+
raiseQuestion: ({ envelope, envelopeId, attempt, error, questionId, reason }) => {
|
|
259
259
|
const current = readLane(hqRoot, laneId);
|
|
260
260
|
const data = envelope;
|
|
261
261
|
raiseLaneQuestion({
|
|
@@ -265,11 +265,11 @@ export async function runLaneLoopController(ctx, laneId, deps) {
|
|
|
265
265
|
raisedVia: "cli",
|
|
266
266
|
at: ctx.now().toISOString(),
|
|
267
267
|
body: {
|
|
268
|
-
question: `Loop phase ${String(data.phase ?? envelopeId)} stalled twice. How should it proceed?`,
|
|
269
|
-
context: `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
|
|
270
|
-
options: ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
|
|
271
|
-
recommended: "Retry with a longer deadline",
|
|
272
|
-
loop_stall: { lane_id: laneId, envelope_id: envelopeId, attempt },
|
|
268
|
+
question: reason === "handoff_needs_repair" ? `Loop phase ${String(data.phase ?? envelopeId)} has an invalid handoff. How should it proceed?` : `Loop phase ${String(data.phase ?? envelopeId)} stalled twice. How should it proceed?`,
|
|
269
|
+
context: reason === "handoff_needs_repair" ? `${error}\nThe worker file is preserved and the phase is parked; later envelopes remain queued. Repair the handoff file before accepting it. Retrying can repeat external effects.` : `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
|
|
270
|
+
options: reason === "handoff_needs_repair" ? [...HANDOFF_HOLD_CHOICES] : ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
|
|
271
|
+
recommended: reason === "handoff_needs_repair" ? HANDOFF_HOLD_CHOICES[0] : "Retry with a longer deadline",
|
|
272
|
+
loop_stall: { lane_id: laneId, envelope_id: envelopeId, attempt, ...(reason ? { reason } : {}) },
|
|
273
273
|
},
|
|
274
274
|
});
|
|
275
275
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { readSessionCompanyLock } from "../lib/session-company-lock.js";
|
|
2
3
|
import { isQuestionImported } from "../lib/lanes/imported-question.js";
|
|
3
4
|
import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
|
|
4
5
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
@@ -6,7 +7,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
|
6
7
|
import { appendCappedHistory, createQuestion, expandLocalLaneIdPrefix, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
|
|
7
8
|
import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
|
|
8
9
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
9
|
-
import { applyLoopStallAnswer, findLoopStallResult } from "../lib/lanes/loop.js";
|
|
10
|
+
import { HANDOFF_HOLD_CHOICES, applyLoopStallAnswer, findLoopStallResult, loopHandoffHoldIdentity } from "../lib/lanes/loop.js";
|
|
10
11
|
export function resolveAnswerChoice(choice, options, freeform) {
|
|
11
12
|
const trimmed = choice.trim();
|
|
12
13
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
@@ -847,11 +848,48 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
847
848
|
const retryingSameLoopAnswer = isLoopStallQuestion && question.status === "answered" && question.answer?.choice === choice &&
|
|
848
849
|
question.answer.answer_kind === incomingAnswerKind && incomingNotesMatch &&
|
|
849
850
|
deps.sameQuestionAnswer(question.answer, { ...question.answer });
|
|
850
|
-
if (question.status !== "pending" && !retryingSameLoopAnswer)
|
|
851
|
-
throwIfQuestionNotPending(question);
|
|
852
851
|
const loopStallResult = isLoopStallQuestion
|
|
853
852
|
? findLoopStallResult(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id), questionId)
|
|
854
853
|
: undefined;
|
|
854
|
+
if (loopStallResult?.hold_reason === "handoff_needs_repair" && !HANDOFF_HOLD_CHOICES.includes(choice)) {
|
|
855
|
+
throw new LanesError("invalid_handoff_disposition", "Choose an explicit handoff repair disposition", { question_id: questionId });
|
|
856
|
+
}
|
|
857
|
+
const handoffIdentity = askingLane.loop_mode ? loopHandoffHoldIdentity(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id)) : undefined;
|
|
858
|
+
const changingHeldAnswer = body.loop_stall?.reason === "handoff_needs_repair" && isLoopStallQuestion &&
|
|
859
|
+
question.status === "answered" && question.answer?.choice === HANDOFF_HOLD_CHOICES[2] &&
|
|
860
|
+
choice !== HANDOFF_HOLD_CHOICES[2] && handoffIdentity?.unresolved === true &&
|
|
861
|
+
handoffIdentity.question_id === questionId && handoffIdentity.envelope_id === body.loop_stall.envelope_id &&
|
|
862
|
+
loopStallResult?.hold_reason === "handoff_needs_repair";
|
|
863
|
+
if (question.status !== "pending" && !retryingSameLoopAnswer && !changingHeldAnswer)
|
|
864
|
+
throwIfQuestionNotPending(question);
|
|
865
|
+
const isHandoffQuestion = body.loop_stall?.reason === "handoff_needs_repair" || loopStallResult?.hold_reason === "handoff_needs_repair" || handoffIdentity !== undefined;
|
|
866
|
+
if (isHandoffQuestion) {
|
|
867
|
+
assertQuestionAnswerAuthorized(ctx, askingLane);
|
|
868
|
+
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "resolve a handoff repair hold on");
|
|
869
|
+
const targetCompany = questionCompanyForLane(question, askingLane, hqRoot);
|
|
870
|
+
const lock = readSessionCompanyLock({ ...ctx.env, HQ_ROOT: hqRoot });
|
|
871
|
+
const actorLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
872
|
+
if (actorLaneId) {
|
|
873
|
+
const actorLane = readLane(hqRoot, actorLaneId);
|
|
874
|
+
const actorCompany = actorLane.company ?? actorLane.mesh_company;
|
|
875
|
+
if (!targetCompany || !actorCompany || !sameQuestionCompany(actorCompany, targetCompany))
|
|
876
|
+
throw new LanesError("handoff_answer_company_mismatch", "Caller lane does not own this handoff company", { question_id: questionId });
|
|
877
|
+
}
|
|
878
|
+
// Lane-only callers may use their owned lane identity; a present session lock
|
|
879
|
+
// always constrains that identity and may never be widened by HQ_LANE_ID.
|
|
880
|
+
if ((lock || !actorLaneId) && (!lock || !targetCompany || !lock.companies.includes(targetCompany.slug))) {
|
|
881
|
+
throw new LanesError("handoff_answer_company_mismatch", "The current session is not bound to this handoff company", { question_id: questionId });
|
|
882
|
+
}
|
|
883
|
+
if (!isLoopStallQuestion || body.loop_stall?.reason !== "handoff_needs_repair" ||
|
|
884
|
+
(handoffIdentity && (handoffIdentity.question_id !== questionId || handoffIdentity.envelope_id !== body.loop_stall.envelope_id)) ||
|
|
885
|
+
(loopStallResult && loopStallResult.envelope_id !== body.loop_stall.envelope_id)) {
|
|
886
|
+
throw new LanesError("handoff_question_identity_mismatch", "The persisted handoff question and hold identities do not match", { question_id: questionId });
|
|
887
|
+
}
|
|
888
|
+
if (!HANDOFF_HOLD_CHOICES.includes(choice))
|
|
889
|
+
throw new LanesError("invalid_handoff_disposition", "Choose an explicit handoff repair disposition", { question_id: questionId });
|
|
890
|
+
if (!loopStallResult && question.status === "pending")
|
|
891
|
+
throw new LanesError("handoff_result_missing", "The handoff hold result is missing; no disposition was applied", { question_id: questionId });
|
|
892
|
+
}
|
|
855
893
|
const at = ctx.now().toISOString();
|
|
856
894
|
const answeredBy = sessionIdFromEnv(ctx.env)
|
|
857
895
|
? `session:${sessionIdFromEnv(ctx.env)}`
|
|
@@ -860,7 +898,14 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
860
898
|
if (retryingSameLoopAnswer && current.status === "answered" && current.answer?.choice === choice &&
|
|
861
899
|
deps.sameQuestionAnswer(current.answer, question.answer))
|
|
862
900
|
return current;
|
|
863
|
-
|
|
901
|
+
const currentHold = changingHeldAnswer ? loopHandoffHoldIdentity(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id)) : undefined;
|
|
902
|
+
const stillHeld = changingHeldAnswer && current.status === "answered" &&
|
|
903
|
+
current.answer?.choice === HANDOFF_HOLD_CHOICES[2] &&
|
|
904
|
+
deps.sameQuestionAnswer(current.answer, question.answer) &&
|
|
905
|
+
currentHold?.unresolved === true && currentHold.question_id === questionId &&
|
|
906
|
+
currentHold.envelope_id === body.loop_stall?.envelope_id;
|
|
907
|
+
if (!stillHeld)
|
|
908
|
+
throwIfQuestionNotPending(current);
|
|
864
909
|
assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
|
|
865
910
|
const recordedCompany = current.company ?? questionCompany;
|
|
866
911
|
const currentForBody = current.company === null
|
|
@@ -882,6 +927,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
882
927
|
body: nextBody,
|
|
883
928
|
status: "answered",
|
|
884
929
|
delivered_at: null,
|
|
930
|
+
...(stillHeld && current.answer ? { answer_history: [...(current.answer_history ?? []), current.answer] } : {}),
|
|
885
931
|
answer: {
|
|
886
932
|
choice,
|
|
887
933
|
answer_kind: freeform ? "freeform" : "option",
|
|
@@ -911,6 +957,10 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
911
957
|
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action });
|
|
912
958
|
return;
|
|
913
959
|
}
|
|
960
|
+
if (isHandoffQuestion && resolved.action === "parked") {
|
|
961
|
+
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: "parked" });
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
914
964
|
const controllerPid = await deps.restartLoopController(ctx, askingLane.lane_id);
|
|
915
965
|
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action, controller_pid: controllerPid });
|
|
916
966
|
return;
|
|
@@ -8,9 +8,21 @@ export interface SessionStartOptions {
|
|
|
8
8
|
stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
9
9
|
resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
|
|
10
10
|
startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
|
|
11
|
+
/** Starts the off-hot-path registry read when an offline flag value is missing or old. */
|
|
12
|
+
refreshFlags?: (env: NodeJS.ProcessEnv) => void;
|
|
11
13
|
}
|
|
14
|
+
/** Offline flag values older than this trigger one background registry read. */
|
|
15
|
+
export declare const SESSION_START_FLAG_REFRESH_AFTER_MS: number;
|
|
12
16
|
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
13
17
|
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, rootScoped?: boolean): void;
|
|
18
|
+
/**
|
|
19
|
+
* Start a detached `hq lanes session-start --refresh-flags` so the registry
|
|
20
|
+
* read never runs inside the SessionStart hook. A short lease keeps a burst of
|
|
21
|
+
* session starts to one refresh. An env without a state dir never spawns.
|
|
22
|
+
*/
|
|
23
|
+
export declare function startSessionStartFlagRefresh(env: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, now?: number): boolean;
|
|
24
|
+
/** Read the session-start flags from the registry, refreshing the offline cache. */
|
|
25
|
+
export declare function refreshSessionStartFlags(env?: NodeJS.ProcessEnv): Promise<void>;
|
|
14
26
|
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
15
27
|
export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
|
|
16
28
|
export declare function registerLaneSessionStartCommand(lanes: Command): void;
|
|
@@ -4,8 +4,17 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
7
|
-
import {
|
|
7
|
+
import { readLanesRegistryFlagOffline, readLanesRegistryFlagResult } from "../lib/lanes/registry-flag.js";
|
|
8
8
|
import { LANE_CONDUCT_SESSION_START_FLAG, LANE_ORPHAN_SWEEP_CONTEXT_FLAG, LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG } from "../lib/lanes/flag-keys.js";
|
|
9
|
+
const SESSION_START_FLAGS = [
|
|
10
|
+
LANE_CONDUCT_SESSION_START_FLAG,
|
|
11
|
+
LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG,
|
|
12
|
+
LANE_ORPHAN_SWEEP_CONTEXT_FLAG,
|
|
13
|
+
];
|
|
14
|
+
/** Offline flag values older than this trigger one background registry read. */
|
|
15
|
+
export const SESSION_START_FLAG_REFRESH_AFTER_MS = 10 * 60 * 1000;
|
|
16
|
+
const SESSION_START_FLAG_REFRESH_LEASE_MS = 60 * 1000;
|
|
17
|
+
const SESSION_START_FLAG_REFRESH_LEASE = "lanes-session-start-flag-refresh.lock";
|
|
9
18
|
const CODEX_DEFAULT_KEYS = ["conduct_engine", "conduct_child_model", "conduct_child_effort", "conduct_default_source"];
|
|
10
19
|
function readSessionMetaValue(text, key) {
|
|
11
20
|
const line = text.split(/\r?\n/).find((entry) => new RegExp(`^\\s*${key}:`).test(entry));
|
|
@@ -247,6 +256,58 @@ export function startSessionStartOrphanSweep(root, env = process.env, spawnDetac
|
|
|
247
256
|
// SessionStart's orphan report is advisory and must never block startup.
|
|
248
257
|
}
|
|
249
258
|
}
|
|
259
|
+
function flagRefreshLeaseDir(env) {
|
|
260
|
+
const stateDir = env.HQ_STATE_DIR?.trim();
|
|
261
|
+
if (stateDir)
|
|
262
|
+
return stateDir;
|
|
263
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
|
264
|
+
return home ? path.join(home, ".hq") : null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Start a detached `hq lanes session-start --refresh-flags` so the registry
|
|
268
|
+
* read never runs inside the SessionStart hook. A short lease keeps a burst of
|
|
269
|
+
* session starts to one refresh. An env without a state dir never spawns.
|
|
270
|
+
*/
|
|
271
|
+
export function startSessionStartFlagRefresh(env, spawnDetached = spawn, now = Date.now()) {
|
|
272
|
+
const dir = flagRefreshLeaseDir(env);
|
|
273
|
+
const cli = process.argv[1];
|
|
274
|
+
if (!dir || !cli)
|
|
275
|
+
return false;
|
|
276
|
+
try {
|
|
277
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
278
|
+
const lease = path.join(dir, SESSION_START_FLAG_REFRESH_LEASE);
|
|
279
|
+
try {
|
|
280
|
+
if (now - fs.statSync(lease).mtimeMs < SESSION_START_FLAG_REFRESH_LEASE_MS)
|
|
281
|
+
return false;
|
|
282
|
+
fs.rmSync(lease, { force: true });
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error.code !== "ENOENT")
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
fs.closeSync(fs.openSync(lease, "wx", 0o600));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const child = spawnDetached(process.execPath, [cli, "lanes", "session-start", "--refresh-flags"], {
|
|
295
|
+
detached: true,
|
|
296
|
+
stdio: "ignore",
|
|
297
|
+
env: { ...env, HQ_NO_UPDATE_CHECK: "1" },
|
|
298
|
+
});
|
|
299
|
+
child.on("error", () => { });
|
|
300
|
+
child.unref();
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Read the session-start flags from the registry, refreshing the offline cache. */
|
|
308
|
+
export async function refreshSessionStartFlags(env = process.env) {
|
|
309
|
+
await Promise.all(SESSION_START_FLAGS.map((flag) => readLanesRegistryFlagResult(flag, { env, defaultValue: true, reportFailure: () => undefined }).catch(() => undefined)));
|
|
310
|
+
}
|
|
250
311
|
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
251
312
|
export async function runLaneSessionStart(options = {}) {
|
|
252
313
|
const env = options.env ?? process.env;
|
|
@@ -278,19 +339,33 @@ export async function runLaneSessionStart(options = {}) {
|
|
|
278
339
|
if (/(^|,)auto-conduct(,|$)/.test(disabledHooks))
|
|
279
340
|
return;
|
|
280
341
|
const root = (options.resolveRoot ?? ((e, cwd) => resolveLanesHqRoot({ env: e, cwd })))(env, options.cwd);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
342
|
+
// Flags come from offline state only. A missing or old value fails open and
|
|
343
|
+
// schedules one background registry read for the next session start.
|
|
344
|
+
let refreshNeeded = false;
|
|
345
|
+
const readFlag = (flag) => {
|
|
346
|
+
let read = null;
|
|
347
|
+
try {
|
|
348
|
+
read = readLanesRegistryFlagOffline(flag, { env, defaultValue: true });
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
read = null;
|
|
352
|
+
}
|
|
353
|
+
if (!read || (read.source !== "local-override" && read.ageMs > SESSION_START_FLAG_REFRESH_AFTER_MS))
|
|
354
|
+
refreshNeeded = true;
|
|
355
|
+
return read?.value ?? true;
|
|
356
|
+
};
|
|
357
|
+
const flagEnabled = readFlag(LANE_CONDUCT_SESSION_START_FLAG);
|
|
358
|
+
const rootScoped = readFlag(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG);
|
|
359
|
+
const contextEnabled = readFlag(LANE_ORPHAN_SWEEP_CONTEXT_FLAG);
|
|
360
|
+
if (refreshNeeded) {
|
|
361
|
+
try {
|
|
362
|
+
(options.refreshFlags ?? ((e) => { startSessionStartFlagRefresh(e); }))(env);
|
|
363
|
+
}
|
|
364
|
+
catch { /* advisory */ }
|
|
365
|
+
}
|
|
289
366
|
if (!flagEnabled)
|
|
290
367
|
return;
|
|
291
|
-
const rootScoped = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG, { env, defaultValue: true });
|
|
292
368
|
(options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn, rootScoped)))(root, env);
|
|
293
|
-
const contextEnabled = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_CONTEXT_FLAG, { env, defaultValue: true });
|
|
294
369
|
if (contextEnabled) {
|
|
295
370
|
const count = readOrphanCount(root, env, rootScoped, (error) => debug(`hq lanes session-start: orphan report read failed (${error instanceof Error ? error.name : "Error"})`));
|
|
296
371
|
if (count > 0)
|
|
@@ -324,6 +399,12 @@ export async function runLaneSessionStart(options = {}) {
|
|
|
324
399
|
export function registerLaneSessionStartCommand(lanes) {
|
|
325
400
|
lanes.command("session-start", { hidden: true })
|
|
326
401
|
.description("Internal SessionStart bootstrap")
|
|
327
|
-
.
|
|
402
|
+
.option("--refresh-flags", "Refresh the offline session-start flag values from the registry")
|
|
403
|
+
.action(async (opts) => {
|
|
404
|
+
if (opts.refreshFlags)
|
|
405
|
+
await refreshSessionStartFlags();
|
|
406
|
+
else
|
|
407
|
+
await runLaneSessionStart();
|
|
408
|
+
});
|
|
328
409
|
}
|
|
329
410
|
//# sourceMappingURL=lanes-session-start.js.map
|
package/dist/commands/lanes.js
CHANGED
|
@@ -717,7 +717,8 @@ export function parseQuestionBody(raw) {
|
|
|
717
717
|
typeof obj.loop_stall.envelope_id !== "string" ||
|
|
718
718
|
typeof obj.loop_stall.attempt !== "number" ||
|
|
719
719
|
!Number.isSafeInteger(obj.loop_stall.attempt) ||
|
|
720
|
-
obj.loop_stall.attempt < 1
|
|
720
|
+
obj.loop_stall.attempt < 1 ||
|
|
721
|
+
(obj.loop_stall.reason !== undefined && obj.loop_stall.reason !== "handoff_needs_repair")))
|
|
721
722
|
return null;
|
|
722
723
|
const options = obj.options.filter((item) => typeof item === "string");
|
|
723
724
|
const body = {
|
|
@@ -9,5 +9,16 @@ export declare const SHORT_ID_MAX_PAGES = 50;
|
|
|
9
9
|
* names no plan: the flat plans are closed.
|
|
10
10
|
*/
|
|
11
11
|
export declare function meetingPaymentRequiredLines(body: Record<string, unknown>, resolvePlan?: () => Promise<PricingPlan | null>): Promise<string[]>;
|
|
12
|
+
/**
|
|
13
|
+
* The company a `hq meetings` call is about: the explicit `--company` value,
|
|
14
|
+
* otherwise the company an explicitly named HQ session is bound to (the session's company
|
|
15
|
+
* lock, from `scope-capability.json` or `meta.yaml`). A session bound to
|
|
16
|
+
* Personal, an unbound session, or an unreadable lock gives undefined so the
|
|
17
|
+
* server's own default applies. Before this, every subcommand sent no
|
|
18
|
+
* company at all without `--company`, so the list, get, transcript and
|
|
19
|
+
* search views silently showed only the caller's personal recordings.
|
|
20
|
+
* Feedback 04771691-fcbc-4847-899a-6c966c561b52.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sessionCompanySlug(env?: NodeJS.ProcessEnv): string | undefined;
|
|
12
23
|
export declare function registerMeetingsCommand(program: Command): void;
|
|
13
24
|
//# sourceMappingURL=meetings.d.ts.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
+
import { readSessionCompanyLock } from "../lib/session-company-lock.js";
|
|
5
|
+
import { sessionIdFromEnv } from "../lib/core-utils/session-id.js";
|
|
4
6
|
import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
|
|
5
7
|
import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
|
|
6
8
|
import { mapWithConcurrency } from "./sync-manifest.js";
|
|
@@ -434,11 +436,39 @@ function printMeetingTable(meetings) {
|
|
|
434
436
|
].join(" "));
|
|
435
437
|
}
|
|
436
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* The company a `hq meetings` call is about: the explicit `--company` value,
|
|
441
|
+
* otherwise the company an explicitly named HQ session is bound to (the session's company
|
|
442
|
+
* lock, from `scope-capability.json` or `meta.yaml`). A session bound to
|
|
443
|
+
* Personal, an unbound session, or an unreadable lock gives undefined so the
|
|
444
|
+
* server's own default applies. Before this, every subcommand sent no
|
|
445
|
+
* company at all without `--company`, so the list, get, transcript and
|
|
446
|
+
* search views silently showed only the caller's personal recordings.
|
|
447
|
+
* Feedback 04771691-fcbc-4847-899a-6c966c561b52.
|
|
448
|
+
*/
|
|
449
|
+
export function sessionCompanySlug(env = process.env) {
|
|
450
|
+
// The shared reader also supports the last-writer-wins .current pointer.
|
|
451
|
+
// It cannot identify a plain terminal or cron caller's session.
|
|
452
|
+
if (!sessionIdFromEnv(env))
|
|
453
|
+
return undefined;
|
|
454
|
+
let lock;
|
|
455
|
+
try {
|
|
456
|
+
lock = readSessionCompanyLock(env);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
// Fall through to the server default, but say why so an unbound call is
|
|
460
|
+
// never mistaken for a working company scope.
|
|
461
|
+
console.error(chalk.dim(` (session company not applied: ${err instanceof Error ? err.message : String(err)})`));
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
const primary = lock?.companies[0];
|
|
465
|
+
return primary && primary !== "personal" ? primary : undefined;
|
|
466
|
+
}
|
|
437
467
|
export function registerMeetingsCommand(program) {
|
|
438
468
|
const meetings = program
|
|
439
469
|
.command("meetings")
|
|
440
470
|
.description("View and search meeting recordings, transcripts, and notes")
|
|
441
|
-
.option("--company <slug>", "Company slug (
|
|
471
|
+
.option("--company <slug>", "Company slug (defaults to the company this HQ session is bound to)")
|
|
442
472
|
.option("--json", "Output raw JSON instead of formatted text");
|
|
443
473
|
// ── hq meetings list ──────────────────────────────────────────────
|
|
444
474
|
meetings
|
|
@@ -450,7 +480,7 @@ export function registerMeetingsCommand(program) {
|
|
|
450
480
|
try {
|
|
451
481
|
const token = await ensureCognitoToken();
|
|
452
482
|
const query = {};
|
|
453
|
-
const companySlug = meetings.opts().company;
|
|
483
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
454
484
|
if (opts.limit)
|
|
455
485
|
query.limit = opts.limit;
|
|
456
486
|
if (opts.next)
|
|
@@ -488,9 +518,9 @@ export function registerMeetingsCommand(program) {
|
|
|
488
518
|
.action(async (recallBotId, opts) => {
|
|
489
519
|
try {
|
|
490
520
|
const token = await ensureCognitoToken();
|
|
491
|
-
const companySlug = meetings.opts().company;
|
|
521
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
492
522
|
if (!companySlug)
|
|
493
|
-
throw new Error("--company <slug|uid> is required for live transcripts");
|
|
523
|
+
throw new Error("--company <slug|uid> is required for live transcripts (this session is not bound to a company)");
|
|
494
524
|
const companyId = companySlug.startsWith("cmp_") ? companySlug : await getCompanyUid(token, companySlug);
|
|
495
525
|
if (!recallBotId) {
|
|
496
526
|
if (opts.follow || opts.since)
|
|
@@ -610,9 +640,9 @@ export function registerMeetingsCommand(program) {
|
|
|
610
640
|
.description("Import a normalized historical transcript JSON file")
|
|
611
641
|
.action(async (file) => {
|
|
612
642
|
try {
|
|
613
|
-
const companySlug = meetings.opts().company;
|
|
643
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
614
644
|
if (!companySlug) {
|
|
615
|
-
throw new Error("--company <slug> is required for historical imports");
|
|
645
|
+
throw new Error("--company <slug> is required for historical imports (this session is not bound to a company)");
|
|
616
646
|
}
|
|
617
647
|
const bytes = await readFile(file);
|
|
618
648
|
if (bytes.byteLength > 5 * 1024 * 1024) {
|
|
@@ -666,7 +696,7 @@ export function registerMeetingsCommand(program) {
|
|
|
666
696
|
.action(async (meetingUrl) => {
|
|
667
697
|
try {
|
|
668
698
|
const token = await ensureCognitoToken();
|
|
669
|
-
const companySlug = meetings.opts().company;
|
|
699
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
670
700
|
// Always name a company. With no companyId the backend files the bot
|
|
671
701
|
// under Personal, which can never record, so an entitled user would
|
|
672
702
|
// get the payment-required refusal. Without --company this uses the
|
|
@@ -707,7 +737,7 @@ export function registerMeetingsCommand(program) {
|
|
|
707
737
|
try {
|
|
708
738
|
const token = await ensureCognitoToken();
|
|
709
739
|
const query = {};
|
|
710
|
-
const companySlug = meetings.opts().company;
|
|
740
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
711
741
|
if (companySlug)
|
|
712
742
|
query.companyId = await getCompanyUid(token, companySlug);
|
|
713
743
|
const meetingId = await resolveShortId(token, rawId, query);
|
|
@@ -832,7 +862,7 @@ export function registerMeetingsCommand(program) {
|
|
|
832
862
|
try {
|
|
833
863
|
const token = await ensureCognitoToken();
|
|
834
864
|
const params = { q: query };
|
|
835
|
-
const companySlug = meetings.opts().company;
|
|
865
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
836
866
|
if (companySlug)
|
|
837
867
|
params.companyId = await getCompanyUid(token, companySlug);
|
|
838
868
|
const res = await vaultApiFetch({
|
|
@@ -863,7 +893,7 @@ export function registerMeetingsCommand(program) {
|
|
|
863
893
|
try {
|
|
864
894
|
const token = await ensureCognitoToken();
|
|
865
895
|
const query = {};
|
|
866
|
-
const companySlug = meetings.opts().company;
|
|
896
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
867
897
|
if (companySlug)
|
|
868
898
|
query.companyId = await getCompanyUid(token, companySlug);
|
|
869
899
|
const meetingId = await resolveShortId(token, rawId, query);
|
|
@@ -932,7 +962,7 @@ export function registerMeetingsCommand(program) {
|
|
|
932
962
|
try {
|
|
933
963
|
const token = await ensureCognitoToken();
|
|
934
964
|
const query = {};
|
|
935
|
-
const companySlug = meetings.opts().company;
|
|
965
|
+
const companySlug = meetings.opts().company ?? sessionCompanySlug();
|
|
936
966
|
if (companySlug)
|
|
937
967
|
query.companyId = await getCompanyUid(token, companySlug);
|
|
938
968
|
const meetingId = await resolveShortId(token, rawId, query);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq sync held` and `hq sync release-held` (feedback_3bf773c5).
|
|
3
|
+
*
|
|
4
|
+
* After a different person signs in on the same computer, hq-cloud holds every
|
|
5
|
+
* upload and cloud delete for each sync scope (the personal vault and each
|
|
6
|
+
* company) until the signed-in person reviews what is held and releases it.
|
|
7
|
+
* These commands are the person's way to see the hold and make that decision.
|
|
8
|
+
*
|
|
9
|
+
* The hq-cloud exports (`listPushIdentityFences`, `releasePushIdentityFences`)
|
|
10
|
+
* ship after the hq-cloud version this CLI pins, so they are feature-detected
|
|
11
|
+
* on the module namespace, the same way `lib/hq-cloud-manifest.ts` does it. A
|
|
12
|
+
* static named import of a missing ESM export would stop every command that
|
|
13
|
+
* loads this file.
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
export interface HeldScope {
|
|
17
|
+
slug: string;
|
|
18
|
+
uid: string;
|
|
19
|
+
sinceMs: number;
|
|
20
|
+
held?: {
|
|
21
|
+
count: number;
|
|
22
|
+
samplePaths: string[];
|
|
23
|
+
at: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface FenceExports {
|
|
27
|
+
listPushIdentityFences: (stateDir: string) => HeldScope[];
|
|
28
|
+
releasePushIdentityFences: (stateDir: string, identities: readonly string[], slugs?: readonly string[]) => string[];
|
|
29
|
+
}
|
|
30
|
+
export declare const HELD_UNAVAILABLE_MESSAGE = "this HQ build cannot list or release held uploads yet; update HQ and try again";
|
|
31
|
+
export declare function fenceExports(mod?: Record<string, unknown>): FenceExports | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* One authenticated snapshot of who is signed in: the Cognito sub of the
|
|
34
|
+
* session token and, when asked for, the personal vault uid resolved with that
|
|
35
|
+
* same token. The sub never comes from the upload-author label, which needs an
|
|
36
|
+
* email; hq-cloud#1040 identifies email-less sessions by sub alone.
|
|
37
|
+
*/
|
|
38
|
+
export interface HeldIdentity {
|
|
39
|
+
sub: string;
|
|
40
|
+
personalUid?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SyncHeldDeps {
|
|
43
|
+
exports: FenceExports | undefined;
|
|
44
|
+
stateDir: string;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the signed-in identity from one token. `interactive` allows a
|
|
47
|
+
* sign-in or refresh; the result is taken after it, never mixed with an
|
|
48
|
+
* identity read before it.
|
|
49
|
+
*/
|
|
50
|
+
identity: (options: {
|
|
51
|
+
needPersonal: boolean;
|
|
52
|
+
interactive: boolean;
|
|
53
|
+
}) => Promise<HeldIdentity>;
|
|
54
|
+
confirm: (question: string) => Promise<boolean>;
|
|
55
|
+
log: (line: string) => void;
|
|
56
|
+
}
|
|
57
|
+
export declare function scopeLabel(slug: string): string;
|
|
58
|
+
export declare function runSyncHeld(deps: SyncHeldDeps, json?: boolean): Promise<number>;
|
|
59
|
+
export interface ReleaseHeldOptions {
|
|
60
|
+
company?: string;
|
|
61
|
+
personal?: boolean;
|
|
62
|
+
yes?: boolean;
|
|
63
|
+
json?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function runReleaseHeld(deps: SyncHeldDeps, options: ReleaseHeldOptions): Promise<number>;
|
|
66
|
+
/**
|
|
67
|
+
* The signed-in identity from the session token `ensureCognitoToken` selects
|
|
68
|
+
* (person or machine, the same choice every vault call makes). The personal
|
|
69
|
+
* vault uid is resolved with that same token: an agent principal uses its own
|
|
70
|
+
* entity uid, as the sync runner does; a person uses the canonical person
|
|
71
|
+
* entity.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveHeldIdentity(options: {
|
|
74
|
+
needPersonal: boolean;
|
|
75
|
+
interactive: boolean;
|
|
76
|
+
}, io: {
|
|
77
|
+
ensureToken: (interactive: boolean) => Promise<string>;
|
|
78
|
+
listPersons: (token: string) => Promise<Array<{
|
|
79
|
+
uid: string;
|
|
80
|
+
type: string;
|
|
81
|
+
createdAt: string;
|
|
82
|
+
}>>;
|
|
83
|
+
}): Promise<HeldIdentity>;
|
|
84
|
+
export declare function createDefaultSyncHeldDeps(): SyncHeldDeps;
|
|
85
|
+
export declare function registerSyncHeldCommands(syncCmd: Command): void;
|
|
86
|
+
export {};
|
|
87
|
+
//# sourceMappingURL=sync-held.d.ts.map
|