@bli-cockpit/cli 0.2.47 → 0.2.49
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/adapters/raw-evidence-attribution-gaps.js +133 -0
- package/dist/adapters/raw-evidence.js +360 -349
- package/dist/autostart-contract.js +79 -0
- package/dist/autostart-darwin-plist.js +265 -0
- package/dist/autostart-darwin.js +171 -0
- package/dist/autostart-windows-scripts.js +310 -0
- package/dist/autostart-windows-task-xml.js +260 -0
- package/dist/autostart-windows.js +237 -0
- package/dist/autostart-xml.js +23 -0
- package/dist/autostart.js +35 -1148
- package/dist/commands/agent-rules-command.js +55 -0
- package/dist/commands/agent-session-report.js +290 -0
- package/dist/commands/analyze.js +131 -0
- package/dist/commands/autostart-command.js +105 -0
- package/dist/commands/backfill.js +824 -551
- package/dist/commands/cli-io.js +13 -0
- package/dist/commands/heartbeat.js +18 -0
- package/dist/commands/install-receipts.js +34 -0
- package/dist/commands/jarvis.js +179 -3
- package/dist/commands/local-arg-values.js +169 -0
- package/dist/commands/local-args-collector.js +578 -0
- package/dist/commands/local-args-tower.js +870 -0
- package/dist/commands/local-args.js +8 -1549
- package/dist/commands/local-help.js +11 -3
- package/dist/commands/local.js +18 -1786
- package/dist/commands/login.js +53 -0
- package/dist/commands/logout.js +66 -0
- package/dist/commands/onboard-receipts.js +66 -0
- package/dist/commands/onboard-report.js +274 -0
- package/dist/commands/onboard.js +449 -0
- package/dist/commands/ops-render.js +36 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/serve.js +13 -0
- package/dist/commands/session-sync.js +513 -534
- package/dist/commands/settings-render.js +28 -0
- package/dist/commands/settings.js +66 -2
- package/dist/commands/start.js +47 -0
- package/dist/commands/sync-followups.js +203 -0
- package/dist/commands/sync.js +381 -0
- package/dist/dev-build.js +186 -0
- package/dist/tower-stream.js +20 -4
- package/package.json +2 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { writeLine } from "./cli-io.js";
|
|
2
|
+
import { pairLocalCollectorWithAuthFallback, requestPairingAccessToken, resolveInteractiveLoginEmail, } from "./local-auth.js";
|
|
3
|
+
import { ensureLocalCollectorConfig } from "../local-state.js";
|
|
4
|
+
export async function runLogin(command, io) {
|
|
5
|
+
// Standalone `cockpit login` must work on a fresh machine: bootstrap a
|
|
6
|
+
// minimal rootless config when onboard/install has not run yet, instead of
|
|
7
|
+
// failing on the missing config file.
|
|
8
|
+
const { config, paths, created } = await ensureLocalCollectorConfig({
|
|
9
|
+
homeDir: command.homeDir,
|
|
10
|
+
dashboardUrl: command.dashboardUrl,
|
|
11
|
+
});
|
|
12
|
+
const dashboardUrl = command.dashboardUrl ?? config.dashboard_url;
|
|
13
|
+
if (created && !command.json) {
|
|
14
|
+
writeLine(io.stdout, `First login on this machine; wrote collector config: ${paths.config_file}`);
|
|
15
|
+
}
|
|
16
|
+
const claimedOwnerEmail = await resolveInteractiveLoginEmail(command, io);
|
|
17
|
+
const pairingAccessToken = await requestPairingAccessToken({
|
|
18
|
+
dashboardUrl,
|
|
19
|
+
email: claimedOwnerEmail,
|
|
20
|
+
noAuth: command.noAuth,
|
|
21
|
+
json: command.json,
|
|
22
|
+
}, io);
|
|
23
|
+
const result = await pairLocalCollectorWithAuthFallback({
|
|
24
|
+
homeDir: command.homeDir,
|
|
25
|
+
dashboardUrl,
|
|
26
|
+
claimedOwnerEmail,
|
|
27
|
+
deviceName: command.deviceName,
|
|
28
|
+
pollIntervalMs: command.pollIntervalMs,
|
|
29
|
+
timeoutMs: command.timeoutMs,
|
|
30
|
+
pairingAccessToken,
|
|
31
|
+
fetch: io.fetch,
|
|
32
|
+
onPairStarted: command.json
|
|
33
|
+
? undefined
|
|
34
|
+
: (request) => {
|
|
35
|
+
writeLine(io.stdout, "Tower device pairing started.");
|
|
36
|
+
writeLine(io.stdout, `Open: ${request.approve_url}`);
|
|
37
|
+
writeLine(io.stdout, `Code: ${request.user_code}`);
|
|
38
|
+
writeLine(io.stdout, "Waiting for dashboard approval...");
|
|
39
|
+
},
|
|
40
|
+
}, io);
|
|
41
|
+
if (command.json) {
|
|
42
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
writeLine(io.stdout, "Tower collector paired.");
|
|
46
|
+
writeLine(io.stdout, `Session: ${result.session_file}`);
|
|
47
|
+
writeLine(io.stdout, `User: ${result.session.email ?? result.session.auth_subject_id}`);
|
|
48
|
+
writeLine(io.stdout, `Device: ${result.session.device_name ?? result.session.device_id ?? "unknown"}`);
|
|
49
|
+
writeLine(io.stdout, config.default_repo_paths.length > 0
|
|
50
|
+
? "Next: run `cockpit start` inside the repo."
|
|
51
|
+
: "Next: run `cockpit onboard` from your repo root so Tower knows which repos to collect.");
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { errorMessage, writeLine } from "./cli-io.js";
|
|
2
|
+
import { asRecord, callTower, openTower } from "./tower-command.js";
|
|
3
|
+
import { getCollectorRuntimePaths, logoutLocalCollector, readLocalCollectorSessionFile, } from "../local-state.js";
|
|
4
|
+
/**
|
|
5
|
+
* `cockpit logout` removes the local session file. `--revoke` also tells Tower
|
|
6
|
+
* to end the device's token (BLI-3461).
|
|
7
|
+
*
|
|
8
|
+
* Those are two different facts, and the difference matters when a laptop is
|
|
9
|
+
* being handed back: without the revoke the machine stops collecting but the
|
|
10
|
+
* token it held is still one the server would honour. The revoke is attempted
|
|
11
|
+
* FIRST, because it needs the token the local logout is about to delete, and a
|
|
12
|
+
* revoke that fails does not stop the local logout — the person asked to be
|
|
13
|
+
* signed out of this machine, and they get that either way. The outcome of each
|
|
14
|
+
* half is reported separately rather than folded into one "ok".
|
|
15
|
+
*/
|
|
16
|
+
export async function runLogout(command, io) {
|
|
17
|
+
const revoke = command.revoke ? await revokeThisDevice(command, io) : null;
|
|
18
|
+
const result = await logoutLocalCollector({ homeDir: command.homeDir });
|
|
19
|
+
writeLine(io.stderr, `[logout] done ${JSON.stringify({
|
|
20
|
+
local_session: result.removed ? "removed" : "absent",
|
|
21
|
+
revoke: revoke ? revoke.outcome : "not_requested",
|
|
22
|
+
})}`);
|
|
23
|
+
if (command.json) {
|
|
24
|
+
writeLine(io.stdout, JSON.stringify({ ...result, revoke: revoke ?? { outcome: "not_requested" } }, null, 2));
|
|
25
|
+
return revoke && revoke.outcome !== "revoked" ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
writeLine(io.stdout, result.removed
|
|
28
|
+
? "Tower collector session removed."
|
|
29
|
+
: "No Tower collector session found.");
|
|
30
|
+
if (revoke) {
|
|
31
|
+
writeLine(io.stdout, revoke.outcome === "revoked"
|
|
32
|
+
? "This device's Tower token is revoked."
|
|
33
|
+
: `This device's Tower token was NOT revoked: ${revoke.detail}`);
|
|
34
|
+
}
|
|
35
|
+
return revoke && revoke.outcome !== "revoked" ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ends this device's own session server-side.
|
|
39
|
+
*
|
|
40
|
+
* The route gates the device arm on `self_revoke_only`, so the device id is
|
|
41
|
+
* read from the paired session rather than accepted as an argument — there is
|
|
42
|
+
* no way to spell "revoke somebody else's laptop" here, and that is deliberate.
|
|
43
|
+
*/
|
|
44
|
+
async function revokeThisDevice(command, io) {
|
|
45
|
+
let tower;
|
|
46
|
+
let deviceId;
|
|
47
|
+
try {
|
|
48
|
+
const session = await readLocalCollectorSessionFile(getCollectorRuntimePaths(command.homeDir));
|
|
49
|
+
deviceId = session.device_id;
|
|
50
|
+
tower = await openTower("logout", command, io);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return { outcome: "no_paired_session", detail: errorMessage(error) };
|
|
54
|
+
}
|
|
55
|
+
const result = await callTower(tower, {
|
|
56
|
+
path: `/api/ambient/devices/${encodeURIComponent(deviceId)}/revoke`,
|
|
57
|
+
method: "POST",
|
|
58
|
+
label: "logout revoke",
|
|
59
|
+
body: { reason: "Signed out from the device" },
|
|
60
|
+
});
|
|
61
|
+
if (!result.ok) {
|
|
62
|
+
return { outcome: result.reason, detail: result.detail };
|
|
63
|
+
}
|
|
64
|
+
const body = asRecord(result.body);
|
|
65
|
+
return { outcome: "revoked", detail: String(asRecord(body.device).revoked_at ?? "") };
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What onboarding records about itself: one install-event step per thing it
|
|
3
|
+
* did, and the blocker label a failure earns.
|
|
4
|
+
*
|
|
5
|
+
* Split out of commands/onboard.ts (BLI-3578), moved verbatim. It is the same
|
|
6
|
+
* ledger `./install-receipts.ts` posts for every command — this module only
|
|
7
|
+
* decides which step a given onboarding outcome belongs to, so the runbook next
|
|
8
|
+
* door never has to.
|
|
9
|
+
*/
|
|
10
|
+
import { addInstallEvent } from "./install-receipts.js";
|
|
11
|
+
import { COLLECTION_ROOT_REQUIRED } from "../onboarding-roots.js";
|
|
12
|
+
export function addOnboardFailureEvent(events, blocker) {
|
|
13
|
+
const step = onboardFailureStep(blocker, events);
|
|
14
|
+
const existing = events.find((event) => event.step === step && event.status === "fail");
|
|
15
|
+
if (existing)
|
|
16
|
+
return;
|
|
17
|
+
addInstallEvent(events, step, "fail", blocker);
|
|
18
|
+
}
|
|
19
|
+
function onboardFailureStep(blocker, events) {
|
|
20
|
+
if (blocker === COLLECTION_ROOT_REQUIRED)
|
|
21
|
+
return "install";
|
|
22
|
+
if (blocker === "device_pairing")
|
|
23
|
+
return "pair";
|
|
24
|
+
if (blocker === "work_context" || blocker === "ticket_binding" || blocker === "ticket") {
|
|
25
|
+
return "work_context";
|
|
26
|
+
}
|
|
27
|
+
if (blocker === "network_or_ingest")
|
|
28
|
+
return "sync";
|
|
29
|
+
if (blocker === "install")
|
|
30
|
+
return "install";
|
|
31
|
+
const lastIncomplete = ["install", "auth", "pair", "work_context", "sync"].find((step) => !events.some((event) => event.step === step));
|
|
32
|
+
return lastIncomplete ?? "sync";
|
|
33
|
+
}
|
|
34
|
+
export function addAutostartInstallEvent(events, result) {
|
|
35
|
+
if (!result) {
|
|
36
|
+
addInstallEvent(events, "autostart", "skipped", "runner_unavailable");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (result.status === "unsupported") {
|
|
40
|
+
addInstallEvent(events, "autostart", "skipped", "unsupported", result.message);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
addInstallEvent(events, "autostart", result.loaded === false ? "fail" : "ok", result.loaded === false ? "autostart_load_failed" : undefined, result.loaded === false ? result.message : undefined);
|
|
44
|
+
}
|
|
45
|
+
export function onboardAutostartFailed(result) {
|
|
46
|
+
return (result !== null &&
|
|
47
|
+
result.status !== "unsupported" &&
|
|
48
|
+
result.loaded === false);
|
|
49
|
+
}
|
|
50
|
+
export function classifyOnboardBlocker(message) {
|
|
51
|
+
if (message.includes(COLLECTION_ROOT_REQUIRED))
|
|
52
|
+
return COLLECTION_ROOT_REQUIRED;
|
|
53
|
+
if (/ticket/i.test(message))
|
|
54
|
+
return "ticket";
|
|
55
|
+
if (/pair|paired|approval|expired|revoked/i.test(message))
|
|
56
|
+
return "device_pairing";
|
|
57
|
+
if (/fetch|network|ENOTFOUND|ECONNREFUSED|HTTP/i.test(message))
|
|
58
|
+
return "network_or_ingest";
|
|
59
|
+
if (/config|install/i.test(message))
|
|
60
|
+
return "install";
|
|
61
|
+
if (/context|start/i.test(message))
|
|
62
|
+
return "work_context";
|
|
63
|
+
if (/bound|binding/i.test(message))
|
|
64
|
+
return "ticket_binding";
|
|
65
|
+
return "unknown";
|
|
66
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { writeLine } from "./cli-io.js";
|
|
2
|
+
import { attributedSyncRunStatus, displayTicketId, rawEvidenceSyncLine, writeAgentSessionSummary, } from "./collection-report.js";
|
|
3
|
+
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, } from "../onboarding-roots.js";
|
|
4
|
+
export function writeOnboardBanner(command, io) {
|
|
5
|
+
writeLine(io.stdout, "Setting up Tower");
|
|
6
|
+
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
|
|
7
|
+
writeLine(io.stdout, `Ticket: ${displayTicketId(command.activeTicketId)}`);
|
|
8
|
+
}
|
|
9
|
+
export function writePairingInstructions(io, request) {
|
|
10
|
+
writeLine(io.stdout, "2/5 Device pairing started.");
|
|
11
|
+
writeLine(io.stdout, `Open: ${request.approve_url}`);
|
|
12
|
+
writeLine(io.stdout, `Code: ${request.user_code}`);
|
|
13
|
+
writeLine(io.stdout, "Waiting for dashboard approval...");
|
|
14
|
+
}
|
|
15
|
+
export function writeOnboardLiveStatus(io, command, roots, options) {
|
|
16
|
+
writeLine(io.stdout, "You're live.");
|
|
17
|
+
writeLine(io.stdout, "Collecting from:");
|
|
18
|
+
for (const root of roots)
|
|
19
|
+
writeLine(io.stdout, `- ${root}`);
|
|
20
|
+
writeLine(io.stdout, `Paired user: ${options.pair?.session.email ??
|
|
21
|
+
options.pair?.session.auth_subject_id ??
|
|
22
|
+
(options.status?.session_state === "valid" ? "existing valid session" : "pending")}`);
|
|
23
|
+
writeLine(io.stdout, `Background sync: ${backgroundSyncLine(options.autostart)}`);
|
|
24
|
+
writeLine(io.stdout, `Initial sync: ${options.initialSyncOk ? options.sync?.status ?? "uploaded" : "blocked"}`);
|
|
25
|
+
writeLine(io.stdout, `Historical backfill: complete (${options.backfill.counts.backfilled} uploaded, ${options.backfill.counts.skipped} explicit skip(s))`);
|
|
26
|
+
writeLine(io.stdout, `Agent rules: ${options.agentRules ? onboardAgentRulesInstallLine(options.agentRules) : "not refreshed"}`);
|
|
27
|
+
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}/my-work`);
|
|
28
|
+
writeLine(io.stdout, "Next: cockpit status");
|
|
29
|
+
}
|
|
30
|
+
function backgroundSyncLine(result) {
|
|
31
|
+
if (!result)
|
|
32
|
+
return "not refreshed (autostart runner unavailable)";
|
|
33
|
+
if (result.status === "unsupported")
|
|
34
|
+
return `unsupported (${result.message})`;
|
|
35
|
+
if (result.status === "installed" && result.loaded)
|
|
36
|
+
return "installed";
|
|
37
|
+
if (result.status === "installed") {
|
|
38
|
+
return `installed with warning (${result.message ?? "operating-system scheduler load failed"})`;
|
|
39
|
+
}
|
|
40
|
+
return result.status;
|
|
41
|
+
}
|
|
42
|
+
export function onboardAgentRulesInstallLine(result) {
|
|
43
|
+
if (result.targets.some((target) => target.stale_block_replaced)) {
|
|
44
|
+
return "updated; replaced stale Tower ticket-binding guidance.";
|
|
45
|
+
}
|
|
46
|
+
switch (result.status) {
|
|
47
|
+
case "created":
|
|
48
|
+
return "installed.";
|
|
49
|
+
case "updated":
|
|
50
|
+
return "updated.";
|
|
51
|
+
case "unchanged":
|
|
52
|
+
return "already current.";
|
|
53
|
+
case "missing":
|
|
54
|
+
return "not installed.";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function writeOnboardAutostartBlocker(io, result) {
|
|
58
|
+
writeLine(io.stderr, "BLOCKED: initial collection succeeded, but recurring background collection is not running.");
|
|
59
|
+
if (result?.message)
|
|
60
|
+
writeLine(io.stderr, `Failure: ${result.message}`);
|
|
61
|
+
writeLine(io.stderr, "Next: run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The blocked-backfill result, identical in both onboarding arms. `extra`
|
|
65
|
+
* carries the arm-specific keys and is spread where those keys appeared before,
|
|
66
|
+
* so the `--json` key order is unchanged.
|
|
67
|
+
*/
|
|
68
|
+
export function writeOnboardBackfillBlockedResult(io, options) {
|
|
69
|
+
if (!options.json) {
|
|
70
|
+
writeOnboardBackfillBlocker(io, options.backfill);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
writeLine(io.stdout, JSON.stringify({
|
|
74
|
+
...options.base,
|
|
75
|
+
...options.extra,
|
|
76
|
+
backfill: onboardBackfillPayload(options.backfill),
|
|
77
|
+
blocker: "backfill_incomplete",
|
|
78
|
+
backfill_failure_reason: options.backfill.failureReason,
|
|
79
|
+
next_step: options.backfill.retryCommand,
|
|
80
|
+
}, null, 2));
|
|
81
|
+
}
|
|
82
|
+
function writeOnboardBackfillBlocker(io, outcome) {
|
|
83
|
+
writeLine(io.stderr, "BLOCKED: all-history Codex and Claude backfill is incomplete for the approved collection roots.");
|
|
84
|
+
writeLine(io.stderr, `Failure: ${outcome.failureReason ?? `backfill_${outcome.status}`}`);
|
|
85
|
+
writeLine(io.stderr, `Retry: ${outcome.retryCommand}`);
|
|
86
|
+
}
|
|
87
|
+
function onboardBackfillPayload(outcome) {
|
|
88
|
+
return (outcome.result ?? {
|
|
89
|
+
status: "blocked",
|
|
90
|
+
failure_reason: outcome.failureReason,
|
|
91
|
+
retry_command: outcome.retryCommand,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Every onboard `--json` payload is `onboardResult` plus the roots this pass
|
|
96
|
+
* resolved plus whatever extra keys that arm of onboarding adds. Assembling it
|
|
97
|
+
* here means the call sites below cannot drift on key order or forget a field
|
|
98
|
+
* a sibling call site remembers.
|
|
99
|
+
*/
|
|
100
|
+
export function onboardJsonPayload(resultStatus, command, install, pair, sync, status, rootsResult, extra = {}) {
|
|
101
|
+
return {
|
|
102
|
+
...onboardResult(resultStatus, command, install, pair, sync, status),
|
|
103
|
+
collection_roots: rootsResult?.roots ?? [],
|
|
104
|
+
root_resolution: rootsResult,
|
|
105
|
+
...extra,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function writeOnboardJson(io, payload) {
|
|
109
|
+
writeLine(io.stdout, JSON.stringify(payload, null, 2));
|
|
110
|
+
}
|
|
111
|
+
function onboardResult(resultStatus, command, install, pair, sync, status) {
|
|
112
|
+
return {
|
|
113
|
+
status: resultStatus,
|
|
114
|
+
dashboard_url: command.dashboardUrl,
|
|
115
|
+
ticket_id: sync?.ticket_id ?? command.activeTicketId ?? null,
|
|
116
|
+
work_context_id: sync?.work_context_id ?? status?.work_context_id ?? null,
|
|
117
|
+
config_file: install?.paths.config_file ?? status?.config_file ?? null,
|
|
118
|
+
session_file: install?.paths.session_file ?? status?.session_file ?? null,
|
|
119
|
+
collection_roots: command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []),
|
|
120
|
+
paired_user: pair?.session.email ?? pair?.session.auth_subject_id ?? null,
|
|
121
|
+
paired_device: pair?.session.device_name ?? pair?.session.device_id ?? null,
|
|
122
|
+
upload_status: sync?.status ?? null,
|
|
123
|
+
upload_state: status?.upload_state ?? null,
|
|
124
|
+
event_count: sync?.event_count ?? null,
|
|
125
|
+
source_scan_count: sync?.source_scan_count ?? null,
|
|
126
|
+
risk_flag_count: sync?.risk_flag_count ?? null,
|
|
127
|
+
pending_upload_count: status?.pending_upload_count ?? null,
|
|
128
|
+
last_upload_success_at: status?.last_upload_success_at ?? null,
|
|
129
|
+
next_dashboard_path: `${command.dashboardUrl}/my-work`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function nextStepForOnboardBlocker(blocker, options = {}) {
|
|
133
|
+
switch (blocker) {
|
|
134
|
+
case COLLECTION_ROOT_REQUIRED:
|
|
135
|
+
return missingCollectionRootMessage(options);
|
|
136
|
+
case "ticket":
|
|
137
|
+
case "ticket_binding":
|
|
138
|
+
return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
|
|
139
|
+
case "device_pairing":
|
|
140
|
+
return "Ask Edward to approve this machine in the dashboard under Ambient -> Collector approvals, then run `cockpit onboard` again.";
|
|
141
|
+
case "network_or_ingest":
|
|
142
|
+
return "Check dashboard URL/network, then run `cockpit sync --json` or rerun `cockpit onboard`.";
|
|
143
|
+
case "install":
|
|
144
|
+
return "Rerun `cockpit onboard` from the repo root; it will reinstall local config.";
|
|
145
|
+
case "work_context":
|
|
146
|
+
return "Run `cockpit start --ticket <id> --workspace \"$PWD\"`, then retry `cockpit sync`.";
|
|
147
|
+
default:
|
|
148
|
+
return "Run `cockpit status --json` and report the blocker label plus last failure reason.";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Step 3 of a single-repo onboard: the work context this machine just bound. */
|
|
152
|
+
export function writeOnboardWorkContextStarted(io, context) {
|
|
153
|
+
writeLine(io.stdout, "3/5 Work context active.");
|
|
154
|
+
writeLine(io.stdout, `Repo: ${context.repo}`);
|
|
155
|
+
writeLine(io.stdout, `Branch: ${context.branch}`);
|
|
156
|
+
writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
|
|
157
|
+
writeLine(io.stdout, `Context: ${context.work_context_id}`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The single-repo arm's blocked-collection result. The retry command is the
|
|
161
|
+
* spooled run's own when there is one, because that one names the pack to send.
|
|
162
|
+
*/
|
|
163
|
+
export function reportOnboardSyncBlocked(ctx, outcome) {
|
|
164
|
+
const { command, resolvedCommand, install, pair, rootsResult, io } = ctx;
|
|
165
|
+
const { run, sync, status, worktreeRoot } = outcome;
|
|
166
|
+
const collectionRunStatus = attributedSyncRunStatus(run);
|
|
167
|
+
if (command.json) {
|
|
168
|
+
writeOnboardJson(io, onboardJsonPayload("blocked", resolvedCommand, install, pair, sync, status, rootsResult, {
|
|
169
|
+
codex_sessions: run.summary,
|
|
170
|
+
collection_status: collectionRunStatus,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const syncFailureReason = sync.status === "spooled" ? sync.failure_reason : null;
|
|
175
|
+
const retryCommand = sync.status === "spooled"
|
|
176
|
+
? sync.retry_command
|
|
177
|
+
: `cockpit sync --workspace ${JSON.stringify(worktreeRoot)}`;
|
|
178
|
+
writeLine(io.stderr, "BLOCKED: initial collection is incomplete; retry until every eligible session has a durable receipt or explicit terminal reason.");
|
|
179
|
+
writeLine(io.stderr, `Failure: ${syncFailureReason ?? (run.summary.report_posted ? collectionRunStatus : run.summary.report_reason)}`);
|
|
180
|
+
writeAgentSessionSummary(io, run.summary);
|
|
181
|
+
writeLine(io.stderr, `Retry: ${retryCommand}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Steps 4 and 5 of a single-repo onboard: what was uploaded, and where to read it. */
|
|
185
|
+
export function writeOnboardUploadNarrative(io, uploaded) {
|
|
186
|
+
const { command, run, sync, status } = uploaded;
|
|
187
|
+
writeLine(io.stdout, "4/5 Uploaded what you worked on.");
|
|
188
|
+
writeLine(io.stdout, `HTTP: ${sync.http_status}`);
|
|
189
|
+
writeLine(io.stdout, `Things recorded: ${sync.event_count}`);
|
|
190
|
+
writeLine(io.stdout, `Sources: ${sync.source_scan_count}`);
|
|
191
|
+
writeLine(io.stdout, `Risk flags: ${sync.risk_flag_count}`);
|
|
192
|
+
writeLine(io.stdout, `Raw evidence files: ${sync.raw_evidence_file_count}`);
|
|
193
|
+
writeLine(io.stdout, rawEvidenceSyncLine(sync));
|
|
194
|
+
writeAgentSessionSummary(io, run.summary);
|
|
195
|
+
writeLine(io.stdout, "5/5 Status ready.");
|
|
196
|
+
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
197
|
+
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The single-repo arm's `--json` result. A scheduler that could not load
|
|
201
|
+
* recurring collection is the one thing that turns a finished setup into a
|
|
202
|
+
* blocked one, and it says so by name.
|
|
203
|
+
*/
|
|
204
|
+
export function writeOnboardSingleRepoResultJson(ctx, finished) {
|
|
205
|
+
const { resolvedCommand, install, pair, rootsResult, io } = ctx;
|
|
206
|
+
const { run, sync, status, backfill, setup } = finished;
|
|
207
|
+
const { agentRules, autostart } = setup;
|
|
208
|
+
const onboardOk = setup.ok;
|
|
209
|
+
writeOnboardJson(io, onboardJsonPayload(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, sync, status, rootsResult, {
|
|
210
|
+
agent_rules: agentRules,
|
|
211
|
+
autostart,
|
|
212
|
+
backfill,
|
|
213
|
+
codex_sessions: run.summary,
|
|
214
|
+
...(!onboardOk
|
|
215
|
+
? {
|
|
216
|
+
blocker: "autostart_load_failed",
|
|
217
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
218
|
+
}
|
|
219
|
+
: {}),
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
/** The parent-folder arm's blocked-collection result; `--json` only, as it always was. */
|
|
223
|
+
export function writeOnboardMultiRepoSyncBlocked(ctx, multi) {
|
|
224
|
+
const { resolvedCommand, install, pair, rootsResult, io } = ctx;
|
|
225
|
+
writeOnboardJson(io, onboardJsonPayload("blocked", resolvedCommand, install, pair, null, null, rootsResult, {
|
|
226
|
+
mode: "multi_repo",
|
|
227
|
+
repos: multi.results,
|
|
228
|
+
codex_sessions: multi.codex_sessions,
|
|
229
|
+
blocker: "sync_blocked",
|
|
230
|
+
next_step: "Run `cockpit sync --json` until every root returns an uploaded receipt.",
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The parent-folder arm's finished result, in whichever of the three shapes
|
|
235
|
+
* applies: the `--json` payload, the live-status page, or the autostart blocker
|
|
236
|
+
* when a supported scheduler refused the registration.
|
|
237
|
+
*/
|
|
238
|
+
export function reportOnboardMultiRepoResult(ctx, finished) {
|
|
239
|
+
const { command, resolvedCommand, collectionRoots, install, pair, rootsResult, io } = ctx;
|
|
240
|
+
const { multi, backfill, setup } = finished;
|
|
241
|
+
const { agentRules, autostart } = setup;
|
|
242
|
+
const onboardOk = setup.ok;
|
|
243
|
+
if (command.json) {
|
|
244
|
+
writeOnboardJson(io, onboardJsonPayload(onboardOk ? "pass" : "blocked", resolvedCommand, install, pair, null, null, rootsResult, {
|
|
245
|
+
agent_rules: agentRules,
|
|
246
|
+
autostart,
|
|
247
|
+
backfill,
|
|
248
|
+
mode: "multi_repo",
|
|
249
|
+
repos: multi.results,
|
|
250
|
+
codex_sessions: multi.codex_sessions,
|
|
251
|
+
...(!onboardOk && multi.ok
|
|
252
|
+
? {
|
|
253
|
+
blocker: "autostart_load_failed",
|
|
254
|
+
next_step: "Run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.",
|
|
255
|
+
}
|
|
256
|
+
: {}),
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
if (onboardOk && !command.json) {
|
|
260
|
+
writeLine(io.stdout, "PASS: Tower is set up and collecting.");
|
|
261
|
+
writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
|
|
262
|
+
pair,
|
|
263
|
+
status: null,
|
|
264
|
+
sync: null,
|
|
265
|
+
agentRules,
|
|
266
|
+
autostart,
|
|
267
|
+
initialSyncOk: true,
|
|
268
|
+
backfill,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else if (multi.ok && !command.json) {
|
|
272
|
+
writeOnboardAutostartBlocker(io, autostart);
|
|
273
|
+
}
|
|
274
|
+
}
|