@indigoai-us/hq-cli 5.345.55 → 5.345.56
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 +12 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +40 -0
- package/dist/command-catalog.generated.js +51 -0
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-session-start.d.ts +2 -7
- package/dist/commands/lanes-session-start.js +9 -29
- package/dist/commands/lanes-shared.d.ts +2 -1
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +117 -16
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/lanes/dropbox.d.ts +4 -2
- package/dist/lib/lanes/dropbox.js +54 -9
- package/dist/lib/lanes/flag-keys.d.ts +8 -0
- package/dist/lib/lanes/flag-keys.js +8 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +32 -2
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -38,9 +38,11 @@ import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
|
38
38
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
39
39
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
40
40
|
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
41
|
+
import { readLaneStatusRowFields } from "../lib/lanes/status-fields.js";
|
|
41
42
|
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
42
43
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
43
|
-
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
44
|
+
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
45
|
+
import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
|
|
44
46
|
import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
|
|
45
47
|
import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
|
|
46
48
|
import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
|
|
@@ -462,6 +464,7 @@ function resolveDeps(deps = {}) {
|
|
|
462
464
|
focusGuardEnabled: deps.focusGuardEnabled,
|
|
463
465
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
464
466
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
467
|
+
withAdmissionLock: deps.withAdmissionLock ?? withLaneAdmissionLock,
|
|
465
468
|
mergeReviewEnabled: deps.mergeReviewEnabled,
|
|
466
469
|
mergeHoldEnabled: deps.mergeHoldEnabled,
|
|
467
470
|
};
|
|
@@ -3365,6 +3368,16 @@ async function runCreate(ctx, opts) {
|
|
|
3365
3368
|
const provider = explicitProvider ??
|
|
3366
3369
|
profile?.meta.provider ??
|
|
3367
3370
|
parseLaneProvider("codex");
|
|
3371
|
+
const continuityEnabled = opts.worker !== undefined && await readLanesRegistryFlagFailOpen(LANE_WORKER_CONTINUITY_FLAG, { company: company ?? meshCompany, env: ctx.env });
|
|
3372
|
+
let workerContinuity = continuityEnabled
|
|
3373
|
+
? findWorkerContinuityLane(loadAllLanes(hqRoot), {
|
|
3374
|
+
senior,
|
|
3375
|
+
worker: resolvedWorker?.id ?? opts.worker,
|
|
3376
|
+
provider,
|
|
3377
|
+
company: company ?? meshCompany,
|
|
3378
|
+
freshThread: opts.freshThread,
|
|
3379
|
+
})
|
|
3380
|
+
: { resumable: false };
|
|
3368
3381
|
const profileDefaultsApply = profile !== undefined && profile.meta.provider === provider;
|
|
3369
3382
|
const cliModel = parseCliModel(opts.model);
|
|
3370
3383
|
// Validated here because `claude --effort <unknown>` only warns and runs.
|
|
@@ -3739,6 +3752,24 @@ async function runCreate(ctx, opts) {
|
|
|
3739
3752
|
// The lane id is created in `persist`; its Work Mesh thread is ensured
|
|
3740
3753
|
// immediately afterward from that stable identity.
|
|
3741
3754
|
const persist = async () => {
|
|
3755
|
+
const host = placementHostOverride ?? (outpostId !== undefined
|
|
3756
|
+
? { kind: "outpost", id: outpostId }
|
|
3757
|
+
: { kind: "local", id: ctx.hostname() });
|
|
3758
|
+
// Repeat the busy check while holding provider admission serialization so
|
|
3759
|
+
// two concurrent creates cannot both claim one worker's provider thread.
|
|
3760
|
+
if (continuityEnabled) {
|
|
3761
|
+
workerContinuity = findWorkerContinuityLane(loadAllLanes(hqRoot), {
|
|
3762
|
+
senior,
|
|
3763
|
+
worker: resolvedWorker?.id ?? opts.worker,
|
|
3764
|
+
provider,
|
|
3765
|
+
company: company ?? meshCompany,
|
|
3766
|
+
freshThread: opts.freshThread,
|
|
3767
|
+
});
|
|
3768
|
+
if (workerContinuity.resumable && workerContinuity.previous &&
|
|
3769
|
+
!sameLaneHost(workerContinuity.previous.host, host)) {
|
|
3770
|
+
workerContinuity = { previous: workerContinuity.previous, resumable: false, fallbackReason: "host_mismatch" };
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3742
3773
|
let admission;
|
|
3743
3774
|
let validation;
|
|
3744
3775
|
if (!outpostId && provider === "claude" && explicitBalanceAccount === undefined &&
|
|
@@ -3842,9 +3873,6 @@ async function runCreate(ctx, opts) {
|
|
|
3842
3873
|
throw admission.error;
|
|
3843
3874
|
}
|
|
3844
3875
|
}
|
|
3845
|
-
const host = placementHostOverride ?? (outpostId !== undefined
|
|
3846
|
-
? { kind: "outpost", id: outpostId }
|
|
3847
|
-
: { kind: "local", id: ctx.hostname() });
|
|
3848
3876
|
const queuedReason = admission.action === "queue" ? admission.reason : undefined;
|
|
3849
3877
|
const admissionReservedAt = admission.action === "start" ? ctx.now().toISOString() : undefined;
|
|
3850
3878
|
const laneInit = {
|
|
@@ -3888,6 +3916,18 @@ async function runCreate(ctx, opts) {
|
|
|
3888
3916
|
keep_alive: keepAlive,
|
|
3889
3917
|
...(keepAlive ? { keep_alive_opted_in_at: createdAt } : {}),
|
|
3890
3918
|
provider,
|
|
3919
|
+
...(workerContinuity.resumable && workerContinuity.previous
|
|
3920
|
+
? {
|
|
3921
|
+
provider_session_id: workerContinuity.previous.provider_session_id,
|
|
3922
|
+
last_message_at: workerContinuity.previous.last_message_at ?? workerContinuity.previous.created_at,
|
|
3923
|
+
pending_resume: {
|
|
3924
|
+
session_id: workerContinuity.previous.provider_session_id,
|
|
3925
|
+
at: createdAt,
|
|
3926
|
+
trigger: "resume",
|
|
3927
|
+
resumed_from: workerContinuity.previous.state,
|
|
3928
|
+
},
|
|
3929
|
+
}
|
|
3930
|
+
: {}),
|
|
3891
3931
|
provider_source: explicitProvider !== undefined
|
|
3892
3932
|
? "cli"
|
|
3893
3933
|
: profile?.meta.provider !== undefined
|
|
@@ -3957,12 +3997,7 @@ async function runCreate(ctx, opts) {
|
|
|
3957
3997
|
return { lane, admission, validation };
|
|
3958
3998
|
};
|
|
3959
3999
|
let persisted;
|
|
3960
|
-
|
|
3961
|
-
persisted = await persist();
|
|
3962
|
-
}
|
|
3963
|
-
else {
|
|
3964
|
-
persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
3965
|
-
}
|
|
4000
|
+
persisted = await ctx.withAdmissionLock(hqRoot, provider, persist, ctx.sleep);
|
|
3966
4001
|
let lane = persisted.lane;
|
|
3967
4002
|
const { admission, validation } = persisted;
|
|
3968
4003
|
if (focusAssessment.enabled && opts.focusOverride !== undefined) {
|
|
@@ -4052,10 +4087,25 @@ async function runCreate(ctx, opts) {
|
|
|
4052
4087
|
ensureDir(path.join(dest, "dropbox"));
|
|
4053
4088
|
ensureDir(path.join(dest, "questions"));
|
|
4054
4089
|
const taskBrief = fs.readFileSync(briefAbs, "utf8");
|
|
4055
|
-
const
|
|
4090
|
+
const previousResult = workerContinuity.previous
|
|
4091
|
+
? readLatestEnvelopeRecord(hqRoot, workerContinuity.previous.lane_id)
|
|
4092
|
+
: undefined;
|
|
4093
|
+
const briefText = `${workerContinuity.resumable && workerContinuity.previous
|
|
4094
|
+
? workerContinuityPrompt(workerContinuity.previous, previousResult?.envelope.summary, previousResult?.ref)
|
|
4095
|
+
: ""}${resolvedWorker
|
|
4056
4096
|
? composeWorkerBrief(resolvedWorker, taskBrief)
|
|
4057
|
-
: composeGeneralBrief(taskBrief)
|
|
4097
|
+
: composeGeneralBrief(taskBrief)}`;
|
|
4058
4098
|
fs.writeFileSync(path.join(dest, "brief.md"), briefText);
|
|
4099
|
+
if (workerContinuity.resumable && lane.pending_resume) {
|
|
4100
|
+
const newBriefPath = path.join(dest, "brief.md");
|
|
4101
|
+
lane = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
|
|
4102
|
+
...current,
|
|
4103
|
+
pending_resume: current.pending_resume ? {
|
|
4104
|
+
...current.pending_resume,
|
|
4105
|
+
text: `The new brief at ${newBriefPath} is the instruction for this turn. Read and follow it.`,
|
|
4106
|
+
} : undefined,
|
|
4107
|
+
}));
|
|
4108
|
+
}
|
|
4059
4109
|
if (timeoutSecs !== undefined) {
|
|
4060
4110
|
writeJsonFile(timeoutPath(hqRoot, lane.lane_id), {
|
|
4061
4111
|
timeout_secs: timeoutSecs,
|
|
@@ -4122,6 +4172,16 @@ async function runCreate(ctx, opts) {
|
|
|
4122
4172
|
stories: laneStories(started),
|
|
4123
4173
|
story_warning: combinedStoryWarning(started, createStoryWarnings),
|
|
4124
4174
|
thread_id: started.thread_id ?? null,
|
|
4175
|
+
...(continuityEnabled ? {
|
|
4176
|
+
worker_continuity: workerContinuity.resumable
|
|
4177
|
+
? { action: "resume", previous_lane_id: workerContinuity.previous?.lane_id }
|
|
4178
|
+
: {
|
|
4179
|
+
action: "fresh",
|
|
4180
|
+
...(workerContinuity.previous ? { previous_lane_id: workerContinuity.previous.lane_id } : {}),
|
|
4181
|
+
...(opts.freshThread ? { reason: "fresh_thread_requested" } : {}),
|
|
4182
|
+
...(workerContinuity.fallbackReason ? { reason: workerContinuity.fallbackReason } : {}),
|
|
4183
|
+
},
|
|
4184
|
+
} : {}),
|
|
4125
4185
|
senior: started.senior,
|
|
4126
4186
|
depth: started.depth,
|
|
4127
4187
|
timeout_secs: timeoutSecs ?? null,
|
|
@@ -5343,6 +5403,24 @@ async function runPickup(ctx, opts) {
|
|
|
5343
5403
|
lane: started,
|
|
5344
5404
|
});
|
|
5345
5405
|
}
|
|
5406
|
+
async function laneStatusFieldsForCommand(ctx, hqRoot, lane, now) {
|
|
5407
|
+
const resolved = resolveLaneCompany(hqRoot, lane).company;
|
|
5408
|
+
let enabled = true;
|
|
5409
|
+
try {
|
|
5410
|
+
enabled = await readLanesRegistryFlagFailOpen(LANE_STATUS_ROW_FIELDS_FLAG, {
|
|
5411
|
+
...(resolved ? { company: resolved } : {}),
|
|
5412
|
+
env: ctx.env,
|
|
5413
|
+
});
|
|
5414
|
+
}
|
|
5415
|
+
catch (err) {
|
|
5416
|
+
process.stderr.write(`hq lanes: status row fields flag read failed open: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
5417
|
+
}
|
|
5418
|
+
if (!enabled)
|
|
5419
|
+
return {};
|
|
5420
|
+
const host = reconcileHostFromCtx(ctx);
|
|
5421
|
+
const local = laneRunsHere(lane, host);
|
|
5422
|
+
return { ...readLaneStatusRowFields(hqRoot, lane, now.getTime(), local) };
|
|
5423
|
+
}
|
|
5346
5424
|
async function runList(ctx, opts) {
|
|
5347
5425
|
if (opts.json && opts.human) {
|
|
5348
5426
|
throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
|
|
@@ -5365,7 +5443,7 @@ async function runList(ctx, opts) {
|
|
|
5365
5443
|
new Set(opts.hierarchy).size !== opts.hierarchy.length)) {
|
|
5366
5444
|
throw new LanesError("lane_list_hierarchy_invalid", "--hierarchy must be repeated with distinct, non-empty lane ids without surrounding whitespace", {});
|
|
5367
5445
|
}
|
|
5368
|
-
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior)) {
|
|
5446
|
+
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior || opts.worker)) {
|
|
5369
5447
|
throw new LanesError("lane_list_snapshot_invalid", "--snapshot only supports owner lane filters and JSON output", {});
|
|
5370
5448
|
}
|
|
5371
5449
|
if (opts.hierarchy !== undefined && !opts.snapshot) {
|
|
@@ -5414,21 +5492,29 @@ async function runList(ctx, opts) {
|
|
|
5414
5492
|
}
|
|
5415
5493
|
if (senior && seniorKey(lane.senior) !== seniorKey(senior))
|
|
5416
5494
|
return false;
|
|
5495
|
+
if (opts.worker && lane.worker !== opts.worker)
|
|
5496
|
+
return false;
|
|
5417
5497
|
return true;
|
|
5418
5498
|
};
|
|
5419
5499
|
const selectLanes = () => laneSource
|
|
5420
5500
|
.filter((lane) => matchesLaneFilters(lane, true))
|
|
5421
5501
|
.sort(compareLaneQueueOrder);
|
|
5422
5502
|
let selectedLanes = selectLanes();
|
|
5503
|
+
const statusFieldsByLane = new Map();
|
|
5504
|
+
const statusNow = ctx.now();
|
|
5505
|
+
await Promise.all(selectedLanes.map(async (lane) => {
|
|
5506
|
+
statusFieldsByLane.set(lane.lane_id, await laneStatusFieldsForCommand(ctx, hqRoot, lane, statusNow));
|
|
5507
|
+
}));
|
|
5423
5508
|
if (opts.snapshot) {
|
|
5424
5509
|
printJson({
|
|
5425
5510
|
ok: true,
|
|
5426
5511
|
action: "list",
|
|
5427
5512
|
lanes: selectedLanes.map((lane) => {
|
|
5513
|
+
const fields = statusFieldsByLane.get(lane.lane_id) ?? {};
|
|
5428
5514
|
if (!isTerminalLaneState(lane.state))
|
|
5429
|
-
return lane;
|
|
5515
|
+
return { ...lane, ...fields };
|
|
5430
5516
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
5431
|
-
return envelope?.summary ? {
|
|
5517
|
+
return { ...lane, ...fields, ...(envelope?.summary ? { envelope_summary: envelope.summary } : {}) };
|
|
5432
5518
|
}),
|
|
5433
5519
|
});
|
|
5434
5520
|
return;
|
|
@@ -5532,6 +5618,7 @@ async function runList(ctx, opts) {
|
|
|
5532
5618
|
!laneRunsHere(lane, host);
|
|
5533
5619
|
return {
|
|
5534
5620
|
...lane,
|
|
5621
|
+
...statusFieldsByLane.get(lane.lane_id),
|
|
5535
5622
|
...hireByLane.get(lane.lane_id),
|
|
5536
5623
|
...(remoteRefresh.unverifiedRemote.has(lane.lane_id) ? { unverified_remote: true } : {}),
|
|
5537
5624
|
stories: laneStories(lane),
|
|
@@ -5901,7 +5988,10 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget
|
|
|
5901
5988
|
})
|
|
5902
5989
|
.slice(0, 20)
|
|
5903
5990
|
.map(({ repo, pr, head_sha, recorded_at }) => ({ repo, pr, head_sha, recorded_at }));
|
|
5904
|
-
const laneForShow = {
|
|
5991
|
+
const laneForShow = {
|
|
5992
|
+
...lane,
|
|
5993
|
+
...await laneStatusFieldsForCommand(ctx, hqRoot, storedLane, ctx.now()),
|
|
5994
|
+
};
|
|
5905
5995
|
delete laneForShow.resume_history;
|
|
5906
5996
|
printJson({
|
|
5907
5997
|
ok: true,
|
|
@@ -9248,6 +9338,15 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9248
9338
|
presenceConnected: ctx.presenceConnected ?? false,
|
|
9249
9339
|
deps: ctx.livenessDeps,
|
|
9250
9340
|
}),
|
|
9341
|
+
stopEnabled: (lane) => {
|
|
9342
|
+
const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
|
|
9343
|
+
const company = resolution.company ?? undefined;
|
|
9344
|
+
return readLanesRegistryFlag(LANE_REAP_STOP_FLAG, {
|
|
9345
|
+
company,
|
|
9346
|
+
...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
|
|
9347
|
+
env: ctx.env,
|
|
9348
|
+
});
|
|
9349
|
+
},
|
|
9251
9350
|
}, wrap);
|
|
9252
9351
|
registerLaneDropboxRepairCommand(lanes, {
|
|
9253
9352
|
hqRoot: ctx.hqRoot,
|
|
@@ -9313,6 +9412,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9313
9412
|
.option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
|
|
9314
9413
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
9315
9414
|
.option("--worker <name>", "HQ worker or reusable profile id")
|
|
9415
|
+
.option("--fresh-thread", "Skip worker continuity and start a cold provider session")
|
|
9316
9416
|
.option("--provider <name>", "claude | codex | grok")
|
|
9317
9417
|
.option("--account <id>", "Provider account id")
|
|
9318
9418
|
.option("--model <id>", "Model id for the provider CLI")
|
|
@@ -9412,6 +9512,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9412
9512
|
.option("--story <id>", "Filter by story")
|
|
9413
9513
|
.option("--state <state>", `Filter by state (${LANE_STATES.join("|")})`)
|
|
9414
9514
|
.option("--senior <ref>", "Filter by senior ref")
|
|
9515
|
+
.option("--worker <id>", "Filter by worker id")
|
|
9415
9516
|
.option("--lane <id>", "Filter by lane id (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
9416
9517
|
.addOption(new Option("--lane-ulid <ulid>", "Filter by lane ULID for internal owner lookup").hideHelp()
|
|
9417
9518
|
.argParser((value, previous = []) => [...previous, value]))
|
|
@@ -48,6 +48,8 @@ export interface InviteOptions {
|
|
|
48
48
|
companyUid: string;
|
|
49
49
|
callerUid: string;
|
|
50
50
|
token: string;
|
|
51
|
+
/** Reports only whether the request returned a 2xx response. */
|
|
52
|
+
onRequestResult?: (ok: boolean) => void;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* Outcome of `hq members invite`. Two shapes depending on server schema:
|
|
@@ -96,6 +98,8 @@ export interface ResendInviteOptions {
|
|
|
96
98
|
companyUid: string;
|
|
97
99
|
callerUid: string;
|
|
98
100
|
token: string;
|
|
101
|
+
/** Reports only whether the request returned a 2xx response. */
|
|
102
|
+
onRequestResult?: (ok: boolean) => void;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* Outcome of `hq members invite --resend`. No new membership row was
|
package/dist/commands/members.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
|
3
3
|
import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
|
|
4
4
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
5
5
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
6
|
+
import { queueCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
6
7
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
8
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
9
|
const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
|
|
@@ -107,6 +108,7 @@ export async function inviteMember(options) {
|
|
|
107
108
|
body,
|
|
108
109
|
});
|
|
109
110
|
if (!res.ok) {
|
|
111
|
+
options.onRequestResult?.(false);
|
|
110
112
|
const err = (await res.json().catch(() => ({})));
|
|
111
113
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
112
114
|
}
|
|
@@ -116,6 +118,7 @@ export async function inviteMember(options) {
|
|
|
116
118
|
throw new Error(`Invite endpoint returned 2xx with no membership row (response keys: ${keys}). ` +
|
|
117
119
|
"This is a server-side regression — file an issue.");
|
|
118
120
|
}
|
|
121
|
+
options.onRequestResult?.(true);
|
|
119
122
|
// Two server schemas in the wild:
|
|
120
123
|
// - Legacy (schemaVersion ≤ 1): response carries a random `inviteToken`
|
|
121
124
|
// the invitee redeems via `hq://accept/{token}`.
|
|
@@ -177,6 +180,7 @@ export async function resendInvite(options) {
|
|
|
177
180
|
},
|
|
178
181
|
});
|
|
179
182
|
if (!res.ok) {
|
|
183
|
+
options.onRequestResult?.(false);
|
|
180
184
|
const err = (await res.json().catch(() => ({})));
|
|
181
185
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
182
186
|
}
|
|
@@ -184,6 +188,7 @@ export async function resendInvite(options) {
|
|
|
184
188
|
if (data.resent !== true) {
|
|
185
189
|
throw new Error("Resend endpoint returned 2xx without `resent: true` — the connected hq-pro likely doesn't support `resend: true` yet. Upgrade hq-pro or re-invite without --resend.");
|
|
186
190
|
}
|
|
191
|
+
options.onRequestResult?.(true);
|
|
187
192
|
return {
|
|
188
193
|
resent: true,
|
|
189
194
|
emailSent: data.emailSent === true,
|
|
@@ -436,6 +441,7 @@ export function registerMembersCommand(program) {
|
|
|
436
441
|
let token;
|
|
437
442
|
let companyUid;
|
|
438
443
|
let companySlug;
|
|
444
|
+
let outcome = "failure";
|
|
439
445
|
try {
|
|
440
446
|
token = (await resolveVaultCredential()).token;
|
|
441
447
|
companySlug = members.opts().company;
|
|
@@ -448,6 +454,9 @@ export function registerMembersCommand(program) {
|
|
|
448
454
|
companyUid,
|
|
449
455
|
callerUid,
|
|
450
456
|
token,
|
|
457
|
+
onRequestResult: (ok) => {
|
|
458
|
+
outcome = ok ? "success" : "failure";
|
|
459
|
+
},
|
|
451
460
|
});
|
|
452
461
|
console.log(chalk.green(`Re-fired invite email for ${target.toLowerCase()} (existing pending row left untouched)`));
|
|
453
462
|
console.log();
|
|
@@ -472,6 +481,9 @@ export function registerMembersCommand(program) {
|
|
|
472
481
|
companyUid,
|
|
473
482
|
callerUid,
|
|
474
483
|
token,
|
|
484
|
+
onRequestResult: (ok) => {
|
|
485
|
+
outcome = ok ? "success" : "failure";
|
|
486
|
+
},
|
|
475
487
|
});
|
|
476
488
|
const agentTarget = detectTarget(target)?.isAgent === true;
|
|
477
489
|
console.log(chalk.green(`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`));
|
|
@@ -612,10 +624,19 @@ export function registerMembersCommand(program) {
|
|
|
612
624
|
}
|
|
613
625
|
if (err instanceof InviteHttpError) {
|
|
614
626
|
console.error(chalk.red(formatInviteHttpError(err.status, err.message, err.code)));
|
|
615
|
-
process.
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
return;
|
|
616
629
|
}
|
|
617
630
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
618
|
-
process.
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
queueCliFeatureUsed({
|
|
636
|
+
kind: "members_invite",
|
|
637
|
+
outcome,
|
|
638
|
+
...(companyUid ? { companyUid } : {}),
|
|
639
|
+
});
|
|
619
640
|
}
|
|
620
641
|
});
|
|
621
642
|
members
|
package/dist/lanes-drain-hook.js
CHANGED
|
@@ -17,7 +17,7 @@ function parseHookInput(text) {
|
|
|
17
17
|
throw new Error("invalid_hook_input_shape: expected a JSON object");
|
|
18
18
|
}
|
|
19
19
|
const value = parsed;
|
|
20
|
-
for (const key of ["transcript_path", "session_id", "hook_event_name"]) {
|
|
20
|
+
for (const key of ["transcript_path", "session_id", "hook_event_name", "hookEventName"]) {
|
|
21
21
|
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string") {
|
|
22
22
|
throw new Error(`invalid_hook_input_field: ${key} must be a string`);
|
|
23
23
|
}
|
|
@@ -25,7 +25,11 @@ function parseHookInput(text) {
|
|
|
25
25
|
return {
|
|
26
26
|
...(typeof value.transcript_path === "string" ? { transcript_path: value.transcript_path } : {}),
|
|
27
27
|
...(typeof value.session_id === "string" ? { session_id: value.session_id } : {}),
|
|
28
|
-
...(typeof value.hook_event_name === "string"
|
|
28
|
+
...(typeof value.hook_event_name === "string"
|
|
29
|
+
? { hook_event_name: value.hook_event_name }
|
|
30
|
+
: typeof value.hookEventName === "string"
|
|
31
|
+
? { hookEventName: value.hookEventName }
|
|
32
|
+
: {}),
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
function laneIdIsSafe(laneId) {
|
|
@@ -59,6 +59,8 @@ export interface SyncManifestUploadStatus {
|
|
|
59
59
|
snapshotId: string | null;
|
|
60
60
|
sequence: number;
|
|
61
61
|
baseUsable: boolean;
|
|
62
|
+
/** Local company slug for company-scoped remediation; absent for personal. */
|
|
63
|
+
companySlug?: string;
|
|
62
64
|
}
|
|
63
65
|
/** Injectable dependencies — defaults are the real collectors. */
|
|
64
66
|
export interface SyncHealthDeps {
|
|
@@ -32,6 +32,7 @@ import * as path from "node:path";
|
|
|
32
32
|
// ../../hq-cloud-manifest.js.
|
|
33
33
|
import * as hqCloud from "@indigoai-us/hq-cloud";
|
|
34
34
|
import { loadManifestExports, MANIFEST_UNAVAILABLE_REASON, } from "../../hq-cloud-manifest.js";
|
|
35
|
+
import { readCompaniesManifestMap } from "../../work-context/repo-remote.js";
|
|
35
36
|
import { CLI_VERSION } from "../../../cli-version.js";
|
|
36
37
|
import { readSyncVersion } from "../../../utils/feedback-versions.js";
|
|
37
38
|
import { readHqVersion } from "../../../utils/pack-contributions.js";
|
|
@@ -52,9 +53,9 @@ function withDefaults(deps, hqRoot) {
|
|
|
52
53
|
return {
|
|
53
54
|
...deps,
|
|
54
55
|
runtime: deps.runtime ?? (() => []),
|
|
55
|
-
manifestStatuses: deps.manifestStatuses ?? defaultManifestStatuses,
|
|
56
|
+
manifestStatuses: deps.manifestStatuses ?? ((journals) => defaultManifestStatuses(hqRoot, journals)),
|
|
56
57
|
manifestAvailable: deps.manifestAvailable ?? (() => loadManifestExports() !== null),
|
|
57
|
-
unresolvedScopes: deps.unresolvedScopes ?? defaultUnresolvedScopes,
|
|
58
|
+
unresolvedScopes: deps.unresolvedScopes ?? ((journals) => defaultUnresolvedScopes(hqRoot, journals)),
|
|
58
59
|
localCompanyExists: deps.localCompanyExists ??
|
|
59
60
|
((slug) => defaultLocalCompanyExists(hqRoot, slug)),
|
|
60
61
|
};
|
|
@@ -78,15 +79,35 @@ function defaultLocalCompanyExists(hqRoot, slug) {
|
|
|
78
79
|
return false;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
/** Resolve locally-known company journal slugs to the UID used by snapshots. */
|
|
83
|
+
function resolvedCompanyScopes(hqRoot, journals) {
|
|
84
|
+
const companies = readCompaniesManifestMap(hqRoot);
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const scopes = [];
|
|
87
|
+
for (const { slug } of journals) {
|
|
88
|
+
if (NON_COMPANY_JOURNAL_SLUGS.has(slug) ||
|
|
89
|
+
!isSafeCompanySlug(slug) ||
|
|
90
|
+
!defaultLocalCompanyExists(hqRoot, slug) ||
|
|
91
|
+
seen.has(slug)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(slug);
|
|
95
|
+
const uid = companies?.[slug]?.cloud_uid;
|
|
96
|
+
if (typeof uid === "string" && /^cmp_[A-Za-z0-9]{3,128}$/.test(uid.trim())) {
|
|
97
|
+
scopes.push({ kind: "company", companyUid: uid.trim(), slug });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return scopes;
|
|
101
|
+
}
|
|
102
|
+
function defaultUnresolvedScopes(hqRoot, journals) {
|
|
103
|
+
const resolved = new Set(resolvedCompanyScopes(hqRoot, journals).map((scope) => scope.slug));
|
|
104
|
+
return [
|
|
105
|
+
...new Set(journals
|
|
106
|
+
.filter((entry) => !NON_COMPANY_JOURNAL_SLUGS.has(entry.slug) &&
|
|
107
|
+
defaultLocalCompanyExists(hqRoot, entry.slug) &&
|
|
108
|
+
!resolved.has(entry.slug))
|
|
109
|
+
.map((entry) => entry.slug)),
|
|
110
|
+
];
|
|
90
111
|
}
|
|
91
112
|
function defaultVersions(hqRoot) {
|
|
92
113
|
return {
|
|
@@ -119,21 +140,15 @@ const DEFAULT_DEPS = {
|
|
|
119
140
|
// than no check.
|
|
120
141
|
journals: () => hqCloud.listJournals(),
|
|
121
142
|
now: () => new Date(),
|
|
122
|
-
manifestStatuses: defaultManifestStatuses,
|
|
123
143
|
runtime: defaultSyncRuntimeResults,
|
|
124
144
|
};
|
|
125
145
|
/**
|
|
126
146
|
* Map the machine's journal shards onto the manifest scopes hq-cloud keys its
|
|
127
|
-
* snapshot store by, then read each one's last upload.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* company. Note the snapshot store keys company scopes by `companyUid`, which
|
|
131
|
-
* the journal listing does not carry — so a company whose uid we cannot name
|
|
132
|
-
* is simply not reported here rather than reported wrongly. That is the honest
|
|
133
|
-
* outcome: this check exists to tell an operator when a manifest STOPPED being
|
|
134
|
-
* uploaded, and inventing a scope key would make it lie in both directions.
|
|
147
|
+
* snapshot store by, then read each one's last upload. Company journal rows
|
|
148
|
+
* carry only slugs, so resolve their UIDs from the local companies manifest;
|
|
149
|
+
* a missing or malformed UID stays explicitly unresolved below.
|
|
135
150
|
*/
|
|
136
|
-
function defaultManifestStatuses(journals) {
|
|
151
|
+
function defaultManifestStatuses(hqRoot, journals) {
|
|
137
152
|
// Both the legacy `personal` shard and the current personal-vault
|
|
138
153
|
// pseudo-slug name the SAME personal tree; a machine mid-migration has both
|
|
139
154
|
// on disk, so collapse them to a single personal scope rather than asking
|
|
@@ -141,6 +156,7 @@ function defaultManifestStatuses(journals) {
|
|
|
141
156
|
const scopes = journals.some((entry) => NON_COMPANY_JOURNAL_SLUGS.has(entry.slug))
|
|
142
157
|
? [{ kind: "personal" }]
|
|
143
158
|
: [];
|
|
159
|
+
scopes.push(...resolvedCompanyScopes(hqRoot, journals));
|
|
144
160
|
if (scopes.length === 0)
|
|
145
161
|
return [];
|
|
146
162
|
// Absent on an older hq-cloud; the NA row is emitted by `manifestResults`
|
|
@@ -148,7 +164,15 @@ function defaultManifestStatuses(journals) {
|
|
|
148
164
|
const exports = loadManifestExports();
|
|
149
165
|
if (!exports)
|
|
150
166
|
return [];
|
|
151
|
-
|
|
167
|
+
const companySlugsByUid = new Map(scopes
|
|
168
|
+
.filter((scope) => scope.kind === "company")
|
|
169
|
+
.map((scope) => [scope.companyUid, scope.slug]));
|
|
170
|
+
const statuses = exports.readManifestUploadStatus(exports.getStateDir(), scopes);
|
|
171
|
+
return statuses.map((status) => {
|
|
172
|
+
const companyUid = /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(status.scopeKey)?.[1];
|
|
173
|
+
const companySlug = companyUid ? companySlugsByUid.get(companyUid) : undefined;
|
|
174
|
+
return companySlug ? { ...status, companySlug } : status;
|
|
175
|
+
});
|
|
152
176
|
}
|
|
153
177
|
/** The legacy journal slug that named the personal (non-company) tree. */
|
|
154
178
|
const PERSONAL_SCOPE_SLUG = "personal";
|
|
@@ -526,7 +550,7 @@ function manifestResults(deps, journals) {
|
|
|
526
550
|
checkId,
|
|
527
551
|
reasonCode: "stale-threshold",
|
|
528
552
|
message: `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`,
|
|
529
|
-
remediation:
|
|
553
|
+
remediation: `Run \`hq sync manifest${entry.companySlug ? ` --scope ${entry.companySlug}` : ""}\` and check HQ_SYNC_MANIFEST_DISABLED and the sync runner.`,
|
|
530
554
|
};
|
|
531
555
|
}
|
|
532
556
|
return {
|
|
@@ -53,12 +53,14 @@ export interface DrainDeps {
|
|
|
53
53
|
}
|
|
54
54
|
export interface LaneDrainHookInput {
|
|
55
55
|
transcript_path?: string | null;
|
|
56
|
-
session_id?: string;
|
|
57
56
|
hook_event_name?: string;
|
|
57
|
+
hookEventName?: string;
|
|
58
|
+
session_id?: string;
|
|
58
59
|
}
|
|
59
60
|
export interface RunDrainHookDeps {
|
|
60
61
|
readLane?: typeof tryReadLane;
|
|
61
62
|
probeCompactionsSince?: typeof import("./compaction.js").probeProviderCompactionsSince;
|
|
63
|
+
readLifecycleDeliveryFlag?: () => Promise<boolean>;
|
|
62
64
|
}
|
|
63
65
|
export declare function mintClaimId(): string;
|
|
64
66
|
export declare function assertSafeLaneId(laneId: string): void;
|
|
@@ -102,7 +104,7 @@ export declare function formatOperatorInstruction(messages: DropboxMessage[]): s
|
|
|
102
104
|
export declare function formatMonitorEvent(message: DropboxMessage): string;
|
|
103
105
|
/** Keep event order while framing monitor output separately from operator messages. */
|
|
104
106
|
export declare function formatDropboxMessages(messages: DropboxMessage[]): string;
|
|
105
|
-
export declare function hookAdditionalContextJson(messages: DropboxMessage[], hookEventName?: "PreToolUse" | "UserPromptSubmit", leadingNote?: string): string;
|
|
107
|
+
export declare function hookAdditionalContextJson(messages: DropboxMessage[], hookEventName?: "PreToolUse" | "PostToolUse" | "UserPromptSubmit", leadingNote?: string): string;
|
|
106
108
|
export interface DrainHookResult {
|
|
107
109
|
exitCode: number;
|
|
108
110
|
stdout: string;
|
|
@@ -14,6 +14,8 @@ import { LanesError } from "./errors.js";
|
|
|
14
14
|
import { MESSAGE_COMPACTION_REPLAY_NOTE, MESSAGE_REPLAY_STATE_NAME, readMessageReplayState, updateMessageReplayTranscript, writeMessageReplayState, } from "./message-replay.js";
|
|
15
15
|
import { LANES_RUNS_REL, readProcStarttime } from "./spawn.js";
|
|
16
16
|
import { tryReadLane } from "./store.js";
|
|
17
|
+
import { readLanesRegistryFlagFailOpen } from "./registry-flag.js";
|
|
18
|
+
import { LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG } from "./flag-keys.js";
|
|
17
19
|
export const DROPBOX_DIR = "dropbox";
|
|
18
20
|
export const INBOX_NAME = "inbox.jsonl";
|
|
19
21
|
export const DRAINING_DIR = "draining";
|
|
@@ -426,6 +428,9 @@ export function hookAdditionalContextJson(messages, hookEventName = "PreToolUse"
|
|
|
426
428
|
},
|
|
427
429
|
})}\n`;
|
|
428
430
|
}
|
|
431
|
+
function hookStopBlockJson(reason) {
|
|
432
|
+
return `${JSON.stringify({ decision: "block", reason })}\n`;
|
|
433
|
+
}
|
|
429
434
|
function replayStatePath(hqRoot, laneId) {
|
|
430
435
|
return path.join(dropboxDir(hqRoot, laneId), MESSAGE_REPLAY_STATE_NAME);
|
|
431
436
|
}
|
|
@@ -465,14 +470,47 @@ export async function runDrainHook(env, input = {}, deps = {}) {
|
|
|
465
470
|
stderr: "hq-lanes-drain: HQ_ROOT is unset while HQ_LANE_ID is set\n",
|
|
466
471
|
};
|
|
467
472
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
recordToolActivity(hqRoot, laneId);
|
|
473
|
+
const hookEvent = input.hook_event_name ?? input.hookEventName ?? "PreToolUse";
|
|
474
|
+
if (hookEvent !== "PreToolUse" && hookEvent !== "PostToolUse" && hookEvent !== "Stop") {
|
|
475
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
473
476
|
}
|
|
474
|
-
|
|
475
|
-
|
|
477
|
+
// Grok invokes lifecycle hooks, but their output cannot reach the model.
|
|
478
|
+
// Preserve queued messages for its next deliverable PreToolUse event.
|
|
479
|
+
if (env.HQ_LANE_PROVIDER === "grok" && hookEvent !== "PreToolUse") {
|
|
480
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
481
|
+
}
|
|
482
|
+
const hasQueuedMessages = dropboxInboxHasMessages(hqRoot, laneId);
|
|
483
|
+
if (hookEvent === "Stop" && !hasQueuedMessages) {
|
|
484
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
485
|
+
}
|
|
486
|
+
if ((hookEvent === "PostToolUse" || hookEvent === "Stop") && hasQueuedMessages) {
|
|
487
|
+
const lane = (deps.readLane ?? tryReadLane)(hqRoot, laneId);
|
|
488
|
+
const company = lane?.mesh_company ?? lane?.company ?? undefined;
|
|
489
|
+
let enabled = true;
|
|
490
|
+
try {
|
|
491
|
+
enabled = deps.readLifecycleDeliveryFlag
|
|
492
|
+
? await deps.readLifecycleDeliveryFlag()
|
|
493
|
+
: await readLanesRegistryFlagFailOpen(LANE_DROPBOX_LIFECYCLE_DELIVERY_FLAG, {
|
|
494
|
+
...(company ? { company } : {}),
|
|
495
|
+
env,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
process.stderr.write(`hq-lanes-drain: lifecycle delivery flag read failed open: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
500
|
+
}
|
|
501
|
+
if (!enabled)
|
|
502
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
503
|
+
}
|
|
504
|
+
// PreToolUse remains the tool-activity liveness touch used by watcher.ts.
|
|
505
|
+
// PostToolUse and Stop add delivery opportunities without extra liveness writes.
|
|
506
|
+
let stderr = "";
|
|
507
|
+
if (hookEvent === "PreToolUse") {
|
|
508
|
+
try {
|
|
509
|
+
recordToolActivity(hqRoot, laneId);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
stderr += `hq-lanes-drain: failed to record tool activity: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
513
|
+
}
|
|
476
514
|
}
|
|
477
515
|
try {
|
|
478
516
|
const turnId = env.HQ_LANE_TURN_ID ?? "";
|
|
@@ -515,15 +553,22 @@ export async function runDrainHook(env, input = {}, deps = {}) {
|
|
|
515
553
|
}
|
|
516
554
|
}
|
|
517
555
|
if (replayNote && replayed && replayed.length > 0) {
|
|
556
|
+
const formatted = `${replayNote}\n\n${formatDropboxMessages(replayed)}`;
|
|
518
557
|
return {
|
|
519
558
|
exitCode: 0,
|
|
520
|
-
stdout:
|
|
559
|
+
stdout: hookEvent === "Stop"
|
|
560
|
+
? hookStopBlockJson(formatted)
|
|
561
|
+
: hookAdditionalContextJson(replayed, hookEvent, replayNote),
|
|
521
562
|
stderr,
|
|
522
563
|
};
|
|
523
564
|
}
|
|
524
565
|
if (messages.length === 0)
|
|
525
566
|
return { exitCode: 0, stdout: "", stderr };
|
|
526
|
-
|
|
567
|
+
const formatted = formatDropboxMessages(messages);
|
|
568
|
+
if (hookEvent === "Stop") {
|
|
569
|
+
return { exitCode: 0, stdout: hookStopBlockJson(formatted), stderr };
|
|
570
|
+
}
|
|
571
|
+
return { exitCode: 0, stdout: hookAdditionalContextJson(messages, hookEvent), stderr };
|
|
527
572
|
}
|
|
528
573
|
catch (err) {
|
|
529
574
|
const message = err instanceof Error ? err.message : String(err);
|