@bli-cockpit/cli 0.2.34 → 0.2.35
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/dist/commands/backfill.js +493 -408
- package/dist/commands/local.js +377 -303
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync.js +323 -210
- package/package.json +2 -2
package/dist/commands/local.js
CHANGED
|
@@ -411,297 +411,355 @@ async function resolveAndSaveDoctorRoots(command, io) {
|
|
|
411
411
|
writeLine(io.stdout, `Saved collection roots: ${resolution.collectionRoots.join(", ")}`);
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Every onboard exit funnels through here so the best-effort install-event
|
|
416
|
+
* report always fires, pass or blocked — replaces a `finish` closure that used
|
|
417
|
+
* to live inside `runOnboard` so the flow functions below can call it too.
|
|
418
|
+
*/
|
|
419
|
+
async function finishOnboardRun(command, installEvents, io, code) {
|
|
420
|
+
await reportInstallEventsBestEffort({
|
|
421
|
+
homeDir: command.homeDir,
|
|
422
|
+
dashboardUrl: command.dashboardUrl,
|
|
423
|
+
command: "onboard",
|
|
424
|
+
events: installEvents,
|
|
425
|
+
json: command.json,
|
|
426
|
+
io,
|
|
427
|
+
});
|
|
428
|
+
return code;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Every onboard `--json` payload is `onboardResult` plus the roots this pass
|
|
432
|
+
* resolved plus whatever extra keys that arm of onboarding adds. Assembling it
|
|
433
|
+
* here means the call sites below cannot drift on key order or forget a field
|
|
434
|
+
* a sibling call site remembers.
|
|
435
|
+
*/
|
|
436
|
+
function onboardJsonPayload(resultStatus, command, install, pair, sync, status, rootsResult, extra = {}) {
|
|
437
|
+
return {
|
|
438
|
+
...onboardResult(resultStatus, command, install, pair, sync, status),
|
|
439
|
+
collection_roots: rootsResult?.roots ?? [],
|
|
440
|
+
root_resolution: rootsResult,
|
|
441
|
+
...extra,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function writeOnboardJson(io, payload) {
|
|
445
|
+
writeLine(io.stdout, JSON.stringify(payload, null, 2));
|
|
446
|
+
}
|
|
447
|
+
/** Step 0: which folders are approved, and who claims to own this machine's uploads. */
|
|
448
|
+
async function prepareOnboardRoots(command, installEvents, io) {
|
|
449
|
+
const resolvedRoots = await resolveOnboardingRootsForCommand(command, io);
|
|
450
|
+
const rootsResult = resolvedRoots.rootsResult;
|
|
451
|
+
const collectionRoots = rootsResult.roots;
|
|
452
|
+
const attributionCollectionRoots = await collectionRootConsentAliases(collectionRoots);
|
|
453
|
+
const primaryRoot = resolvedRoots.primaryRoot;
|
|
454
|
+
const resolvedCommand = {
|
|
455
|
+
...command,
|
|
456
|
+
repoRoot: primaryRoot,
|
|
457
|
+
collectionRoots,
|
|
458
|
+
};
|
|
459
|
+
if (!command.json) {
|
|
460
|
+
writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
|
|
461
|
+
}
|
|
462
|
+
if (rootsResult.homeRootOptIn) {
|
|
463
|
+
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
resolvedRoots,
|
|
467
|
+
rootsResult,
|
|
468
|
+
collectionRoots,
|
|
469
|
+
attributionCollectionRoots,
|
|
470
|
+
primaryRoot,
|
|
471
|
+
existingConfig: resolvedRoots.existingConfig,
|
|
472
|
+
resolvedCommand,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/** Step 1: write the local config that makes this machine a known collector. */
|
|
476
|
+
async function installOnboardConfig(command, resolvedRoots, installEvents, io) {
|
|
477
|
+
const install = await persistOnboardingRootConfig(command, resolvedRoots);
|
|
478
|
+
addInstallEvent(installEvents, "install", "ok");
|
|
479
|
+
if (!command.json) {
|
|
480
|
+
writeLine(io.stdout, "1/5 Installed local collector.");
|
|
481
|
+
writeLine(io.stdout, `Config: ${install.paths.config_file}`);
|
|
482
|
+
}
|
|
483
|
+
return install;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* The onboarding runbook: resolve roots, install, pair, discover worktrees,
|
|
487
|
+
* then hand off to whichever arm matches what was found — one repo runs the
|
|
488
|
+
* single-repo sync-then-backfill sequence (`runOnboardSingleRepoFlow`),
|
|
489
|
+
* several run the parent-folder one (`runOnboardMultiRepoFlow`). Any failure
|
|
490
|
+
* anywhere in either arm unwinds to the single catch below.
|
|
491
|
+
*/
|
|
414
492
|
async function runOnboard(command, io) {
|
|
415
493
|
const installEvents = [];
|
|
416
|
-
const finish = async (code) => {
|
|
417
|
-
await reportInstallEventsBestEffort({
|
|
418
|
-
homeDir: command.homeDir,
|
|
419
|
-
dashboardUrl: command.dashboardUrl,
|
|
420
|
-
command: "onboard",
|
|
421
|
-
events: installEvents,
|
|
422
|
-
json: command.json,
|
|
423
|
-
io,
|
|
424
|
-
});
|
|
425
|
-
return code;
|
|
426
|
-
};
|
|
427
494
|
let install = null;
|
|
428
495
|
let pair = null;
|
|
429
|
-
let sync = null;
|
|
430
|
-
let status = null;
|
|
431
496
|
let rootsResult = null;
|
|
432
|
-
|
|
433
|
-
|
|
497
|
+
// `sync` has to survive a throw from inside either flow function, so it is
|
|
498
|
+
// mutated through this holder rather than returned — a return only happens
|
|
499
|
+
// on the success path, and the failure path needs the value too.
|
|
500
|
+
const syncHolder = {
|
|
501
|
+
current: null,
|
|
502
|
+
};
|
|
434
503
|
try {
|
|
435
504
|
if (!command.json)
|
|
436
505
|
writeOnboardBanner(command, io);
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
const resolvedCommand = {
|
|
444
|
-
...command,
|
|
445
|
-
repoRoot: primaryRoot,
|
|
446
|
-
collectionRoots,
|
|
447
|
-
};
|
|
448
|
-
if (!command.json) {
|
|
449
|
-
writeLine(io.stdout, `Collecting from: ${collectionRoots.join(", ")}`);
|
|
450
|
-
}
|
|
451
|
-
if (rootsResult.homeRootOptIn) {
|
|
452
|
-
addInstallEvent(installEvents, "home_root_optin", "ok");
|
|
453
|
-
}
|
|
454
|
-
const claimedOwnerEmail = await resolveOnboardEmail(resolvedCommand, collectionRoots, existingConfig, io);
|
|
455
|
-
install = await persistOnboardingRootConfig(command, resolvedRoots);
|
|
456
|
-
addInstallEvent(installEvents, "install", "ok");
|
|
457
|
-
if (!command.json) {
|
|
458
|
-
writeLine(io.stdout, "1/5 Installed local collector.");
|
|
459
|
-
writeLine(io.stdout, `Config: ${install.paths.config_file}`);
|
|
460
|
-
}
|
|
461
|
-
pair = await pairForOnboarding(command, { primaryRoot, claimedOwnerEmail }, installEvents, io);
|
|
462
|
-
const worktrees = await discoverCommandWorktrees(collectionRoots, {
|
|
506
|
+
const roots = await prepareOnboardRoots(command, installEvents, io);
|
|
507
|
+
rootsResult = roots.rootsResult;
|
|
508
|
+
const claimedOwnerEmail = await resolveOnboardEmail(roots.resolvedCommand, roots.collectionRoots, roots.existingConfig, io);
|
|
509
|
+
install = await installOnboardConfig(command, roots.resolvedRoots, installEvents, io);
|
|
510
|
+
pair = await pairForOnboarding(command, { primaryRoot: roots.primaryRoot, claimedOwnerEmail }, installEvents, io);
|
|
511
|
+
const worktrees = await discoverCommandWorktrees(roots.collectionRoots, {
|
|
463
512
|
maxDepth: command.maxDepth,
|
|
464
513
|
maxRepos: command.maxRepos,
|
|
465
514
|
homeDir: command.homeDir,
|
|
466
515
|
allowEmpty: true,
|
|
467
516
|
}, io);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
root_resolution: rootsResult,
|
|
478
|
-
mode: "multi_repo",
|
|
479
|
-
repos: multi.results,
|
|
480
|
-
codex_sessions: multi.codex_sessions,
|
|
481
|
-
blocker: "sync_blocked",
|
|
482
|
-
next_step: "Run `cockpit sync --json` until every root returns an uploaded receipt.",
|
|
483
|
-
}, null, 2));
|
|
484
|
-
}
|
|
485
|
-
return finish(1);
|
|
486
|
-
}
|
|
487
|
-
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
488
|
-
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
489
|
-
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
490
|
-
? undefined
|
|
491
|
-
: backfill.failureReason ?? "backfill_incomplete");
|
|
492
|
-
if (!completedBackfill) {
|
|
493
|
-
writeOnboardBackfillBlockedResult(io, {
|
|
494
|
-
json: command.json,
|
|
495
|
-
base: {
|
|
496
|
-
...onboardResult("blocked", resolvedCommand, install, pair, null, null),
|
|
497
|
-
collection_roots: collectionRoots,
|
|
498
|
-
root_resolution: rootsResult,
|
|
499
|
-
},
|
|
500
|
-
extra: {
|
|
501
|
-
mode: "multi_repo",
|
|
502
|
-
repos: multi.results,
|
|
503
|
-
codex_sessions: multi.codex_sessions,
|
|
504
|
-
},
|
|
505
|
-
backfill,
|
|
506
|
-
});
|
|
507
|
-
return finish(1);
|
|
508
|
-
}
|
|
509
|
-
const multiSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
510
|
-
agentRules = multiSetup.agentRules;
|
|
511
|
-
autostart = multiSetup.autostart;
|
|
512
|
-
const onboardOk = multiSetup.ok;
|
|
513
|
-
if (command.json) {
|
|
514
|
-
writeLine(io.stdout, JSON.stringify({
|
|
515
|
-
...onboardResult(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, null, null),
|
|
516
|
-
collection_roots: collectionRoots,
|
|
517
|
-
root_resolution: rootsResult,
|
|
518
|
-
agent_rules: agentRules,
|
|
519
|
-
autostart,
|
|
520
|
-
backfill: completedBackfill,
|
|
521
|
-
mode: "multi_repo",
|
|
522
|
-
repos: multi.results,
|
|
523
|
-
codex_sessions: multi.codex_sessions,
|
|
524
|
-
...(!onboardOk && multi.ok
|
|
525
|
-
? {
|
|
526
|
-
blocker: "autostart_load_failed",
|
|
527
|
-
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
528
|
-
}
|
|
529
|
-
: {}),
|
|
530
|
-
}, null, 2));
|
|
531
|
-
}
|
|
532
|
-
if (onboardOk && !command.json) {
|
|
533
|
-
writeLine(io.stdout, "PASS: Tower is set up and collecting.");
|
|
534
|
-
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
535
|
-
pair,
|
|
536
|
-
status,
|
|
537
|
-
sync: null,
|
|
538
|
-
agentRules,
|
|
539
|
-
autostart,
|
|
540
|
-
initialSyncOk: true,
|
|
541
|
-
backfill: completedBackfill,
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
else if (multi.ok && !command.json) {
|
|
545
|
-
writeOnboardAutostartBlocker(io, autostart);
|
|
546
|
-
}
|
|
547
|
-
return finish(onboardOk ? 0 : 1);
|
|
548
|
-
}
|
|
549
|
-
const worktreeRoot = worktrees[0]?.repo_root ?? primaryRoot;
|
|
550
|
-
const context = await startLocalWorkContext({
|
|
551
|
-
homeDir: command.homeDir,
|
|
552
|
-
repoRoot: worktreeRoot,
|
|
553
|
-
branch: command.branch,
|
|
554
|
-
activeTicketId: command.activeTicketId,
|
|
555
|
-
});
|
|
556
|
-
if (!command.json) {
|
|
557
|
-
writeLine(io.stdout, "3/5 Work context active.");
|
|
558
|
-
writeLine(io.stdout, `Repo: ${context.repo}`);
|
|
559
|
-
writeLine(io.stdout, `Branch: ${context.branch}`);
|
|
560
|
-
writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
|
|
561
|
-
writeLine(io.stdout, `Context: ${context.work_context_id}`);
|
|
562
|
-
}
|
|
563
|
-
addInstallEvent(installEvents, "work_context", "ok");
|
|
564
|
-
const run = await runAttributedWorktreeSync({
|
|
565
|
-
homeDir: command.homeDir,
|
|
566
|
-
dashboardUrl: command.dashboardUrl,
|
|
567
|
-
collectionRoots: attributionCollectionRoots,
|
|
568
|
-
startContexts: false,
|
|
517
|
+
const ctx = {
|
|
518
|
+
command,
|
|
519
|
+
resolvedCommand: roots.resolvedCommand,
|
|
520
|
+
collectionRoots: roots.collectionRoots,
|
|
521
|
+
rootsResult: roots.rootsResult,
|
|
522
|
+
install,
|
|
523
|
+
pair,
|
|
524
|
+
installEvents,
|
|
525
|
+
io,
|
|
569
526
|
worktrees,
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (!sync) {
|
|
574
|
-
throw new Error("Onboard sync produced no result for the repo worktree.");
|
|
527
|
+
};
|
|
528
|
+
if (worktrees.length > 1) {
|
|
529
|
+
return await runOnboardMultiRepoFlow(ctx);
|
|
575
530
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
531
|
+
return await runOnboardSingleRepoFlow(ctx, {
|
|
532
|
+
attributionCollectionRoots: roots.attributionCollectionRoots,
|
|
533
|
+
primaryRoot: roots.primaryRoot,
|
|
534
|
+
syncHolder,
|
|
580
535
|
});
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
else {
|
|
594
|
-
const syncFailureReason = sync.status === "spooled" ? sync.failure_reason : null;
|
|
595
|
-
const retryCommand = sync.status === "spooled"
|
|
596
|
-
? sync.retry_command
|
|
597
|
-
: `cockpit sync --workspace ${JSON.stringify(worktreeRoot)}`;
|
|
598
|
-
writeLine(io.stderr, "BLOCKED: initial collection is incomplete; retry until every eligible session has a durable receipt or explicit terminal reason.");
|
|
599
|
-
writeLine(io.stderr, `Failure: ${syncFailureReason ?? (run.summary.report_posted ? collectionRunStatus : run.summary.report_reason)}`);
|
|
600
|
-
writeAgentSessionSummary(io, run.summary);
|
|
601
|
-
writeLine(io.stderr, `Retry: ${retryCommand}`);
|
|
602
|
-
}
|
|
603
|
-
return finish(1);
|
|
604
|
-
}
|
|
605
|
-
addInstallEvent(installEvents, "sync", "ok");
|
|
606
|
-
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
607
|
-
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
608
|
-
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
609
|
-
? undefined
|
|
610
|
-
: backfill.failureReason ?? "backfill_incomplete");
|
|
611
|
-
if (!completedBackfill) {
|
|
612
|
-
writeOnboardBackfillBlockedResult(io, {
|
|
613
|
-
json: command.json,
|
|
614
|
-
base: {
|
|
615
|
-
...onboardResult("blocked", resolvedCommand, install, pair, sync, status),
|
|
616
|
-
collection_roots: collectionRoots,
|
|
617
|
-
root_resolution: rootsResult,
|
|
618
|
-
},
|
|
619
|
-
extra: { codex_sessions: run.summary },
|
|
620
|
-
backfill,
|
|
621
|
-
});
|
|
622
|
-
return finish(1);
|
|
623
|
-
}
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
return await handleOnboardFailure(error, command, rootsResult, install, pair, syncHolder.current, installEvents, io);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/** The parent-folder arm: sync every discovered worktree, then backfill and finish setup once. */
|
|
542
|
+
async function runOnboardMultiRepoFlow(ctx) {
|
|
543
|
+
const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
|
|
544
|
+
const multi = await runMultiRepoOnboard(resolvedCommand, io, worktrees);
|
|
545
|
+
addInstallEvent(installEvents, "work_context", "ok");
|
|
546
|
+
addInstallEvent(installEvents, "sync", multi.ok ? "ok" : "fail", multi.ok ? undefined : "sync_blocked");
|
|
547
|
+
if (!multi.ok) {
|
|
624
548
|
if (command.json) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
root_resolution: rootsResult,
|
|
633
|
-
agent_rules: agentRules,
|
|
634
|
-
autostart,
|
|
635
|
-
backfill: completedBackfill,
|
|
636
|
-
codex_sessions: run.summary,
|
|
637
|
-
...(!onboardOk
|
|
638
|
-
? {
|
|
639
|
-
blocker: "autostart_load_failed",
|
|
640
|
-
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
641
|
-
}
|
|
642
|
-
: {}),
|
|
643
|
-
}, null, 2));
|
|
644
|
-
return finish(onboardOk ? 0 : 1);
|
|
645
|
-
}
|
|
646
|
-
writeLine(io.stdout, "4/5 Uploaded what you worked on.");
|
|
647
|
-
writeLine(io.stdout, `HTTP: ${sync.http_status}`);
|
|
648
|
-
writeLine(io.stdout, `Things recorded: ${sync.event_count}`);
|
|
649
|
-
writeLine(io.stdout, `Sources: ${sync.source_scan_count}`);
|
|
650
|
-
writeLine(io.stdout, `Risk flags: ${sync.risk_flag_count}`);
|
|
651
|
-
writeLine(io.stdout, `Raw evidence files: ${sync.raw_evidence_file_count}`);
|
|
652
|
-
writeLine(io.stdout, rawEvidenceSyncLine(sync));
|
|
653
|
-
writeAgentSessionSummary(io, run.summary);
|
|
654
|
-
writeLine(io.stdout, "5/5 Status ready.");
|
|
655
|
-
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
656
|
-
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
657
|
-
const setup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
658
|
-
agentRules = setup.agentRules;
|
|
659
|
-
autostart = setup.autostart;
|
|
660
|
-
if (!setup.ok) {
|
|
661
|
-
writeOnboardAutostartBlocker(io, autostart);
|
|
662
|
-
return finish(1);
|
|
549
|
+
writeOnboardJson(io, onboardJsonPayload("blocked", resolvedCommand, install, pair, null, null, rootsResult, {
|
|
550
|
+
mode: "multi_repo",
|
|
551
|
+
repos: multi.results,
|
|
552
|
+
codex_sessions: multi.codex_sessions,
|
|
553
|
+
blocker: "sync_blocked",
|
|
554
|
+
next_step: "Run `cockpit sync --json` until every root returns an uploaded receipt.",
|
|
555
|
+
}));
|
|
663
556
|
}
|
|
557
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
558
|
+
}
|
|
559
|
+
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
560
|
+
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
561
|
+
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
562
|
+
? undefined
|
|
563
|
+
: backfill.failureReason ?? "backfill_incomplete");
|
|
564
|
+
if (!completedBackfill) {
|
|
565
|
+
writeOnboardBackfillBlockedResult(io, {
|
|
566
|
+
json: command.json,
|
|
567
|
+
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, null, null, rootsResult),
|
|
568
|
+
extra: {
|
|
569
|
+
mode: "multi_repo",
|
|
570
|
+
repos: multi.results,
|
|
571
|
+
codex_sessions: multi.codex_sessions,
|
|
572
|
+
},
|
|
573
|
+
backfill,
|
|
574
|
+
});
|
|
575
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
576
|
+
}
|
|
577
|
+
const multiSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
578
|
+
const { agentRules, autostart } = multiSetup;
|
|
579
|
+
const onboardOk = multiSetup.ok;
|
|
580
|
+
if (command.json) {
|
|
581
|
+
writeOnboardJson(io, onboardJsonPayload(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, null, null, rootsResult, {
|
|
582
|
+
agent_rules: agentRules,
|
|
583
|
+
autostart,
|
|
584
|
+
backfill: completedBackfill,
|
|
585
|
+
mode: "multi_repo",
|
|
586
|
+
repos: multi.results,
|
|
587
|
+
codex_sessions: multi.codex_sessions,
|
|
588
|
+
...(!onboardOk && multi.ok
|
|
589
|
+
? {
|
|
590
|
+
blocker: "autostart_load_failed",
|
|
591
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
592
|
+
}
|
|
593
|
+
: {}),
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
if (onboardOk && !command.json) {
|
|
664
597
|
writeLine(io.stdout, "PASS: Tower is set up and collecting.");
|
|
665
598
|
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
666
599
|
pair,
|
|
667
|
-
status,
|
|
668
|
-
sync,
|
|
600
|
+
status: null,
|
|
601
|
+
sync: null,
|
|
669
602
|
agentRules,
|
|
670
603
|
autostart,
|
|
671
604
|
initialSyncOk: true,
|
|
672
605
|
backfill: completedBackfill,
|
|
673
606
|
});
|
|
674
|
-
return finish(0);
|
|
675
607
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
608
|
+
else if (multi.ok && !command.json) {
|
|
609
|
+
writeOnboardAutostartBlocker(io, autostart);
|
|
610
|
+
}
|
|
611
|
+
return finishOnboardRun(command, installEvents, io, onboardOk ? 0 : 1);
|
|
612
|
+
}
|
|
613
|
+
/** The single-repo arm: start the work context, sync it, backfill, then finish setup once. */
|
|
614
|
+
async function runOnboardSingleRepoFlow(ctx, single) {
|
|
615
|
+
const { command, resolvedCommand, collectionRoots, rootsResult, install, pair, installEvents, io, worktrees, } = ctx;
|
|
616
|
+
const worktreeRoot = worktrees[0]?.repo_root ?? single.primaryRoot;
|
|
617
|
+
const context = await startLocalWorkContext({
|
|
618
|
+
homeDir: command.homeDir,
|
|
619
|
+
repoRoot: worktreeRoot,
|
|
620
|
+
branch: command.branch,
|
|
621
|
+
activeTicketId: command.activeTicketId,
|
|
622
|
+
});
|
|
623
|
+
if (!command.json) {
|
|
624
|
+
writeLine(io.stdout, "3/5 Work context active.");
|
|
625
|
+
writeLine(io.stdout, `Repo: ${context.repo}`);
|
|
626
|
+
writeLine(io.stdout, `Branch: ${context.branch}`);
|
|
627
|
+
writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
|
|
628
|
+
writeLine(io.stdout, `Context: ${context.work_context_id}`);
|
|
629
|
+
}
|
|
630
|
+
addInstallEvent(installEvents, "work_context", "ok");
|
|
631
|
+
const run = await runAttributedWorktreeSync({
|
|
632
|
+
homeDir: command.homeDir,
|
|
633
|
+
dashboardUrl: command.dashboardUrl,
|
|
634
|
+
collectionRoots: single.attributionCollectionRoots,
|
|
635
|
+
startContexts: false,
|
|
636
|
+
worktrees,
|
|
637
|
+
fetchImpl: io.fetch,
|
|
638
|
+
});
|
|
639
|
+
const sync = run.outcomes[0]?.sync ?? null;
|
|
640
|
+
single.syncHolder.current = sync;
|
|
641
|
+
if (!sync) {
|
|
642
|
+
throw new Error("Onboard sync produced no result for the repo worktree.");
|
|
643
|
+
}
|
|
644
|
+
const status = await inspectLocalCollectorStatus({
|
|
645
|
+
homeDir: command.homeDir,
|
|
646
|
+
repoRoot: worktreeRoot,
|
|
647
|
+
branch: command.branch,
|
|
648
|
+
});
|
|
649
|
+
if (!run.ok || sync.status !== "uploaded") {
|
|
650
|
+
addInstallEvent(installEvents, "sync", "fail", "sync_blocked");
|
|
651
|
+
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
689
652
|
if (command.json) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
blocker,
|
|
695
|
-
message,
|
|
696
|
-
next_step: nextStep,
|
|
697
|
-
}, null, 2));
|
|
653
|
+
writeOnboardJson(io, onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult, {
|
|
654
|
+
codex_sessions: run.summary,
|
|
655
|
+
collection_status: collectionRunStatus,
|
|
656
|
+
}));
|
|
698
657
|
}
|
|
699
658
|
else {
|
|
700
|
-
|
|
701
|
-
|
|
659
|
+
const syncFailureReason = sync.status === "spooled" ? sync.failure_reason : null;
|
|
660
|
+
const retryCommand = sync.status === "spooled"
|
|
661
|
+
? sync.retry_command
|
|
662
|
+
: `cockpit sync --workspace ${JSON.stringify(worktreeRoot)}`;
|
|
663
|
+
writeLine(io.stderr, "BLOCKED: initial collection is incomplete; retry until every eligible session has a durable receipt or explicit terminal reason.");
|
|
664
|
+
writeLine(io.stderr, `Failure: ${syncFailureReason ?? (run.summary.report_posted ? collectionRunStatus : run.summary.report_reason)}`);
|
|
665
|
+
writeAgentSessionSummary(io, run.summary);
|
|
666
|
+
writeLine(io.stderr, `Retry: ${retryCommand}`);
|
|
702
667
|
}
|
|
703
|
-
return
|
|
668
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
669
|
+
}
|
|
670
|
+
addInstallEvent(installEvents, "sync", "ok");
|
|
671
|
+
const backfill = await runRequiredOnboardBackfill(resolvedCommand, io);
|
|
672
|
+
const completedBackfill = backfill.status === "complete" ? backfill.result : null;
|
|
673
|
+
addInstallEvent(installEvents, "backfill", completedBackfill ? "ok" : "fail", completedBackfill
|
|
674
|
+
? undefined
|
|
675
|
+
: backfill.failureReason ?? "backfill_incomplete");
|
|
676
|
+
if (!completedBackfill) {
|
|
677
|
+
writeOnboardBackfillBlockedResult(io, {
|
|
678
|
+
json: command.json,
|
|
679
|
+
base: onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult),
|
|
680
|
+
extra: { codex_sessions: run.summary },
|
|
681
|
+
backfill,
|
|
682
|
+
});
|
|
683
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
684
|
+
}
|
|
685
|
+
if (command.json) {
|
|
686
|
+
const jsonSetup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
687
|
+
const { agentRules, autostart } = jsonSetup;
|
|
688
|
+
const onboardOk = jsonSetup.ok;
|
|
689
|
+
writeOnboardJson(io, onboardJsonPayload(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, sync, status, rootsResult, {
|
|
690
|
+
agent_rules: agentRules,
|
|
691
|
+
autostart,
|
|
692
|
+
backfill: completedBackfill,
|
|
693
|
+
codex_sessions: run.summary,
|
|
694
|
+
...(!onboardOk
|
|
695
|
+
? {
|
|
696
|
+
blocker: "autostart_load_failed",
|
|
697
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
698
|
+
}
|
|
699
|
+
: {}),
|
|
700
|
+
}));
|
|
701
|
+
return finishOnboardRun(command, installEvents, io, onboardOk ? 0 : 1);
|
|
702
|
+
}
|
|
703
|
+
writeLine(io.stdout, "4/5 Uploaded what you worked on.");
|
|
704
|
+
writeLine(io.stdout, `HTTP: ${sync.http_status}`);
|
|
705
|
+
writeLine(io.stdout, `Things recorded: ${sync.event_count}`);
|
|
706
|
+
writeLine(io.stdout, `Sources: ${sync.source_scan_count}`);
|
|
707
|
+
writeLine(io.stdout, `Risk flags: ${sync.risk_flag_count}`);
|
|
708
|
+
writeLine(io.stdout, `Raw evidence files: ${sync.raw_evidence_file_count}`);
|
|
709
|
+
writeLine(io.stdout, rawEvidenceSyncLine(sync));
|
|
710
|
+
writeAgentSessionSummary(io, run.summary);
|
|
711
|
+
writeLine(io.stdout, "5/5 Status ready.");
|
|
712
|
+
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
713
|
+
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
714
|
+
const setup = await completeOnboardingSetup(resolvedCommand, collectionRoots, installEvents, io);
|
|
715
|
+
const { agentRules, autostart } = setup;
|
|
716
|
+
if (!setup.ok) {
|
|
717
|
+
writeOnboardAutostartBlocker(io, autostart);
|
|
718
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
719
|
+
}
|
|
720
|
+
writeLine(io.stdout, "PASS: Tower is set up and collecting.");
|
|
721
|
+
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
722
|
+
pair,
|
|
723
|
+
status,
|
|
724
|
+
sync,
|
|
725
|
+
agentRules,
|
|
726
|
+
autostart,
|
|
727
|
+
initialSyncOk: true,
|
|
728
|
+
backfill: completedBackfill,
|
|
729
|
+
});
|
|
730
|
+
return finishOnboardRun(command, installEvents, io, 0);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Every uncaught error from either onboarding arm lands here. `status` is
|
|
734
|
+
* always recomputed fresh against whichever root got furthest, never trusted
|
|
735
|
+
* from before the throw — the arms already recompute it themselves on their
|
|
736
|
+
* own success paths, so nothing upstream is lost by not threading it through.
|
|
737
|
+
*/
|
|
738
|
+
async function handleOnboardFailure(error, command, rootsResult, install, pair, sync, installEvents, io) {
|
|
739
|
+
const message = errorMessage(error);
|
|
740
|
+
const statusRoot = rootsResult?.roots[0] ?? command.repoRoot;
|
|
741
|
+
const status = statusRoot
|
|
742
|
+
? await inspectLocalCollectorStatus({
|
|
743
|
+
homeDir: command.homeDir,
|
|
744
|
+
repoRoot: statusRoot,
|
|
745
|
+
branch: command.branch,
|
|
746
|
+
}).catch(() => null)
|
|
747
|
+
: null;
|
|
748
|
+
const blocker = classifyOnboardBlocker(message);
|
|
749
|
+
addOnboardFailureEvent(installEvents, blocker);
|
|
750
|
+
const nextStep = nextStepForOnboardBlocker(blocker, command);
|
|
751
|
+
if (command.json) {
|
|
752
|
+
writeOnboardJson(io, onboardJsonPayload("blocked", command, install, pair, sync, status, rootsResult, {
|
|
753
|
+
blocker,
|
|
754
|
+
message,
|
|
755
|
+
next_step: nextStep,
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
writeLine(io.stderr, `BLOCKED: ${message}`);
|
|
760
|
+
writeLine(io.stderr, `Next: ${nextStep}`);
|
|
704
761
|
}
|
|
762
|
+
return finishOnboardRun(command, installEvents, io, 1);
|
|
705
763
|
}
|
|
706
764
|
async function runMultiRepoOnboard(command, io, worktrees) {
|
|
707
765
|
if (!command.json) {
|
|
@@ -1207,6 +1265,13 @@ function syncResult(run) {
|
|
|
1207
1265
|
failureReasons: run.ok ? [] : run.failure_reasons,
|
|
1208
1266
|
};
|
|
1209
1267
|
}
|
|
1268
|
+
/**
|
|
1269
|
+
* The sync runbook: resolve which roots to collect, dedup staged packs before
|
|
1270
|
+
* touching anything else, discover worktrees, sync them, then report — one of
|
|
1271
|
+
* three shapes depending on how many worktrees came back. The three shapes
|
|
1272
|
+
* share nothing but `dedup`, so each gets its own step function below rather
|
|
1273
|
+
* than one branchy body.
|
|
1274
|
+
*/
|
|
1210
1275
|
async function runSyncLocked(command, io) {
|
|
1211
1276
|
const collectionRoots = await resolveSyncCollectionRoots(command);
|
|
1212
1277
|
// Before anything is collected: collapse byte-identical staged packs. It runs
|
|
@@ -1233,33 +1298,7 @@ async function runSyncLocked(command, io) {
|
|
|
1233
1298
|
fetchImpl: io.fetch,
|
|
1234
1299
|
});
|
|
1235
1300
|
if (run.outcomes.length > 1) {
|
|
1236
|
-
|
|
1237
|
-
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
1238
|
-
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1239
|
-
if (command.json) {
|
|
1240
|
-
writeLine(io.stdout, JSON.stringify({
|
|
1241
|
-
mode: "multi_repo",
|
|
1242
|
-
status: collectionRunStatus,
|
|
1243
|
-
collection_complete: run.ok,
|
|
1244
|
-
results: run.outcomes.map((outcome) => outcome.sync),
|
|
1245
|
-
repos: rows,
|
|
1246
|
-
codex_sessions: run.summary,
|
|
1247
|
-
raw_evidence_gc: gc,
|
|
1248
|
-
raw_evidence_dedup: dedup,
|
|
1249
|
-
}, null, 2));
|
|
1250
|
-
return syncResult(run);
|
|
1251
|
-
}
|
|
1252
|
-
writeLine(run.ok ? io.stdout : io.stderr, `Tower parent sync ${collectionRunStatus} ${run.outcomes.filter((outcome) => outcome.sync.status === "uploaded").length}/${run.outcomes.length} worktree(s).`);
|
|
1253
|
-
for (const outcome of run.outcomes) {
|
|
1254
|
-
const { worktree, sync } = outcome;
|
|
1255
|
-
const uploaded = sync.status === "uploaded";
|
|
1256
|
-
const failureSuffix = sync.status === "spooled" ? ` reason:${sync.failure_reason}` : "";
|
|
1257
|
-
writeLine(uploaded ? io.stdout : io.stderr, `- ${worktree.repo_label}/${worktree.worktree_label} (${worktree.branch}) head:${shortSha(sync.head_sha ?? worktree.head_sha)} ${sync.status} objects:${sync.raw_evidence_uploaded_object_count} chunks:${sync.raw_evidence_uploaded_chunk_count} reused:${sync.raw_evidence_reused_count} failed:${sync.raw_evidence_failed_count} cursor:${sync.cursor_tracked_object_count}${failureSuffix}`);
|
|
1258
|
-
}
|
|
1259
|
-
writeAgentSessionSummary(io, run.summary);
|
|
1260
|
-
if (gc && !gc.skipped)
|
|
1261
|
-
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1262
|
-
return syncResult(run);
|
|
1301
|
+
return reportMultiRepoSync(command, io, run, dedup);
|
|
1263
1302
|
}
|
|
1264
1303
|
// Zero worktrees is a legitimate steady state, not a failure: an approved
|
|
1265
1304
|
// root can hold no git repos, and sessions upload independently of
|
|
@@ -1268,25 +1307,60 @@ async function runSyncLocked(command, io) {
|
|
|
1268
1307
|
// fleet machine with a single empty root and taught people to ignore
|
|
1269
1308
|
// sync_failed (BLI-2722). A genuinely broken run still fails via run.ok.
|
|
1270
1309
|
if (run.outcomes.length === 0) {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1310
|
+
return reportNoWorktreeSync(command, io, run, dedup);
|
|
1311
|
+
}
|
|
1312
|
+
return reportSingleRepoSync(command, io, run, dedup);
|
|
1313
|
+
}
|
|
1314
|
+
async function reportMultiRepoSync(command, io, run, dedup) {
|
|
1315
|
+
const rows = run.outcomes.map((outcome) => worktreeSyncRow(outcome, run));
|
|
1316
|
+
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
1317
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1318
|
+
if (command.json) {
|
|
1319
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1320
|
+
mode: "multi_repo",
|
|
1321
|
+
status: collectionRunStatus,
|
|
1322
|
+
collection_complete: run.ok,
|
|
1323
|
+
results: run.outcomes.map((outcome) => outcome.sync),
|
|
1324
|
+
repos: rows,
|
|
1325
|
+
codex_sessions: run.summary,
|
|
1326
|
+
raw_evidence_gc: gc,
|
|
1327
|
+
raw_evidence_dedup: dedup,
|
|
1328
|
+
}, null, 2));
|
|
1288
1329
|
return syncResult(run);
|
|
1289
1330
|
}
|
|
1331
|
+
writeLine(run.ok ? io.stdout : io.stderr, `Tower parent sync ${collectionRunStatus} ${run.outcomes.filter((outcome) => outcome.sync.status === "uploaded").length}/${run.outcomes.length} worktree(s).`);
|
|
1332
|
+
for (const outcome of run.outcomes) {
|
|
1333
|
+
const { worktree, sync } = outcome;
|
|
1334
|
+
const uploaded = sync.status === "uploaded";
|
|
1335
|
+
const failureSuffix = sync.status === "spooled" ? ` reason:${sync.failure_reason}` : "";
|
|
1336
|
+
writeLine(uploaded ? io.stdout : io.stderr, `- ${worktree.repo_label}/${worktree.worktree_label} (${worktree.branch}) head:${shortSha(sync.head_sha ?? worktree.head_sha)} ${sync.status} objects:${sync.raw_evidence_uploaded_object_count} chunks:${sync.raw_evidence_uploaded_chunk_count} reused:${sync.raw_evidence_reused_count} failed:${sync.raw_evidence_failed_count} cursor:${sync.cursor_tracked_object_count}${failureSuffix}`);
|
|
1337
|
+
}
|
|
1338
|
+
writeAgentSessionSummary(io, run.summary);
|
|
1339
|
+
if (gc && !gc.skipped)
|
|
1340
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1341
|
+
return syncResult(run);
|
|
1342
|
+
}
|
|
1343
|
+
async function reportNoWorktreeSync(command, io, run, dedup) {
|
|
1344
|
+
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
1345
|
+
const gc = run.ok ? await runSyncRawEvidenceGc(command, io) : null;
|
|
1346
|
+
if (command.json) {
|
|
1347
|
+
writeLine(io.stdout, JSON.stringify({
|
|
1348
|
+
mode: "no_worktrees",
|
|
1349
|
+
status: collectionRunStatus,
|
|
1350
|
+
collection_complete: run.ok,
|
|
1351
|
+
codex_sessions: run.summary,
|
|
1352
|
+
raw_evidence_gc: gc,
|
|
1353
|
+
raw_evidence_dedup: dedup,
|
|
1354
|
+
}, null, 2));
|
|
1355
|
+
return syncResult(run);
|
|
1356
|
+
}
|
|
1357
|
+
writeLine(run.ok ? io.stdout : io.stderr, `Tower sync ${collectionRunStatus}: no git worktrees under this root; session scan ran.`);
|
|
1358
|
+
writeAgentSessionSummary(io, run.summary);
|
|
1359
|
+
if (gc && !gc.skipped)
|
|
1360
|
+
writeLine(io.stdout, rawEvidenceGcSummary(gc));
|
|
1361
|
+
return syncResult(run);
|
|
1362
|
+
}
|
|
1363
|
+
async function reportSingleRepoSync(command, io, run, dedup) {
|
|
1290
1364
|
const result = run.outcomes[0]?.sync;
|
|
1291
1365
|
if (!result) {
|
|
1292
1366
|
throw new Error("Sync produced no result for the repo worktree.");
|