@bli-cockpit/cli 0.1.16 → 0.1.18

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,17 +39,21 @@ 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 agent ticket-binding guardrail.
43
44
 
44
- On machines where Codex agents will do ticketed work, install the user-scope
45
- agent rule once:
45
+ On machines where Codex or Claude agents will do ticketed work, install the
46
+ user-scope agent rule once:
46
47
 
47
48
  ```bash
48
49
  cockpit agent-rules install
49
50
  ```
50
51
 
51
- That updates `~/.codex/AGENTS.md` with the Cockpit rule to bind known Linear
52
- tickets before edits, or ask once when the ticket ID is missing.
52
+ That updates `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` with the Cockpit
53
+ rule to bind known Linear tickets before edits, or ask once when the ticket ID
54
+ is missing. That binding starts attributing the session's work to the specific
55
+ ticket in Cockpit. It checks for the managed block first, so current files are
56
+ left unchanged and stale managed blocks are replaced.
53
57
 
54
58
  Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
55
59
  repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
@@ -97,7 +101,7 @@ cockpit start \
97
101
  --repo "$PWD"
98
102
 
99
103
  cockpit sync \
100
- --repo "$PWD" \
104
+ --workspace "$PWD" \
101
105
  --json
102
106
  ```
103
107
 
@@ -149,7 +153,7 @@ repo, ticket, and raw JSONL object, see
149
153
  Local attribution preview:
150
154
 
151
155
  ```bash
