@bli-cockpit/cli 0.1.17 → 0.1.19
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 +13 -5
- package/dist/agent-rules.js +174 -22
- package/dist/commands/local-args.js +9 -2
- package/dist/commands/local.js +83 -27
- package/dist/commands/public-root.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,17 +40,25 @@ 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
42
|
`cockpit sync --workspace "$PWD" --json`. `--repo <path>` remains supported
|
|
43
|
-
for older prompts and the
|
|
43
|
+
for older prompts and the agent ticket-binding guardrail.
|
|
44
44
|
|
|
45
|
-
On machines where Codex agents will do ticketed work,
|
|
46
|
-
|
|
45
|
+
On machines where Codex or Claude agents will do ticketed work, interactive
|
|
46
|
+
`cockpit onboard` checks `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` after
|
|
47
|
+
harvest proof. If equivalent Cockpit ticket-binding guidance already exists, it
|
|
48
|
+
leaves the files alone. If guidance is missing or clearly stale, it asks whether
|
|
49
|
+
to install or replace it.
|
|
47
50
|
|
|
48
51
|
```bash
|
|
52
|
+
# Repair/manual path, or headless/json onboarding where Cockpit cannot prompt.
|
|
49
53
|
cockpit agent-rules install
|
|
50
54
|
```
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
tickets before edits, or ask once when
|
|
56
|
+
The direct command updates `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` with
|
|
57
|
+
the Cockpit rule to bind known Linear tickets before edits, or ask once when
|
|
58
|
+
the ticket ID is missing. That binding starts attributing the session's work to
|
|
59
|
+
the specific ticket in Cockpit. It checks for managed or equivalent guidance
|
|
60
|
+
first, so current files are left unchanged and stale Cockpit ticket-binding
|
|
61
|
+
sections are replaced.
|
|
54
62
|
|
|
55
63
|
Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
|
|
56
64
|
repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
|
package/dist/agent-rules.js
CHANGED
|
@@ -4,56 +4,96 @@ 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
|
-
|
|
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(
|
|
22
|
+
existing = await readFile(rulesFile, "utf8");
|
|
13
23
|
}
|
|
14
24
|
catch {
|
|
15
25
|
existed = false;
|
|
16
26
|
}
|
|
17
|
-
const
|
|
27
|
+
const prepared = prepareManagedBlockInstall(existing, block);
|
|
28
|
+
if (!prepared.next) {
|
|
29
|
+
return agentRulesResult(host, rulesFile, "unchanged", block, prepared.state);
|
|
30
|
+
}
|
|
31
|
+
const next = prepared.next;
|
|
18
32
|
if (next === existing) {
|
|
19
|
-
return
|
|
33
|
+
return agentRulesResult(host, rulesFile, "unchanged", block, prepared.state);
|
|
20
34
|
}
|
|
21
|
-
await mkdir(path.dirname(
|
|
22
|
-
await writeFile(
|
|
23
|
-
return
|
|
35
|
+
await mkdir(path.dirname(rulesFile), { recursive: true });
|
|
36
|
+
await writeFile(rulesFile, next, "utf8");
|
|
37
|
+
return agentRulesResult(host, rulesFile, existed ? "updated" : "created", block, "managed", prepared.state === "stale");
|
|
24
38
|
}
|
|
25
39
|
export async function uninstallCodexAgentRules(options = {}) {
|
|
26
|
-
|
|
40
|
+
return uninstallAgentRulesForHost("codex", options);
|
|
41
|
+
}
|
|
42
|
+
export async function uninstallClaudeAgentRules(options = {}) {
|
|
43
|
+
return uninstallAgentRulesForHost("claude", options);
|
|
44
|
+
}
|
|
45
|
+
export async function uninstallAgentRules(options = {}) {
|
|
46
|
+
const targets = await Promise.all(targetHosts(options.hosts).map((host) => uninstallAgentRulesForHost(host, options)));
|
|
47
|
+
return aggregateAgentRulesResult(targets);
|
|
48
|
+
}
|
|
49
|
+
async function uninstallAgentRulesForHost(host, options = {}) {
|
|
50
|
+
const rulesFile = agentRulesFile(host, options.homeDir);
|
|
27
51
|
const block = cockpitAgentRulesBlock();
|
|
28
52
|
let existing = "";
|
|
29
53
|
try {
|
|
30
|
-
existing = await readFile(
|
|
54
|
+
existing = await readFile(rulesFile, "utf8");
|
|
31
55
|
}
|
|
32
56
|
catch {
|
|
33
|
-
return
|
|
57
|
+
return agentRulesResult(host, rulesFile, "missing", block, "missing");
|
|
34
58
|
}
|
|
35
59
|
const next = removeManagedBlock(existing);
|
|
36
60
|
if (next === existing) {
|
|
37
|
-
return
|
|
61
|
+
return agentRulesResult(host, rulesFile, "missing", block, "missing");
|
|
38
62
|
}
|
|
39
|
-
await writeFile(
|
|
40
|
-
return
|
|
63
|
+
await writeFile(rulesFile, next, "utf8");
|
|
64
|
+
return agentRulesResult(host, rulesFile, "updated", block, "missing");
|
|
41
65
|
}
|
|
42
66
|
export async function inspectCodexAgentRules(options = {}) {
|
|
43
|
-
|
|
67
|
+
return inspectAgentRulesForHost("codex", options);
|
|
68
|
+
}
|
|
69
|
+
export async function inspectClaudeAgentRules(options = {}) {
|
|
70
|
+
return inspectAgentRulesForHost("claude", options);
|
|
71
|
+
}
|
|
72
|
+
export async function inspectAgentRules(options = {}) {
|
|
73
|
+
const targets = await Promise.all(targetHosts(options.hosts).map((host) => inspectAgentRulesForHost(host, options)));
|
|
74
|
+
return {
|
|
75
|
+
...aggregateAgentRulesResult(targets),
|
|
76
|
+
installed: targets.every((target) => target.installed),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function inspectAgentRulesForHost(host, options = {}) {
|
|
80
|
+
const rulesFile = agentRulesFile(host, options.homeDir);
|
|
44
81
|
const block = cockpitAgentRulesBlock();
|
|
45
82
|
let existing = "";
|
|
46
83
|
try {
|
|
47
|
-
existing = await readFile(
|
|
84
|
+
existing = await readFile(rulesFile, "utf8");
|
|
48
85
|
}
|
|
49
86
|
catch {
|
|
50
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
...agentRulesResult(host, rulesFile, "missing", block, "missing"),
|
|
89
|
+
installed: false,
|
|
90
|
+
};
|
|
51
91
|
}
|
|
92
|
+
const state = inspectAgentRulesContents(existing);
|
|
93
|
+
const installed = state === "managed" || state === "equivalent";
|
|
52
94
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
block,
|
|
56
|
-
installed: hasManagedBlock(existing),
|
|
95
|
+
...agentRulesResult(host, rulesFile, installed ? "unchanged" : "missing", block, state),
|
|
96
|
+
installed,
|
|
57
97
|
};
|
|
58
98
|
}
|
|
59
99
|
export function cockpitAgentRulesBlock() {
|
|
@@ -61,7 +101,7 @@ export function cockpitAgentRulesBlock() {
|
|
|
61
101
|
MANAGED_BLOCK_START,
|
|
62
102
|
"## Cockpit Ticket Binding",
|
|
63
103
|
"",
|
|
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.",
|
|
104
|
+
"- 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
105
|
"- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
|
|
66
106
|
"- 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
107
|
"- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
|
|
@@ -86,12 +126,124 @@ export function removeManagedBlock(contents) {
|
|
|
86
126
|
return contents;
|
|
87
127
|
return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
|
|
88
128
|
}
|
|
89
|
-
function
|
|
129
|
+
function prepareManagedBlockInstall(contents, block) {
|
|
130
|
+
if (!contents.trim())
|
|
131
|
+
return { next: `${block}\n`, state: "missing" };
|
|
132
|
+
if (hasManagedBlock(contents)) {
|
|
133
|
+
return { next: upsertManagedBlock(contents, block), state: "managed" };
|
|
134
|
+
}
|
|
135
|
+
if (hasEquivalentUnmanagedTicketBinding(contents)) {
|
|
136
|
+
return { next: null, state: "equivalent" };
|
|
137
|
+
}
|
|
138
|
+
const staleBlock = findStaleUnmanagedTicketBindingBlock(contents);
|
|
139
|
+
if (staleBlock) {
|
|
140
|
+
return {
|
|
141
|
+
next: replaceLineSpan(contents, staleBlock.startLine, staleBlock.endLine, block),
|
|
142
|
+
state: "stale",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return { next: `${contents.replace(/\s+$/u, "")}\n\n${block}\n`, state: "missing" };
|
|
146
|
+
}
|
|
147
|
+
function inspectAgentRulesContents(contents) {
|
|
148
|
+
if (hasManagedBlock(contents))
|
|
149
|
+
return "managed";
|
|
150
|
+
if (hasEquivalentUnmanagedTicketBinding(contents))
|
|
151
|
+
return "equivalent";
|
|
152
|
+
if (findStaleUnmanagedTicketBindingBlock(contents))
|
|
153
|
+
return "stale";
|
|
154
|
+
return "missing";
|
|
155
|
+
}
|
|
156
|
+
function agentRulesFile(host, homeDir = os.homedir()) {
|
|
157
|
+
if (host === "claude") {
|
|
158
|
+
return path.join(homeDir, ".claude", "CLAUDE.md");
|
|
159
|
+
}
|
|
90
160
|
return path.join(homeDir, ".codex", "AGENTS.md");
|
|
91
161
|
}
|
|
162
|
+
function agentRulesResult(host, rulesFile, status, block, state, staleBlockReplaced = false) {
|
|
163
|
+
return {
|
|
164
|
+
host,
|
|
165
|
+
status,
|
|
166
|
+
rules_file: rulesFile,
|
|
167
|
+
agents_file: rulesFile,
|
|
168
|
+
block,
|
|
169
|
+
state,
|
|
170
|
+
...(staleBlockReplaced ? { stale_block_replaced: true } : {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function targetHosts(hosts) {
|
|
174
|
+
return hosts && hosts.length > 0 ? hosts : ["codex", "claude"];
|
|
175
|
+
}
|
|
176
|
+
function aggregateAgentRulesResult(targets) {
|
|
177
|
+
if (targets.every((target) => target.status === "unchanged")) {
|
|
178
|
+
return { status: "unchanged", targets };
|
|
179
|
+
}
|
|
180
|
+
if (targets.every((target) => target.status === "missing")) {
|
|
181
|
+
return { status: "missing", targets };
|
|
182
|
+
}
|
|
183
|
+
if (targets.every((target) => target.status === "created")) {
|
|
184
|
+
return { status: "created", targets };
|
|
185
|
+
}
|
|
186
|
+
return { status: "updated", targets };
|
|
187
|
+
}
|
|
92
188
|
function managedBlockPattern() {
|
|
93
189
|
return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
|
|
94
190
|
}
|
|
191
|
+
function hasEquivalentUnmanagedTicketBinding(contents) {
|
|
192
|
+
const text = normalizeRuleText(contents);
|
|
193
|
+
if (!hasTicketBindingCues(text))
|
|
194
|
+
return false;
|
|
195
|
+
const signals = [
|
|
196
|
+
/cockpit\s+start\s+--ticket\b/u,
|
|
197
|
+
/before\s+(?:the\s+)?first\s+code\s+edit|before\s+ticketed\s+implementation/u,
|
|
198
|
+
/ticket\s+id\s+(?:is\s+)?(?:missing|visible)|ask\s+once/u,
|
|
199
|
+
/general\s+ambient/u,
|
|
200
|
+
/cockpit\s+sync\s+--repo|fresh\s+ticket\/session\s+binding\s+metadata/u,
|
|
201
|
+
/use\s+--ticket|do\s+not\s+invent\s+--ticketid/u,
|
|
202
|
+
];
|
|
203
|
+
const score = signals.filter((signal) => signal.test(text)).length;
|
|
204
|
+
return score >= 5;
|
|
205
|
+
}
|
|
206
|
+
function findStaleUnmanagedTicketBindingBlock(contents) {
|
|
207
|
+
const lines = contents.split("\n");
|
|
208
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
209
|
+
if (!/^#{1,6}\s+.*(?:cockpit\s+)?ticket\s+binding\b/iu.test(lines[index] ?? "")) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
let endLine = lines.length;
|
|
213
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
214
|
+
if (/^#{1,6}\s+\S/u.test(lines[cursor] ?? "")) {
|
|
215
|
+
endLine = cursor;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const candidate = lines.slice(index, endLine).join("\n");
|
|
220
|
+
const normalized = normalizeRuleText(candidate);
|
|
221
|
+
if (hasTicketBindingCues(normalized) && !hasEquivalentUnmanagedTicketBinding(candidate)) {
|
|
222
|
+
return { startLine: index, endLine };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
function replaceLineSpan(contents, startLine, endLine, replacement) {
|
|
228
|
+
const lines = contents.split("\n");
|
|
229
|
+
const before = lines.slice(0, startLine).join("\n").trimEnd();
|
|
230
|
+
const after = lines.slice(endLine).join("\n").trimStart();
|
|
231
|
+
return [before, replacement, after]
|
|
232
|
+
.filter((part) => part.trim().length > 0)
|
|
233
|
+
.join("\n\n")
|
|
234
|
+
.replace(/\n{3,}/gu, "\n\n")
|
|
235
|
+
.trimEnd() + "\n";
|
|
236
|
+
}
|
|
237
|
+
function hasTicketBindingCues(text) {
|
|
238
|
+
return /\bcockpit\b/u.test(text) && /\bticket\b/u.test(text) && /binding|agent|linear/u.test(text);
|
|
239
|
+
}
|
|
240
|
+
function normalizeRuleText(contents) {
|
|
241
|
+
return contents
|
|
242
|
+
.toLowerCase()
|
|
243
|
+
.replace(/[`"'<>]/gu, "")
|
|
244
|
+
.replace(/\s+/gu, " ")
|
|
245
|
+
.trim();
|
|
246
|
+
}
|
|
95
247
|
function escapeRegExp(value) {
|
|
96
248
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
97
249
|
}
|
|
@@ -345,8 +345,8 @@ function parseAutostartArgs(args) {
|
|
|
345
345
|
}
|
|
346
346
|
function parseAgentRulesArgs(args) {
|
|
347
347
|
const values = parseNamedArgs(args, {
|
|
348
|
-
allowedFlags: ["--home", "--json"],
|
|
349
|
-
valueFlags: ["--home"],
|
|
348
|
+
allowedFlags: ["--home", "--host", "--json"],
|
|
349
|
+
valueFlags: ["--home", "--host"],
|
|
350
350
|
});
|
|
351
351
|
if (values.positionals.length > 1) {
|
|
352
352
|
throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
|
|
@@ -358,10 +358,17 @@ function parseAgentRulesArgs(args) {
|
|
|
358
358
|
return {
|
|
359
359
|
kind: "agent-rules",
|
|
360
360
|
action,
|
|
361
|
+
host: parseAgentRulesHost(values.flags.get("--host")),
|
|
361
362
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
362
363
|
json: values.booleans.has("--json"),
|
|
363
364
|
};
|
|
364
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
|
+
}
|
|
365
372
|
function parseNamedArgs(args, options) {
|
|
366
373
|
const allowed = new Set(options.allowedFlags);
|
|
367
374
|
const valueFlags = new Set(options.valueFlags);
|
package/dist/commands/local.js
CHANGED
|
@@ -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 {
|
|
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";
|
|
@@ -86,7 +86,7 @@ export function localCommandHelp(command) {
|
|
|
86
86
|
" cockpit sessions [--source codex|claude] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
87
87
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
88
88
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
89
|
-
" cockpit agent-rules [install|uninstall|status] [--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");
|
|
@@ -104,6 +104,7 @@ function localSubcommandHelp(command) {
|
|
|
104
104
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
105
105
|
"Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
|
|
106
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).",
|
|
107
|
+
"Interactive runs also offer to add Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md after readiness proof.",
|
|
107
108
|
],
|
|
108
109
|
],
|
|
109
110
|
[
|
|
@@ -208,11 +209,13 @@ function localSubcommandHelp(command) {
|
|
|
208
209
|
[
|
|
209
210
|
"agent-rules",
|
|
210
211
|
[
|
|
211
|
-
"Usage: cockpit agent-rules [install|uninstall|status] [--json]",
|
|
212
|
+
"Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--json]",
|
|
212
213
|
"",
|
|
213
|
-
"Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md
|
|
214
|
-
"
|
|
215
|
-
"
|
|
214
|
+
"Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md",
|
|
215
|
+
"and ~/.claude/CLAUDE.md by default. Pass --host to manage only one.",
|
|
216
|
+
"This gives Codex and Claude agents a user-scope rule to run",
|
|
217
|
+
"`cockpit start --ticket <id>` before ticketed implementation work, or ask",
|
|
218
|
+
"once when the ticket ID is missing.",
|
|
216
219
|
"Action defaults to `install`.",
|
|
217
220
|
],
|
|
218
221
|
],
|
|
@@ -308,6 +311,48 @@ async function maybeOfferAutostart(command, io) {
|
|
|
308
311
|
: "Background autostart installed, but launchctl load reported a problem; check `cockpit autostart status`.");
|
|
309
312
|
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
310
313
|
}
|
|
314
|
+
async function maybeOfferAgentRules(command, io) {
|
|
315
|
+
if (command.json || !isInteractiveStdin(io))
|
|
316
|
+
return;
|
|
317
|
+
const current = await inspectAgentRules({ homeDir: command.homeDir });
|
|
318
|
+
if (current.installed) {
|
|
319
|
+
writeLine(io.stdout, onboardAgentRulesAlreadyInstalledLine(current));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const answer = (await readLine(io, "Add Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md? [Y/n] "))
|
|
323
|
+
.trim()
|
|
324
|
+
.toLowerCase();
|
|
325
|
+
if (answer === "n" || answer === "no") {
|
|
326
|
+
writeLine(io.stdout, "Skipped agent rules. Run `cockpit agent-rules install` anytime.");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const result = await installAgentRules({ homeDir: command.homeDir });
|
|
330
|
+
writeLine(io.stdout, `Agent rules: ${onboardAgentRulesInstallLine(result)}`);
|
|
331
|
+
for (const target of result.targets) {
|
|
332
|
+
writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function onboardAgentRulesAlreadyInstalledLine(result) {
|
|
336
|
+
const hasEquivalent = result.targets.some((target) => target.state === "equivalent");
|
|
337
|
+
return hasEquivalent
|
|
338
|
+
? "Agent rules: matching Cockpit ticket-binding guidance already exists."
|
|
339
|
+
: "Agent rules: already current in AGENTS.md and CLAUDE.md.";
|
|
340
|
+
}
|
|
341
|
+
function onboardAgentRulesInstallLine(result) {
|
|
342
|
+
if (result.targets.some((target) => target.stale_block_replaced)) {
|
|
343
|
+
return "updated; replaced stale Cockpit ticket-binding guidance.";
|
|
344
|
+
}
|
|
345
|
+
switch (result.status) {
|
|
346
|
+
case "created":
|
|
347
|
+
return "installed.";
|
|
348
|
+
case "updated":
|
|
349
|
+
return "updated.";
|
|
350
|
+
case "unchanged":
|
|
351
|
+
return "already current.";
|
|
352
|
+
case "missing":
|
|
353
|
+
return "not installed.";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
311
356
|
async function runOnboard(command, io) {
|
|
312
357
|
let install = null;
|
|
313
358
|
let pair = null;
|
|
@@ -381,8 +426,10 @@ async function runOnboard(command, io) {
|
|
|
381
426
|
codex_sessions: multi.codex_sessions,
|
|
382
427
|
}, null, 2));
|
|
383
428
|
}
|
|
384
|
-
if (multi.ok)
|
|
429
|
+
if (multi.ok) {
|
|
430
|
+
await maybeOfferAgentRules(command, io);
|
|
385
431
|
await maybeOfferAutostart(command, io);
|
|
432
|
+
}
|
|
386
433
|
return multi.ok ? 0 : 1;
|
|
387
434
|
}
|
|
388
435
|
const context = await startLocalWorkContext({
|
|
@@ -447,6 +494,7 @@ async function runOnboard(command, io) {
|
|
|
447
494
|
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
448
495
|
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
449
496
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
497
|
+
await maybeOfferAgentRules(command, io);
|
|
450
498
|
await maybeOfferAutostart(command, io);
|
|
451
499
|
return 0;
|
|
452
500
|
}
|
|
@@ -1032,39 +1080,47 @@ async function runAutostart(command, io) {
|
|
|
1032
1080
|
return result.status === "unsupported" ? 1 : 0;
|
|
1033
1081
|
}
|
|
1034
1082
|
async function runAgentRules(command, io) {
|
|
1083
|
+
const hosts = agentRuleHosts(command.host);
|
|
1035
1084
|
const result = command.action === "install"
|
|
1036
|
-
? await
|
|
1085
|
+
? await installAgentRules({ homeDir: command.homeDir, hosts })
|
|
1037
1086
|
: command.action === "uninstall"
|
|
1038
|
-
? await
|
|
1039
|
-
: await
|
|
1087
|
+
? await uninstallAgentRules({ homeDir: command.homeDir, hosts })
|
|
1088
|
+
: await inspectAgentRules({ homeDir: command.homeDir, hosts });
|
|
1040
1089
|
if (command.json) {
|
|
1041
1090
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
1042
1091
|
return 0;
|
|
1043
1092
|
}
|
|
1044
1093
|
if ("installed" in result) {
|
|
1045
1094
|
writeLine(io.stdout, result.installed
|
|
1046
|
-
? "Cockpit
|
|
1047
|
-
: "Cockpit
|
|
1095
|
+
? "Cockpit agent rules are installed."
|
|
1096
|
+
: "Cockpit agent rules are not installed.");
|
|
1048
1097
|
}
|
|
1049
1098
|
else {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
}
|
|
1099
|
+
writeLine(io.stdout, agentRulesStatusLine(result));
|
|
1100
|
+
}
|
|
1101
|
+
for (const target of result.targets) {
|
|
1102
|
+
writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
|
|
1064
1103
|
}
|
|
1065
|
-
writeLine(io.stdout, `AGENTS.md: ${result.agents_file}`);
|
|
1066
1104
|
return 0;
|
|
1067
1105
|
}
|
|
1106
|
+
function agentRuleHosts(host) {
|
|
1107
|
+
return host === "all" ? ["codex", "claude"] : [host];
|
|
1108
|
+
}
|
|
1109
|
+
function agentRulesStatusLine(result) {
|
|
1110
|
+
switch (result.status) {
|
|
1111
|
+
case "created":
|
|
1112
|
+
return "Cockpit agent rules installed.";
|
|
1113
|
+
case "updated":
|
|
1114
|
+
return "Cockpit agent rules updated.";
|
|
1115
|
+
case "unchanged":
|
|
1116
|
+
return "Cockpit agent rules already current.";
|
|
1117
|
+
case "missing":
|
|
1118
|
+
return "Cockpit agent rules were not installed.";
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function agentRuleHostLabel(host) {
|
|
1122
|
+
return host === "claude" ? "CLAUDE.md" : "AGENTS.md";
|
|
1123
|
+
}
|
|
1068
1124
|
function writeAutostartResult(io, result) {
|
|
1069
1125
|
switch (result.status) {
|
|
1070
1126
|
case "installed":
|
|
@@ -25,7 +25,7 @@ function cockpitHelp() {
|
|
|
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
27
|
"Already onboarded: run `cockpit sync --workspace \"$PWD\" --json`.",
|
|
28
|
-
"Agent setup:
|
|
28
|
+
"Agent setup: interactive `cockpit onboard` offers AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install` for repair/headless setup.",
|
|
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`.",
|
|
31
31
|
].join("\n");
|