@bli-cockpit/cli 0.2.7 → 0.2.8

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/README.md CHANGED
@@ -64,7 +64,7 @@ families before its exact version is promoted to npm `latest`.
64
64
  | `cockpit sync` | Captures and uploads once, right now. This is what the background agent runs every 15 min — you almost never type it yourself. | Named for what it does: synchronize local session files up to the dashboard. |
65
65
  | `cockpit analyze` | Runs a fresh sync, then queues batch analysis of your latest uploaded sessions. It returns immediately; status and results appear in My Work. | One command when you want newly finished work uploaded and judged now. |
66
66
  | `cockpit start --ticket <id>` | Tags your CURRENT work with a Linear ticket so sessions attribute to it. `--clear-ticket` returns to general capture. | "Start (working on) X." Agents usually run this for you per the agent rules. |
67
- | `cockpit login` / `cockpit pair` | Just the sign-in + device-registration step, standalone (onboard already includes it). Two names, one command: `login` is what humans guess, `pair` is what the dashboard's device screen calls it. | Recovery path when a session expires or you switch accounts. |
67
+ | `cockpit login` / `cockpit pair` | Just the sign-in + device-registration step, standalone (onboard already includes it). Works on a fresh machine too: it writes a minimal local config first (no collection roots — run `cockpit onboard` afterwards to register those). Two names, one command: `login` is what humans guess, `pair` is what the dashboard's device screen calls it. | Recovery path when a session expires or you switch accounts, or sign-in first on a brand-new machine. |
68
68
  | `cockpit logout` | Deletes this machine's session. | The undo of login. |
69
69
  | `cockpit update` / `cockpit upgrade` | Updates the CLI itself from npm, then reruns onboard. Two names, one command — aliases because half of people guess each. | Self-update; run when Edward announces a new version. |
70
70
  | `cockpit install` | Writes local config only (step 1 of onboard, standalone). | Plumbing; exists so onboard's pieces are individually runnable. You'll likely never type it. |
@@ -107,20 +107,25 @@ async function inspectAgentRulesForHost(host, options = {}) {
107
107
  export function cockpitAgentRulesBlock(options = {}) {
108
108
  const scopePaths = normalizeScopePaths(options);
109
109
  const scopeLine = scopePaths.length === 1
110
- ? `- This guidance only applies when the current working directory is inside the Cockpit-onboarded workspace/repo: \`${scopePaths[0]}\`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.`
110
+ ? `- Only applies when the current working directory is inside the Cockpit-onboarded workspace/repo: \`${scopePaths[0]}\`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.`
111
111
  : scopePaths.length > 1
112
- ? `- This guidance only applies when the current working directory is inside one of these Cockpit-onboarded workspace roots: ${scopePaths.map((scopePath) => `\`${scopePath}\``).join(", ")}. Outside those folders, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.`
113
- : "- This guidance only applies when the current working directory is inside the workspace/repo that ran `cockpit onboard` or `cockpit agent-rules install`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.";
112
+ ? `- Only applies when the current working directory is inside one of these Cockpit-onboarded workspace roots: ${scopePaths.map((scopePath) => `\`${scopePath}\``).join(", ")}. Outside those folders, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.`
113
+ : "- Only applies when the current working directory is inside the workspace/repo that ran `cockpit onboard` or `cockpit agent-rules install`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.";
114
114
  return [
115
115
  MANAGED_BLOCK_START,
116
116
  "## Cockpit Ticket Binding",
117
117
  "",
118
118
  scopeLine,
119
- "- For implementation, debugging, review, PR, or ship work that already has a clear Linear ticket, run `cockpit start --ticket <ticket-id> --workspace \"$PWD\"` before the first code edit or mutating tool call. This starts attributing the session's work to that specific ticket in Cockpit.",
120
- "- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
121
- "- If the user mentions ticketed work but no ticket ID is visible, search Linear for an existing ticket before editing. If none exists and the work is ticket-worthy, create a narrow Linear ticket, then run the canonical `cockpit start --ticket ...` command. Ask the user only when ticket creation would be ambiguous or the user has forbidden creating one.",
122
- "- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
123
- "- After the first meaningful checkpoint, run `cockpit sync --workspace \"$PWD\" --json` so Cockpit has fresh ticket/session binding metadata.",
119
+ "- Ticketed work (implement / debug / review / PR / ship): run `cockpit start --ticket <ticket-id> --workspace \"$PWD\"` before the first code edit (the flag is `--ticket`). No ticket ID visible → search Linear first; none exists and the work is ticket-worthy → create a narrow Linear ticket, then bind.",
120
+ "- Truly no ticket → say the session stays in general ambient capture; never invent one.",
121
+ "- After the first meaningful checkpoint, run `cockpit sync --workspace \"$PWD\" --json`.",
122
+ "",
123
+ "## Definition of done — QA receipts",
124
+ "",
125
+ "- A change a user can see or click is NOT done until a computer-use QA pass has exercised it and left receipts (screenshots or logs of the actual run). Green CI alone does not close a user-visible change.",
126
+ "- Computer use first, always. The human can take over the browser at any point; when that happens, stop driving, observe, and record what the human verified.",
127
+ "- A reviewer or QA agent spawned by the builder does not count as independent verification by itself — the receipt is what counts, not the reviewer's opinion.",
128
+ "- QA catches the cheap majority of bugs, not all of them. Do the pass, attach the receipts, ship; remaining bugs get fixed as normal bugs.",
124
129
  MANAGED_BLOCK_END,