152
- cockpit sessions --repo "$PWD" --json
156
+ cockpit sessions --workspace "$PWD" --json
153
157
  ```
154
158
 
155
159
  Remote metadata path:
@@ -4,56 +4,88 @@ import path from "node:path";
4
4
  const MANAGED_BLOCK_START = "<!-- BLI_COCKPIT_AGENT_RULES:START -->";
5
5
  const MANAGED_BLOCK_END = "<!-- BLI_COCKPIT_AGENT_RULES:END -->";
6
6
  export async function installCodexAgentRules(options = {}) {
7
- const agentsFile = codexAgentsFile(options.homeDir);
7
+ return installAgentRulesForHost("codex", options);
8
+ }
9
+ export async function installClaudeAgentRules(options = {}) {
10
+ return installAgentRulesForHost("claude", options);
11
+ }
12
+ export async function installAgentRules(options = {}) {
13
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => installAgentRulesForHost(host, options)));
14
+ return aggregateAgentRulesResult(targets);
15
+ }
16
+ async function installAgentRulesForHost(host, options = {}) {
17
+ const rulesFile = agentRulesFile(host, options.homeDir);
8
18
  const block = cockpitAgentRulesBlock();
9
19
  let existing = "";
10
20
  let existed = true;
11
21
  try {
12
- existing = await readFile(agentsFile, "utf8");
22
+ existing = await readFile(rulesFile, "utf8");
13
23
  }
14
24
  catch {
15
25
  existed = false;
16
26
  }
17
27
  const next = upsertManagedBlock(existing, block);
18
28
  if (next === existing) {
19
- return { status: "unchanged", agents_file: agentsFile, block };
29
+ return agentRulesResult(host, rulesFile, "unchanged", block);
20
30
  }
21
- await mkdir(path.dirname(agentsFile), { recursive: true });
22
- await writeFile(agentsFile, next, "utf8");
23
- return { status: existed ? "updated" : "created", agents_file: agentsFile, block };
31
+ await mkdir(path.dirname(rulesFile), { recursive: true });
32
+ await writeFile(rulesFile, next, "utf8");
33
+ return agentRulesResult(host, rulesFile, existed ? "updated" : "created", block);
24
34
  }
25
35
  export async function uninstallCodexAgentRules(options = {}) {
26
- const agentsFile = codexAgentsFile(options.homeDir);
36
+ return uninstallAgentRulesForHost("codex", options);
37
+ }
38
+ export async function uninstallClaudeAgentRules(options = {}) {
39
+ return uninstallAgentRulesForHost("claude", options);
40
+ }
41
+ export async function uninstallAgentRules(options = {}) {
42
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => uninstallAgentRulesForHost(host, options)));
43
+ return aggregateAgentRulesResult(targets);
44
+ }
45
+ async function uninstallAgentRulesForHost(host, options = {}) {
46
+ const rulesFile = agentRulesFile(host, options.homeDir);
27
47
  const block = cockpitAgentRulesBlock();
28
48
  let existing = "";
29
49
  try {
30
- existing = await readFile(agentsFile, "utf8");
50
+ existing = await readFile(rulesFile, "utf8");
31
51
  }
32
52
  catch {
33
- return { status: "missing", agents_file: agentsFile, block };
53
+ return agentRulesResult(host, rulesFile, "missing", block);
34
54
  }
35
55
  const next = removeManagedBlock(existing);
36
56
  if (next === existing) {
37
- return { status: "missing", agents_file: agentsFile, block };
57
+ return agentRulesResult(host, rulesFile, "missing", block);
38
58
  }
39
- await writeFile(agentsFile, next, "utf8");
40
- return { status: "updated", agents_file: agentsFile, block };
59
+ await writeFile(rulesFile, next, "utf8");
60
+ return agentRulesResult(host, rulesFile, "updated", block);
41
61
  }
42
62
  export async function inspectCodexAgentRules(options = {}) {
43
- const agentsFile = codexAgentsFile(options.homeDir);
63
+ return inspectAgentRulesForHost("codex", options);
64
+ }
65
+ export async function inspectClaudeAgentRules(options = {}) {
66
+ return inspectAgentRulesForHost("claude", options);
67
+ }
68
+ export async function inspectAgentRules(options = {}) {
69
+ const targets = await Promise.all(targetHosts(options.hosts).map((host) => inspectAgentRulesForHost(host, options)));
70
+ return {
71
+ ...aggregateAgentRulesResult(targets),
72
+ installed: targets.every((target) => target.installed),
73
+ };
74
+ }
75
+ async function inspectAgentRulesForHost(host, options = {}) {
76
+ const rulesFile = agentRulesFile(host, options.homeDir);
44
77
  const block = cockpitAgentRulesBlock();
45
78
  let existing = "";
46
79
  try {
47
- existing = await readFile(agentsFile, "utf8");
80
+ existing = await readFile(rulesFile, "utf8");
48
81
  }
49
82
  catch {
50
- return { status: "missing", agents_file: agentsFile, block, installed: false };
83
+ return { ...agentRulesResult(host, rulesFile, "missing", block), installed: false };
51
84
  }
85
+ const installed = hasManagedBlock(existing);
52
86
  return {
53
- status: hasManagedBlock(existing) ? "unchanged" : "missing",
54
- agents_file: agentsFile,
55
- block,
56
- installed: hasManagedBlock(existing),
87
+ ...agentRulesResult(host, rulesFile, installed ? "unchanged" : "missing", block),
88
+ installed,
57
89
  };
58
90
  }
59
91
  export function cockpitAgentRulesBlock() {
@@ -61,7 +93,7 @@ export function cockpitAgentRulesBlock() {
61
93
  MANAGED_BLOCK_START,
62
94
  "## Cockpit Ticket Binding",
63
95
  "",
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.",
96
+ "- 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. This starts attributing the session's work to that specific ticket in Cockpit.",
65
97
  "- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
66
98
  "- 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
99
  "- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
@@ -86,9 +118,36 @@ export function removeManagedBlock(contents) {
86
118
  return contents;
87
119
  return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
88
120
  }
89
- function codexAgentsFile(homeDir = os.homedir()) {
121
+ function agentRulesFile(host, homeDir = os.homedir()) {
122
+ if (host === "claude") {
123
+ return path.join(homeDir, ".claude", "CLAUDE.md");
124
+ }
90
125
  return path.join(homeDir, ".codex", "AGENTS.md");
91
126
  }
127
+ function agentRulesResult(host, rulesFile, status, block) {
128
+ return {
129
+ host,
130
+ status,
131
+ rules_file: rulesFile,
132
+ agents_file: rulesFile,
133
+ block,
134
+ };
135
+ }
136
+ function targetHosts(hosts) {
137
+ return hosts && hosts.length > 0 ? hosts : ["codex", "claude"];
138
+ }
139
+ function aggregateAgentRulesResult(targets) {
140
+ if (targets.every((target) => target.status === "unchanged")) {
141
+ return { status: "unchanged", targets };
142
+ }
143
+ if (targets.every((target) => target.status === "missing")) {
144
+ return { status: "missing", targets };
145
+ }
146
+ if (targets.every((target) => target.status === "created")) {
147
+ return { status: "created", targets };
148
+ }
149
+ return { status: "updated", targets };
150
+ }
92
151
  function managedBlockPattern() {
93
152
  return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
94
153
  }
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) {
@@ -40,6 +41,7 @@ function parseOnboardArgs(args) {
40
41
  allowedFlags: [
41
42
  "--home",
42
43
  "--repo",
44
+ "--workspace",
43
45
  "--dashboard-url",
44
46
  "--email",
45
47
  "--device-name",
@@ -54,6 +56,7 @@ function parseOnboardArgs(args) {
54
56
  valueFlags: [
55
57
  "--home",
56
58
  "--repo",
59
+ "--workspace",
57
60
  "--dashboard-url",
58
61
  "--email",
59
62
  "--device-name",
@@ -69,7 +72,7 @@ function parseOnboardArgs(args) {
69
72
  return {
70
73
  kind: "onboard",
71
74
  homeDir: optionalNonEmpty(values.flags.get("--home")),
72
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
75
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
73
76
  dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
74
77
  claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
75
78
  deviceName: optionalNonEmpty(values.flags.get("--device-name")),
@@ -87,17 +90,24 @@ function parseInstallArgs(args) {
87
90
  allowedFlags: [
88
91
  "--home",
89
92
  "--repo",
93
+ "--workspace",
90
94
  "--dashboard-url",
91
95
  "--supabase-url",
92
96
  "--json",
93
97
  ],
94
- valueFlags: ["--home", "--repo", "--dashboard-url", "--supabase-url"],
98
+ valueFlags: [
99
+ "--home",
100
+ "--repo",
101
+ "--workspace",
102
+ "--dashboard-url",
103
+ "--supabase-url",
104
+ ],
95
105
  });
96
106
  assertNoPositionals(values.positionals, "install");
97
107
  return {
98
108
  kind: "install",
99
109
  homeDir: optionalNonEmpty(values.flags.get("--home")),
100
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
110
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
101
111
  dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
102
112
  supabaseUrl: optionalNonEmpty(values.flags.get("--supabase-url")),
103
113
  json: values.booleans.has("--json"),
@@ -152,6 +162,7 @@ function parseStartArgs(args) {
152
162
  allowedFlags: [
153
163
  "--home",
154
164
  "--repo",
165
+ "--workspace",
155
166
  "--branch",
156
167
  "--ticket",
157
168
  "--operator-id",
@@ -163,6 +174,7 @@ function parseStartArgs(args) {
163
174
  valueFlags: [
164
175
  "--home",
165
176
  "--repo",
177
+ "--workspace",
166
178
  "--branch",
167
179
  "--ticket",
168
180
  "--operator-id",
@@ -175,7 +187,7 @@ function parseStartArgs(args) {
175
187
  return {
176
188
  kind: "start",
177
189
  homeDir: optionalNonEmpty(values.flags.get("--home")),
178
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
190
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
179
191
  branch: optionalNonEmpty(values.flags.get("--branch")),
180
192
  activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
181
193
  operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
@@ -187,14 +199,29 @@ function parseStartArgs(args) {
187
199
  }
188
200
  function parseSyncArgs(args) {
189
201
  const values = parseNamedArgs(args, {
190
- allowedFlags: ["--home", "--repo", "--dashboard-url", "--json", "--max-depth", "--max-repos"],
191
- 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
+ ],
192
219
  });
193
220
  assertNoPositionals(values.positionals, "sync");
194
221
  return {
195
222
  kind: "sync",
196
223
  homeDir: optionalNonEmpty(values.flags.get("--home")),
197
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
224
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
198
225
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
199
226
  json: values.booleans.has("--json"),
200
227
  maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
@@ -203,14 +230,27 @@ function parseSyncArgs(args) {
203
230
  }
204
231
  function parseStatusArgs(args) {
205
232
  const values = parseNamedArgs(args, {
206
- allowedFlags: ["--home", "--repo", "--json", "--max-depth", "--max-repos"],
207
- 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
+ ],
208
248
  });
209
249
  assertNoPositionals(values.positionals, "status");
210
250
  return {
211
251
  kind: "status",
212
252
  homeDir: optionalNonEmpty(values.flags.get("--home")),
213
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
253
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
214
254
  json: values.booleans.has("--json"),
215
255
  maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
216
256
  maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
@@ -218,8 +258,23 @@ function parseStatusArgs(args) {
218
258
  }
219
259
  function parseSessionsArgs(args) {
220
260
  const values = parseNamedArgs(args, {
221
- allowedFlags: ["--home", "--repo", "--source", "--json", "--max-depth", "--max-repos"],
222
- 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
+ ],
223
278
  });
224
279
  assertNoPositionals(values.positionals, "sessions");
225
280
  const source = values.flags.get("--source");
@@ -229,7 +284,7 @@ function parseSessionsArgs(args) {
229
284
  return {
230
285
  kind: "sessions",
231
286
  homeDir: optionalNonEmpty(values.flags.get("--home")),
232
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
287
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
233
288
  source,
234
289
  json: values.booleans.has("--json"),
235
290
  maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
@@ -238,8 +293,8 @@ function parseSessionsArgs(args) {
238
293
  }
239
294
  function parseServeArgs(args) {
240
295
  const values = parseNamedArgs(args, {
241
- allowedFlags: ["--home", "--repo", "--port"],
242
- valueFlags: ["--home", "--repo", "--port"],
296
+ allowedFlags: ["--home", "--repo", "--workspace", "--port"],
297
+ valueFlags: ["--home", "--repo", "--workspace", "--port"],
243
298
  });
244
299
  assertNoPositionals(values.positionals, "serve");
245
300
  const port = Number(values.flags.get("--port") ?? "4174");
@@ -249,7 +304,7 @@ function parseServeArgs(args) {
249
304
  return {
250
305
  kind: "serve",
251
306
  homeDir: optionalNonEmpty(values.flags.get("--home")),
252
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
307
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
253
308
  port,
254
309
  };
255
310
  }
@@ -258,11 +313,18 @@ function parseAutostartArgs(args) {
258
313
  allowedFlags: [
259
314
  "--home",
260
315
  "--repo",
316
+ "--workspace",
261
317
  "--dashboard-url",
262
318
  "--interval-seconds",
263
319
  "--json",
264
320
  ],
265
- valueFlags: ["--home", "--repo", "--dashboard-url", "--interval-seconds"],
321
+ valueFlags: [
322
+ "--home",
323
+ "--repo",
324
+ "--workspace",
325
+ "--dashboard-url",
326
+ "--interval-seconds",
327
+ ],
266
328
  });
267
329
  if (values.positionals.length > 1) {
268
330
  throw new Error("autostart accepts at most one action (install|uninstall|status).");
@@ -275,7 +337,7 @@ function parseAutostartArgs(args) {
275
337
  kind: "autostart",
276
338
  action,
277
339
  homeDir: optionalNonEmpty(values.flags.get("--home")),
278
- repoRoot: optionalNonEmpty(values.flags.get("--repo")),
340
+ repoRoot: optionalNonEmpty(workRootFlagValue(values)),
279
341
  dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
280
342
  intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? DEFAULT_AUTOSTART_INTERVAL_SECONDS,
281
343
  json: values.booleans.has("--json"),
@@ -283,8 +345,8 @@ function parseAutostartArgs(args) {
283
345
  }
284
346
  function parseAgentRulesArgs(args) {
285
347
  const values = parseNamedArgs(args, {
286
- allowedFlags: ["--home", "--json"],
287
- valueFlags: ["--home"],
348
+ allowedFlags: ["--home", "--host", "--json"],
349
+ valueFlags: ["--home", "--host"],
288
350
  });
289
351
  if (values.positionals.length > 1) {
290
352
  throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
@@ -296,10 +358,17 @@ function parseAgentRulesArgs(args) {
296
358
  return {
297
359
  kind: "agent-rules",
298
360
  action,
361
+ host: parseAgentRulesHost(values.flags.get("--host")),
299
362
  homeDir: optionalNonEmpty(values.flags.get("--home")),
300
363
  json: values.booleans.has("--json"),
301
364
  };
302
365
  }
366
+ function parseAgentRulesHost(value) {
367
+ const host = value?.trim().toLowerCase() || "all";
368
+ if (host === "codex" || host === "claude" || host === "all")
369
+ return host;
370
+ throw new Error("agent-rules --host must be codex, claude, or all.");
371
+ }
303
372
  function parseNamedArgs(args, options) {
304
373
  const allowed = new Set(options.allowedFlags);
305
374
  const valueFlags = new Set(options.valueFlags);
@@ -334,6 +403,16 @@ function parseNamedArgs(args, options) {
334
403
  }
335
404
  return { flags, booleans, positionals };
336
405
  }
406
+ function workRootFlagValue(values) {
407
+ const provided = WORK_ROOT_FLAGS.filter((flag) => values.flags.has(flag));
408
+ if (provided.length === 0)
409
+ return undefined;
410
+ const uniqueValues = new Set(provided.map((flag) => values.flags.get(flag)).filter(Boolean));
411
+ if (uniqueValues.size > 1) {
412
+ throw new Error("--repo and --workspace must point to the same path.");
413
+ }
414
+ return values.flags.get("--workspace") ?? values.flags.get("--repo");
415
+ }
337
416
  function assertNoPositionals(positionals, command) {
338
417
  if (positionals.length > 0) {
339
418
  throw new Error(`${command} does not accept positional arguments.`);
@@ -2,7 +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
+ import { inspectAgentRules, installAgentRules, uninstallAgentRules, } from "../agent-rules.js";
6
6
  import { parseLocalArgs, normalizeUrl } from "./local-args.js";
7
7
  import { autostartStatus, installAutostartAgent, uninstallAutostartAgent } from "../autostart.js";
8
8
  import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext } from "../local-state.js";
@@ -75,18 +75,18 @@ export function localCommandHelp(command) {
75
75
  if (command)
76
76
  return localSubcommandHelp(command);
77
77
  return [
78
- " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
79
- " 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]",
80
80
  " cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
81
81
  " cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
82
82
  " cockpit logout",
83
- " cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
84
- " cockpit sync [--repo <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
85
- " cockpit status [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
86
- " cockpit sessions [--source codex|claude] [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
87
- " cockpit serve [--port <port>] [--repo <path>]",
88
- " cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
89
- " cockpit agent-rules [install|uninstall|status] [--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] [--host codex|claude|all] [--json]",
90
90
  "",
91
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.`,
92
92
  ].join("\n");
