@bli-cockpit/cli 0.1.15 → 0.1.17

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
@@ -19,7 +19,7 @@ npm install -g @bli-cockpit/cli@latest
19
19
  cockpit onboard \
20
20
  --email <APPROVED_EMAIL> \
21
21
  --device-name "$COCKPIT_DEVICE_NAME" \
22
- --repo "$PWD"
22
+ --workspace "$PWD"
23
23
  ```
24
24
 
25
25
  If a work folder contains multiple repos, run the same command from the parent:
@@ -31,7 +31,7 @@ npm install -g @bli-cockpit/cli@latest
31
31
  cockpit onboard \
32
32
  --email <APPROVED_EMAIL> \
33
33
  --device-name "$COCKPIT_DEVICE_NAME" \
34
- --repo "$PWD"
34
+ --workspace "$PWD"
35
35
  ```
36
36
 
37
37
  The CLI defaults to the production dashboard. Normal intern/operator setup,
@@ -39,7 +39,18 @@ updates, and syncs omit `--dashboard-url`. Pass `--dashboard-url` only for
39
39
  staging, a custom dashboard, or deliberately forcing a different dashboard
40
40
  pairing. Already-onboarded users update with
41
41
  `npm install -g @bli-cockpit/cli@latest`, then run
42
- `cockpit sync --repo "$PWD" --json`.
42
+ `cockpit sync --workspace "$PWD" --json`. `--repo <path>` remains supported
43
+ for older prompts and the Codex ticket-binding guardrail.
44
+
45
+ On machines where Codex agents will do ticketed work, install the user-scope
46
+ agent rule once:
47
+
48
+ ```bash
49
+ cockpit agent-rules install
50
+ ```
51
+
52
+ That updates `~/.codex/AGENTS.md` with the Cockpit rule to bind known Linear
53
+ tickets before edits, or ask once when the ticket ID is missing.
43
54
 
44
55
  Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
45
56
  repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
@@ -87,7 +98,7 @@ cockpit start \
87
98
  --repo "$PWD"
88
99
 
89
100
  cockpit sync \
90
- --repo "$PWD" \
101
+ --workspace "$PWD" \
91
102
  --json
92
103
  ```
93
104
 
@@ -139,7 +150,7 @@ repo, ticket, and raw JSONL object, see
139
150
  Local attribution preview:
140
151
 
141
152
  ```bash