125
130
  ].join("\n");
126
131
  }
@@ -233,16 +238,25 @@ function hasEquivalentUnmanagedTicketBinding(contents, scopePaths = []) {
233
238
  return false;
234
239
  if (!hasTicketLookupOrCreationCue(text))
235
240
  return false;
241
+ // Contents without the QA-receipts rule are the older vocabulary and must
242
+ // read as stale — otherwise machines with a hand-written ticket-binding
243
+ // section never receive the definition-of-done rule.
244
+ if (!hasQaReceiptsCue(text))
245
+ return false;
236
246
  const signals = [
237
247
  /cockpit\s+start\s+--ticket\b/u,
238
248
  /before\s+(?:the\s+)?first\s+code\s+edit|before\s+ticketed\s+implementation/u,
239
249
  /general\s+ambient/u,
240
- /cockpit\s+sync\s+--repo|fresh\s+ticket\/session\s+binding\s+metadata/u,
241
- /use\s+--ticket|do\s+not\s+invent\s+--ticketid/u,
250
+ /cockpit\s+sync\s+--repo|cockpit\s+sync\s+--workspace|fresh\s+ticket\/session\s+binding\s+metadata/u,
251
+ /use\s+--ticket|the\s+flag\s+is\s+--ticket|do\s+not\s+invent\s+--ticketid/u,
242
252
  ];
243
253
  const score = signals.filter((signal) => signal.test(text)).length;
244
254
  return score >= 4;
245
255
  }
256
+ function hasQaReceiptsCue(text) {
257
+ return /computer-use\s+qa\s+pass/u.test(text)
258
+ && /receipts/u.test(text);
259
+ }
246
260
  function hasTicketLookupOrCreationCue(text) {
247
261
  return /search\s+linear|create\s+(?:a\s+)?(?:narrow\s+)?linear\s+ticket|new\s+linear\s+ticket/u.test(text);
248
262
  }
@@ -9,7 +9,7 @@ import { runDoctor } from "./doctor.js";
9
9
  import { inspectBackfillLock } from "../backfill-lock.js";
10
10
  import { parseLocalArgs, normalizeUrl } from "./local-args.js";
11
11
  import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from "../autostart.js";