@@ -96,10 +96,11 @@ function localSubcommandHelp(command) {
96
96
  [
97
97
  "onboard",
98
98
  [
99
- "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]",
100
100
  "",
101
101
  "Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
102
- "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.",
103
104
  `Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
104
105
  "Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
105
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).",
@@ -108,9 +109,10 @@ function localSubcommandHelp(command) {
108
109
  [
109
110
  "install",
110
111
  [
111
- "Usage: cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
112
+ "Usage: cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
112
113
  "",
113
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.",
114
116
  "Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
115
117
  ],
116
118
  ],
@@ -135,19 +137,21 @@ function localSubcommandHelp(command) {
135
137
  [
136
138
  "start",
137
139
  [
138
- "Usage: cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--json]",
140
+ "Usage: cockpit start [--ticket <id>] [--workspace <path>] [--branch <name>] [--json]",
139
141
  "",
140
142
  "Starts local ambient capture. Parent folders start each child git worktree.",
141
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.",
142
145
  ],
143
146
  ],
144
147
  [
145
148
  "sync",
146
149
  [
147
- "Usage: cockpit sync [--repo <path>] [--dashboard-url <url>] [--json]",
150
+ "Usage: cockpit sync [--workspace <path>] [--dashboard-url <url>] [--json]",
148
151
  "",
149
152
  "Uploads latest local ambient envelope(s), or spools safe retries if blocked.",
150
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.",
151
155
  "Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
152
156
  "transcripts (and Claude subagent sidecars) are attributed to repos",
153
157
  "deterministically and ambiguous transcripts are retained as unattributed",
@@ -161,38 +165,42 @@ function localSubcommandHelp(command) {
161
165
  [
162
166
  "status",
163
167
  [
164
- "Usage: cockpit status [--repo <path>] [--json]",
168
+ "Usage: cockpit status [--workspace <path>] [--json]",
165
169
  "",
166
170
  "Prints install, pairing, active work, upload, and retry state.",
171
+ "`--repo <path>` remains supported as a backward-compatible alias.",
167
172
  ],
168
173
  ],
169
174
  [
170
175
  "sessions",
171
176
  [
172
- "Usage: cockpit sessions [--source codex|claude] [--repo <path>] [--json]",
177
+ "Usage: cockpit sessions [--source codex|claude] [--workspace <path>] [--json]",
173
178
  "",
174
179
  "Read-only: re-runs Codex + Claude session attribution and prints each",
175
180
  "session's id, source, state, reason, scores, signals, and per-sidecar",
176
181
  "skip reasons. No upload, no cursor writes. Answers \"why is session X",
177
182
  "missing?\" locally — counts and labels only, never paths or content.",
183
+ "`--repo <path>` remains supported as a backward-compatible alias.",
178
184
  ],
179
185
  ],
180
186
  [
181
187
  "serve",
182
188
  [
183
- "Usage: cockpit serve [--port <port>] [--repo <path>]",
189
+ "Usage: cockpit serve [--port <port>] [--workspace <path>]",
184
190
  "",
185
191
  "Starts the local collector HTTP status server.",
192
+ "`--repo <path>` remains supported as a backward-compatible alias.",
186
193
  ],
187
194
  ],
188
195
  [
189
196
  "autostart",
190
197
  [
191
- "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]",
192
199
  "",
193
200
  "Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
194
201
  "every 15 min (default), surviving reboots — so machines never drift to Stale.",
195
- "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.",
196
204
  "Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
197
205
  "macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
198
206
  ],
@@ -200,11 +208,13 @@ function localSubcommandHelp(command) {
200
208
  [
201
209
  "agent-rules",
202
210
  [
203
- "Usage: cockpit agent-rules [install|uninstall|status] [--json]",
211
+ "Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--json]",
204
212
  "",
205
- "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md.",
206
- "This gives Codex agents a user-scope rule to run `cockpit start --ticket <id>`",
207
- "before ticketed implementation work, or ask once when the ticket ID is missing.",
213
+ "Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md",
214
+ "and ~/.claude/CLAUDE.md by default. Pass --host to manage only one.",
215
+ "This gives Codex and Claude agents a user-scope rule to run",
216
+ "`cockpit start --ticket <id>` before ticketed implementation work, or ask",
217
+ "once when the ticket ID is missing.",
208
218
  "Action defaults to `install`.",
209
219
  ],
210
220
  ],
@@ -1024,39 +1034,47 @@ async function runAutostart(command, io) {
1024
1034
  return result.status === "unsupported" ? 1 : 0;
1025
1035
  }
1026
1036
  async function runAgentRules(command, io) {
1037
+ const hosts = agentRuleHosts(command.host);
1027
1038
  const result = command.action === "install"
1028
- ? await installCodexAgentRules({ homeDir: command.homeDir })
1039
+ ? await installAgentRules({ homeDir: command.homeDir, hosts })
1029
1040
  : command.action === "uninstall"
1030
- ? await uninstallCodexAgentRules({ homeDir: command.homeDir })
1031
- : await inspectCodexAgentRules({ homeDir: command.homeDir });
1041
+ ? await uninstallAgentRules({ homeDir: command.homeDir, hosts })
1042
+ : await inspectAgentRules({ homeDir: command.homeDir, hosts });
1032
1043
  if (command.json) {
1033
1044
  writeLine(io.stdout, JSON.stringify(result, null, 2));
1034
1045
  return 0;
1035
1046
  }
1036
1047
  if ("installed" in result) {
1037
1048
  writeLine(io.stdout, result.installed
1038
- ? "Cockpit Codex agent rules are installed."
1039
- : "Cockpit Codex agent rules are not installed.");
1049
+ ? "Cockpit agent rules are installed."
1050
+ : "Cockpit agent rules are not installed.");
1040
1051
  }
1041
1052
  else {
1042
- switch (result.status) {
1043
- case "created":
1044
- writeLine(io.stdout, "Cockpit Codex agent rules installed.");
1045
- break;
1046
- case "updated":
1047
- writeLine(io.stdout, "Cockpit Codex agent rules updated.");
1048
- break;
1049
- case "unchanged":
1050
- writeLine(io.stdout, "Cockpit Codex agent rules already current.");
1051
- break;
1052
- case "missing":
1053
- writeLine(io.stdout, "Cockpit Codex agent rules were not installed.");
1054
- break;
1055
- }
1053
+ writeLine(io.stdout, agentRulesStatusLine(result));
1054
+ }
1055
+ for (const target of result.targets) {
1056
+ writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
1056
1057
  }
1057
- writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
1058
1058
  return 0;
1059
1059
  }
1060
+ function agentRuleHosts(host) {
1061
+ return host === "all" ? ["codex", "claude"] : [host];
1062
+ }
1063
+ function agentRulesStatusLine(result) {
1064
+ switch (result.status) {
1065
+ case "created":
1066
+ return "Cockpit agent rules installed.";
1067
+ case "updated":
1068
+ return "Cockpit agent rules updated.";
1069
+ case "unchanged":
1070
+ return "Cockpit agent rules already current.";
1071
+ case "missing":
1072
+ return "Cockpit agent rules were not installed.";
1073
+ }
1074
+ }
1075
+ function agentRuleHostLabel(host) {
1076
+ return host === "claude" ? "CLAUDE.md" : "AGENTS.md";
1077
+ }
1060
1078
  function writeAutostartResult(io, result) {
1061
1079
  switch (result.status) {
1062
1080
  case "installed":
@@ -24,7 +24,7 @@ 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
28
  "Agent setup: run `cockpit agent-rules install` so Codex asks for or binds Linear tickets before edits.",
29
29
  "Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
30
30
  "Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`, `agent-rules`.",
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.16",
3
+ "version": "0.1.18",
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/",