@indigoai-us/hq-cli 5.147.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 +29 -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/mcp-registration.d.ts +29 -16
- package/dist/commands/mcp-registration.js +254 -17
- 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/dist/sentry.js +15 -7
- package/dist/utils/sentry-fingerprint.d.ts +11 -0
- package/dist/utils/sentry-fingerprint.js +56 -11
- package/package.json +5 -3
- package/scripts/mesh-daemon-if-stale.mjs +16 -0
package/dist/commands/lanes.js
CHANGED
|
@@ -12,22 +12,24 @@ import * as os from "node:os";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
14
14
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
15
|
-
import { buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
15
|
+
import { buildOutpostLaneCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
16
16
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
17
17
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
18
18
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
19
19
|
import { createLaneMesh, emitLaneDecisionEvents, } from "../lib/lanes/mesh-events.js";
|
|
20
20
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
21
21
|
import { AccountResolutionError, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountMaxRunning, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, isLoggedOut, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasRecordedSessionId, providerSupportsSessionResume, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
22
|
+
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
22
23
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
23
24
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
24
25
|
import { providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
25
26
|
import { parseCliEffort, resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
26
27
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
27
28
|
import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
28
|
-
import { SESSION_MOVE_REASON
|
|
29
|
+
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
30
|
+
import { ensureSessionHome } from "../lib/lanes/session-relink.js";
|
|
29
31
|
import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
|
|
30
|
-
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
32
|
+
import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
|
|
31
33
|
export { spawnLaneWorker };
|
|
32
34
|
export { questionBodyAbsPath };
|
|
33
35
|
export { MAX_LANE_DEPTH };
|
|
@@ -40,8 +42,21 @@ const VENDOR_LOGIN = {
|
|
|
40
42
|
grok: { command: "grok", args: ["login"] },
|
|
41
43
|
};
|
|
42
44
|
export { emitLaneDecisionEvents };
|
|
45
|
+
function withoutProviderHome(value) {
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return value.map(withoutProviderHome);
|
|
48
|
+
if (value === null || typeof value !== "object")
|
|
49
|
+
return value;
|
|
50
|
+
const safe = {};
|
|
51
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
52
|
+
if (key === "provider_home_at_spawn")
|
|
53
|
+
continue;
|
|
54
|
+
safe[key] = withoutProviderHome(nested);
|
|
55
|
+
}
|
|
56
|
+
return safe;
|
|
57
|
+
}
|
|
43
58
|
function printJson(value) {
|
|
44
|
-
console.log(JSON.stringify(value, null, 2));
|
|
59
|
+
console.log(JSON.stringify(withoutProviderHome(value), null, 2));
|
|
45
60
|
}
|
|
46
61
|
function failJson(err) {
|
|
47
62
|
if (isLanesError(err)) {
|
|
@@ -1196,6 +1211,26 @@ async function emitLaneMeshEvent(ctx, company, threadId, event, payload) {
|
|
|
1196
1211
|
return;
|
|
1197
1212
|
await ctx.mesh.appendThreadEvent(company, threadId, event, payload);
|
|
1198
1213
|
}
|
|
1214
|
+
async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
1215
|
+
if (!threadId)
|
|
1216
|
+
return;
|
|
1217
|
+
const failures = [];
|
|
1218
|
+
for (const story of laneStories(lane)) {
|
|
1219
|
+
try {
|
|
1220
|
+
await emitLaneMeshEvent(ctx, company, threadId, "lane_started", {
|
|
1221
|
+
lane_id: lane.lane_id,
|
|
1222
|
+
project_id: story.project_id,
|
|
1223
|
+
story_id: story.story_id,
|
|
1224
|
+
});
|
|
1225
|
+
await ctx.mesh.patchStoryStatus(company, story.project_id, story.story_id, "in_progress");
|
|
1226
|
+
}
|
|
1227
|
+
catch (err) {
|
|
1228
|
+
failures.push(err);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (failures.length > 0)
|
|
1232
|
+
throw failures[0];
|
|
1233
|
+
}
|
|
1199
1234
|
export const PICKUP_TERMINAL_REASONS = [
|
|
1200
1235
|
"account_not_logged_in",
|
|
1201
1236
|
"unknown_account",
|
|
@@ -1375,6 +1410,7 @@ async function runCreate(ctx, opts) {
|
|
|
1375
1410
|
const meshCompanyFlag = opts.meshCompany?.trim();
|
|
1376
1411
|
const project = opts.project?.trim();
|
|
1377
1412
|
const story = opts.story?.trim();
|
|
1413
|
+
const additionalStories = (opts.additionalStory ?? []).map((value) => value.trim());
|
|
1378
1414
|
const briefFile = opts.briefFile?.trim();
|
|
1379
1415
|
if (!project) {
|
|
1380
1416
|
throw new LanesError("missing_project", "--project is required", {});
|
|
@@ -1382,6 +1418,16 @@ async function runCreate(ctx, opts) {
|
|
|
1382
1418
|
if (!story) {
|
|
1383
1419
|
throw new LanesError("missing_story", "--story is required", {});
|
|
1384
1420
|
}
|
|
1421
|
+
if (additionalStories.some((value) => value.length === 0)) {
|
|
1422
|
+
throw new LanesError("missing_story", "--additional-story values must be non-empty", {});
|
|
1423
|
+
}
|
|
1424
|
+
const storyIds = [story, ...additionalStories];
|
|
1425
|
+
if (new Set(storyIds).size !== storyIds.length) {
|
|
1426
|
+
throw new LanesError("duplicate_story", "A lane cannot carry the same project/story more than once", {
|
|
1427
|
+
project_id: project,
|
|
1428
|
+
stories: storyIds,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1385
1431
|
if (!briefFile) {
|
|
1386
1432
|
throw new LanesError("missing_brief_file", "--brief-file is required", {});
|
|
1387
1433
|
}
|
|
@@ -1689,6 +1735,13 @@ async function runCreate(ctx, opts) {
|
|
|
1689
1735
|
: {}),
|
|
1690
1736
|
project_id: project,
|
|
1691
1737
|
story_id: story,
|
|
1738
|
+
stories: storyIds.map((storyId) => ({
|
|
1739
|
+
project_id: project,
|
|
1740
|
+
story_id: storyId,
|
|
1741
|
+
status: "in_progress",
|
|
1742
|
+
added_at: createdAt,
|
|
1743
|
+
updated_at: createdAt,
|
|
1744
|
+
})),
|
|
1692
1745
|
thread_id: threadId,
|
|
1693
1746
|
depth: laneDepth,
|
|
1694
1747
|
senior,
|
|
@@ -1771,14 +1824,8 @@ async function runCreate(ctx, opts) {
|
|
|
1771
1824
|
runCompaction: ctx.runCompaction,
|
|
1772
1825
|
locateTranscript: ctx.locateTranscript,
|
|
1773
1826
|
});
|
|
1774
|
-
if (meshCompany)
|
|
1775
|
-
await
|
|
1776
|
-
lane_id: started.lane_id,
|
|
1777
|
-
project_id: project,
|
|
1778
|
-
story_id: story,
|
|
1779
|
-
});
|
|
1780
|
-
await ctx.mesh.patchStoryStatus(meshCompany, project, story, "in_progress");
|
|
1781
|
-
}
|
|
1827
|
+
if (meshCompany)
|
|
1828
|
+
await emitLaneStartedForStories(ctx, meshCompany, started, threadId);
|
|
1782
1829
|
}
|
|
1783
1830
|
else if (outpostId) {
|
|
1784
1831
|
started = await dispatchCreatedOutpostLane(ctx, {
|
|
@@ -1810,6 +1857,8 @@ async function runCreate(ctx, opts) {
|
|
|
1810
1857
|
company: started.company,
|
|
1811
1858
|
project_id: started.project_id,
|
|
1812
1859
|
story_id: started.story_id,
|
|
1860
|
+
stories: laneStories(started),
|
|
1861
|
+
story_warning: storyWarning(started) ?? null,
|
|
1813
1862
|
thread_id: started.thread_id ?? null,
|
|
1814
1863
|
senior: started.senior,
|
|
1815
1864
|
depth: started.depth,
|
|
@@ -1906,6 +1955,9 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
1906
1955
|
}
|
|
1907
1956
|
}
|
|
1908
1957
|
function parseRemotePickupJson(stdout) {
|
|
1958
|
+
return parseRemoteJson(stdout, "Outpost pickup");
|
|
1959
|
+
}
|
|
1960
|
+
function parseRemoteJson(stdout, surface) {
|
|
1909
1961
|
const trimmed = stdout.trim();
|
|
1910
1962
|
if (!trimmed)
|
|
1911
1963
|
return null;
|
|
@@ -1914,14 +1966,92 @@ function parseRemotePickupJson(stdout) {
|
|
|
1914
1966
|
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1915
1967
|
return parsed;
|
|
1916
1968
|
}
|
|
1917
|
-
process.stderr.write(
|
|
1969
|
+
process.stderr.write(`hq lanes: ${surface} stdout is not a JSON object\n`);
|
|
1918
1970
|
}
|
|
1919
1971
|
catch (err) {
|
|
1920
|
-
process.stderr.write(`hq lanes: failed to parse
|
|
1972
|
+
process.stderr.write(`hq lanes: failed to parse ${surface} JSON: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1921
1973
|
return null;
|
|
1922
1974
|
}
|
|
1923
1975
|
return null;
|
|
1924
1976
|
}
|
|
1977
|
+
function laneIsHostedHere(ctx, lane) {
|
|
1978
|
+
if (lane.host.kind !== "outpost")
|
|
1979
|
+
return true;
|
|
1980
|
+
const identity = ctx.readOutpostIdentity();
|
|
1981
|
+
if (identity.kind === "unknown") {
|
|
1982
|
+
throw new LanesError("outpost_identity_unknown", `Cannot determine whether this process is Outpost ${lane.host.id}; refusing local lane delivery`, { lane_id: lane.lane_id, outpost_id: lane.host.id });
|
|
1983
|
+
}
|
|
1984
|
+
return identity.kind === "id" && identity.id === lane.host.id;
|
|
1985
|
+
}
|
|
1986
|
+
async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
1987
|
+
if (lane.host.kind !== "outpost") {
|
|
1988
|
+
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
|
|
1989
|
+
}
|
|
1990
|
+
const outpostId = lane.host.id;
|
|
1991
|
+
let dispatched;
|
|
1992
|
+
try {
|
|
1993
|
+
dispatched = await ctx.dispatchOutpostExec({
|
|
1994
|
+
outpostId,
|
|
1995
|
+
command: buildOutpostLaneCommand({
|
|
1996
|
+
action,
|
|
1997
|
+
laneId: lane.lane_id,
|
|
1998
|
+
text: options.text,
|
|
1999
|
+
project: options.project,
|
|
2000
|
+
story: options.story,
|
|
2001
|
+
account: options.account,
|
|
2002
|
+
model: options.model,
|
|
2003
|
+
compact: options.compact,
|
|
2004
|
+
}),
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
catch (err) {
|
|
2008
|
+
throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
2009
|
+
}
|
|
2010
|
+
if (dispatched.done !== true ||
|
|
2011
|
+
dispatched.status !== "Success" ||
|
|
2012
|
+
dispatched.exitCode !== 0) {
|
|
2013
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} did not confirm the ${action} command`, {
|
|
2014
|
+
lane_id: lane.lane_id,
|
|
2015
|
+
outpost_id: outpostId,
|
|
2016
|
+
command_id: dispatched.commandId,
|
|
2017
|
+
status: dispatched.status,
|
|
2018
|
+
done: dispatched.done ?? false,
|
|
2019
|
+
exit_code: dispatched.exitCode,
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane command");
|
|
2023
|
+
if (remote?.error !== undefined) {
|
|
2024
|
+
throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
|
|
2025
|
+
? remote.message
|
|
2026
|
+
: `Outpost ${outpostId} rejected the ${action} command`, {
|
|
2027
|
+
lane_id: lane.lane_id,
|
|
2028
|
+
outpost_id: outpostId,
|
|
2029
|
+
command_id: dispatched.commandId,
|
|
2030
|
+
remote_error: remote.error,
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
const resultConfirmed = remote !== null &&
|
|
2034
|
+
remote.ok === true &&
|
|
2035
|
+
remote.action === action &&
|
|
2036
|
+
remote.lane_id === lane.lane_id &&
|
|
2037
|
+
(action === "message"
|
|
2038
|
+
? remote.accepted === true
|
|
2039
|
+
: remote.delivery_status === "worker_spawned" ||
|
|
2040
|
+
remote.delivery_status === "queued_on_target_account");
|
|
2041
|
+
if (!resultConfirmed) {
|
|
2042
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
|
|
2043
|
+
lane_id: lane.lane_id,
|
|
2044
|
+
outpost_id: outpostId,
|
|
2045
|
+
command_id: dispatched.commandId,
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
printJson({
|
|
2049
|
+
...remote,
|
|
2050
|
+
outpost_id: outpostId,
|
|
2051
|
+
outpost_command_id: dispatched.commandId,
|
|
2052
|
+
outpost_transport: "outposts_exec",
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
1925
2055
|
function ingestPickupPayload(hqRoot, payload, now) {
|
|
1926
2056
|
const laneId = payload.lane.lane_id;
|
|
1927
2057
|
if (!laneId) {
|
|
@@ -2116,10 +2246,11 @@ async function runList(ctx, opts) {
|
|
|
2116
2246
|
.filter((lane) => {
|
|
2117
2247
|
if (state && lane.state !== state)
|
|
2118
2248
|
return false;
|
|
2119
|
-
if (opts.project
|
|
2120
|
-
|
|
2121
|
-
|
|
2249
|
+
if ((opts.project || opts.story) &&
|
|
2250
|
+
!laneStories(lane).some((story) => (!opts.project || story.project_id === opts.project) &&
|
|
2251
|
+
(!opts.story || story.story_id === opts.story))) {
|
|
2122
2252
|
return false;
|
|
2253
|
+
}
|
|
2123
2254
|
if (company && !recordMatchesCompany(lane, company)) {
|
|
2124
2255
|
return false;
|
|
2125
2256
|
}
|
|
@@ -2222,6 +2353,9 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2222
2353
|
if (!providerSupportsSessionResume(lane.provider)) {
|
|
2223
2354
|
throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
|
|
2224
2355
|
}
|
|
2356
|
+
if (lane.provider === "grok") {
|
|
2357
|
+
throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
|
|
2358
|
+
}
|
|
2225
2359
|
if (!hasRecordedSessionId(lane)) {
|
|
2226
2360
|
throw new LanesError("no_provider_session", `Lane ${laneId} has no recorded provider session id, so there is no session to move`, { lane_id: laneId, provider: lane.provider });
|
|
2227
2361
|
}
|
|
@@ -2282,12 +2416,14 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2282
2416
|
throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
2283
2417
|
}
|
|
2284
2418
|
// Checks done. Copy the session files, then write the record.
|
|
2285
|
-
const copy =
|
|
2286
|
-
|
|
2287
|
-
targetHome: targetCfg.home,
|
|
2288
|
-
cwd: hqRoot,
|
|
2419
|
+
const copy = ensureSessionHome({
|
|
2420
|
+
provider: lane.provider,
|
|
2289
2421
|
sessionId,
|
|
2290
|
-
|
|
2422
|
+
cwd: hqRoot,
|
|
2423
|
+
targetHome: targetCfg.home,
|
|
2424
|
+
registeredHomes: Object.values(providerCfg.accounts).map((account) => account.home),
|
|
2425
|
+
providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceCfg.home,
|
|
2426
|
+
});
|
|
2291
2427
|
const at = now.toISOString();
|
|
2292
2428
|
const from = lane.account ?? null;
|
|
2293
2429
|
// The capacity decision and the record write run under the target
|
|
@@ -2317,6 +2453,8 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
2317
2453
|
const next = {
|
|
2318
2454
|
...current,
|
|
2319
2455
|
account: target,
|
|
2456
|
+
provider_session_id: sessionId,
|
|
2457
|
+
provider_session_id_absent: undefined,
|
|
2320
2458
|
...(input.cliModel !== undefined
|
|
2321
2459
|
? { model: input.cliModel, model_source: "cli" }
|
|
2322
2460
|
: {}),
|
|
@@ -2344,6 +2482,11 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2344
2482
|
throw new LanesError("missing_text", "--text is required", {});
|
|
2345
2483
|
}
|
|
2346
2484
|
let lane = readLane(hqRoot, laneId);
|
|
2485
|
+
if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
|
|
2486
|
+
await dispatchOutpostLaneCommand(ctx, lane, "message", opts);
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
2347
2490
|
// An account move is refused on its own terms first: a running lane and a
|
|
2348
2491
|
// provider without session resume get the move's named errors, not the
|
|
2349
2492
|
// generic delivery refusal below.
|
|
@@ -2413,7 +2556,6 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2413
2556
|
// inbox, which would hand it to the drain hook a second time) and the
|
|
2414
2557
|
// spawn happens before any attribution write so a failed spawn leaves
|
|
2415
2558
|
// the record untouched.
|
|
2416
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
2417
2559
|
resumed = spawnLaneWorker(hqRoot, lane, {
|
|
2418
2560
|
env: ctx.env,
|
|
2419
2561
|
modelDeps: {
|
|
@@ -2433,6 +2575,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2433
2575
|
trigger: { kind: "message", text, message_id: messageId },
|
|
2434
2576
|
compact: opts.compact !== false,
|
|
2435
2577
|
});
|
|
2578
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
2436
2579
|
}
|
|
2437
2580
|
// Every accepted message moves the compaction reference forward.
|
|
2438
2581
|
let next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
@@ -2448,6 +2591,18 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2448
2591
|
...current,
|
|
2449
2592
|
project_id: projectId,
|
|
2450
2593
|
story_id: storyId,
|
|
2594
|
+
stories: laneStory(current, projectId, storyId)
|
|
2595
|
+
? laneStories(current)
|
|
2596
|
+
: [
|
|
2597
|
+
...laneStories(current),
|
|
2598
|
+
{
|
|
2599
|
+
project_id: projectId,
|
|
2600
|
+
story_id: storyId,
|
|
2601
|
+
status: "in_progress",
|
|
2602
|
+
added_at: at,
|
|
2603
|
+
updated_at: at,
|
|
2604
|
+
},
|
|
2605
|
+
],
|
|
2451
2606
|
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
2452
2607
|
project_id: projectId,
|
|
2453
2608
|
story_id: storyId,
|
|
@@ -2493,6 +2648,8 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
2493
2648
|
account_move: move ? accountMoveSummary(move) : null,
|
|
2494
2649
|
project_id: next.project_id,
|
|
2495
2650
|
story_id: next.story_id,
|
|
2651
|
+
stories: laneStories(next),
|
|
2652
|
+
story_warning: storyWarning(next) ?? null,
|
|
2496
2653
|
lane: next,
|
|
2497
2654
|
});
|
|
2498
2655
|
}
|
|
@@ -2508,6 +2665,30 @@ function accountMoveSummary(move) {
|
|
|
2508
2665
|
target_dir: move.copy.target_dir,
|
|
2509
2666
|
};
|
|
2510
2667
|
}
|
|
2668
|
+
function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
2669
|
+
if (lane.provider === "grok")
|
|
2670
|
+
return lane;
|
|
2671
|
+
const manifest = readSpawnManifest(laneRunDir(hqRoot, lane.lane_id));
|
|
2672
|
+
const sessionId = (typeof lane.provider_session_id === "string" && lane.provider_session_id.trim()
|
|
2673
|
+
? lane.provider_session_id
|
|
2674
|
+
: undefined) ??
|
|
2675
|
+
(typeof lane.last_resume?.session_id === "string" && lane.last_resume.session_id.trim()
|
|
2676
|
+
? lane.last_resume.session_id
|
|
2677
|
+
: undefined) ??
|
|
2678
|
+
(typeof manifest.resume?.session_id === "string" && manifest.resume.session_id.trim()
|
|
2679
|
+
? manifest.resume.session_id
|
|
2680
|
+
: undefined);
|
|
2681
|
+
const providerHome = lane.provider_home_at_spawn ?? manifest.provider_home_at_spawn;
|
|
2682
|
+
return {
|
|
2683
|
+
...lane,
|
|
2684
|
+
...(lane.provider_session_id === undefined && sessionId !== undefined
|
|
2685
|
+
? { provider_session_id: sessionId, provider_session_id_absent: undefined }
|
|
2686
|
+
: {}),
|
|
2687
|
+
...(lane.provider_home_at_spawn === undefined && providerHome !== undefined
|
|
2688
|
+
? { provider_home_at_spawn: providerHome }
|
|
2689
|
+
: {}),
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2511
2692
|
/**
|
|
2512
2693
|
* `hq lanes resume <lane-id> [--text <msg>]`: the explicit opt-in that
|
|
2513
2694
|
* continues a lane in `interrupted`, `done` or `blocked` on its recorded
|
|
@@ -2520,6 +2701,11 @@ function accountMoveSummary(move) {
|
|
|
2520
2701
|
async function runResume(ctx, laneId, opts) {
|
|
2521
2702
|
const hqRoot = ctx.hqRoot();
|
|
2522
2703
|
let lane = readLane(hqRoot, laneId);
|
|
2704
|
+
if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
|
|
2705
|
+
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
2523
2709
|
if (opts.model !== undefined && opts.account === undefined) {
|
|
2524
2710
|
throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
|
|
2525
2711
|
}
|
|
@@ -2618,6 +2804,18 @@ async function runResume(ctx, laneId, opts) {
|
|
|
2618
2804
|
...current,
|
|
2619
2805
|
project_id: projectId,
|
|
2620
2806
|
story_id: storyId,
|
|
2807
|
+
stories: laneStory(current, projectId, storyId)
|
|
2808
|
+
? laneStories(current)
|
|
2809
|
+
: [
|
|
2810
|
+
...laneStories(current),
|
|
2811
|
+
{
|
|
2812
|
+
project_id: projectId,
|
|
2813
|
+
story_id: storyId,
|
|
2814
|
+
status: "in_progress",
|
|
2815
|
+
added_at: at,
|
|
2816
|
+
updated_at: at,
|
|
2817
|
+
},
|
|
2818
|
+
],
|
|
2621
2819
|
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
2622
2820
|
project_id: projectId,
|
|
2623
2821
|
story_id: storyId,
|
|
@@ -2644,9 +2842,245 @@ async function runResume(ctx, laneId, opts) {
|
|
|
2644
2842
|
: null,
|
|
2645
2843
|
project_id: next.project_id,
|
|
2646
2844
|
story_id: next.story_id,
|
|
2845
|
+
stories: laneStories(next),
|
|
2846
|
+
story_warning: storyWarning(next) ?? null,
|
|
2647
2847
|
lane: next,
|
|
2648
2848
|
});
|
|
2649
2849
|
}
|
|
2850
|
+
function storyCommandTarget(lane, storyId, project) {
|
|
2851
|
+
const projectId = project?.trim() || lane.project_id;
|
|
2852
|
+
const normalizedStoryId = storyId.trim();
|
|
2853
|
+
if (!projectId) {
|
|
2854
|
+
throw new LanesError("missing_project", "--project is required", {});
|
|
2855
|
+
}
|
|
2856
|
+
if (!normalizedStoryId) {
|
|
2857
|
+
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
2858
|
+
}
|
|
2859
|
+
const story = laneStory(lane, projectId, normalizedStoryId);
|
|
2860
|
+
if (!story) {
|
|
2861
|
+
throw new LanesError("story_not_on_lane", `Story ${projectId}/${normalizedStoryId} is not carried by lane ${lane.lane_id}`, { lane_id: lane.lane_id, project_id: projectId, story_id: normalizedStoryId });
|
|
2862
|
+
}
|
|
2863
|
+
return { projectId, storyId: normalizedStoryId, story };
|
|
2864
|
+
}
|
|
2865
|
+
function updateCarriedStory(lane, projectId, storyId, status, at, extras = {}) {
|
|
2866
|
+
const stories = laneStories(lane).map((story) => story.project_id === projectId && story.story_id === storyId
|
|
2867
|
+
? {
|
|
2868
|
+
...story,
|
|
2869
|
+
status,
|
|
2870
|
+
updated_at: at,
|
|
2871
|
+
blocked_reason: extras.blocked_reason,
|
|
2872
|
+
verified_at: extras.verified_at,
|
|
2873
|
+
verified_by: extras.verified_by,
|
|
2874
|
+
}
|
|
2875
|
+
: story);
|
|
2876
|
+
return {
|
|
2877
|
+
...lane,
|
|
2878
|
+
project_id: projectId,
|
|
2879
|
+
story_id: storyId,
|
|
2880
|
+
stories,
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
function addCarriedStory(lane, projectId, storyId, at) {
|
|
2884
|
+
if (laneStory(lane, projectId, storyId)) {
|
|
2885
|
+
throw new LanesError("story_already_on_lane", `Story ${projectId}/${storyId} is already carried by lane ${lane.lane_id}`, {
|
|
2886
|
+
lane_id: lane.lane_id,
|
|
2887
|
+
project_id: projectId,
|
|
2888
|
+
story_id: storyId,
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
return {
|
|
2892
|
+
...lane,
|
|
2893
|
+
project_id: projectId,
|
|
2894
|
+
story_id: storyId,
|
|
2895
|
+
stories: [
|
|
2896
|
+
...laneStories(lane),
|
|
2897
|
+
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
2898
|
+
],
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function callerMayVerifyStory(ctx, lane) {
|
|
2902
|
+
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
2903
|
+
if (callerLaneId) {
|
|
2904
|
+
return lane.senior.kind === "lane" && lane.senior.id === callerLaneId;
|
|
2905
|
+
}
|
|
2906
|
+
if (lane.senior.kind === "user")
|
|
2907
|
+
return true;
|
|
2908
|
+
if (lane.senior.kind === "lane")
|
|
2909
|
+
return false;
|
|
2910
|
+
return sessionIdFromEnv(ctx.env) === lane.senior.id;
|
|
2911
|
+
}
|
|
2912
|
+
async function ensureStoryThread(ctx, lane, projectId) {
|
|
2913
|
+
const company = lane.mesh_company ?? lane.company;
|
|
2914
|
+
if (!company)
|
|
2915
|
+
return lane;
|
|
2916
|
+
const threadId = await ctx.mesh.ensureProjectThread(company, projectId);
|
|
2917
|
+
if (threadId === lane.thread_id)
|
|
2918
|
+
return lane;
|
|
2919
|
+
return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
2920
|
+
...current,
|
|
2921
|
+
thread_id: threadId,
|
|
2922
|
+
}));
|
|
2923
|
+
}
|
|
2924
|
+
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}) {
|
|
2925
|
+
const company = lane.mesh_company ?? lane.company;
|
|
2926
|
+
if (!company)
|
|
2927
|
+
return;
|
|
2928
|
+
const ready = await ensureStoryThread(ctx, lane, projectId);
|
|
2929
|
+
if (!ready.thread_id)
|
|
2930
|
+
return;
|
|
2931
|
+
await emitLaneMeshEvent(ctx, company, ready.thread_id, event, {
|
|
2932
|
+
lane_id: ready.lane_id,
|
|
2933
|
+
project_id: projectId,
|
|
2934
|
+
story_id: storyId,
|
|
2935
|
+
...payload,
|
|
2936
|
+
});
|
|
2937
|
+
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
2938
|
+
}
|
|
2939
|
+
function sameCommittedStory(current, expected, projectId, storyId) {
|
|
2940
|
+
const currentStory = laneStory(current, projectId, storyId);
|
|
2941
|
+
const expectedStory = laneStory(expected, projectId, storyId);
|
|
2942
|
+
return (current.project_id === expected.project_id &&
|
|
2943
|
+
current.story_id === expected.story_id &&
|
|
2944
|
+
JSON.stringify(currentStory) === JSON.stringify(expectedStory));
|
|
2945
|
+
}
|
|
2946
|
+
async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}) {
|
|
2947
|
+
try {
|
|
2948
|
+
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload);
|
|
2949
|
+
}
|
|
2950
|
+
catch (err) {
|
|
2951
|
+
try {
|
|
2952
|
+
const current = readLane(ctx.hqRoot(), next.lane_id);
|
|
2953
|
+
if (!sameCommittedStory(current, next, projectId, storyId)) {
|
|
2954
|
+
throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed while mesh publication failed`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
|
|
2955
|
+
}
|
|
2956
|
+
updateLane(ctx.hqRoot(), next.lane_id, current.version, "command", (latest) => {
|
|
2957
|
+
if (!sameCommittedStory(latest, next, projectId, storyId)) {
|
|
2958
|
+
throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed before rollback`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
|
|
2959
|
+
}
|
|
2960
|
+
return {
|
|
2961
|
+
...latest,
|
|
2962
|
+
project_id: before.project_id,
|
|
2963
|
+
story_id: before.story_id,
|
|
2964
|
+
stories: before.stories,
|
|
2965
|
+
};
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
catch (rollbackErr) {
|
|
2969
|
+
process.stderr.write(`hq lanes: story transition rollback failed for ${next.lane_id}/${projectId}/${storyId}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}\n`);
|
|
2970
|
+
}
|
|
2971
|
+
throw err;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
async function runStoryList(ctx, laneId) {
|
|
2975
|
+
const lane = readLane(ctx.hqRoot(), laneId);
|
|
2976
|
+
printJson({
|
|
2977
|
+
ok: true,
|
|
2978
|
+
action: "story.list",
|
|
2979
|
+
lane_id: laneId,
|
|
2980
|
+
stories: laneStories(lane),
|
|
2981
|
+
active_stories: activeLaneStories(lane),
|
|
2982
|
+
story_warning: storyWarning(lane) ?? null,
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
2986
|
+
const hqRoot = ctx.hqRoot();
|
|
2987
|
+
let lane = readLane(hqRoot, laneId);
|
|
2988
|
+
const projectId = opts.project?.trim() || lane.project_id;
|
|
2989
|
+
if (!projectId)
|
|
2990
|
+
throw new LanesError("missing_project", "--project is required", {});
|
|
2991
|
+
const normalizedStoryId = storyId.trim();
|
|
2992
|
+
if (!normalizedStoryId)
|
|
2993
|
+
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
2994
|
+
const at = ctx.now().toISOString();
|
|
2995
|
+
lane = await ensureStoryThread(ctx, lane, projectId);
|
|
2996
|
+
const before = lane;
|
|
2997
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => addCarriedStory(current, projectId, normalizedStoryId, at));
|
|
2998
|
+
await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
|
|
2999
|
+
const finalLane = readLane(hqRoot, laneId);
|
|
3000
|
+
printJson({
|
|
3001
|
+
ok: true,
|
|
3002
|
+
action: "story.add",
|
|
3003
|
+
lane_id: laneId,
|
|
3004
|
+
project_id: projectId,
|
|
3005
|
+
story_id: normalizedStoryId,
|
|
3006
|
+
status: "in_progress",
|
|
3007
|
+
story_warning: storyWarning(finalLane) ?? null,
|
|
3008
|
+
lane: finalLane,
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
3012
|
+
const hqRoot = ctx.hqRoot();
|
|
3013
|
+
let lane = readLane(hqRoot, laneId);
|
|
3014
|
+
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
3015
|
+
if (target.story.status === "done") {
|
|
3016
|
+
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
|
|
3017
|
+
lane_id: laneId,
|
|
3018
|
+
project_id: target.projectId,
|
|
3019
|
+
story_id: target.storyId,
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
const at = ctx.now().toISOString();
|
|
3023
|
+
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
3024
|
+
const before = lane;
|
|
3025
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at));
|
|
3026
|
+
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
|
|
3027
|
+
const finalLane = readLane(hqRoot, laneId);
|
|
3028
|
+
printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
3029
|
+
}
|
|
3030
|
+
async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
3031
|
+
const hqRoot = ctx.hqRoot();
|
|
3032
|
+
let lane = readLane(hqRoot, laneId);
|
|
3033
|
+
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
3034
|
+
if (target.story.status === "done") {
|
|
3035
|
+
throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
|
|
3036
|
+
lane_id: laneId,
|
|
3037
|
+
project_id: target.projectId,
|
|
3038
|
+
story_id: target.storyId,
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
const reason = opts.reason?.trim();
|
|
3042
|
+
if (!reason)
|
|
3043
|
+
throw new LanesError("missing_block_reason", "--reason is required", {});
|
|
3044
|
+
const at = ctx.now().toISOString();
|
|
3045
|
+
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
3046
|
+
const before = lane;
|
|
3047
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason }));
|
|
3048
|
+
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
|
|
3049
|
+
const finalLane = readLane(hqRoot, laneId);
|
|
3050
|
+
printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
3051
|
+
}
|
|
3052
|
+
async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
3053
|
+
const hqRoot = ctx.hqRoot();
|
|
3054
|
+
let lane = readLane(hqRoot, laneId);
|
|
3055
|
+
if (!opts.verified) {
|
|
3056
|
+
throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
|
|
3057
|
+
}
|
|
3058
|
+
if (!callerMayVerifyStory(ctx, lane)) {
|
|
3059
|
+
throw new LanesError("senior_verification_required", `Caller is not the senior for lane ${laneId}`, {
|
|
3060
|
+
lane_id: laneId,
|
|
3061
|
+
senior: lane.senior,
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
const target = storyCommandTarget(lane, storyId, opts.project);
|
|
3065
|
+
if (target.story.status !== "review") {
|
|
3066
|
+
throw new LanesError("story_not_ready_for_verification", `Story ${target.projectId}/${target.storyId} must be in review before senior verification`, {
|
|
3067
|
+
lane_id: laneId,
|
|
3068
|
+
project_id: target.projectId,
|
|
3069
|
+
story_id: target.storyId,
|
|
3070
|
+
status: target.story.status,
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
const at = ctx.now().toISOString();
|
|
3074
|
+
lane = await ensureStoryThread(ctx, lane, target.projectId);
|
|
3075
|
+
const before = lane;
|
|
3076
|
+
const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
|
|
3077
|
+
verified_at: at,
|
|
3078
|
+
verified_by: current.senior,
|
|
3079
|
+
}));
|
|
3080
|
+
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
|
|
3081
|
+
const finalLane = readLane(hqRoot, laneId);
|
|
3082
|
+
printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
|
|
3083
|
+
}
|
|
2650
3084
|
/**
|
|
2651
3085
|
* Registration state of the drain hook in the lane's account home, read the
|
|
2652
3086
|
* way the provider reads it. Null when the lane has no bound account yet
|
|
@@ -3443,6 +3877,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
3443
3877
|
.option("--mesh-company <slug|uid>", "Explicit Work Mesh company for a personal lane; membership is verified")
|
|
3444
3878
|
.requiredOption("--project <slug>", "HQ project slug")
|
|
3445
3879
|
.requiredOption("--story <id>", "Story id from the project's prd.json")
|
|
3880
|
+
.option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
|
|
3446
3881
|
.requiredOption("--brief-file <path>", "Path to the lane brief")
|
|
3447
3882
|
.option("--worker <name>", "HQ worker id from the registry")
|
|
3448
3883
|
.option("--provider <name>", "claude | codex | grok")
|
|
@@ -3489,6 +3924,54 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
3489
3924
|
.action(wrap(async (laneId) => {
|
|
3490
3925
|
await runShow(ctx, laneId);
|
|
3491
3926
|
}));
|
|
3927
|
+
const stories = lanes
|
|
3928
|
+
.command("story")
|
|
3929
|
+
.description("Track several project stories on one lane");
|
|
3930
|
+
stories
|
|
3931
|
+
.command("list")
|
|
3932
|
+
.description("List the stories carried by a lane")
|
|
3933
|
+
.argument("<lane-id>", "Lane id")
|
|
3934
|
+
.action(wrap(async (laneId) => {
|
|
3935
|
+
await runStoryList(ctx, laneId);
|
|
3936
|
+
}));
|
|
3937
|
+
stories
|
|
3938
|
+
.command("add")
|
|
3939
|
+
.description("Add and start another story on a lane")
|
|
3940
|
+
.argument("<lane-id>", "Lane id")
|
|
3941
|
+
.argument("<story-id>", "Story id")
|
|
3942
|
+
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
3943
|
+
.action(wrap(async (laneId, storyId, opts) => {
|
|
3944
|
+
await runStoryAdd(ctx, laneId, storyId, opts);
|
|
3945
|
+
}));
|
|
3946
|
+
stories
|
|
3947
|
+
.command("start")
|
|
3948
|
+
.description("Start or resume a carried story")
|
|
3949
|
+
.argument("<lane-id>", "Lane id")
|
|
3950
|
+
.argument("<story-id>", "Story id")
|
|
3951
|
+
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
3952
|
+
.action(wrap(async (laneId, storyId, opts) => {
|
|
3953
|
+
await runStoryStart(ctx, laneId, storyId, opts);
|
|
3954
|
+
}));
|
|
3955
|
+
stories
|
|
3956
|
+
.command("block")
|
|
3957
|
+
.description("Mark one carried story blocked")
|
|
3958
|
+
.argument("<lane-id>", "Lane id")
|
|
3959
|
+
.argument("<story-id>", "Story id")
|
|
3960
|
+
.requiredOption("--reason <text>", "Why the story is blocked")
|
|
3961
|
+
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
3962
|
+
.action(wrap(async (laneId, storyId, opts) => {
|
|
3963
|
+
await runStoryBlock(ctx, laneId, storyId, opts);
|
|
3964
|
+
}));
|
|
3965
|
+
stories
|
|
3966
|
+
.command("done")
|
|
3967
|
+
.description("Mark a reviewed story done after senior verification")
|
|
3968
|
+
.argument("<lane-id>", "Lane id")
|
|
3969
|
+
.argument("<story-id>", "Story id")
|
|
3970
|
+
.requiredOption("--verified", "Confirm senior review or merge")
|
|
3971
|
+
.option("--project <slug>", "Project id; defaults to the lane's current project")
|
|
3972
|
+
.action(wrap(async (laneId, storyId, opts) => {
|
|
3973
|
+
await runStoryDone(ctx, laneId, storyId, opts);
|
|
3974
|
+
}));
|
|
3492
3975
|
lanes
|
|
3493
3976
|
.command("message")
|
|
3494
3977
|
.description("Send a follow-up message to a lane")
|