12
- import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
12
+ import { DEFAULT_DASHBOARD_URL, ensureLocalCollectorConfig, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
13
13
  import { CODEX_ATTRIBUTION_SCAN_WINDOW_SESSION_LIMIT, CODEX_ATTRIBUTION_SCAN_WINDOW_MINUTES, defaultCodexSessionDirs, scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
14
14
  import { scanAndAttributeClaudeSessions } from "../adapters/claude-attribution.js";
15
15
  import { backfillCompletionCovers, emptyBackfillCursorState, prepareBackfillCursorForScope, readBackfillCompletionMarker, readBackfillCursor, } from "../cursors/backfill-cursor.js";
@@ -855,7 +855,7 @@ const OTP_INVALID_MESSAGE = [
855
855
  " Cockpit expects digits only, length 6 to 10.",
856
856
  "What you can do:",
857
857
  " 1) Rerun and paste the latest email code.",
858
- " 2) Use manual approval: cockpit onboard --no-auth --email <you@buildlaunchiterate.ca>",
858
+ " 2) Use manual approval: rerun this command with --no-auth --email <you@buildlaunchiterate.ca>",
859
859
  ].join("\n");
860
860
  async function resolveOnboardEmail(command, roots, config, io) {
861
861
  if (command.claimedOwnerEmail)
@@ -1774,8 +1774,18 @@ function worktreeSyncRow(outcome, run) {
1774
1774
  };
1775
1775
  }
1776
1776
  async function runLogin(command, io) {
1777
+ // Standalone `cockpit login` must work on a fresh machine: bootstrap a
1778
+ // minimal rootless config when onboard/install has not run yet, instead of
1779
+ // failing on the missing config file.
1780
+ const { config, paths, created } = await ensureLocalCollectorConfig({
1781
+ homeDir: command.homeDir,
1782
+ dashboardUrl: command.dashboardUrl,
1783
+ });
1784
+ const dashboardUrl = command.dashboardUrl ?? config.dashboard_url;
1785
+ if (created && !command.json) {
1786
+ writeLine(io.stdout, `First login on this machine; wrote collector config: ${paths.config_file}`);
1787
+ }
1777
1788
  const claimedOwnerEmail = await resolveInteractiveLoginEmail(command, io);
1778
- const dashboardUrl = await resolveLoginDashboardUrl(command);
1779
1789
  const pairingAccessToken = await requestPairingAccessToken({
1780
1790
  dashboardUrl,
1781
1791
  email: claimedOwnerEmail,
@@ -1784,7 +1794,7 @@ async function runLogin(command, io) {
1784
1794
  }, io);
1785
1795
  const result = await pairLocalCollectorWithAuthFallback({
1786
1796
  homeDir: command.homeDir,
1787
- dashboardUrl: command.dashboardUrl,
1797
+ dashboardUrl,
1788
1798
  claimedOwnerEmail,
1789
1799
  deviceName: command.deviceName,
1790
1800
  pollIntervalMs: command.pollIntervalMs,
@@ -1808,16 +1818,11 @@ async function runLogin(command, io) {
1808
1818
  writeLine(io.stdout, `Session: ${result.session_file}`);
1809
1819
  writeLine(io.stdout, `User: ${result.session.email ?? result.session.auth_subject_id}`);
1810
1820
  writeLine(io.stdout, `Device: ${result.session.device_name ?? result.session.device_id ?? "unknown"}`);
1811
- writeLine(io.stdout, "Next: run `cockpit start` inside the repo.");
1821
+ writeLine(io.stdout, config.default_repo_paths.length > 0
1822
+ ? "Next: run `cockpit start` inside the repo."
1823
+ : "Next: run `cockpit onboard` from your repo root so Cockpit knows which repos to collect.");
1812
1824
  return 0;
1813
1825
  }
1814
- async function resolveLoginDashboardUrl(command) {
1815
- if (command.dashboardUrl)
1816
- return command.dashboardUrl;
1817
- const paths = getCollectorRuntimePaths(command.homeDir);
1818
- const config = await readLocalCollectorConfig(paths);
1819
- return config.dashboard_url;
1820
- }
1821
1826
  function writePairingInstructions(io, request) {
1822
1827
  writeLine(io.stdout, "2/5 Device pairing started.");
1823
1828
  writeLine(io.stdout, `Open: ${request.approve_url}`);
@@ -8,7 +8,7 @@ export async function runCockpitCli(argv, io) {
8
8
  }
9
9
 
10
10
  if (command === "--version" || command === "-V" || command === "version") {
11
- writeLine(io?.stdout ?? process.stdout, "0.2.7");
11
+ writeLine(io?.stdout ?? process.stdout, "0.2.8");
12
12
  return 0;
13
13
  }
14
14
 
@@ -74,12 +74,48 @@ function normalizeRepoRoots(repoRoots) {
74
74
  }
75
75
  return normalizeCollectionRoots(candidates);
76
76
  }
77
+ /**
78
+ * Reads the collector config, creating a minimal rootless one when the file
79
+ * does not exist yet (fresh machine where `cockpit onboard`/`install` has not
80
+ * run). Never registers collection roots: root consent stays with
81
+ * onboard/install. A corrupt existing config is never overwritten.
82
+ */
83
+ export async function ensureLocalCollectorConfig(options = {}) {
84
+ const homeDir = options.homeDir ?? os.homedir();
85
+ const paths = getCollectorRuntimePaths(homeDir);
86
+ await ensureRuntimeDirectories(paths);
87
+ let existing = null;
88
+ try {
89
+ existing = await readLocalCollectorConfig(paths);
90
+ }
91
+ catch (error) {
92
+ if (!isMissingFileError(error)) {
93
+ const reason = error instanceof Error ? error.message : String(error);
94
+ throw new Error(`Local collector config at ${paths.config_file} is unreadable (${reason}). ` +
95
+ "Run `cockpit onboard` to repair it.");
96
+ }
97
+ }
98
+ if (existing)
99
+ return { config: existing, paths, created: false };
100
+ const config = LocalCollectorConfigSchema.parse({
101
+ schema_version: "telemetry-core.v1",
102
+ dashboard_url: normalizeDashboardUrl(options.dashboardUrl ?? DEFAULT_DASHBOARD_URL),
103
+ collector_version: LOCAL_COLLECTOR_VERSION,
104
+ device_id: `device-${crypto.randomUUID()}`,
105
+ device_name: defaultDeviceName(),
106
+ default_repo_paths: [],
107
+ session_file_path: paths.session_file,
108
+ state_dir_path: paths.state_dir,
109
+ });
110
+ await writeJsonFile(paths.config_file, config);
111
+ return { config, paths, created: true };
112
+ }
77
113
  export async function pairLocalCollector(options = {}) {
78
114
  const homeDir = options.homeDir ?? os.homedir();
79
115
  const paths = getCollectorRuntimePaths(homeDir);
80
116
  await ensureRuntimeDirectories(paths);
81
117
  const config = await readLocalCollectorConfig(paths).catch(() => {
82
- throw new Error("Local config missing. Run `cockpit install` before `cockpit login`.");
118
+ throw new Error("Local config missing. Run `cockpit onboard` (or `cockpit install`) first, then retry `cockpit login`.");
83
119
  });
84
120
  const dashboardUrl = normalizeDashboardUrl(options.dashboardUrl ?? config.dashboard_url);
85
121
  const deviceId = config.device_id ?? `device-${crypto.randomUUID()}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,6 @@
26
26
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
27
27
  },
28
28
  "dependencies": {
29
- "@bli-cockpit/telemetry-core": "0.1.13"
29
+ "@bli-cockpit/telemetry-core": "0.1.14"
30
30
  }
31
31
  }