@bli-cockpit/cli 0.1.18 → 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 +12 -7
- package/dist/agent-rules.js +103 -10
- package/dist/commands/local.js +47 -1
- package/dist/commands/public-root.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,18 +42,23 @@ pairing. Already-onboarded users update with
|
|
|
42
42
|
`cockpit sync --workspace "$PWD" --json`. `--repo <path>` remains supported
|
|
43
43
|
for older prompts and the agent ticket-binding guardrail.
|
|
44
44
|
|
|
45
|
-
On machines where Codex or Claude 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
|
-
rule to bind known Linear tickets before edits, or ask once when
|
|
54
|
-
is missing. That binding starts attributing the session's work to
|
|
55
|
-
ticket in Cockpit. It checks for
|
|
56
|
-
left unchanged and stale
|
|
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.
|
|
57
62
|
|
|
58
63
|
Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
|
|
59
64
|
repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
|
package/dist/agent-rules.js
CHANGED
|
@@ -24,13 +24,17 @@ async function installAgentRulesForHost(host, options = {}) {
|
|
|
24
24
|
catch {
|
|
25
25
|
existed = false;
|
|
26
26
|
}
|
|
27
|
-
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;
|
|
28
32
|
if (next === existing) {
|
|
29
|
-
return agentRulesResult(host, rulesFile, "unchanged", block);
|
|
33
|
+
return agentRulesResult(host, rulesFile, "unchanged", block, prepared.state);
|
|
30
34
|
}
|
|
31
35
|
await mkdir(path.dirname(rulesFile), { recursive: true });
|
|
32
36
|
await writeFile(rulesFile, next, "utf8");
|
|
33
|
-
return agentRulesResult(host, rulesFile, existed ? "updated" : "created", block);
|
|
37
|
+
return agentRulesResult(host, rulesFile, existed ? "updated" : "created", block, "managed", prepared.state === "stale");
|
|
34
38
|
}
|
|
35
39
|
export async function uninstallCodexAgentRules(options = {}) {
|
|
36
40
|
return uninstallAgentRulesForHost("codex", options);
|
|
@@ -50,14 +54,14 @@ async function uninstallAgentRulesForHost(host, options = {}) {
|
|
|
50
54
|
existing = await readFile(rulesFile, "utf8");
|
|
51
55
|
}
|
|
52
56
|
catch {
|
|
53
|
-
return agentRulesResult(host, rulesFile, "missing", block);
|
|
57
|
+
return agentRulesResult(host, rulesFile, "missing", block, "missing");
|
|
54
58
|
}
|
|
55
59
|
const next = removeManagedBlock(existing);
|
|
56
60
|
if (next === existing) {
|
|
57
|
-
return agentRulesResult(host, rulesFile, "missing", block);
|
|
61
|
+
return agentRulesResult(host, rulesFile, "missing", block, "missing");
|
|
58
62
|
}
|
|
59
63
|
await writeFile(rulesFile, next, "utf8");
|
|
60
|
-
return agentRulesResult(host, rulesFile, "updated", block);
|
|
64
|
+
return agentRulesResult(host, rulesFile, "updated", block, "missing");
|
|
61
65
|
}
|
|
62
66
|
export async function inspectCodexAgentRules(options = {}) {
|
|
63
67
|
return inspectAgentRulesForHost("codex", options);
|
|
@@ -80,11 +84,15 @@ async function inspectAgentRulesForHost(host, options = {}) {
|
|
|
80
84
|
existing = await readFile(rulesFile, "utf8");
|
|
81
85
|
}
|
|
82
86
|
catch {
|
|
83
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
...agentRulesResult(host, rulesFile, "missing", block, "missing"),
|
|
89
|
+
installed: false,
|
|
90
|
+
};
|
|
84
91
|
}
|
|
85
|
-
const
|
|
92
|
+
const state = inspectAgentRulesContents(existing);
|
|
93
|
+
const installed = state === "managed" || state === "equivalent";
|
|
86
94
|
return {
|
|
87
|
-
...agentRulesResult(host, rulesFile, installed ? "unchanged" : "missing", block),
|
|
95
|
+
...agentRulesResult(host, rulesFile, installed ? "unchanged" : "missing", block, state),
|
|
88
96
|
installed,
|
|
89
97
|
};
|
|
90
98
|
}
|
|
@@ -118,19 +126,48 @@ export function removeManagedBlock(contents) {
|
|
|
118
126
|
return contents;
|
|
119
127
|
return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
|
|
120
128
|
}
|
|
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
|
+
}
|
|
121
156
|
function agentRulesFile(host, homeDir = os.homedir()) {
|
|
122
157
|
if (host === "claude") {
|
|
123
158
|
return path.join(homeDir, ".claude", "CLAUDE.md");
|
|
124
159
|
}
|
|
125
160
|
return path.join(homeDir, ".codex", "AGENTS.md");
|
|
126
161
|
}
|
|
127
|
-
function agentRulesResult(host, rulesFile, status, block) {
|
|
162
|
+
function agentRulesResult(host, rulesFile, status, block, state, staleBlockReplaced = false) {
|
|
128
163
|
return {
|
|
129
164
|
host,
|
|
130
165
|
status,
|
|
131
166
|
rules_file: rulesFile,
|
|
132
167
|
agents_file: rulesFile,
|
|
133
168
|
block,
|
|
169
|
+
state,
|
|
170
|
+
...(staleBlockReplaced ? { stale_block_replaced: true } : {}),
|
|
134
171
|
};
|
|
135
172
|
}
|
|
136
173
|
function targetHosts(hosts) {
|
|
@@ -151,6 +188,62 @@ function aggregateAgentRulesResult(targets) {
|
|
|
151
188
|
function managedBlockPattern() {
|
|
152
189
|
return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
|
|
153
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
|
+
}
|
|
154
247
|
function escapeRegExp(value) {
|
|
155
248
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
156
249
|
}
|
package/dist/commands/local.js
CHANGED
|
@@ -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
|
[
|
|
@@ -310,6 +311,48 @@ async function maybeOfferAutostart(command, io) {
|
|
|
310
311
|
: "Background autostart installed, but launchctl load reported a problem; check `cockpit autostart status`.");
|
|
311
312
|
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
312
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
|
+
}
|
|
313
356
|
async function runOnboard(command, io) {
|
|
314
357
|
let install = null;
|
|
315
358
|
let pair = null;
|
|
@@ -383,8 +426,10 @@ async function runOnboard(command, io) {
|
|
|
383
426
|
codex_sessions: multi.codex_sessions,
|
|
384
427
|
}, null, 2));
|
|
385
428
|
}
|
|
386
|
-
if (multi.ok)
|
|
429
|
+
if (multi.ok) {
|
|
430
|
+
await maybeOfferAgentRules(command, io);
|
|
387
431
|
await maybeOfferAutostart(command, io);
|
|
432
|
+
}
|
|
388
433
|
return multi.ok ? 0 : 1;
|
|
389
434
|
}
|
|
390
435
|
const context = await startLocalWorkContext({
|
|
@@ -449,6 +494,7 @@ async function runOnboard(command, io) {
|
|
|
449
494
|
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
450
495
|
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
451
496
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
497
|
+
await maybeOfferAgentRules(command, io);
|
|
452
498
|
await maybeOfferAutostart(command, io);
|
|
453
499
|
return 0;
|
|
454
500
|
}
|
|
@@ -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");
|