@bli-cockpit/cli 0.2.33 → 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/agent-rules.js +4 -4
- package/dist/autostart.js +4 -4
- package/dist/commands/backfill.js +493 -408
- package/dist/commands/collection-roots.js +3 -3
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/install-update.js +6 -6
- package/dist/commands/jarvis.js +40 -1
- package/dist/commands/local-args.js +5 -2
- package/dist/commands/local-auth.js +3 -3
- package/dist/commands/local-discovery.js +2 -2
- package/dist/commands/local-help.js +11 -9
- package/dist/commands/local.js +411 -337
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync.js +323 -210
- package/dist/commands/sessions.js +1 -1
- package/dist/commands/status.js +2 -2
- package/dist/onboarding-roots.js +1 -1
- package/package.json +4 -3
package/dist/commands/local.js
CHANGED
|
@@ -185,7 +185,7 @@ async function refreshOnboardAutostart(command, roots, io) {
|
|
|
185
185
|
return result;
|
|
186
186
|
}
|
|
187
187
|
writeLine(io.stdout, result.loaded
|
|
188
|
-
? "Background autostart refreshed;
|
|
188
|
+
? "Background autostart refreshed; Tower syncs at login and every 15 min."
|
|
189
189
|
: "Background autostart refreshed, but the operating-system scheduler reported a problem; check `cockpit autostart status`.");
|
|
190
190
|
writeLine(io.stdout, autostartLocationLine(result));
|
|
191
191
|
return result;
|
|
@@ -205,7 +205,7 @@ async function refreshOnboardAgentRules(command, roots, io) {
|
|
|
205
205
|
}
|
|
206
206
|
function onboardAgentRulesInstallLine(result) {
|
|
207
207
|
if (result.targets.some((target) => target.stale_block_replaced)) {
|
|
208
|
-
return "updated; replaced stale
|
|
208
|
+
return "updated; replaced stale Tower ticket-binding guidance.";
|
|
209
209
|
}
|
|
210
210
|
switch (result.status) {
|
|
211
211
|
case "created":
|
|
@@ -311,7 +311,7 @@ function backgroundSyncLine(result) {
|
|
|
311
311
|
return result.status;
|
|
312
312
|
}
|
|
313
313
|
function writeOnboardBanner(command, io) {
|
|
314
|
-
writeLine(io.stdout, "Setting up
|
|
314
|
+
writeLine(io.stdout, "Setting up Tower");
|
|
315
315
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
|
|
316
316
|
writeLine(io.stdout, `Ticket: ${displayTicketId(command.activeTicketId)}`);
|
|
317
317
|
}
|
|
@@ -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: Cockpit 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
|
}
|
|
664
|
-
|
|
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) {
|
|
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) {
|
|
@@ -736,7 +794,7 @@ async function runMultiRepoOnboard(command, io, worktrees) {
|
|
|
736
794
|
writeLine(io.stdout, "Live sync receipts complete; verifying all-history Codex and Claude backfill.");
|
|
737
795
|
}
|
|
738
796
|
else {
|
|
739
|
-
writeLine(io.stderr, `BLOCKED:
|
|
797
|
+
writeLine(io.stderr, `BLOCKED: Tower collection is ${collectionRunStatus}; retry until every eligible session has a durable receipt or explicit terminal reason.`);
|
|
740
798
|
}
|
|
741
799
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
742
800
|
}
|
|
@@ -773,7 +831,7 @@ async function runLogin(command, io) {
|
|
|
773
831
|
onPairStarted: command.json
|
|
774
832
|
? undefined
|
|
775
833
|
: (request) => {
|
|
776
|
-
writeLine(io.stdout, "
|
|
834
|
+
writeLine(io.stdout, "Tower device pairing started.");
|
|
777
835
|
writeLine(io.stdout, `Open: ${request.approve_url}`);
|
|
778
836
|
writeLine(io.stdout, `Code: ${request.user_code}`);
|
|
779
837
|
writeLine(io.stdout, "Waiting for dashboard approval...");
|
|
@@ -783,13 +841,13 @@ async function runLogin(command, io) {
|
|
|
783
841
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
784
842
|
return 0;
|
|
785
843
|
}
|
|
786
|
-
writeLine(io.stdout, "
|
|
844
|
+
writeLine(io.stdout, "Tower collector paired.");
|
|
787
845
|
writeLine(io.stdout, `Session: ${result.session_file}`);
|
|
788
846
|
writeLine(io.stdout, `User: ${result.session.email ?? result.session.auth_subject_id}`);
|
|
789
847
|
writeLine(io.stdout, `Device: ${result.session.device_name ?? result.session.device_id ?? "unknown"}`);
|
|
790
848
|
writeLine(io.stdout, config.default_repo_paths.length > 0
|
|
791
849
|
? "Next: run `cockpit start` inside the repo."
|
|
792
|
-
: "Next: run `cockpit onboard` from your repo root so
|
|
850
|
+
: "Next: run `cockpit onboard` from your repo root so Tower knows which repos to collect.");
|
|
793
851
|
return 0;
|
|
794
852
|
}
|
|
795
853
|
function writePairingInstructions(io, request) {
|
|
@@ -862,8 +920,8 @@ async function runLogout(command, io) {
|
|
|
862
920
|
return 0;
|
|
863
921
|
}
|
|
864
922
|
writeLine(io.stdout, result.removed
|
|
865
|
-
? "
|
|
866
|
-
: "No
|
|
923
|
+
? "Tower collector session removed."
|
|
924
|
+
: "No Tower collector session found.");
|
|
867
925
|
return 0;
|
|
868
926
|
}
|
|
869
927
|
async function runStart(command, io) {
|
|
@@ -888,7 +946,7 @@ async function runStart(command, io) {
|
|
|
888
946
|
writeLine(io.stdout, JSON.stringify({ mode: "multi_repo", contexts }, null, 2));
|
|
889
947
|
return 0;
|
|
890
948
|
}
|
|
891
|
-
writeLine(io.stdout, `
|
|
949
|
+
writeLine(io.stdout, `Tower parent work context active for ${contexts.length} worktree(s).`);
|
|
892
950
|
for (const context of contexts) {
|
|
893
951
|
writeLine(io.stdout, `- ${context.repo_label ?? context.repo}/${context.worktree_label ?? "worktree"} · ${context.branch} · ${context.work_context_id}`);
|
|
894
952
|
}
|
|
@@ -899,7 +957,7 @@ async function runStart(command, io) {
|
|
|
899
957
|
writeLine(io.stdout, JSON.stringify(context, null, 2));
|
|
900
958
|
return 0;
|
|
901
959
|
}
|
|
902
|
-
writeLine(io.stdout, "
|
|
960
|
+
writeLine(io.stdout, "Tower work context active.");
|
|
903
961
|
writeLine(io.stdout, `Repo: ${context.repo}`);
|
|
904
962
|
writeLine(io.stdout, `Branch: ${context.branch}`);
|
|
905
963
|
writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
|
|
@@ -1156,7 +1214,7 @@ async function runSyncWithHealthReceipt(command, io) {
|
|
|
1156
1214
|
}, null, 2));
|
|
1157
1215
|
}
|
|
1158
1216
|
else {
|
|
1159
|
-
writeLine(io.stdout, "
|
|
1217
|
+
writeLine(io.stdout, "Tower sync already running; skipping this run.");
|
|
1160
1218
|
}
|
|
1161
1219
|
return {
|
|
1162
1220
|
exitCode: 0,
|
|
@@ -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, `Cockpit 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.");
|
|
@@ -1305,7 +1379,7 @@ async function runSyncLocked(command, io) {
|
|
|
1305
1379
|
return syncResult(run);
|
|
1306
1380
|
}
|
|
1307
1381
|
if (run.ok) {
|
|
1308
|
-
writeLine(io.stdout, "
|
|
1382
|
+
writeLine(io.stdout, "Tower uploaded this session.");
|
|
1309
1383
|
writeLine(io.stdout, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
1310
1384
|
writeLine(io.stdout, `Context: ${result.work_context_id}`);
|
|
1311
1385
|
writeLine(io.stdout, `Head: ${shortSha(result.head_sha)}`);
|
|
@@ -1320,11 +1394,11 @@ async function runSyncLocked(command, io) {
|
|
|
1320
1394
|
return syncResult(run);
|
|
1321
1395
|
}
|
|
1322
1396
|
if (result.status === "uploaded") {
|
|
1323
|
-
writeLine(io.stderr, "
|
|
1397
|
+
writeLine(io.stderr, "Tower uploaded, but some sessions did not make it. Run `cockpit sync` again.");
|
|
1324
1398
|
writeAgentSessionSummary(io, run.summary);
|
|
1325
1399
|
return syncResult(run);
|
|
1326
1400
|
}
|
|
1327
|
-
writeLine(io.stderr, "
|
|
1401
|
+
writeLine(io.stderr, "Tower could not upload. It saved a note to retry and will try again on the next sync.");
|
|
1328
1402
|
writeLine(io.stderr, `Ticket: ${displayTicketId(result.ticket_id)}`);
|
|
1329
1403
|
writeLine(io.stderr, `Failure: ${result.failure_reason}`);
|
|
1330
1404
|
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
@@ -1364,8 +1438,8 @@ async function runAnalyze(command, io) {
|
|
|
1364
1438
|
sync: syncOutput,
|
|
1365
1439
|
error: syncStderr.join("").trim()
|
|
1366
1440
|
|| (syncExitCode === 0
|
|
1367
|
-
? "
|
|
1368
|
-
: "
|
|
1441
|
+
? "Tower sync did not upload fresh evidence."
|
|
1442
|
+
: "Tower sync failed."),
|
|
1369
1443
|
}, null, 2));
|
|
1370
1444
|
}
|
|
1371
1445
|
else {
|
|
@@ -1387,7 +1461,7 @@ async function runAnalyze(command, io) {
|
|
|
1387
1461
|
...describeError(error),
|
|
1388
1462
|
}));
|
|
1389
1463
|
}
|
|
1390
|
-
throw new Error("
|
|
1464
|
+
throw new Error("Tower is not signed in. Run `cockpit onboard` or `cockpit login` first.");
|
|
1391
1465
|
});
|
|
1392
1466
|
const dashboardUrl = normalizeUrl(command.dashboardUrl ?? session.dashboard_url ?? DEFAULT_DASHBOARD_URL);
|
|
1393
1467
|
const response = await io.fetch(`${dashboardUrl}/api/ambient/analyze`, {
|
|
@@ -1433,8 +1507,8 @@ async function runAnalyze(command, io) {
|
|
|
1433
1507
|
return 0;
|
|
1434
1508
|
}
|
|
1435
1509
|
replayCaptured(io.stderr, syncStderr);
|
|
1436
|
-
writeLine(io.stdout, "
|
|
1437
|
-
writeLine(io.stdout, "
|
|
1510
|
+
writeLine(io.stdout, "Tower uploaded your latest work.");
|
|
1511
|
+
writeLine(io.stdout, "Tower analysis queued.");
|
|
1438
1512
|
if (jobId)
|
|
1439
1513
|
writeLine(io.stdout, `Job: ${jobId}`);
|
|
1440
1514
|
writeLine(io.stdout, `Status: ${jobStatus === "pending" ? "queued" : jobStatus}`);
|
|
@@ -1476,7 +1550,7 @@ async function runServe(command, io) {
|
|
|
1476
1550
|
await new Promise((resolve) => {
|
|
1477
1551
|
server.listen(command.port, "127.0.0.1", resolve);
|
|
1478
1552
|
});
|
|
1479
|
-
writeLine(io.stdout, `
|
|
1553
|
+
writeLine(io.stdout, `Tower collector serving on http://127.0.0.1:${command.port}`);
|
|
1480
1554
|
await new Promise((resolve) => {
|
|
1481
1555
|
server.on("close", resolve);
|
|
1482
1556
|
});
|
|
@@ -1544,8 +1618,8 @@ async function runAgentRules(command, io) {
|
|
|
1544
1618
|
}
|
|
1545
1619
|
if ("installed" in result) {
|
|
1546
1620
|
writeLine(io.stdout, result.installed
|
|
1547
|
-
? "
|
|
1548
|
-
: "
|
|
1621
|
+
? "Tower agent rules are installed."
|
|
1622
|
+
: "Tower agent rules are not installed.");
|
|
1549
1623
|
}
|
|
1550
1624
|
else {
|
|
1551
1625
|
writeLine(io.stdout, agentRulesStatusLine(result));
|
|
@@ -1561,13 +1635,13 @@ function agentRuleHosts(host) {
|
|
|
1561
1635
|
function agentRulesStatusLine(result) {
|
|
1562
1636
|
switch (result.status) {
|
|
1563
1637
|
case "created":
|
|
1564
|
-
return "
|
|
1638
|
+
return "Tower agent rules installed.";
|
|
1565
1639
|
case "updated":
|
|
1566
|
-
return "
|
|
1640
|
+
return "Tower agent rules updated.";
|
|
1567
1641
|
case "unchanged":
|
|
1568
|
-
return "
|
|
1642
|
+
return "Tower agent rules already current.";
|
|
1569
1643
|
case "missing":
|
|
1570
|
-
return "
|
|
1644
|
+
return "Tower agent rules were not installed.";
|
|
1571
1645
|
}
|
|
1572
1646
|
}
|
|
1573
1647
|
function agentRuleHostLabel(host) {
|
|
@@ -1577,8 +1651,8 @@ function writeAutostartResult(io, result) {
|
|
|
1577
1651
|
switch (result.status) {
|
|
1578
1652
|
case "installed":
|
|
1579
1653
|
writeLine(io.stdout, result.loaded
|
|
1580
|
-
? "
|
|
1581
|
-
: "
|
|
1654
|
+
? "Tower autostart installed and loaded."
|
|
1655
|
+
: "Tower autostart installed (the operating-system scheduler reported a problem).");
|
|
1582
1656
|
writeLine(io.stdout, `Label: ${result.label}`);
|
|
1583
1657
|
writeLine(io.stdout, autostartLocationLine(result));
|
|
1584
1658
|
writeLine(io.stdout, `Interval: every ${result.interval_seconds}s`);
|
|
@@ -1588,19 +1662,19 @@ function writeAutostartResult(io, result) {
|
|
|
1588
1662
|
writeLine(io.stderr, result.message);
|
|
1589
1663
|
return;
|
|
1590
1664
|
case "uninstalled":
|
|
1591
|
-
writeLine(io.stdout, "
|
|
1665
|
+
writeLine(io.stdout, "Tower autostart removed.");
|
|
1592
1666
|
writeLine(io.stdout, autostartLocationLine(result));
|
|
1593
1667
|
return;
|
|
1594
1668
|
case "absent":
|
|
1595
|
-
writeLine(io.stdout, "
|
|
1669
|
+
writeLine(io.stdout, "Tower autostart is not installed.");
|
|
1596
1670
|
writeLine(io.stdout, autostartLocationLine(result));
|
|
1597
1671
|
return;
|
|
1598
1672
|
case "loaded":
|
|
1599
|
-
writeLine(io.stdout, "
|
|
1673
|
+
writeLine(io.stdout, "Tower autostart is installed and loaded.");
|
|
1600
1674
|
writeLine(io.stdout, autostartLocationLine(result));
|
|
1601
1675
|
return;
|
|
1602
1676
|
case "not_loaded":
|
|
1603
|
-
writeLine(io.stdout, "
|
|
1677
|
+
writeLine(io.stdout, "Tower autostart exists but is disabled or not loaded; run `cockpit autostart install` to repair it.");
|
|
1604
1678
|
writeLine(io.stdout, autostartLocationLine(result));
|
|
1605
1679
|
if (result.message)
|
|
1606
1680
|
writeLine(io.stderr, result.message);
|