142
- cockpit sessions --repo "$PWD" --json
153
+ cockpit sessions --workspace "$PWD" --json
143
154
  ```
144
155
 
145
156
  Remote metadata path:
@@ -0,0 +1,97 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const MANAGED_BLOCK_START = "<!-- BLI_COCKPIT_AGENT_RULES:START -->";
5
+ const MANAGED_BLOCK_END = "<!-- BLI_COCKPIT_AGENT_RULES:END -->";
6
+ export async function installCodexAgentRules(options = {}) {
7
+ const agentsFile = codexAgentsFile(options.homeDir);
8
+ const block = cockpitAgentRulesBlock();
9
+ let existing = "";
10
+ let existed = true;
11
+ try {
12
+ existing = await readFile(agentsFile, "utf8");
13
+ }
14
+ catch {
15
+ existed = false;
16
+ }
17
+ const next = upsertManagedBlock(existing, block);
18
+ if (next === existing) {
19
+ return { status: "unchanged", agents_file: agentsFile, block };
20
+ }
21
+ await mkdir(path.dirname(agentsFile), { recursive: true });
22
+ await writeFile(agentsFile, next, "utf8");
23
+ return { status: existed ? "updated" : "created", agents_file: agentsFile, block };
24
+ }
25
+ export async function uninstallCodexAgentRules(options = {}) {
26
+ const agentsFile = codexAgentsFile(options.homeDir);
27
+ const block = cockpitAgentRulesBlock();
28
+ let existing = "";
29
+ try {
30
+ existing = await readFile(agentsFile, "utf8");
31
+ }
32
+ catch {
33
+ return { status: "missing", agents_file: agentsFile, block };
34
+ }
35
+ const next = removeManagedBlock(existing);
36
+ if (next === existing) {
37
+ return { status: "missing", agents_file: agentsFile, block };
38
+ }
39
+ await writeFile(agentsFile, next, "utf8");
40
+ return { status: "updated", agents_file: agentsFile, block };
41
+ }
42
+ export async function inspectCodexAgentRules(options = {}) {
43
+ const agentsFile = codexAgentsFile(options.homeDir);
44
+ const block = cockpitAgentRulesBlock();
45
+ let existing = "";
46
+ try {
47
+ existing = await readFile(agentsFile, "utf8");
48
+ }
49
+ catch {
50
+ return { status: "missing", agents_file: agentsFile, block, installed: false };
51
+ }
52
+ return {
53
+ status: hasManagedBlock(existing) ? "unchanged" : "missing",
54
+ agents_file: agentsFile,
55
+ block,
56
+ installed: hasManagedBlock(existing),
57
+ };
58
+ }
59
+ export function cockpitAgentRulesBlock() {
60
+ return [
61
+ MANAGED_BLOCK_START,
62
+ "## Cockpit Ticket Binding",
63
+ "",
64
+ "- For implementation, debugging, review, PR, or ship work tied to a clear Linear ticket, run `cockpit start --ticket <ticket-id> --repo \"$PWD\"` before the first code edit or mutating tool call.",
65
+ "- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
66
+ "- If the user mentions ticketed work but no ticket ID is visible, ask once for the Linear ticket ID before editing. Agents cannot reliably infer it from context.",
67
+ "- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
68
+ "- After the first meaningful checkpoint, run `cockpit sync --repo \"$PWD\" --json` so Cockpit has fresh ticket/session binding metadata.",
69
+ MANAGED_BLOCK_END,
70
+ ].join("\n");
71
+ }
72
+ export function hasManagedBlock(contents) {
73
+ return contents.includes(MANAGED_BLOCK_START) && contents.includes(MANAGED_BLOCK_END);
74
+ }
75
+ export function upsertManagedBlock(contents, block) {
76
+ if (!contents.trim())
77
+ return `${block}\n`;
78
+ if (!hasManagedBlock(contents)) {
79
+ return `${contents.replace(/\s+$/u, "")}\n\n${block}\n`;
80
+ }
81
+ const pattern = managedBlockPattern();
82
+ return contents.replace(pattern, block);
83
+ }
84
+ export function removeManagedBlock(contents) {
85
+ if (!hasManagedBlock(contents))
86
+ return contents;
87
+ return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
88
+ }
89
+ function codexAgentsFile(homeDir = os.homedir()) {
90
+ return path.join(homeDir, ".codex", "AGENTS.md");
91
+ }
92
+ function managedBlockPattern() {
93
+ return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
94
+ }
95
+ function escapeRegExp(value) {
96
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
97
+ }
package/dist/autostart.js CHANGED
@@ -126,12 +126,12 @@ async function existingWatchPaths(homeDir) {
126
126
  return candidates.filter((_, i) => present[i]);
127
127
  }
