@indigoai-us/hq-cli 5.167.0 → 5.169.0
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 +30 -0
- package/dist/command-catalog.generated.d.ts +88 -1
- package/dist/command-catalog.generated.js +112 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +79 -22
- package/dist/commands/lanes.js +146 -31
- package/dist/commands/mesh.js +13 -19
- package/dist/commands/outpost-pruner.d.ts +3 -0
- package/dist/commands/outpost-pruner.js +125 -0
- package/dist/lib/daemon/catalog.d.ts +11 -1
- package/dist/lib/daemon/catalog.js +31 -6
- package/dist/lib/daemon/install.d.ts +7 -0
- package/dist/lib/daemon/install.js +18 -2
- package/dist/lib/daemon/legacy.js +6 -1
- package/dist/lib/daemon/machine-type.d.ts +4 -1
- package/dist/lib/daemon/machine-type.js +29 -9
- package/dist/lib/daemon/migrate.js +2 -2
- package/dist/lib/daemon/remote-control.d.ts +32 -9
- package/dist/lib/daemon/remote-control.js +55 -11
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +43 -2
- package/dist/lib/daemon/tools.d.ts +52 -0
- package/dist/lib/daemon/tools.js +95 -0
- package/dist/lib/daemon/updater.d.ts +23 -15
- package/dist/lib/daemon/updater.js +97 -60
- package/dist/lib/lanes/mesh-events.d.ts +4 -0
- package/dist/lib/lanes/mesh-events.js +72 -4
- package/dist/lib/lanes/outpost-pruner-install.d.ts +37 -0
- package/dist/lib/lanes/outpost-pruner-install.js +226 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.d.ts +19 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.js +76 -0
- package/dist/lib/lanes/outpost-worktree-pruner.d.ts +60 -0
- package/dist/lib/lanes/outpost-worktree-pruner.js +1135 -0
- package/dist/lib/mesh/live/session-events-client.js +3 -2
- package/dist/lib/mesh/project-board.d.ts +7 -0
- package/dist/lib/mesh/project-board.js +29 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
13
14
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
14
15
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
15
16
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
@@ -1339,13 +1340,23 @@ async function emitLaneTerminalStateSoft(ctx, lane) {
|
|
|
1339
1340
|
}
|
|
1340
1341
|
}
|
|
1341
1342
|
async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1343
|
+
let ready = lane;
|
|
1344
|
+
if (ctx.mesh.ensureLaneThread) {
|
|
1345
|
+
try {
|
|
1346
|
+
ready = await ensureLaneThreadRecord(ctx, lane, lane.project_id, lane.story_id, "running");
|
|
1347
|
+
}
|
|
1348
|
+
catch (err) {
|
|
1349
|
+
const warning = storyNotOnBoardWarning(err);
|
|
1350
|
+
if (warning !== undefined)
|
|
1351
|
+
return [warning];
|
|
1352
|
+
throw err;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1345
1355
|
const stableThreadId = ready.thread_id ?? threadId;
|
|
1346
1356
|
if (!stableThreadId)
|
|
1347
|
-
return;
|
|
1357
|
+
return [];
|
|
1348
1358
|
const failures = [];
|
|
1359
|
+
const warnings = [];
|
|
1349
1360
|
for (const story of laneStories(ready)) {
|
|
1350
1361
|
try {
|
|
1351
1362
|
await emitLaneMeshEvent(ctx, company, stableThreadId, "lane_started", {
|
|
@@ -1354,14 +1365,59 @@ async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
|
1354
1365
|
project_id: story.project_id,
|
|
1355
1366
|
story_id: story.story_id,
|
|
1356
1367
|
});
|
|
1357
|
-
await ctx.mesh.patchStoryStatus(company, story.project_id, story.story_id, "in_progress");
|
|
1358
1368
|
}
|
|
1359
1369
|
catch (err) {
|
|
1370
|
+
const warning = storyNotOnBoardWarning(err);
|
|
1371
|
+
if (warning !== undefined) {
|
|
1372
|
+
warnings.push(warning);
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1360
1375
|
failures.push(err);
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
await ctx.mesh.patchStoryStatus(company, story.project_id, story.story_id, "in_progress");
|
|
1380
|
+
}
|
|
1381
|
+
catch (err) {
|
|
1382
|
+
const warning = storyNotOnBoardWarning(err);
|
|
1383
|
+
if (warning !== undefined)
|
|
1384
|
+
warnings.push(warning);
|
|
1385
|
+
else
|
|
1386
|
+
failures.push(err);
|
|
1361
1387
|
}
|
|
1362
1388
|
}
|
|
1363
1389
|
if (failures.length > 0)
|
|
1364
1390
|
throw failures[0];
|
|
1391
|
+
return warnings;
|
|
1392
|
+
}
|
|
1393
|
+
async function assertStoriesOnBoard(ctx, company, projectId, storyIds) {
|
|
1394
|
+
const check = ctx.mesh.assertProjectStoriesOnBoard;
|
|
1395
|
+
if (!check) {
|
|
1396
|
+
throw new LanesError("story_board_check_unavailable", "Cannot confirm stories are on the Work Mesh Board; refusing to write lane state", { project_id: projectId, story_id: storyIds[0] ?? null });
|
|
1397
|
+
}
|
|
1398
|
+
await check.call(ctx.mesh, company, projectId, storyIds);
|
|
1399
|
+
}
|
|
1400
|
+
function storyNotOnBoardWarning(err) {
|
|
1401
|
+
return isLanesError(err) && err.error === "story_not_on_board"
|
|
1402
|
+
? err.message
|
|
1403
|
+
: undefined;
|
|
1404
|
+
}
|
|
1405
|
+
function combinedStoryWarning(lane, additional = []) {
|
|
1406
|
+
const warnings = [storyWarning(lane), ...additional]
|
|
1407
|
+
.filter((warning) => warning !== undefined && warning.trim().length > 0);
|
|
1408
|
+
return [...new Set(warnings)].join(" ") || null;
|
|
1409
|
+
}
|
|
1410
|
+
async function storyBoardWarning(ctx, company, projectId, storyIds) {
|
|
1411
|
+
try {
|
|
1412
|
+
await assertStoriesOnBoard(ctx, company, projectId, storyIds);
|
|
1413
|
+
return undefined;
|
|
1414
|
+
}
|
|
1415
|
+
catch (err) {
|
|
1416
|
+
const warning = storyNotOnBoardWarning(err);
|
|
1417
|
+
if (warning !== undefined)
|
|
1418
|
+
return warning;
|
|
1419
|
+
throw err;
|
|
1420
|
+
}
|
|
1365
1421
|
}
|
|
1366
1422
|
export const PICKUP_TERMINAL_REASONS = [
|
|
1367
1423
|
"account_not_logged_in",
|
|
@@ -1630,6 +1686,12 @@ async function runCreate(ctx, opts) {
|
|
|
1630
1686
|
}
|
|
1631
1687
|
const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
|
|
1632
1688
|
assertSameTenant(company, senior, seniorCompany);
|
|
1689
|
+
const createStoryWarnings = [];
|
|
1690
|
+
if (meshCompany) {
|
|
1691
|
+
const warning = await storyBoardWarning(ctx, meshCompany, project, storyIds);
|
|
1692
|
+
if (warning !== undefined)
|
|
1693
|
+
createStoryWarnings.push(warning);
|
|
1694
|
+
}
|
|
1633
1695
|
let resolvedWorker;
|
|
1634
1696
|
if (opts.worker) {
|
|
1635
1697
|
resolvedWorker = resolveLaneWorker(hqRoot, opts.worker, company?.slug);
|
|
@@ -2078,19 +2140,25 @@ async function runCreate(ctx, opts) {
|
|
|
2078
2140
|
try {
|
|
2079
2141
|
lane = await ensureLaneThreadRecord(ctx, lane);
|
|
2080
2142
|
}
|
|
2081
|
-
catch {
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
state: "failed",
|
|
2086
|
-
queued_reason: undefined,
|
|
2087
|
-
terminal_reason: "mesh_thread_registration_failed",
|
|
2088
|
-
}));
|
|
2143
|
+
catch (err) {
|
|
2144
|
+
const warning = storyNotOnBoardWarning(err);
|
|
2145
|
+
if (warning !== undefined) {
|
|
2146
|
+
createStoryWarnings.push(warning);
|
|
2089
2147
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2148
|
+
else {
|
|
2149
|
+
try {
|
|
2150
|
+
lane = casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => ({
|
|
2151
|
+
...current,
|
|
2152
|
+
state: "failed",
|
|
2153
|
+
queued_reason: undefined,
|
|
2154
|
+
terminal_reason: "mesh_thread_registration_failed",
|
|
2155
|
+
}));
|
|
2156
|
+
}
|
|
2157
|
+
catch {
|
|
2158
|
+
throw new LanesError("mesh_thread_registration_failed", `Work Mesh lane registration failed for ${lane.lane_id}, and its local record could not be marked terminal.`, { lane_id: lane.lane_id });
|
|
2159
|
+
}
|
|
2160
|
+
throw new LanesError("mesh_thread_registration_failed", `Work Mesh lane registration failed; lane ${lane.lane_id} was marked failed.`, { lane_id: lane.lane_id, state: lane.state });
|
|
2092
2161
|
}
|
|
2093
|
-
throw new LanesError("mesh_thread_registration_failed", `Work Mesh lane registration failed; lane ${lane.lane_id} was marked failed.`, { lane_id: lane.lane_id, state: lane.state });
|
|
2094
2162
|
}
|
|
2095
2163
|
}
|
|
2096
2164
|
// Thread registration runs after persistence; anchor the reservation after
|
|
@@ -2136,8 +2204,9 @@ async function runCreate(ctx, opts) {
|
|
|
2136
2204
|
runCompaction: ctx.runCompaction,
|
|
2137
2205
|
locateTranscript: ctx.locateTranscript,
|
|
2138
2206
|
});
|
|
2139
|
-
if (meshCompany)
|
|
2140
|
-
await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id);
|
|
2207
|
+
if (meshCompany) {
|
|
2208
|
+
createStoryWarnings.push(...await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id));
|
|
2209
|
+
}
|
|
2141
2210
|
}
|
|
2142
2211
|
else if (outpostId) {
|
|
2143
2212
|
started = await dispatchCreatedOutpostLane(ctx, {
|
|
@@ -2173,7 +2242,7 @@ async function runCreate(ctx, opts) {
|
|
|
2173
2242
|
project_id: started.project_id,
|
|
2174
2243
|
story_id: started.story_id,
|
|
2175
2244
|
stories: laneStories(started),
|
|
2176
|
-
story_warning:
|
|
2245
|
+
story_warning: combinedStoryWarning(started, createStoryWarnings),
|
|
2177
2246
|
thread_id: started.thread_id ?? null,
|
|
2178
2247
|
senior: started.senior,
|
|
2179
2248
|
depth: started.depth,
|
|
@@ -4137,6 +4206,17 @@ async function ensureStoryThread(ctx, lane, projectId, storyId = lane.story_id)
|
|
|
4137
4206
|
return lane;
|
|
4138
4207
|
return ensureLaneThreadRecord(ctx, lane, projectId, storyId);
|
|
4139
4208
|
}
|
|
4209
|
+
async function ensureStoryThreadForTransition(ctx, lane, projectId, storyId) {
|
|
4210
|
+
try {
|
|
4211
|
+
return { lane: await ensureStoryThread(ctx, lane, projectId, storyId) };
|
|
4212
|
+
}
|
|
4213
|
+
catch (err) {
|
|
4214
|
+
const warning = storyNotOnBoardWarning(err);
|
|
4215
|
+
if (warning === undefined)
|
|
4216
|
+
throw err;
|
|
4217
|
+
return { lane, warning };
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4140
4220
|
async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
|
|
4141
4221
|
const company = lane.mesh_company ?? lane.company;
|
|
4142
4222
|
if (!company)
|
|
@@ -4184,8 +4264,12 @@ function sameCommittedStory(current, expected, projectId, storyId) {
|
|
|
4184
4264
|
async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}) {
|
|
4185
4265
|
try {
|
|
4186
4266
|
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload);
|
|
4267
|
+
return undefined;
|
|
4187
4268
|
}
|
|
4188
4269
|
catch (err) {
|
|
4270
|
+
const warning = storyNotOnBoardWarning(err);
|
|
4271
|
+
if (warning !== undefined)
|
|
4272
|
+
return warning;
|
|
4189
4273
|
try {
|
|
4190
4274
|
const current = readLane(ctx.hqRoot(), next.lane_id);
|
|
4191
4275
|
if (!sameCommittedStory(current, next, projectId, storyId)) {
|
|
@@ -4230,14 +4314,26 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
4230
4314
|
const normalizedStoryId = storyId.trim();
|
|
4231
4315
|
if (!normalizedStoryId)
|
|
4232
4316
|
throw new LanesError("missing_story", "<story-id> is required", {});
|
|
4317
|
+
const company = lane.mesh_company ?? lane.company;
|
|
4318
|
+
const warnings = [];
|
|
4319
|
+
if (company) {
|
|
4320
|
+
const warning = await storyBoardWarning(ctx, company, projectId, [normalizedStoryId]);
|
|
4321
|
+
if (warning !== undefined)
|
|
4322
|
+
warnings.push(warning);
|
|
4323
|
+
}
|
|
4233
4324
|
const at = ctx.now().toISOString();
|
|
4234
|
-
|
|
4325
|
+
const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
|
|
4326
|
+
lane = thread.lane;
|
|
4327
|
+
if (thread.warning !== undefined)
|
|
4328
|
+
warnings.push(thread.warning);
|
|
4235
4329
|
const before = lane;
|
|
4236
4330
|
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4237
4331
|
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4238
4332
|
return addCarriedStory(current, projectId, normalizedStoryId, at);
|
|
4239
4333
|
});
|
|
4240
|
-
await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
|
|
4334
|
+
const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
|
|
4335
|
+
if (transitionWarning !== undefined)
|
|
4336
|
+
warnings.push(transitionWarning);
|
|
4241
4337
|
const finalLane = readLane(hqRoot, laneId);
|
|
4242
4338
|
printJson({
|
|
4243
4339
|
ok: true,
|
|
@@ -4246,7 +4342,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
4246
4342
|
project_id: projectId,
|
|
4247
4343
|
story_id: normalizedStoryId,
|
|
4248
4344
|
status: "in_progress",
|
|
4249
|
-
story_warning:
|
|
4345
|
+
story_warning: combinedStoryWarning(finalLane, warnings),
|
|
4250
4346
|
lane: finalLane,
|
|
4251
4347
|
});
|
|
4252
4348
|
}
|
|
@@ -4263,15 +4359,21 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
|
|
|
4263
4359
|
});
|
|
4264
4360
|
}
|
|
4265
4361
|
const at = ctx.now().toISOString();
|
|
4266
|
-
|
|
4362
|
+
const warnings = [];
|
|
4363
|
+
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
4364
|
+
lane = thread.lane;
|
|
4365
|
+
if (thread.warning !== undefined)
|
|
4366
|
+
warnings.push(thread.warning);
|
|
4267
4367
|
const before = lane;
|
|
4268
4368
|
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4269
4369
|
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4270
4370
|
return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
|
|
4271
4371
|
});
|
|
4272
|
-
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
|
|
4372
|
+
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
|
|
4373
|
+
if (transitionWarning !== undefined)
|
|
4374
|
+
warnings.push(transitionWarning);
|
|
4273
4375
|
const finalLane = readLane(hqRoot, laneId);
|
|
4274
|
-
printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning:
|
|
4376
|
+
printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
4275
4377
|
}
|
|
4276
4378
|
async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
4277
4379
|
const hqRoot = ctx.hqRoot();
|
|
@@ -4289,15 +4391,21 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
|
|
|
4289
4391
|
if (!reason)
|
|
4290
4392
|
throw new LanesError("missing_block_reason", "--reason is required", {});
|
|
4291
4393
|
const at = ctx.now().toISOString();
|
|
4292
|
-
|
|
4394
|
+
const warnings = [];
|
|
4395
|
+
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
4396
|
+
lane = thread.lane;
|
|
4397
|
+
if (thread.warning !== undefined)
|
|
4398
|
+
warnings.push(thread.warning);
|
|
4293
4399
|
const before = lane;
|
|
4294
4400
|
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4295
4401
|
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
4296
4402
|
return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
|
|
4297
4403
|
});
|
|
4298
|
-
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
|
|
4404
|
+
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
|
|
4405
|
+
if (transitionWarning !== undefined)
|
|
4406
|
+
warnings.push(transitionWarning);
|
|
4299
4407
|
const finalLane = readLane(hqRoot, laneId);
|
|
4300
|
-
printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning:
|
|
4408
|
+
printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
4301
4409
|
}
|
|
4302
4410
|
async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
4303
4411
|
const hqRoot = ctx.hqRoot();
|
|
@@ -4322,7 +4430,11 @@ async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
|
4322
4430
|
});
|
|
4323
4431
|
}
|
|
4324
4432
|
const at = ctx.now().toISOString();
|
|
4325
|
-
|
|
4433
|
+
const warnings = [];
|
|
4434
|
+
const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
|
|
4435
|
+
lane = thread.lane;
|
|
4436
|
+
if (thread.warning !== undefined)
|
|
4437
|
+
warnings.push(thread.warning);
|
|
4326
4438
|
const before = lane;
|
|
4327
4439
|
const next = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4328
4440
|
assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
|
|
@@ -4331,9 +4443,11 @@ async function runStoryDone(ctx, laneId, storyId, opts) {
|
|
|
4331
4443
|
verified_by: current.senior,
|
|
4332
4444
|
});
|
|
4333
4445
|
});
|
|
4334
|
-
await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
|
|
4446
|
+
const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
|
|
4447
|
+
if (transitionWarning !== undefined)
|
|
4448
|
+
warnings.push(transitionWarning);
|
|
4335
4449
|
const finalLane = readLane(hqRoot, laneId);
|
|
4336
|
-
printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning:
|
|
4450
|
+
printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
|
|
4337
4451
|
}
|
|
4338
4452
|
/**
|
|
4339
4453
|
* Registration state of the drain hook in the lane's account home, read the
|
|
@@ -5385,6 +5499,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
5385
5499
|
const lanes = parent
|
|
5386
5500
|
.command("lanes", { hidden: true })
|
|
5387
5501
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
5502
|
+
registerOutpostPrunerCommand(lanes);
|
|
5388
5503
|
lanes
|
|
5389
5504
|
.command("create")
|
|
5390
5505
|
.description("Register a lane, run admission, and return immediately")
|
package/dist/commands/mesh.js
CHANGED
|
@@ -14,7 +14,7 @@ import chalk from "chalk";
|
|
|
14
14
|
import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
|
|
15
15
|
import * as readline from "node:readline/promises";
|
|
16
16
|
import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
17
|
-
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard } from "../lib/mesh/project-board.js";
|
|
17
|
+
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, formatStoryStatusChanges, mergeProjectStories, } from "../lib/mesh/project-board.js";
|
|
18
18
|
import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
|
|
19
19
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
20
20
|
import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
@@ -307,23 +307,6 @@ function httpStatus(err) {
|
|
|
307
307
|
}
|
|
308
308
|
return undefined;
|
|
309
309
|
}
|
|
310
|
-
function mergeProjectStories(current, planned) {
|
|
311
|
-
const currentById = new Map(current.map((story) => [story.id, story]));
|
|
312
|
-
const plannedIds = new Set(planned.map((story) => story.id));
|
|
313
|
-
const merged = planned.map((story) => {
|
|
314
|
-
const live = currentById.get(story.id);
|
|
315
|
-
if (!live)
|
|
316
|
-
return story;
|
|
317
|
-
const status = live.status || story.status;
|
|
318
|
-
return {
|
|
319
|
-
...story,
|
|
320
|
-
...(status ? { status } : {}),
|
|
321
|
-
passes: status === "done" ? true : typeof live.passes === "boolean" ? live.passes : story.passes,
|
|
322
|
-
...(live.assigneeUid ? { assigneeUid: live.assigneeUid } : {}),
|
|
323
|
-
};
|
|
324
|
-
});
|
|
325
|
-
return [...merged, ...current.filter((story) => !plannedIds.has(story.id))];
|
|
326
|
-
}
|
|
327
310
|
function mergeProjectRepos(current, planned) {
|
|
328
311
|
const plannedPaths = new Set(planned.map((repo) => repo.path));
|
|
329
312
|
return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
|
|
@@ -386,6 +369,10 @@ async function runProjectSet(projectId, opts) {
|
|
|
386
369
|
registration = ensured.registration;
|
|
387
370
|
}
|
|
388
371
|
else if (opts.name !== undefined || opts.description !== undefined || source !== undefined || explicitRepos !== undefined) {
|
|
372
|
+
const stories = source
|
|
373
|
+
? mergeProjectStories(current.stories, source.stories, { resetStatus: Boolean(opts.resetStatus) })
|
|
374
|
+
: current.stories;
|
|
375
|
+
const statusChangeLine = source ? formatStoryStatusChanges(current.stories, stories) : undefined;
|
|
389
376
|
try {
|
|
390
377
|
view = await putProjectView(token, company.companyUid, projectId, {
|
|
391
378
|
name: opts.name?.trim() || source?.name || current.name,
|
|
@@ -394,7 +381,7 @@ async function runProjectSet(projectId, opts) {
|
|
|
394
381
|
: source?.description !== undefined
|
|
395
382
|
? { description: source.description }
|
|
396
383
|
: { description: current.description }),
|
|
397
|
-
stories
|
|
384
|
+
stories,
|
|
398
385
|
repos: explicitRepos ?? source?.repos ?? current.repos,
|
|
399
386
|
...(current.version !== undefined ? { expectedVersion: current.version } : {}),
|
|
400
387
|
});
|
|
@@ -409,6 +396,12 @@ async function runProjectSet(projectId, opts) {
|
|
|
409
396
|
}
|
|
410
397
|
throw err;
|
|
411
398
|
}
|
|
399
|
+
if (statusChangeLine) {
|
|
400
|
+
if (opts.json)
|
|
401
|
+
console.error(statusChangeLine);
|
|
402
|
+
else
|
|
403
|
+
console.log(statusChangeLine);
|
|
404
|
+
}
|
|
412
405
|
}
|
|
413
406
|
else {
|
|
414
407
|
view = current;
|
|
@@ -1816,6 +1809,7 @@ export function registerMeshCommand(program) {
|
|
|
1816
1809
|
.option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
|
|
1817
1810
|
.option("--channel <channelId>", "Bind the project to an existing company or project channel")
|
|
1818
1811
|
.option("--create", "Create the Board view when the GET is a 404")
|
|
1812
|
+
.option("--reset-status", "Overwrite live story status from the PRD (prints a one-line summary of changes)")
|
|
1819
1813
|
.option("--json", "Print machine-readable JSON")
|
|
1820
1814
|
.action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
|
|
1821
1815
|
project
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { outpostWorktreePrunerEnabled, OUTPOST_WORKTREE_PRUNER_FLAG } from "../lib/lanes/outpost-worktree-pruner-flag.js";
|
|
5
|
+
import { OUTPOST_WORKTREE_PRUNER_DEFAULT_MIN_AGE_DAYS, runOutpostWorktreePruner, } from "../lib/lanes/outpost-worktree-pruner.js";
|
|
6
|
+
import { defaultOutpostPrunerSystemdDir, installOutpostPrunerTimers, uninstallOutpostPrunerTimers, } from "../lib/lanes/outpost-pruner-install.js";
|
|
7
|
+
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
8
|
+
function errorType(error) {
|
|
9
|
+
return error instanceof Error ? error.name : "UnknownError";
|
|
10
|
+
}
|
|
11
|
+
function printJson(value) {
|
|
12
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
13
|
+
}
|
|
14
|
+
function printErrorJson(event, error, fields = {}) {
|
|
15
|
+
printJson({ event, ...fields, error: errorType(error) });
|
|
16
|
+
}
|
|
17
|
+
function runSystemctl(binary, args) {
|
|
18
|
+
const result = spawnSync(binary, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
19
|
+
return {
|
|
20
|
+
status: result.status ?? 1,
|
|
21
|
+
stdout: result.stdout ?? "",
|
|
22
|
+
stderr: result.stderr ?? "",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function systemdRoot(option) {
|
|
26
|
+
return path.resolve(option ?? defaultOutpostPrunerSystemdDir(os.homedir()));
|
|
27
|
+
}
|
|
28
|
+
function skippedResult(trigger, minAgeDays, reason) {
|
|
29
|
+
return {
|
|
30
|
+
event: "outpost.worktree_pruner.run",
|
|
31
|
+
trigger,
|
|
32
|
+
dry_run: false,
|
|
33
|
+
feature_enabled: false,
|
|
34
|
+
skipped: reason,
|
|
35
|
+
flag: OUTPOST_WORKTREE_PRUNER_FLAG,
|
|
36
|
+
min_age_days: minAgeDays,
|
|
37
|
+
candidates: [],
|
|
38
|
+
removed: [],
|
|
39
|
+
kept: [],
|
|
40
|
+
bytes_freed: 0,
|
|
41
|
+
bytes_freed_basis: "statfs_available_byte_delta",
|
|
42
|
+
package_store: { attempted: false, reason: "not_run" },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function registerOutpostPrunerCommand(lanes) {
|
|
46
|
+
const pruner = lanes
|
|
47
|
+
.command("outpost-pruner")
|
|
48
|
+
.description("Inspect, install, and run safe outpost worktree pruning");
|
|
49
|
+
pruner
|
|
50
|
+
.command("run")
|
|
51
|
+
.description("Run the guarded outpost worktree and user-cache cleanup")
|
|
52
|
+
.option("--trigger <kind>", "pressure | daily | manual", "manual")
|
|
53
|
+
.option("--min-age-days <days>", "Minimum age before cleanup (1–365 days)", (value) => Number(value), OUTPOST_WORKTREE_PRUNER_DEFAULT_MIN_AGE_DAYS)
|
|
54
|
+
.option("--dry-run", "List eligible worktrees without deleting files")
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
const trigger = options.trigger ?? "manual";
|
|
57
|
+
const minAgeDays = options.minAgeDays ?? OUTPOST_WORKTREE_PRUNER_DEFAULT_MIN_AGE_DAYS;
|
|
58
|
+
try {
|
|
59
|
+
if (!["pressure", "daily", "manual"].includes(trigger)) {
|
|
60
|
+
throw new TypeError("invalid trigger");
|
|
61
|
+
}
|
|
62
|
+
const featureEnabled = options.dryRun ? false : await outpostWorktreePrunerEnabled();
|
|
63
|
+
if (!options.dryRun && !featureEnabled) {
|
|
64
|
+
printJson(skippedResult(trigger, minAgeDays, "feature_flag_off"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const result = runOutpostWorktreePruner({
|
|
68
|
+
hqRoot: resolveLanesHqRoot(),
|
|
69
|
+
trigger,
|
|
70
|
+
dryRun: Boolean(options.dryRun),
|
|
71
|
+
featureEnabled,
|
|
72
|
+
minAgeDays,
|
|
73
|
+
});
|
|
74
|
+
printJson({ ...result, flag: OUTPOST_WORKTREE_PRUNER_FLAG });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
printErrorJson("outpost.worktree_pruner.run", error, {
|
|
78
|
+
trigger,
|
|
79
|
+
dry_run: Boolean(options.dryRun),
|
|
80
|
+
});
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
pruner
|
|
85
|
+
.command("install")
|
|
86
|
+
.description("Write the pressure and daily systemd user timers")
|
|
87
|
+
.option("--systemd-root <path>", "Systemd user unit directory")
|
|
88
|
+
.option("--enable", "Enable both timers after writing the units")
|
|
89
|
+
.action((options) => {
|
|
90
|
+
try {
|
|
91
|
+
const result = installOutpostPrunerTimers({
|
|
92
|
+
systemdRoot: systemdRoot(options.systemdRoot),
|
|
93
|
+
hqRoot: resolveLanesHqRoot(),
|
|
94
|
+
home: os.homedir(),
|
|
95
|
+
enable: Boolean(options.enable),
|
|
96
|
+
runCommand: options.enable ? runSystemctl : undefined,
|
|
97
|
+
});
|
|
98
|
+
printJson(result);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
printErrorJson("outpost.worktree_pruner.install", error);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
pruner
|
|
106
|
+
.command("uninstall")
|
|
107
|
+
.description("Disable and remove the pressure and daily systemd user timers")
|
|
108
|
+
.option("--systemd-root <path>", "Systemd user unit directory")
|
|
109
|
+
.option("--disable", "Disable both timers before removing their units")
|
|
110
|
+
.action((options) => {
|
|
111
|
+
try {
|
|
112
|
+
const result = uninstallOutpostPrunerTimers({
|
|
113
|
+
systemdRoot: systemdRoot(options.systemdRoot),
|
|
114
|
+
disable: Boolean(options.disable),
|
|
115
|
+
runCommand: options.disable ? runSystemctl : undefined,
|
|
116
|
+
});
|
|
117
|
+
printJson(result);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
printErrorJson("outpost.worktree_pruner.uninstall", error);
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=outpost-pruner.js.map
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* them instead of silently dropping them.
|
|
11
11
|
*/
|
|
12
12
|
import type { MachineType } from "./machine-type.js";
|
|
13
|
-
export declare const SERVICE_NAMES: readonly ["sync", "mesh", "heartbeat", "inbox", "bots", "remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
|
|
13
|
+
export declare const SERVICE_NAMES: readonly ["sync", "mesh", "heartbeat", "inbox", "bots", "remote-control", "codex-remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
|
|
14
14
|
export type ServiceName = (typeof SERVICE_NAMES)[number];
|
|
15
15
|
export declare function isServiceName(value: unknown): value is ServiceName;
|
|
16
16
|
export declare const SERVICE_DESCRIPTIONS: Record<ServiceName, string>;
|
|
@@ -79,7 +79,17 @@ export interface PlanContext {
|
|
|
79
79
|
tmuxPath: string | null;
|
|
80
80
|
/** Short host name, used to name this machine's remote-control session. */
|
|
81
81
|
hostName: string;
|
|
82
|
+
/** The session name the box's own runner used (`--name` in its script), kept when the daemon takes over. */
|
|
83
|
+
remoteControlName: string | null;
|
|
84
|
+
/** Absolute path of the login user's codex binary, or null. */
|
|
85
|
+
codexPath: string | null;
|
|
86
|
+
/** The box's Codex remote-control runner script, when it has one. */
|
|
87
|
+
codexRunnerScript: string | null;
|
|
82
88
|
}
|
|
89
|
+
/** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
|
|
90
|
+
export declare const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
|
|
91
|
+
/** The outpost's Claude runner script, read for the session name it uses. */
|
|
92
|
+
export declare const CLAUDE_RUNNER_SCRIPT = "/usr/local/bin/outpost-runner.sh";
|
|
83
93
|
/** Turn enabled services into concrete units for this host. */
|
|
84
94
|
export declare function planUnits(services: readonly ServiceName[], ctx: PlanContext): UnitPlan[];
|
|
85
95
|
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -16,6 +16,7 @@ export const SERVICE_NAMES = [
|
|
|
16
16
|
"inbox",
|
|
17
17
|
"bots",
|
|
18
18
|
"remote-control",
|
|
19
|
+
"codex-remote-control",
|
|
19
20
|
"session-host",
|
|
20
21
|
"lanes-reconcile",
|
|
21
22
|
"search-index",
|
|
@@ -33,16 +34,17 @@ export const SERVICE_DESCRIPTIONS = {
|
|
|
33
34
|
inbox: "picks up the agent's incoming messages and tasks",
|
|
34
35
|
bots: "runs local bots",
|
|
35
36
|
"remote-control": "keeps Claude remote control running",
|
|
37
|
+
"codex-remote-control": "keeps Codex remote control running",
|
|
36
38
|
"session-host": "lets the desktop app open this machine's Claude sessions",
|
|
37
39
|
"lanes-reconcile": "cleans up stalled background lanes",
|
|
38
40
|
"search-index": "keeps local search up to date",
|
|
39
41
|
"jobs-reconcile": "keeps scheduled jobs matched to the cloud",
|
|
40
|
-
updater: "keeps the hq CLI
|
|
42
|
+
updater: "keeps the hq CLI, Claude, Codex and Grok up to date",
|
|
41
43
|
housekeeping: "rotates daemon logs",
|
|
42
44
|
};
|
|
43
45
|
/** The services each machine type runs without being asked. */
|
|
44
46
|
export const DEFAULT_SERVICES = {
|
|
45
|
-
desktop: ["sync", "mesh", "heartbeat", "bots", "lanes-reconcile", "search-index", "housekeeping"],
|
|
47
|
+
desktop: ["sync", "mesh", "heartbeat", "bots", "lanes-reconcile", "search-index", "updater", "housekeeping"],
|
|
46
48
|
"self-hosted": [
|
|
47
49
|
"sync",
|
|
48
50
|
"mesh",
|
|
@@ -59,6 +61,7 @@ export const DEFAULT_SERVICES = {
|
|
|
59
61
|
"mesh",
|
|
60
62
|
"heartbeat",
|
|
61
63
|
"remote-control",
|
|
64
|
+
"codex-remote-control",
|
|
62
65
|
"session-host",
|
|
63
66
|
"lanes-reconcile",
|
|
64
67
|
"search-index",
|
|
@@ -93,6 +96,10 @@ function firstBlock(checks) {
|
|
|
93
96
|
const hit = checks.find(([blocked]) => blocked);
|
|
94
97
|
return hit ? { state: "waiting", reason: hit[1] } : undefined;
|
|
95
98
|
}
|
|
99
|
+
/** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
|
|
100
|
+
export const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
|
|
101
|
+
/** The outpost's Claude runner script, read for the session name it uses. */
|
|
102
|
+
export const CLAUDE_RUNNER_SCRIPT = "/usr/local/bin/outpost-runner.sh";
|
|
96
103
|
const NO_HQ_ROOT = "no HQ folder configured (run `hq daemon install --hq-root <path>`)";
|
|
97
104
|
const NOT_SIGNED_IN = "not signed in (run `hq login`)";
|
|
98
105
|
const KIT_NOT_SET_UP = "the agent kit is not set up on this machine (run `hq agent kit install`)";
|
|
@@ -205,9 +212,6 @@ function planService(service, ctx) {
|
|
|
205
212
|
intervalSec: 6 * 3600,
|
|
206
213
|
initialDelaySec: 900,
|
|
207
214
|
command: { type: "hq", args: ["daemon", "service", "self-update"] },
|
|
208
|
-
blocked: firstBlock([
|
|
209
|
-
[!ctx.cliSelfUpdatable, "this hq CLI is not a global install, so it is updated with the project or app that owns it"],
|
|
210
|
-
]),
|
|
211
215
|
}),
|
|
212
216
|
];
|
|
213
217
|
case "jobs-reconcile": {
|
|
@@ -232,12 +236,33 @@ function planService(service, ctx) {
|
|
|
232
236
|
}),
|
|
233
237
|
];
|
|
234
238
|
}
|
|
239
|
+
case "codex-remote-control": {
|
|
240
|
+
if (ctx.platform === "win32")
|
|
241
|
+
return [unavailable(service, "not supported on Windows")];
|
|
242
|
+
const script = ctx.codexRunnerScript;
|
|
243
|
+
return [
|
|
244
|
+
base(service, {
|
|
245
|
+
command: {
|
|
246
|
+
type: "hq",
|
|
247
|
+
args: ["daemon", "service", "codex-remote-control", "--script", script ?? "", "--codex", ctx.codexPath ?? ""],
|
|
248
|
+
},
|
|
249
|
+
env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
|
|
250
|
+
blocked: firstBlock([
|
|
251
|
+
[!script, `this machine has no Codex remote-control runner (${CODEX_RUNNER_SCRIPT})`],
|
|
252
|
+
[!ctx.codexPath, "the codex CLI is not installed"],
|
|
253
|
+
[!ctx.tmuxPath, "tmux is not installed (Codex remote control runs inside tmux)"],
|
|
254
|
+
[!ctx.hqRoot, NO_HQ_ROOT],
|
|
255
|
+
]),
|
|
256
|
+
}),
|
|
257
|
+
];
|
|
258
|
+
}
|
|
235
259
|
case "remote-control": {
|
|
236
260
|
if (ctx.platform === "win32")
|
|
237
261
|
return [unavailable(service, "not supported on Windows yet")];
|
|
238
262
|
// Outposts keep the runner's session name and permission mode.
|
|
239
263
|
const outpost = ctx.machineType === "outpost";
|
|
240
|
-
const
|
|
264
|
+
const name = ctx.remoteControlName ?? (outpost ? "outpost-1" : ctx.hostName);
|
|
265
|
+
const args = ["daemon", "service", "remote-control", "--claude", ctx.claudePath ?? "", "--name", name];
|
|
241
266
|
if (outpost)
|
|
242
267
|
args.push("--permission-mode", "bypassPermissions");
|
|
243
268
|
return [
|
|
@@ -64,6 +64,13 @@ export interface DaemonUnitResult {
|
|
|
64
64
|
export interface InstallDeps {
|
|
65
65
|
run: RunCommand;
|
|
66
66
|
}
|
|
67
|
+
export declare const ROOT_REFUSAL = "hq daemon runs its services as the machine's login user, never as root.";
|
|
68
|
+
/**
|
|
69
|
+
* Why the daemon must not run here as root, or null when it may. Agent kits
|
|
70
|
+
* are the one exception: they run in containers where root is often the only
|
|
71
|
+
* account.
|
|
72
|
+
*/
|
|
73
|
+
export declare function daemonRootRefusal(isRoot: boolean, machineType: MachineType): string | null;
|
|
67
74
|
/** Boxes run the daemon as a system unit; everything else per user. */
|
|
68
75
|
export declare function installScopeFor(machineType: MachineType): InstallScope;
|
|
69
76
|
export declare function planDaemonUnit(host: DaemonHost, options: InstallOptions): DaemonUnitPlan;
|