@indigoai-us/hq-cli 5.286.0 → 5.287.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 +19 -0
- package/assets/scaffold/core/scripts/clean-worktree-reconcile-handoff.sh +171 -0
- package/assets/scaffold/core/scripts/resume-thread-lock.sh +285 -0
- package/assets/scaffold/core/scripts/verify-story-deliverables.sh +148 -0
- package/assets/scaffold/core/scripts/worktree-gc.sh +362 -0
- package/dist/command-catalog.generated.d.ts +52 -0
- package/dist/command-catalog.generated.js +64 -0
- package/dist/commands/cloud-provision.js +99 -78
- package/dist/commands/core-native-handoff-flag.d.ts +4 -0
- package/dist/commands/core-native-handoff-flag.js +7 -0
- package/dist/commands/core.js +45 -0
- package/dist/commands/lanes.js +1 -28
- package/dist/commands/team-sync.js +71 -10
- package/dist/fast-lanes-list-route.js +2 -0
- package/dist/fast-lanes-list.js +1 -29
- package/dist/lib/core-utils/sentry-report.js +5 -32
- package/dist/lib/core-utils/worktree-resume.d.ts +18 -0
- package/dist/lib/core-utils/worktree-resume.js +1182 -0
- package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +21 -0
- package/dist/lib/lanes/outpost-snapshot-refresh.js +329 -0
- package/package.json +1 -1
|
@@ -266,6 +266,21 @@ export function writeCompanyConfig(hqRoot, slug, config) {
|
|
|
266
266
|
return true;
|
|
267
267
|
}
|
|
268
268
|
// ── Vault HTTP client (real impl) ────────────────────────────────────────────
|
|
269
|
+
const VAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
270
|
+
/** Run a complete Vault HTTP operation under a finite deadline. */
|
|
271
|
+
async function withVaultRequest(url, init, readResponse) {
|
|
272
|
+
const deadline = AbortSignal.timeout(VAULT_REQUEST_TIMEOUT_MS);
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch(url, { ...init, signal: deadline });
|
|
275
|
+
return await readResponse(res);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
if (deadline.aborted) {
|
|
279
|
+
throw new ProvisionError(1, `Vault request timed out after ${VAULT_REQUEST_TIMEOUT_MS}ms`);
|
|
280
|
+
}
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
269
284
|
/**
|
|
270
285
|
* Default vault HTTP client backed by global `fetch`. Uses the `/v1/entities`
|
|
271
286
|
* route surface (matches AppBar `vault_client.rs` and hq-pro handler routes).
|
|
@@ -284,52 +299,56 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
|
|
|
284
299
|
return {
|
|
285
300
|
async listMyPersonEntities() {
|
|
286
301
|
const url = `${apiUrl.replace(/\/$/, "")}/entity/by-type/person`;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
302
|
+
return withVaultRequest(url, { method: "GET", headers }, async (res) => {
|
|
303
|
+
if (res.status === 404)
|
|
304
|
+
return [];
|
|
305
|
+
if (!res.ok) {
|
|
306
|
+
const body = await safeBody(res);
|
|
307
|
+
throw new ProvisionError(1, `Vault GET /entity/by-type/person failed: ${res.status} ${res.statusText} — ${body}`);
|
|
308
|
+
}
|
|
309
|
+
const data = (await res.json());
|
|
310
|
+
return data.entities ?? [];
|
|
311
|
+
});
|
|
296
312
|
},
|
|
297
313
|
async findCompanyBySlug(slug) {
|
|
298
314
|
const url = `${apiUrl.replace(/\/$/, "")}/entity/by-slug/company/${encodeURIComponent(slug)}`;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
315
|
+
return withVaultRequest(url, { method: "GET", headers }, async (res) => {
|
|
316
|
+
if (res.status === 404)
|
|
317
|
+
return null;
|
|
318
|
+
if (!res.ok) {
|
|
319
|
+
const body = await safeBody(res);
|
|
320
|
+
throw new ProvisionError(1, `Vault GET by-slug failed: ${res.status} ${res.statusText} — ${body}`);
|
|
321
|
+
}
|
|
322
|
+
const data = (await res.json());
|
|
323
|
+
if (!data.entity) {
|
|
324
|
+
throw new ProvisionError(1, `Vault GET by-slug returned 200 with no entity body`);
|
|
325
|
+
}
|
|
326
|
+
return data.entity;
|
|
327
|
+
});
|
|
311
328
|
},
|
|
312
329
|
async checkSlugInMyNamespace(slug) {
|
|
313
330
|
const url = `${apiUrl.replace(/\/$/, "")}/entity/check-slug/me?type=company&slug=${encodeURIComponent(slug)}`;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
331
|
+
return withVaultRequest(url, { method: "GET", headers }, async (res) => {
|
|
332
|
+
if (!res.ok) {
|
|
333
|
+
const body = await safeBody(res);
|
|
334
|
+
throw new ProvisionError(1, `Vault GET /entity/check-slug/me failed: ${res.status} ${res.statusText} — ${body}`);
|
|
335
|
+
}
|
|
336
|
+
return (await res.json());
|
|
337
|
+
});
|
|
320
338
|
},
|
|
321
339
|
async getCompanyByUid(uid) {
|
|
322
340
|
const url = `${apiUrl.replace(/\/$/, "")}/entity/${encodeURIComponent(uid)}`;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
341
|
+
return withVaultRequest(url, { method: "GET", headers }, async (res) => {
|
|
342
|
+
if (!res.ok) {
|
|
343
|
+
const body = await safeBody(res);
|
|
344
|
+
throw new ProvisionError(1, `Vault GET /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
|
|
345
|
+
}
|
|
346
|
+
const data = (await res.json());
|
|
347
|
+
if (!data.entity) {
|
|
348
|
+
throw new ProvisionError(1, `Vault GET /entity/${uid} returned 200 with no entity body`);
|
|
349
|
+
}
|
|
350
|
+
return data.entity;
|
|
351
|
+
});
|
|
333
352
|
},
|
|
334
353
|
async createCompanyEntity(input) {
|
|
335
354
|
const url = `${apiUrl.replace(/\/$/, "")}/entity`;
|
|
@@ -340,54 +359,56 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
|
|
|
340
359
|
};
|
|
341
360
|
if (input.ownerUid)
|
|
342
361
|
body.ownerUid = input.ownerUid;
|
|
343
|
-
|
|
362
|
+
return withVaultRequest(url, {
|
|
344
363
|
method: "POST",
|
|
345
364
|
headers,
|
|
346
365
|
body: JSON.stringify(body),
|
|
366
|
+
}, async (res) => {
|
|
367
|
+
if (!res.ok) {
|
|
368
|
+
const text = await safeBody(res);
|
|
369
|
+
// 409 SLUG_IN_USE_FOR_PERSON: the caller already has the slug
|
|
370
|
+
// in their namespace (owned ∪ active-member-of). Under the
|
|
371
|
+
// per-user-namespace model this is the new same-user-collision
|
|
372
|
+
// signal — distinct from the legacy global EntityAlreadyExists.
|
|
373
|
+
// The CLI normally reaches `createCompanyEntity` only after
|
|
374
|
+
// `checkSlugInMyNamespace` reported `available: true`, so a
|
|
375
|
+
// 409 here means a race between the pre-check and the POST.
|
|
376
|
+
// Surface the response body verbatim so the caller can see the
|
|
377
|
+
// `code` + `conflictingCompanyUid` and resolve / retry.
|
|
378
|
+
throw new ProvisionError(1, `Vault POST /entity failed: ${res.status} ${res.statusText} — ${text}`);
|
|
379
|
+
}
|
|
380
|
+
const data = (await res.json());
|
|
381
|
+
if (!data.entity) {
|
|
382
|
+
throw new ProvisionError(1, `Vault POST /entity returned ${res.status} with no entity body`);
|
|
383
|
+
}
|
|
384
|
+
return data.entity;
|
|
347
385
|
});
|
|
348
|
-
if (!res.ok) {
|
|
349
|
-
const text = await safeBody(res);
|
|
350
|
-
// 409 SLUG_IN_USE_FOR_PERSON: the caller already has the slug
|
|
351
|
-
// in their namespace (owned ∪ active-member-of). Under the
|
|
352
|
-
// per-user-namespace model this is the new same-user-collision
|
|
353
|
-
// signal — distinct from the legacy global EntityAlreadyExists.
|
|
354
|
-
// The CLI normally reaches `createCompanyEntity` only after
|
|
355
|
-
// `checkSlugInMyNamespace` reported `available: true`, so a
|
|
356
|
-
// 409 here means a race between the pre-check and the POST.
|
|
357
|
-
// Surface the response body verbatim so the caller can see the
|
|
358
|
-
// `code` + `conflictingCompanyUid` and resolve / retry.
|
|
359
|
-
throw new ProvisionError(1, `Vault POST /entity failed: ${res.status} ${res.statusText} — ${text}`);
|
|
360
|
-
}
|
|
361
|
-
const data = (await res.json());
|
|
362
|
-
if (!data.entity) {
|
|
363
|
-
throw new ProvisionError(1, `Vault POST /entity returned ${res.status} with no entity body`);
|
|
364
|
-
}
|
|
365
|
-
return data.entity;
|
|
366
386
|
},
|
|
367
387
|
async softDeleteCompany(uid) {
|
|
368
388
|
const url = `${apiUrl.replace(/\/$/, "")}/entity/${encodeURIComponent(uid)}`;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
389
|
+
return withVaultRequest(url, { method: "DELETE", headers }, async (res) => {
|
|
390
|
+
if (res.status === 403) {
|
|
391
|
+
const body = await safeBody(res);
|
|
392
|
+
throw new ProvisionError(2, `Refusing to retire '${uid}': only the company owner can soft-tombstone it. ${body}`);
|
|
393
|
+
}
|
|
394
|
+
if (res.status === 404) {
|
|
395
|
+
const body = await safeBody(res);
|
|
396
|
+
throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: not found — ${body}`);
|
|
397
|
+
}
|
|
398
|
+
if (res.status === 400) {
|
|
399
|
+
const body = await safeBody(res);
|
|
400
|
+
throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
|
|
401
|
+
}
|
|
402
|
+
if (!res.ok) {
|
|
403
|
+
const body = await safeBody(res);
|
|
404
|
+
throw new ProvisionError(1, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
|
|
405
|
+
}
|
|
406
|
+
const data = (await res.json());
|
|
407
|
+
if (data.deleted !== true || typeof data.uid !== "string") {
|
|
408
|
+
throw new ProvisionError(1, `Vault DELETE /entity/${uid} returned ${res.status} with no deleted confirmation`);
|
|
409
|
+
}
|
|
410
|
+
return { deleted: true, uid: data.uid };
|
|
411
|
+
});
|
|
391
412
|
},
|
|
392
413
|
};
|
|
393
414
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Default-off rollout gate for the native Wave 2 worktree/resume helpers. */
|
|
2
|
+
export declare const CORE_NATIVE_HANDOFF_FLAG = "core-native-handoff";
|
|
3
|
+
export declare function coreNativeHandoffEnabled(): Promise<boolean>;
|
|
4
|
+
//# sourceMappingURL=core-native-handoff-flag.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Default-off rollout gate for the native Wave 2 worktree/resume helpers. */
|
|
2
|
+
export const CORE_NATIVE_HANDOFF_FLAG = "core-native-handoff";
|
|
3
|
+
export async function coreNativeHandoffEnabled() {
|
|
4
|
+
const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
|
|
5
|
+
return resolveProcessFlagGate(CORE_NATIVE_HANDOFF_FLAG, { fallback: false }, () => false);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=core-native-handoff-flag.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -52,6 +52,8 @@ import { qmdReindexAfterSync } from "../lib/core-utils/qmd-reindex-after-sync.js
|
|
|
52
52
|
import { softTimeoutCommand } from "../lib/core-utils/soft-timeout.js";
|
|
53
53
|
import { timeoutGuardCommand } from "../lib/core-utils/timeout-guard.js";
|
|
54
54
|
import { readSentryReportStdin, reportSentryEvent } from "../lib/core-utils/sentry-report.js";
|
|
55
|
+
import { CLEAN_HANDOFF_ASSET, RESUME_LOCK_ASSET, VERIFY_DELIVERABLES_ASSET, WORKTREE_GC_ASSET, runCleanWorktreeReconcileHandoff, runResumeThreadLock, runVerifyStoryDeliverables, runWorktreeGc, } from "../lib/core-utils/worktree-resume.js";
|
|
56
|
+
import { coreNativeHandoffEnabled } from "./core-native-handoff-flag.js";
|
|
55
57
|
import { describeCoreCommand, registeredCoreCommandListing, } from "./core-command-catalog.js";
|
|
56
58
|
/**
|
|
57
59
|
* These commands are now native implementations. Their retained shell assets
|
|
@@ -216,6 +218,12 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
216
218
|
{ asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
|
|
217
219
|
{ asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
|
|
218
220
|
{ asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
|
|
221
|
+
// Wave 2 handoff/worktree commands retain their old scripts for one release,
|
|
222
|
+
// behind the shared default-off rollback flag.
|
|
223
|
+
{ asset: WORKTREE_GC_ASSET, root: "live", summary: "Rollback companion for native worktree-gc" },
|
|
224
|
+
{ asset: CLEAN_HANDOFF_ASSET, root: "cwd", summary: "Rollback companion for native clean-worktree-reconcile-handoff" },
|
|
225
|
+
{ asset: RESUME_LOCK_ASSET, root: "live", summary: "Rollback companion for native resume-thread-lock" },
|
|
226
|
+
{ asset: VERIFY_DELIVERABLES_ASSET, root: "live", summary: "Rollback companion for native verify-story-deliverables" },
|
|
219
227
|
// Wave 1a delegate shells remain bundled as the default-off rollback path.
|
|
220
228
|
// Their roots mirror the original scripts; pickup does not read an HQ root.
|
|
221
229
|
{ asset: "core/scripts/hq-delegate-resolve.sh", root: "live", summary: "Rollback shell for native delegate resolve" },
|
|
@@ -320,6 +328,43 @@ export function registerCoreCommands(program) {
|
|
|
320
328
|
registerCoreDelegateCommand(core);
|
|
321
329
|
registerCoreKnowledgePrefsCommand(core);
|
|
322
330
|
registerCorePolicyCommand(core);
|
|
331
|
+
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
|
332
|
+
// as a default-off rollback path for one release. Keep the gate helper small
|
|
333
|
+
// so sibling Wave 2 work can reuse it without editing this command table.
|
|
334
|
+
const handoffCommands = [
|
|
335
|
+
{ name: "worktree-gc", asset: WORKTREE_GC_ASSET, root: "live", summary: "Safely collect old, preserved HQ worktrees", run: (args, root) => runWorktreeGc(args, root) },
|
|
336
|
+
{ name: "clean-worktree-reconcile-handoff", asset: CLEAN_HANDOFF_ASSET, root: "cwd", summary: "Remove only byte-identical incoming handoff mirrors", run: (args) => runCleanWorktreeReconcileHandoff(args) },
|
|
337
|
+
{ name: "resume-thread-lock", asset: RESUME_LOCK_ASSET, root: "live", summary: "Inspect or acquire a durable resumework lock", run: (args, root) => runResumeThreadLock(args, root) },
|
|
338
|
+
{ name: "verify-story-deliverables", asset: VERIFY_DELIVERABLES_ASSET, root: "live", summary: "Check declared and returned story deliverables", run: (args, root) => runVerifyStoryDeliverables(args, root) },
|
|
339
|
+
];
|
|
340
|
+
for (const entry of handoffCommands) {
|
|
341
|
+
const command = core
|
|
342
|
+
.command(entry.name)
|
|
343
|
+
.description(entry.summary)
|
|
344
|
+
.allowUnknownOption()
|
|
345
|
+
.allowExcessArguments()
|
|
346
|
+
.helpOption(false)
|
|
347
|
+
.argument("[args...]")
|
|
348
|
+
.action(async (args = [], _options, cmd) => {
|
|
349
|
+
const scope = core.opts();
|
|
350
|
+
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
351
|
+
if (!await coreNativeHandoffEnabled()) {
|
|
352
|
+
if (entry.root === "live")
|
|
353
|
+
runEntry(entry, scope, operands);
|
|
354
|
+
else {
|
|
355
|
+
const result = runBundledScript({ asset: entry.asset, args: operands, cwd: process.cwd() });
|
|
356
|
+
if (result.code !== 0)
|
|
357
|
+
process.exit(result.code);
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const { cwd, hqRoot } = resolveEntryRoot(entry, scope);
|
|
362
|
+
const code = await entry.run(operands, hqRoot ?? cwd);
|
|
363
|
+
if (code !== 0)
|
|
364
|
+
process.exit(code);
|
|
365
|
+
});
|
|
366
|
+
describeCoreCommand(command, { execution: "native", asset: entry.asset, root: entry.root });
|
|
367
|
+
}
|
|
323
368
|
// These command names intentionally own native implementations. Their
|
|
324
369
|
// similarly named scaffold assets are forwarders, so sending them through
|
|
325
370
|
// runEntry would recurse back into this command forever.
|
package/dist/commands/lanes.js
CHANGED
|
@@ -29,6 +29,7 @@ import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
|
29
29
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
30
30
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
31
31
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
32
|
+
import { startOutpostLaneSnapshotRefresh } from "../lib/lanes/outpost-snapshot-refresh.js";
|
|
32
33
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
33
34
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
34
35
|
import { getOrFetchOutpostSnapshot, OUTPOST_SNAPSHOT_CACHE_TTL_MS, outpostLaneListSnapshotKey, OutpostSnapshotInvocationMemo, } from "../lib/lanes/outpost-snapshot-cache.js";
|
|
@@ -1025,34 +1026,6 @@ async function readOutpostSnapshotWithCache(ctx, key, isValue, fetch) {
|
|
|
1025
1026
|
return fetch();
|
|
1026
1027
|
return getOrFetchOutpostSnapshot({ hqRoot: ctx.hqRoot(), key, isValue, fetch });
|
|
1027
1028
|
}
|
|
1028
|
-
function startOutpostLaneSnapshotRefresh(root, env) {
|
|
1029
|
-
let entry;
|
|
1030
|
-
try {
|
|
1031
|
-
const currentEntry = process.argv[1];
|
|
1032
|
-
if (!currentEntry)
|
|
1033
|
-
throw new Error("process.argv[1] is empty");
|
|
1034
|
-
entry = fs.realpathSync(currentEntry);
|
|
1035
|
-
}
|
|
1036
|
-
catch (err) {
|
|
1037
|
-
process.stderr.write(`lanes snapshot refresh could not resolve the running CLI entry: ${dispatchErrorMessage(err)}\n`);
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
try {
|
|
1041
|
-
const child = spawn(process.execPath, [...process.execArgv, entry, "lanes", "list"], {
|
|
1042
|
-
cwd: root,
|
|
1043
|
-
detached: true,
|
|
1044
|
-
stdio: ["ignore", "ignore", "inherit"],
|
|
1045
|
-
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1", HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT: "1" },
|
|
1046
|
-
});
|
|
1047
|
-
child.once("error", (err) => {
|
|
1048
|
-
process.stderr.write(`lanes snapshot refresh could not start: ${dispatchErrorMessage(err)}\n`);
|
|
1049
|
-
});
|
|
1050
|
-
child.unref();
|
|
1051
|
-
}
|
|
1052
|
-
catch (err) {
|
|
1053
|
-
process.stderr.write(`lanes snapshot refresh could not start: ${dispatchErrorMessage(err)}\n`);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
1029
|
function isLaneRecordArray(value) {
|
|
1057
1030
|
if (!Array.isArray(value))
|
|
1058
1031
|
return false;
|
|
@@ -16,6 +16,7 @@ import chalk from 'chalk';
|
|
|
16
16
|
import simpleGit from 'simple-git';
|
|
17
17
|
import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
|
|
18
18
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
19
|
+
import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
|
|
19
20
|
// ─── API helpers ────────────────────────────────────────────────────────────
|
|
20
21
|
const API_BASE = 'https://example.com/api';
|
|
21
22
|
async function apiGet(urlPath, token) {
|
|
@@ -137,15 +138,38 @@ function getTrackedFiles(repoDir) {
|
|
|
137
138
|
function discoverTeamDirs(hqRoot) {
|
|
138
139
|
const companiesDir = path.join(hqRoot, 'companies');
|
|
139
140
|
if (!fs.existsSync(companiesDir)) {
|
|
140
|
-
return
|
|
141
|
+
return {
|
|
142
|
+
teams: [],
|
|
143
|
+
cloudSyncedCompanyNames: [],
|
|
144
|
+
nonCloudBackedCompanyNames: [],
|
|
145
|
+
diagnostics: [`companies directory ${companiesDir}: missing`],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
let entries;
|
|
149
|
+
try {
|
|
150
|
+
entries = fs.readdirSync(companiesDir, { withFileTypes: true });
|
|
141
151
|
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
teams: [],
|
|
155
|
+
cloudSyncedCompanyNames: [],
|
|
156
|
+
nonCloudBackedCompanyNames: [],
|
|
157
|
+
diagnostics: [
|
|
158
|
+
`companies directory ${companiesDir}: could not read (${error instanceof Error ? error.message : 'unknown error'})`,
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const companyDirs = entries.filter((entry) => entry.isDirectory());
|
|
142
163
|
const results = [];
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
const missingTeamJson = [];
|
|
165
|
+
const invalidTeamJson = [];
|
|
166
|
+
const companiesManifest = readCompaniesManifestMap(hqRoot) ?? {};
|
|
167
|
+
for (const entry of companyDirs) {
|
|
146
168
|
const teamJsonPath = path.join(companiesDir, entry.name, 'team.json');
|
|
147
|
-
if (!fs.existsSync(teamJsonPath))
|
|
169
|
+
if (!fs.existsSync(teamJsonPath)) {
|
|
170
|
+
missingTeamJson.push(teamJsonPath);
|
|
148
171
|
continue;
|
|
172
|
+
}
|
|
149
173
|
try {
|
|
150
174
|
const raw = fs.readFileSync(teamJsonPath, 'utf-8');
|
|
151
175
|
const meta = JSON.parse(raw);
|
|
@@ -155,12 +179,31 @@ function discoverTeamDirs(hqRoot) {
|
|
|
155
179
|
meta,
|
|
156
180
|
});
|
|
157
181
|
}
|
|
182
|
+
else {
|
|
183
|
+
invalidTeamJson.push(teamJsonPath);
|
|
184
|
+
}
|
|
158
185
|
}
|
|
159
186
|
catch {
|
|
160
|
-
|
|
187
|
+
invalidTeamJson.push(teamJsonPath);
|
|
161
188
|
}
|
|
162
189
|
}
|
|
163
|
-
|
|
190
|
+
const missingTeamJsonCompanyNames = missingTeamJson.map((file) => path.basename(path.dirname(file)));
|
|
191
|
+
const isCloudBacked = (name) => {
|
|
192
|
+
const cloudUid = companiesManifest[name]?.cloud_uid;
|
|
193
|
+
return typeof cloudUid === 'string' && cloudUid.trim().length > 0;
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
teams: results,
|
|
197
|
+
cloudSyncedCompanyNames: missingTeamJsonCompanyNames.filter(isCloudBacked),
|
|
198
|
+
nonCloudBackedCompanyNames: missingTeamJsonCompanyNames.filter((name) => !isCloudBacked(name)),
|
|
199
|
+
diagnostics: [
|
|
200
|
+
`HQ root resolver: resolveDefaultHqRoot() returned ${hqRoot}`,
|
|
201
|
+
`companies directory ${companiesDir}: present; ${companyDirs.length} direct company directories found`,
|
|
202
|
+
`team.json discovery: ${results.length} valid, ${missingTeamJson.length} missing, ${invalidTeamJson.length} unreadable or invalid`,
|
|
203
|
+
...missingTeamJson.map((file) => `${file}: missing`),
|
|
204
|
+
...invalidTeamJson.map((file) => `${file}: unreadable or invalid`),
|
|
205
|
+
],
|
|
206
|
+
};
|
|
164
207
|
}
|
|
165
208
|
/**
|
|
166
209
|
* Sync a single team directory:
|
|
@@ -308,9 +351,27 @@ export function registerTeamSyncCommand(program) {
|
|
|
308
351
|
try {
|
|
309
352
|
const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
|
|
310
353
|
// 1. Discover team directories
|
|
311
|
-
|
|
354
|
+
const discovery = discoverTeamDirs(hqRoot);
|
|
355
|
+
let teamDirs = discovery.teams;
|
|
312
356
|
if (teamDirs.length === 0) {
|
|
313
|
-
console.log(chalk.yellow('No team directories found
|
|
357
|
+
console.log(chalk.yellow('No joined team directories found; no team sync ran.'));
|
|
358
|
+
if (discovery.cloudSyncedCompanyNames.length > 0) {
|
|
359
|
+
const companyNames = discovery.cloudSyncedCompanyNames
|
|
360
|
+
.map((name) => `"${name}"`)
|
|
361
|
+
.join(', ');
|
|
362
|
+
console.log(chalk.yellow(`Companies ${companyNames} are cloud-synced and do not have team.json. ` +
|
|
363
|
+
'hq team-sync only handles git team repositories with team.json.'));
|
|
364
|
+
console.log(chalk.dim('Use `hq sync pull --all` to sync company vault files and `hq sync status` to check their local sync state.'));
|
|
365
|
+
}
|
|
366
|
+
if (discovery.nonCloudBackedCompanyNames.length > 0) {
|
|
367
|
+
const companyNames = discovery.nonCloudBackedCompanyNames
|
|
368
|
+
.map((name) => `"${name}"`)
|
|
369
|
+
.join(', ');
|
|
370
|
+
console.log(`Companies ${companyNames} have no team.json and are not cloud-backed.`);
|
|
371
|
+
}
|
|
372
|
+
for (const diagnostic of discovery.diagnostics) {
|
|
373
|
+
console.log(chalk.dim(` ${diagnostic}`));
|
|
374
|
+
}
|
|
314
375
|
return;
|
|
315
376
|
}
|
|
316
377
|
// Filter to specific team if requested
|
|
@@ -318,7 +379,7 @@ export function registerTeamSyncCommand(program) {
|
|
|
318
379
|
teamDirs = teamDirs.filter((t) => t.meta.team_slug === options.team);
|
|
319
380
|
if (teamDirs.length === 0) {
|
|
320
381
|
console.log(chalk.red(`Team "${options.team}" not found. Available teams:`));
|
|
321
|
-
for (const t of discoverTeamDirs(hqRoot)) {
|
|
382
|
+
for (const t of discoverTeamDirs(hqRoot).teams) {
|
|
322
383
|
console.log(` - ${t.meta.team_slug} (${t.meta.team_name})`);
|
|
323
384
|
}
|
|
324
385
|
process.exit(1);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isFastCachedLanesListRequest } from "./fast-scaffold.js";
|
|
2
2
|
import { fastFlagEnabled } from "./native-fast-route.js";
|
|
3
|
+
import { registerOutpostLaneSnapshotRefreshChild } from "./lib/lanes/outpost-snapshot-refresh.js";
|
|
3
4
|
import { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG } from "./lib/lanes/outpost-hire-cache.js";
|
|
4
5
|
import { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lib/lanes/flag-keys.js";
|
|
5
6
|
/** Returns false unless this exact list form is safely enabled in the local flag snapshot. */
|
|
@@ -14,6 +15,7 @@ export async function runFastLanesListRequest(argv, env = process.env, deps = {
|
|
|
14
15
|
await runFastCachedLanesListFromEntrypoint();
|
|
15
16
|
},
|
|
16
17
|
}) {
|
|
18
|
+
registerOutpostLaneSnapshotRefreshChild(env);
|
|
17
19
|
if (!isFastCachedLanesListRequest(argv))
|
|
18
20
|
return false;
|
|
19
21
|
if (env.HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT === "1")
|
package/dist/fast-lanes-list.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
2
|
import * as os from "node:os";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import { compareLaneQueueOrder, laneRunsOnHost } from "./lib/lanes/admission.js";
|
|
6
5
|
import { readOutpostIdentity, resolveAdmissionHost } from "./lib/lanes/admission-host.js";
|
|
7
6
|
import { LaneSchemaError, isLanesError } from "./lib/lanes/errors.js";
|
|
7
|
+
import { startOutpostLaneSnapshotRefresh } from "./lib/lanes/outpost-snapshot-refresh.js";
|
|
8
8
|
import { OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS, outpostHireSummaryCacheKey, readOutpostHireSummaryCacheIncludingStale, } from "./lib/lanes/outpost-hire-cache.js";
|
|
9
9
|
import { resolveLanesHqRoot } from "./lib/lanes/hq-root.js";
|
|
10
10
|
import { laneStories } from "./lib/lanes/stories.js";
|
|
@@ -82,34 +82,6 @@ function isOverdue(hqRoot, laneId, now) {
|
|
|
82
82
|
return false;
|
|
83
83
|
return now.getTime() > at;
|
|
84
84
|
}
|
|
85
|
-
function startOutpostLaneSnapshotRefresh(root, env) {
|
|
86
|
-
let entry;
|
|
87
|
-
try {
|
|
88
|
-
const currentEntry = process.argv[1];
|
|
89
|
-
if (!currentEntry)
|
|
90
|
-
throw new Error("process.argv[1] is empty");
|
|
91
|
-
entry = fs.realpathSync(currentEntry);
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
process.stderr.write(`lanes snapshot refresh could not resolve the running CLI entry: ${errorMessage(err)}\n`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const child = spawn(process.execPath, [...process.execArgv, entry, "lanes", "list"], {
|
|
99
|
-
cwd: root,
|
|
100
|
-
detached: true,
|
|
101
|
-
stdio: ["ignore", "ignore", "inherit"],
|
|
102
|
-
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1", HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT: "1" },
|
|
103
|
-
});
|
|
104
|
-
child.once("error", (err) => {
|
|
105
|
-
process.stderr.write(`lanes snapshot refresh could not start: ${errorMessage(err)}\n`);
|
|
106
|
-
});
|
|
107
|
-
child.unref();
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
process.stderr.write(`lanes snapshot refresh could not start: ${errorMessage(err)}\n`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
85
|
function isOwnerLane(lane, host) {
|
|
114
86
|
return lane.host.kind === "outpost" &&
|
|
115
87
|
!isTerminalLaneState(lane.state) &&
|
|
@@ -44,9 +44,6 @@ const HOOK_TIMEOUT_EXCEEDED_TYPE = "hook_timeout_exceeded";
|
|
|
44
44
|
const HOOK_LATE_FINISH_TYPE = "hook_late_finish";
|
|
45
45
|
const HOOK_TIMEOUT_WARNING_METRIC = "hq.hook_timeout_warning";
|
|
46
46
|
const HOOK_TIMEOUT_EXCEEDED_METRIC = "hq.hook_timeout_exceeded";
|
|
47
|
-
const HOOK_TIMEOUT_EXCEEDED_FINGERPRINT = "hook-timeout-exceeded";
|
|
48
|
-
const HOOK_TIMEOUT_WARNING_FINGERPRINT = "hook-timeout";
|
|
49
|
-
const HOOK_TIMEOUT_MACHINE_WIDE_FINGERPRINT = "hq-hook-slow";
|
|
50
47
|
const MAX_HOOK_SEQUENCE_ENTRIES = 20;
|
|
51
48
|
const MAX_HOOK_TIMEOUT_DEBUG_PHASES = 24;
|
|
52
49
|
const MAX_HOOK_TIMEOUT_DEBUG_MS = 86_400_000;
|
|
@@ -580,10 +577,9 @@ function hookTimeoutDebugContextTags(context) {
|
|
|
580
577
|
}
|
|
581
578
|
function safeSlowChild(metadata) {
|
|
582
579
|
const value = metadata.slow_child;
|
|
583
|
-
if (typeof value !== "string" || value
|
|
580
|
+
if (typeof value !== "string" || !SAFE_HOOK_SCRIPT.test(value))
|
|
584
581
|
return "other";
|
|
585
|
-
|
|
586
|
-
return KNOWN_HOOK_SCRIPTS.has(script) ? script : "other";
|
|
582
|
+
return value;
|
|
587
583
|
}
|
|
588
584
|
function isMachineWideHookSlowness(metadata) {
|
|
589
585
|
const spawnMs = metadata.spawn_ms;
|
|
@@ -621,6 +617,7 @@ function hookTimeoutTags(event) {
|
|
|
621
617
|
const loadAverage = parsedLoadAverage(metadata);
|
|
622
618
|
const slowChildMs = metadata.slow_child_ms;
|
|
623
619
|
return {
|
|
620
|
+
hook_timeout_kind: hookTimeoutKind(event),
|
|
624
621
|
hook_event: hookEvent(metadata),
|
|
625
622
|
hook_script: hookScript(metadata),
|
|
626
623
|
elapsed_bucket: elapsedBucket(elapsedMs),
|
|
@@ -680,37 +677,13 @@ function scrubbedSentryEnvelope(event, overrides, additionalTags = {}) {
|
|
|
680
677
|
...sentryEnvelope(event, overrides, additionalTags),
|
|
681
678
|
}, {});
|
|
682
679
|
}
|
|
683
|
-
/**
|
|
684
|
-
* Keep timeout faults visible at warning level while grouping them by the
|
|
685
|
-
* hook event and installation-independent script basename. A genuine
|
|
686
|
-
* exceeded or late-finish payload gets its own stable condition group.
|
|
687
|
-
*/
|
|
680
|
+
/** Keep all hook-timeout conditions together by script while retaining tags for triage. */
|
|
688
681
|
function hookTimeoutExceededOverrides(event) {
|
|
689
|
-
const eventName = hookEvent(event.metadata);
|
|
690
682
|
if (!isHookTimeoutEvent(event))
|
|
691
683
|
return undefined;
|
|
692
|
-
const kind = hookTimeoutKind(event);
|
|
693
|
-
if (isMachineWideHookSlowness(event.metadata)) {
|
|
694
|
-
return {
|
|
695
|
-
level: "warning",
|
|
696
|
-
fingerprint: [
|
|
697
|
-
HOOK_TIMEOUT_MACHINE_WIDE_FINGERPRINT,
|
|
698
|
-
"machine-wide",
|
|
699
|
-
operatingSystemName(event.metadata),
|
|
700
|
-
eventName,
|
|
701
|
-
kind,
|
|
702
|
-
],
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
if (kind === "warning") {
|
|
706
|
-
return {
|
|
707
|
-
level: "warning",
|
|
708
|
-
fingerprint: [HOOK_TIMEOUT_WARNING_FINGERPRINT, eventName, hookScript(event.metadata)],
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
684
|
return {
|
|
712
685
|
level: "warning",
|
|
713
|
-
fingerprint: [
|
|
686
|
+
fingerprint: ["hook-timeout", hookScript(event.metadata)],
|
|
714
687
|
};
|
|
715
688
|
}
|
|
716
689
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Native TypeScript ports of the Wave 2 worktree and resume helpers. */
|
|
2
|
+
export declare const WORKTREE_GC_ASSET = "core/scripts/worktree-gc.sh";
|
|
3
|
+
export declare const CLEAN_HANDOFF_ASSET = "core/scripts/clean-worktree-reconcile-handoff.sh";
|
|
4
|
+
export declare const RESUME_LOCK_ASSET = "core/scripts/resume-thread-lock.sh";
|
|
5
|
+
export declare const VERIFY_DELIVERABLES_ASSET = "core/scripts/verify-story-deliverables.sh";
|
|
6
|
+
export type CoreIo = {
|
|
7
|
+
stdout?: (value: string) => void;
|
|
8
|
+
stderr?: (value: string) => void;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
now?: () => Date;
|
|
12
|
+
};
|
|
13
|
+
export declare function worktreeGcPathKey(value: string): string;
|
|
14
|
+
export declare function runWorktreeGc(args: readonly string[], hqRoot: string, io?: CoreIo): number;
|
|
15
|
+
export declare function runCleanWorktreeReconcileHandoff(args: readonly string[], io?: CoreIo): number;
|
|
16
|
+
export declare function runResumeThreadLock(args: readonly string[], hqRoot: string, io?: CoreIo): number;
|
|
17
|
+
export declare function runVerifyStoryDeliverables(args: readonly string[], hqRoot: string, io?: CoreIo): Promise<number>;
|
|
18
|
+
//# sourceMappingURL=worktree-resume.d.ts.map
|