128
128
  function renderPlist(options) {
129
- // The repo path is shell-quoted because it lands inside a `/bin/zsh -lc "…"`
129
+ // The work path is shell-quoted because it lands inside a `/bin/zsh -lc "…"`
130
130
  // command string; the whole command is then XML-escaped for the <string>.
131
131
  const dashboardArg = options.dashboardUrl === DEFAULT_DASHBOARD_URL
132
132
  ? ""
133
133
  : ` --dashboard-url ${shellQuote(options.dashboardUrl)}`;
134
- const command = `cockpit sync --repo ${shellQuote(options.workDir)}${dashboardArg} --json`;
134
+ const command = `cockpit sync --workspace ${shellQuote(options.workDir)}${dashboardArg} --json`;
135
135
  return [
136
136
  '<?xml version="1.0" encoding="UTF-8"?>',
137
137
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
@@ -5,6 +5,7 @@
5
5
  // Behavior-preserving extraction: functions moved verbatim, no logic change.
6
6
  import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
7
7
  import { DEFAULT_AUTOSTART_INTERVAL_SECONDS } from "../autostart.js";
8
+ const WORK_ROOT_FLAGS = ["--repo", "--workspace"];
8
9
  export function parseLocalArgs(argv) {
9
10
  const command = argv[0];
10
11
  switch (command) {
@@ -29,6 +30,8 @@ export function parseLocalArgs(argv) {
29
30
  return parseServeArgs(argv.slice(1));
30
31
  case "autostart":
31
32
  return parseAutostartArgs(argv.slice(1));
33
+ case "agent-rules":
34
+ return parseAgentRulesArgs(argv.slice(1));
32
35
  default:
33
36
  throw new Error(`Unknown local command: ${command ?? ""}`);
34
37
  }
@@ -38,6 +41,7 @@ function parseOnboardArgs(args) {
38
41
  allowedFlags: [
39
42
  "--home",
40
43
  "--repo",
44
+ "--workspace",
41
45
  "--dashboard-url",
42
46
  "--email",
43
47
  "--device-name",
@@ -52,6 +56,7 @@ function parseOnboardArgs(args) {
52
56
  valueFlags: [
53
57
  "--home",
54
58
  "--repo",
59
+ "--workspace",
55
60
  "--dashboard-url",
56
61
  "--email",
57
62
  "--device-name",
@@ -67,7 +72,7 @@ function parseOnboardArgs(args) {
67
72
  return {
68
73
  kind: "onboard",
69
74
  homeDir: optionalNonEmpty(values.flags.get("--home")),
70
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
75
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
71
76
  dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
72
77
  claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
73
78
  deviceName: optionalNonEmpty(values.flags.get("--device-name")),
@@ -85,17 +90,24 @@ function parseInstallArgs(args) {
85
90
  allowedFlags: [
86
91
  "--home",
87
92
  "--repo",
93
+ "--workspace",
88
94
  "--dashboard-url",
89
95
  "--supabase-url",
90
96
  "--json",
91
97
  ],
92
- valueFlags: ["--home", "--repo", "--dashboard-url", "--supabase-url"],
98
+ valueFlags: [
99
+ "--home",
100
+ "--repo",
101
+ "--workspace",
102
+ "--dashboard-url",
103
+ "--supabase-url",
104
+ ],
93
105
  });
94
106
  assertNoPositionals(values.positionals, "install");
95
107
  return {
96
108
  kind: "install",
97
109
  homeDir: optionalNonEmpty(values.flags.get("--home")),
98
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
110
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
99
111
  dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
100
112
  supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
101
113
  json: values.booleans.has("--json"),
@@ -150,6 +162,7 @@ function parseStartArgs(args) {
150
162
  allowedFlags: [
151
163
  "--home",
152
164
  "--repo",
165
+ "--workspace",
153
166
  "--branch",
154
167
  "--ticket",
155
168
  "--operator-id",
@@ -161,6 +174,7 @@ function parseStartArgs(args) {
161
174
  valueFlags: [
162
175
  "--home",
163
176
  "--repo",
177
+ "--workspace",
164
178
  "--branch",
165
179
  "--ticket",
166
180
  "--operator-id",
@@ -173,7 +187,7 @@ function parseStartArgs(args) {
173
187
  return {
174
188
  kind: "start",
175
189
  homeDir: optionalNonEmpty(values.flags.get("--home")),
176
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
190
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
177
191
  branch: optionalNonEmpty(values.flags.get("--branch")),
178
192
  activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
179
193
  operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
@@ -185,14 +199,29 @@ function parseStartArgs(args) {
185
199
  }
186
200
  function parseSyncArgs(args) {
187
201
  const values = parseNamedArgs(args, {
188
- allowedFlags: ["--home", "--repo", "--dashboard-url", "--json", "--max-depth", "--max-repos"],
189
- valueFlags: ["--home", "--repo", "--dashboard-url", "--max-depth", "--max-repos"],
202
+ allowedFlags: [
203
+ "--home",
204
+ "--repo",
205
+ "--workspace",
206
+ "--dashboard-url",
207
+ "--json",
208
+ "--max-depth",
209
+ "--max-repos",
210
+ ],
211
+ valueFlags: [
212
+ "--home",
213
+ "--repo",
214
+ "--workspace",
215
+ "--dashboard-url",
216
+ "--max-depth",
217
+ "--max-repos",
218
+ ],
190
219
  });
191
220
  assertNoPositionals(values.positionals, "sync");
192
221
  return {
193
222
  kind: "sync",
194
223
  homeDir: optionalNonEmpty(values.flags.get("--home")),
195
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
224
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
196
225
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
197
226
  json: values.booleans.has("--json"),
198
227
  maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
@@ -201,14 +230,27 @@ function parseSyncArgs(args) {
201
230
  }
202
231
  function parseStatusArgs(args) {
203
232
  const values = parseNamedArgs(args, {
204
- allowedFlags: ["--home", "--repo", "--json", "--max-depth", "--max-repos"],
205
- valueFlags: ["--home", "--repo", "--max-depth", "--max-repos"],
233
+ allowedFlags: [
234
+ "--home",
235
+ "--repo",
236
+ "--workspace",
237
+ "--json",
238
+ "--max-depth",
239
+ "--max-repos",
240
+ ],
241
+ valueFlags: [
242
+ "--home",
243
+ "--repo",
244
+ "--workspace",
245
+ "--max-depth",
246
+ "--max-repos",
247
+ ],
206
248
  });
207
249
  assertNoPositionals(values.positionals, "status");
208
250
  return {
209
251
  kind: "status",
210
252
  homeDir: optionalNonEmpty(values.flags.get("--home")),
211
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
253
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
212
254
  json: values.booleans.has("--json"),
213
255
  maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
214
256
  maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
@@ -216,8 +258,23 @@ function parseStatusArgs(args) {
216
258
  }
217
259
  function parseSessionsArgs(args) {
218
260
  const values = parseNamedArgs(args, {
219
- allowedFlags: ["--home", "--repo", "--source", "--json", "--max-depth", "--max-repos"],
220
- valueFlags: ["--home", "--repo", "--source", "--max-depth", "--max-repos"],
261
+ allowedFlags: [
262
+ "--home",
263
+ "--repo",
264
+ "--workspace",
265
+ "--source",
266
+ "--json",
267
+ "--max-depth",
268
+ "--max-repos",
269
+ ],
270
+ valueFlags: [
271
+ "--home",
272
+ "--repo",
273
+ "--workspace",
274
+ "--source",
275
+ "--max-depth",
276
+ "--max-repos",
277
+ ],
221
278
  });
222
279
  assertNoPositionals(values.positionals, "sessions");
223
280
  const source = values.flags.get("--source");
@@ -227,7 +284,7 @@ function parseSessionsArgs(args) {
227
284
  return {
228
285
  kind: "sessions",
229
286
  homeDir: optionalNonEmpty(values.flags.get("--home")),
230
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
287
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
231
288
  source,
232
289
  json: values.booleans.has("--json"),
233
290
  maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
@@ -236,8 +293,8 @@ function parseSessionsArgs(args) {
236
293
  }
237
294
  function parseServeArgs(args) {
238
295
  const values = parseNamedArgs(args, {
239
- allowedFlags: ["--home", "--repo", "--port"],
240
- valueFlags: ["--home", "--repo", "--port"],
296
+ allowedFlags: ["--home", "--repo", "--workspace", "--port"],
297
+ valueFlags: ["--home", "--repo", "--workspace", "--port"],
241
298
  });
242
299
  assertNoPositionals(values.positionals, "serve");
243
300
  const port = Number(values.flags.get("--port") ?? "4174");
@@ -247,7 +304,7 @@ function parseServeArgs(args) {
247
304
  return {
248
305
  kind: "serve",
249
306
  homeDir: optionalNonEmpty(values.flags.get("--home")),
250
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
307
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
251
308
  port,
252
309
  };
253
310
  }
@@ -256,11 +313,18 @@ function parseAutostartArgs(args) {
256
313
  allowedFlags: [
257
314
  "--home",
258
315
  "--repo",
316
+ "--workspace",
259
317
  "--dashboard-url",
260
318
  "--interval-seconds",
261
319
  "--json",
262
320
  ],
263
- valueFlags: ["--home", "--repo", "--dashboard-url", "--interval-seconds"],
321
+ valueFlags: [
322
+ "--home",
323
+ "--repo",
324
+ "--workspace",
325
+ "--dashboard-url",
326
+ "--interval-seconds",
327
+ ],
264
328
  });
265
329
  if (values.positionals.length > 1) {
266
330
  throw new Error("autostart accepts at most one action (install|uninstall|status).");
@@ -273,12 +337,31 @@ function parseAutostartArgs(args) {
273
337
  kind: "autostart",
274
338
  action,
275
339
  homeDir: optionalNonEmpty(values.flags.get("--home")),
276
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
340
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
277
341
  dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
278
342
  intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? DEFAULT_AUTOSTART_INTERVAL_SECONDS,
279
343
  json: values.booleans.has("--json"),
280
344
  };
281
345
  }
346
+ function parseAgentRulesArgs(args) {
347
+ const values = parseNamedArgs(args, {
348
+ allowedFlags: ["--home", "--json"],
349
+ valueFlags: ["--home"],
350
+ });
351
+ if (values.positionals.length > 1) {
352
+ throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
353
+ }
354
+ const action = values.positionals[0] ?? "install";
355
+ if (action !== "install" && action !== "uninstall" && action !== "status") {
356
+ throw new Error("agent-rules action must be install, uninstall, or status.");
357
+ }
358
+ return {
359
+ kind: "agent-rules",
360
+ action,
361
+ homeDir: optionalNonEmpty(values.flags.get("--home")),
362
+ json: values.booleans.has("--json"),
363
+ };
364
+ }
282
365
  function parseNamedArgs(args, options) {
283
366
  const allowed = new Set(options.allowedFlags);
284
367
  const valueFlags = new Set(options.valueFlags);
@@ -313,6 +396,16 @@ function parseNamedArgs(args, options) {
313
396
  }
314
397
  return { flags, booleans, positionals };
315
398
  }
399
+ function workRootFlagValue(values) {
400
+ const provided = WORK_ROOT_FLAGS.filter((flag) => values.flags.has(flag));
401
+ if (provided.length === 0)
402
+ return undefined;
403
+ const uniqueValues = new Set(provided.map((flag) => values.flags.get(flag)).filter(Boolean));
404
+ if (uniqueValues.size > 1) {
405
+ throw new Error("--repo and --workspace must point to the same path.");
406
+ }
407
+ return values.flags.get("--workspace") ?? values.flags.get("--repo");
408
+ }
316
409
  function assertNoPositionals(positionals, command) {
317
410
  if (positionals.length > 0) {
318
411
  throw new Error(`${command} does not accept positional arguments.`);
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { createCollectorServer } from "../server.js";
5
+ import { inspectCodexAgentRules, installCodexAgentRules, uninstallCodexAgentRules, } from "../agent-rules.js";
5
6
  import { parseLocalArgs, normalizeUrl } from "./local-args.js";
6
7
  import { autostartStatus, installAutostartAgent, uninstallAutostartAgent } from "../autostart.js";
7
8
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
@@ -22,6 +23,7 @@ export const rootCommandNames = new Set([
22
23
  "sessions",
23
24
  "serve",
24
25
  "autostart",
26
+ "agent-rules",
25
27
  ]);
26
28
  export async function runLocalCockpitCli(argv, io = defaultIo()) {
27
29
  if (isLocalHelpRequest(argv)) {
@@ -60,6 +62,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
60
62
  return await runServe(command, io);
61
63
  case "autostart":
62
64
  return await runAutostart(command, io);
65
+ case "agent-rules":
66
+ return await runAgentRules(command, io);
63
67
  }
64
68
  }
65
69
  catch (error) {
@@ -71,17 +75,18 @@ export function localCommandHelp(command) {
71
75
  if (command)
72
76
  return localSubcommandHelp(command);
73
77
  return [
74
- " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
75
- " cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
78
+ " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
79
+ " cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
76
80
  " cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
77
81
  " cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
78
82
  " cockpit logout",
79
- " cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
80
- " cockpit sync [--repo <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
81
- " cockpit status [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
82
- " cockpit sessions [--source codex|claude] [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
83
- " cockpit serve [--port <port>] [--repo <path>]",
84
- " cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
83
+ " cockpit start [--ticket <id>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
84
+ " cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
85
+ " cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
86
+ " cockpit sessions [--source codex|claude] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
87
+ " cockpit serve [--port <port>] [--workspace <path>]",
88
+ " cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
89
+ " cockpit agent-rules [install|uninstall|status] [--json]",
85
90
  "",
86
91
  `Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
87
92
  ].join("\n");
@@ -91,10 +96,11 @@ function localSubcommandHelp(command) {
91
96
  [
92
97
  "onboard",
93
98
  [
94
- "Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>] [--branch <name>] [--json]",
99
+ "Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--json]",
95
100
  "",
96
101
  "Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
97
- "If --repo is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
102
+ "If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
103
+ "`--repo <path>` remains supported as a backward-compatible alias.",
98
104
  `Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
99
105
  "Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
100
106
  "Run with no flags in a terminal and it prompts for the dashboard email; pass --email to skip the prompt (and on shared/reused machines, where mismatched sessions are re-paired).",
@@ -103,9 +109,10 @@ function localSubcommandHelp(command) {
103
109
  [
104
110
  "install",
105
111
  [
106
- "Usage: cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
112
+ "Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
107
113
  "",
108
114
  "Writes local collector config. Pair with `cockpit login`, then run `cockpit start` when work begins.",
115
+ "`--repo <path>` remains supported as a backward-compatible alias.",
109
116
  "Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
110
117
  ],
111
118
  ],
@@ -130,19 +137,21 @@ function localSubcommandHelp(command) {
130
137
  [
131
138
  "start",
132
139
  [
133
- "Usage: cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--json]",
140
+ "Usage: cockpit start [--ticket <id>] [--workspace <path>] [--branch <name>] [--json]",
134
141
  "",
135
142
  "Starts local ambient capture. Parent folders start each child git worktree.",
136
143
  "Add --ticket only when the work already has a visible ticket.",
144
+ "`--repo <path>` remains supported and is still the canonical agent-rule spelling.",
137
145
  ],
138
146
  ],
139
147
  [
140
148
  "sync",
141
149
  [
142
- "Usage: cockpit sync [--repo <path>] [--dashboard-url <url>] [--json]",
150
+ "Usage: cockpit sync [--workspace <path>] [--dashboard-url <url>] [--json]",
143
151
  "",
144
152
  "Uploads latest local ambient envelope(s), or spools safe retries if blocked.",
145
153
  "Omit --dashboard-url for normal production sync; pass it only for staging/custom dashboards or forced re-pairing.",
154
+ "`--repo <path>` remains supported as a backward-compatible alias.",
146
155
  "Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
147
156
  "transcripts (and Claude subagent sidecars) are attributed to repos",
148
157
  "deterministically and ambiguous transcripts are retained as unattributed",
@@ -156,42 +165,57 @@ function localSubcommandHelp(command) {
156
165
  [
157
166
  "status",
158
167
  [
159
- "Usage: cockpit status [--repo <path>] [--json]",
168
+ "Usage: cockpit status [--workspace <path>] [--json]",
160
169
  "",
161
170
  "Prints install, pairing, active work, upload, and retry state.",
171
+ "`--repo <path>` remains supported as a backward-compatible alias.",
162
172
  ],
163
173
  ],
164
174
  [
165
175
  "sessions",
166
176
  [
167
- "Usage: cockpit sessions [--source codex|claude] [--repo <path>] [--json]",
177
+ "Usage: cockpit sessions [--source codex|claude] [--workspace <path>] [--json]",
168
178
  "",
169
179
  "Read-only: re-runs Codex + Claude session attribution and prints each",
170
180
  "session's id, source, state, reason, scores, signals, and per-sidecar",
171
181
  "skip reasons. No upload, no cursor writes. Answers \"why is session X",
172
182
  "missing?\" locally — counts and labels only, never paths or content.",
183
+ "`--repo <path>` remains supported as a backward-compatible alias.",
173
184
  ],
174
185
  ],
175
186
  [
176
187
  "serve",
177
188
  [
178
- "Usage: cockpit serve [--port <port>] [--repo <path>]",
189
+ "Usage: cockpit serve [--port <port>] [--workspace <path>]",
179
190
  "",
180
191
  "Starts the local collector HTTP status server.",
192
+ "`--repo <path>` remains supported as a backward-compatible alias.",
181
193
  ],
182
194
  ],
183
195
  [
184
196
  "autostart",
185
197
  [
186
- "Usage: cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
198
+ "Usage: cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
187
199
  "",
188
200
  "Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
189
201
  "every 15 min (default), surviving reboots — so machines never drift to Stale.",
190
- "Action defaults to `install`. `--repo` is the parent work folder to sync.",
202
+ "Action defaults to `install`. `--workspace` is the parent work folder to sync.",
203
+ "`--repo <path>` remains supported as a backward-compatible alias.",
191
204
  "Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
192
205
  "macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
193
206
  ],
194
207
  ],
208
+ [
209
+ "agent-rules",
210
+ [
211
+ "Usage: cockpit agent-rules [install|uninstall|status] [--json]",
212
+ "",
213
+ "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md.",
214
+ "This gives Codex agents a user-scope rule to run `cockpit start --ticket <id>`",
215
+ "before ticketed implementation work, or ask once when the ticket ID is missing.",
216
+ "Action defaults to `install`.",
217
+ ],
218
+ ],
195
219
  ]);
196
220
  return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
197
221
  }
@@ -1007,6 +1031,40 @@ async function runAutostart(command, io) {
1007
1031
  writeAutostartResult(io, result);
1008
1032
  return result.status === "unsupported" ? 1 : 0;
1009
1033
  }
1034
+ async function runAgentRules(command, io) {
1035
+ const result = command.action === "install"
1036
+ ? await installCodexAgentRules({ homeDir: command.homeDir })
1037
+ : command.action === "uninstall"
1038
+ ? await uninstallCodexAgentRules({ homeDir: command.homeDir })
1039
+ : await inspectCodexAgentRules({ homeDir: command.homeDir });
1040
+ if (command.json) {
1041
+ writeLine(io.stdout, JSON.stringify(result, null, 2));
1042
+ return 0;
1043
+ }
1044
+ if ("installed" in result) {
1045
+ writeLine(io.stdout, result.installed
1046
+ ? "Cockpit Codex agent rules are installed."
1047
+ : "Cockpit Codex agent rules are not installed.");
1048
+ }
1049
+ else {
1050
+ switch (result.status) {
1051
+ case "created":
1052
+ writeLine(io.stdout, "Cockpit Codex agent rules installed.");
1053
+ break;
1054
+ case "updated":
1055
+ writeLine(io.stdout, "Cockpit Codex agent rules updated.");
1056
+ break;
1057
+ case "unchanged":
1058
+ writeLine(io.stdout, "Cockpit Codex agent rules already current.");
1059
+ break;
1060
+ case "missing":
1061
+ writeLine(io.stdout, "Cockpit Codex agent rules were not installed.");
1062
+ break;
1063
+ }
1064
+ }
1065
+ writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
1066
+ return 0;
1067
+ }
1010
1068
  function writeAutostartResult(io, result) {
1011
1069
  switch (result.status) {
1012
1070
  case "installed":
@@ -24,9 +24,10 @@ function cockpitHelp() {
24
24
  "",
25
25
  "Install/update: `npm install -g @bli-cockpit/cli@latest`.",
26
26
  "Intern path: run `cockpit onboard` from the repo root; add `--ticket <id>` only when work already has a ticket.",
27
- "Already onboarded: run `cockpit sync --repo \"$PWD\" --json`.",
27
+ "Already onboarded: run `cockpit sync --workspace \"$PWD\" --json`.",
28
+ "Agent setup: run `cockpit agent-rules install` so Codex asks for or binds Linear tickets before edits.",
28
29
  "Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
29
- "Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`.",
30
+ "Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`, `agent-rules`.",
30
31
  ].join("\n");
31
32
  }
32
33
 
package/dist/upload.js CHANGED
@@ -19,7 +19,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
19
19
  const now = options.now ?? new Date();
20
20
  const paths = getCollectorRuntimePaths(options.homeDir);
21
21
  const config = await readLocalCollectorConfig(paths).catch(() => {
22
- throw new LocalUploadBlockedError("not_installed", "Local collector config missing. Install/update the CLI, then run `cockpit onboard` before `cockpit sync`.", "npm install -g @bli-cockpit/cli@latest && cockpit onboard --email <email> --repo \"$PWD\"");
22
+ throw new LocalUploadBlockedError("not_installed", "Local collector config missing. Install/update the CLI, then run `cockpit onboard` before `cockpit sync`.", "npm install -g @bli-cockpit/cli@latest && cockpit onboard --email <email> --workspace \"$PWD\"");
23
23
  });
24
24
  const sessionFile = await readLocalCollectorSessionFile(paths).catch(() => {
25
25
  throw new LocalUploadBlockedError("unpaired", "No paired collector session found. Run `cockpit login` or `cockpit pair` before `cockpit sync`.", "cockpit login");
@@ -32,7 +32,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
32
32
  }
33
33
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
34
34
  const activeContext = await readLocalWorkContextForRepo(paths, repoRoot).catch(() => {
35
- throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --repo \"$PWD\"` before `cockpit sync`.", "cockpit start --repo \"$PWD\"");
35
+ throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --workspace \"$PWD\"` before `cockpit sync`.", "cockpit start --workspace \"$PWD\"");
36
36
  });
37
37
  const repoLabel = safeRepoLabel(activeContext.repo_label ?? repoRoot);
38
38
  const uploadContext = makeUploadWorkContext({
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
- "cockpit": "./dist/cli.js"
7
+ "cockpit": "dist/cli.js"
8
8
  },
9
9
  "files": [
10
10
  "dist/",