@bli-cockpit/cli 0.1.19 → 0.1.21
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 +34 -6
- package/dist/adapters/claude-attribution.js +47 -4
- package/dist/adapters/codex-attribution.js +41 -5
- package/dist/adapters/local-sources.js +2 -0
- package/dist/adapters/raw-evidence.js +487 -35
- package/dist/agent-rules.js +44 -19
- package/dist/commands/local-args.js +51 -2
- package/dist/commands/local.js +31 -15
- package/dist/commands/public-root.js +2 -2
- package/dist/commands/session-sync.js +14 -0
- package/dist/local-state.js +7 -1
- package/dist/upload.js +117 -1
- package/package.json +2 -2
package/dist/agent-rules.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function installAgentRules(options = {}) {
|
|
|
15
15
|
}
|
|
16
16
|
async function installAgentRulesForHost(host, options = {}) {
|
|
17
17
|
const rulesFile = agentRulesFile(host, options.homeDir);
|
|
18
|
-
const block = cockpitAgentRulesBlock();
|
|
18
|
+
const block = cockpitAgentRulesBlock({ scopePath: options.scopePath });
|
|
19
19
|
let existing = "";
|
|
20
20
|
let existed = true;
|
|
21
21
|
try {
|
|
@@ -24,7 +24,7 @@ async function installAgentRulesForHost(host, options = {}) {
|
|
|
24
24
|
catch {
|
|
25
25
|
existed = false;
|
|
26
26
|
}
|
|
27
|
-
const prepared = prepareManagedBlockInstall(existing, block);
|
|
27
|
+
const prepared = prepareManagedBlockInstall(existing, block, options.scopePath);
|
|
28
28
|
if (!prepared.next) {
|
|
29
29
|
return agentRulesResult(host, rulesFile, "unchanged", block, prepared.state);
|
|
30
30
|
}
|
|
@@ -78,7 +78,7 @@ export async function inspectAgentRules(options = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
async function inspectAgentRulesForHost(host, options = {}) {
|
|
80
80
|
const rulesFile = agentRulesFile(host, options.homeDir);
|
|
81
|
-
const block = cockpitAgentRulesBlock();
|
|
81
|
+
const block = cockpitAgentRulesBlock({ scopePath: options.scopePath });
|
|
82
82
|
let existing = "";
|
|
83
83
|
try {
|
|
84
84
|
existing = await readFile(rulesFile, "utf8");
|
|
@@ -89,23 +89,28 @@ async function inspectAgentRulesForHost(host, options = {}) {
|
|
|
89
89
|
installed: false,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
-
const state = inspectAgentRulesContents(existing);
|
|
92
|
+
const state = inspectAgentRulesContents(existing, block, options.scopePath);
|
|
93
93
|
const installed = state === "managed" || state === "equivalent";
|
|
94
94
|
return {
|
|
95
95
|
...agentRulesResult(host, rulesFile, installed ? "unchanged" : "missing", block, state),
|
|
96
96
|
installed,
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
export function cockpitAgentRulesBlock() {
|
|
99
|
+
export function cockpitAgentRulesBlock(options = {}) {
|
|
100
|
+
const scopePath = options.scopePath ? path.resolve(options.scopePath) : null;
|
|
101
|
+
const scopeLine = scopePath
|
|
102
|
+
? `- This guidance only applies when the current working directory is inside the Cockpit-onboarded workspace/repo: \`${scopePath}\`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.`
|
|
103
|
+
: "- This guidance only applies when the current working directory is inside the workspace/repo that ran `cockpit onboard` or `cockpit agent-rules install`. Outside that folder, do not run Cockpit ticket binding or sync commands for private chats or unrelated repos.";
|
|
100
104
|
return [
|
|
101
105
|
MANAGED_BLOCK_START,
|
|
102
106
|
"## Cockpit Ticket Binding",
|
|
103
107
|
"",
|
|
104
|
-
|
|
108
|
+
scopeLine,
|
|
109
|
+
"- For implementation, debugging, review, PR, or ship work tied to a clear Linear ticket, run `cockpit start --ticket <ticket-id> --workspace \"$PWD\"` before the first code edit or mutating tool call. This starts attributing the session's work to that specific ticket in Cockpit.",
|
|
105
110
|
"- Use `--ticket`; do not invent `--ticketId` or other flag shapes.",
|
|
106
111
|
"- 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.",
|
|
107
112
|
"- If there is truly no ticket, state that the work remains in general ambient capture and do not invent a ticket.",
|
|
108
|
-
"- After the first meaningful checkpoint, run `cockpit sync --
|
|
113
|
+
"- After the first meaningful checkpoint, run `cockpit sync --workspace \"$PWD\" --json` so Cockpit has fresh ticket/session binding metadata.",
|
|
109
114
|
MANAGED_BLOCK_END,
|
|
110
115
|
].join("\n");
|
|
111
116
|
}
|
|
@@ -126,16 +131,19 @@ export function removeManagedBlock(contents) {
|
|
|
126
131
|
return contents;
|
|
127
132
|
return contents.replace(managedBlockPattern(), "").replace(/\n{3,}/gu, "\n\n").trimEnd() + "\n";
|
|
128
133
|
}
|
|
129
|
-
function prepareManagedBlockInstall(contents, block) {
|
|
134
|
+
function prepareManagedBlockInstall(contents, block, scopePath) {
|
|
130
135
|
if (!contents.trim())
|
|
131
136
|
return { next: `${block}\n`, state: "missing" };
|
|
132
137
|
if (hasManagedBlock(contents)) {
|
|
133
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
next: upsertManagedBlock(contents, block),
|
|
140
|
+
state: extractManagedBlock(contents) === block ? "managed" : "stale",
|
|
141
|
+
};
|
|
134
142
|
}
|
|
135
|
-
if (hasEquivalentUnmanagedTicketBinding(contents)) {
|
|
143
|
+
if (hasEquivalentUnmanagedTicketBinding(contents, scopePath)) {
|
|
136
144
|
return { next: null, state: "equivalent" };
|
|
137
145
|
}
|
|
138
|
-
const staleBlock = findStaleUnmanagedTicketBindingBlock(contents);
|
|
146
|
+
const staleBlock = findStaleUnmanagedTicketBindingBlock(contents, scopePath);
|
|
139
147
|
if (staleBlock) {
|
|
140
148
|
return {
|
|
141
149
|
next: replaceLineSpan(contents, staleBlock.startLine, staleBlock.endLine, block),
|
|
@@ -144,12 +152,13 @@ function prepareManagedBlockInstall(contents, block) {
|
|
|
144
152
|
}
|
|
145
153
|
return { next: `${contents.replace(/\s+$/u, "")}\n\n${block}\n`, state: "missing" };
|
|
146
154
|
}
|
|
147
|
-
function inspectAgentRulesContents(contents) {
|
|
148
|
-
if (hasManagedBlock(contents))
|
|
149
|
-
return "managed";
|
|
150
|
-
|
|
155
|
+
function inspectAgentRulesContents(contents, block, scopePath) {
|
|
156
|
+
if (hasManagedBlock(contents)) {
|
|
157
|
+
return extractManagedBlock(contents) === block ? "managed" : "stale";
|
|
158
|
+
}
|
|
159
|
+
if (hasEquivalentUnmanagedTicketBinding(contents, scopePath))
|
|
151
160
|
return "equivalent";
|
|
152
|
-
if (findStaleUnmanagedTicketBindingBlock(contents))
|
|
161
|
+
if (findStaleUnmanagedTicketBindingBlock(contents, scopePath))
|
|
153
162
|
return "stale";
|
|
154
163
|
return "missing";
|
|
155
164
|
}
|
|
@@ -188,10 +197,17 @@ function aggregateAgentRulesResult(targets) {
|
|
|
188
197
|
function managedBlockPattern() {
|
|
189
198
|
return new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}`, "u");
|
|
190
199
|
}
|
|
191
|
-
function
|
|
200
|
+
function extractManagedBlock(contents) {
|
|
201
|
+
return contents.match(managedBlockPattern())?.[0] ?? null;
|
|
202
|
+
}
|
|
203
|
+
function hasEquivalentUnmanagedTicketBinding(contents, scopePath) {
|
|
192
204
|
const text = normalizeRuleText(contents);
|
|
193
205
|
if (!hasTicketBindingCues(text))
|
|
194
206
|
return false;
|
|
207
|
+
if (!hasRepoScopeGuard(text))
|
|
208
|
+
return false;
|
|
209
|
+
if (scopePath && !hasScopePath(text, scopePath))
|
|
210
|
+
return false;
|
|
195
211
|
const signals = [
|
|
196
212
|
/cockpit\s+start\s+--ticket\b/u,
|
|
197
213
|
/before\s+(?:the\s+)?first\s+code\s+edit|before\s+ticketed\s+implementation/u,
|
|
@@ -203,7 +219,15 @@ function hasEquivalentUnmanagedTicketBinding(contents) {
|
|
|
203
219
|
const score = signals.filter((signal) => signal.test(text)).length;
|
|
204
220
|
return score >= 5;
|
|
205
221
|
}
|
|
206
|
-
function
|
|
222
|
+
function hasScopePath(text, scopePath) {
|
|
223
|
+
return text.includes(normalizeRuleText(path.resolve(scopePath)));
|
|
224
|
+
}
|
|
225
|
+
function hasRepoScopeGuard(text) {
|
|
226
|
+
return (/only\s+applies\s+when\s+the\s+current\s+working\s+directory\s+is\s+inside/u.test(text) ||
|
|
227
|
+
/outside\s+that\s+(?:folder|workspace|repo).*(?:do\s+not|dont)\s+run\s+cockpit/u.test(text) ||
|
|
228
|
+
/private\s+chats\s+or\s+unrelated\s+repos/u.test(text));
|
|
229
|
+
}
|
|
230
|
+
function findStaleUnmanagedTicketBindingBlock(contents, scopePath) {
|
|
207
231
|
const lines = contents.split("\n");
|
|
208
232
|
for (let index = 0; index < lines.length; index += 1) {
|
|
209
233
|
if (!/^#{1,6}\s+.*(?:cockpit\s+)?ticket\s+binding\b/iu.test(lines[index] ?? "")) {
|
|
@@ -218,7 +242,8 @@ function findStaleUnmanagedTicketBindingBlock(contents) {
|
|
|
218
242
|
}
|
|
219
243
|
const candidate = lines.slice(index, endLine).join("\n");
|
|
220
244
|
const normalized = normalizeRuleText(candidate);
|
|
221
|
-
if (hasTicketBindingCues(normalized) &&
|
|
245
|
+
if (hasTicketBindingCues(normalized) &&
|
|
246
|
+
!hasEquivalentUnmanagedTicketBinding(candidate, scopePath)) {
|
|
222
247
|
return { startLine: index, endLine };
|
|
223
248
|
}
|
|
224
249
|
}
|
|
@@ -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
|
+
import { IntentSourceSchema, WorkIntentSchema, WorkPhaseSchema, } from "@bli-cockpit/telemetry-core";
|
|
8
9
|
const WORK_ROOT_FLAGS = ["--repo", "--workspace"];
|
|
9
10
|
export function parseLocalArgs(argv) {
|
|
10
11
|
const command = argv[0];
|
|
@@ -165,6 +166,12 @@ function parseStartArgs(args) {
|
|
|
165
166
|
"--workspace",
|
|
166
167
|
"--branch",
|
|
167
168
|
"--ticket",
|
|
169
|
+
"--topic",
|
|
170
|
+
"--topic-summary",
|
|
171
|
+
"--intent",
|
|
172
|
+
"--phase",
|
|
173
|
+
"--intent-source",
|
|
174
|
+
"--intent-confidence",
|
|
168
175
|
"--operator-id",
|
|
169
176
|
"--session-id",
|
|
170
177
|
"--json",
|
|
@@ -177,6 +184,12 @@ function parseStartArgs(args) {
|
|
|
177
184
|
"--workspace",
|
|
178
185
|
"--branch",
|
|
179
186
|
"--ticket",
|
|
187
|
+
"--topic",
|
|
188
|
+
"--topic-summary",
|
|
189
|
+
"--intent",
|
|
190
|
+
"--phase",
|
|
191
|
+
"--intent-source",
|
|
192
|
+
"--intent-confidence",
|
|
180
193
|
"--operator-id",
|
|
181
194
|
"--session-id",
|
|
182
195
|
"--max-depth",
|
|
@@ -184,12 +197,28 @@ function parseStartArgs(args) {
|
|
|
184
197
|
],
|
|
185
198
|
});
|
|
186
199
|
assertNoPositionals(values.positionals, "start");
|
|
200
|
+
const topicLabel = optionalNonEmpty(values.flags.get("--topic"));
|
|
201
|
+
const topicSummaryRedacted = optionalNonEmpty(values.flags.get("--topic-summary"));
|
|
202
|
+
const workIntent = optionalSchemaValue(WorkIntentSchema, values.flags.get("--intent"), "--intent");
|
|
203
|
+
const workPhase = optionalSchemaValue(WorkPhaseSchema, values.flags.get("--phase"), "--phase");
|
|
204
|
+
const intentConfidence = optionalConfidence(values.flags.get("--intent-confidence"), "--intent-confidence");
|
|
205
|
+
const explicitIntentMetadata = Boolean(topicLabel ||
|
|
206
|
+
topicSummaryRedacted ||
|
|
207
|
+
workIntent ||
|
|
208
|
+
workPhase ||
|
|
209
|
+
intentConfidence !== undefined);
|
|
187
210
|
return {
|
|
188
211
|
kind: "start",
|
|
189
212
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
190
213
|
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
191
214
|
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
192
215
|
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
216
|
+
topicLabel,
|
|
217
|
+
topicSummaryRedacted,
|
|
218
|
+
workIntent,
|
|
219
|
+
workPhase,
|
|
220
|
+
intentSource: optionalSchemaValue(IntentSourceSchema, values.flags.get("--intent-source"), "--intent-source") ?? (explicitIntentMetadata ? "explicit_user" : undefined),
|
|
221
|
+
intentConfidence,
|
|
193
222
|
operatorId: optionalNonEmpty(values.flags.get("--operator-id")),
|
|
194
223
|
sessionId: optionalNonEmpty(values.flags.get("--session-id")),
|
|
195
224
|
json: values.booleans.has("--json"),
|
|
@@ -345,8 +374,8 @@ function parseAutostartArgs(args) {
|
|
|
345
374
|
}
|
|
346
375
|
function parseAgentRulesArgs(args) {
|
|
347
376
|
const values = parseNamedArgs(args, {
|
|
348
|
-
allowedFlags: ["--home", "--host", "--json"],
|
|
349
|
-
valueFlags: ["--home", "--host"],
|
|
377
|
+
allowedFlags: ["--home", "--host", "--repo", "--workspace", "--json"],
|
|
378
|
+
valueFlags: ["--home", "--host", "--repo", "--workspace"],
|
|
350
379
|
});
|
|
351
380
|
if (values.positionals.length > 1) {
|
|
352
381
|
throw new Error("agent-rules accepts at most one action (install|uninstall|status).");
|
|
@@ -360,6 +389,7 @@ function parseAgentRulesArgs(args) {
|
|
|
360
389
|
action,
|
|
361
390
|
host: parseAgentRulesHost(values.flags.get("--host")),
|
|
362
391
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
392
|
+
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
363
393
|
json: values.booleans.has("--json"),
|
|
364
394
|
};
|
|
365
395
|
}
|
|
@@ -443,6 +473,25 @@ function optionalPositiveInteger(value, flag) {
|
|
|
443
473
|
}
|
|
444
474
|
return parsed;
|
|
445
475
|
}
|
|
476
|
+
function optionalConfidence(value, flag) {
|
|
477
|
+
if (value === undefined)
|
|
478
|
+
return undefined;
|
|
479
|
+
const parsed = Number(value);
|
|
480
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
481
|
+
throw new Error(`${flag} must be a number between 0 and 1.`);
|
|
482
|
+
}
|
|
483
|
+
return parsed;
|
|
484
|
+
}
|
|
485
|
+
function optionalSchemaValue(schema, value, flag) {
|
|
486
|
+
const trimmed = optionalNonEmpty(value);
|
|
487
|
+
if (!trimmed)
|
|
488
|
+
return undefined;
|
|
489
|
+
const parsed = schema.safeParse(trimmed);
|
|
490
|
+
if (!parsed.success || parsed.data === undefined) {
|
|
491
|
+
throw new Error(`${flag} has an unsupported value.`);
|
|
492
|
+
}
|
|
493
|
+
return parsed.data;
|
|
494
|
+
}
|
|
446
495
|
export function normalizeUrl(value) {
|
|
447
496
|
const trimmed = value.trim().replace(/\/+$/, "");
|
|
448
497
|
if (!trimmed)
|
package/dist/commands/local.js
CHANGED
|
@@ -80,13 +80,13 @@ export function localCommandHelp(command) {
|
|
|
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>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
83
|
+
" cockpit start [--ticket <id>] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
84
84
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
85
85
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
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] [--host codex|claude|all] [--json]",
|
|
89
|
+
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--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");
|
|
@@ -138,11 +138,14 @@ function localSubcommandHelp(command) {
|
|
|
138
138
|
[
|
|
139
139
|
"start",
|
|
140
140
|
[
|
|
141
|
-
"Usage: cockpit start [--ticket <id>] [--workspace <path>] [--branch <name>] [--json]",
|
|
141
|
+
"Usage: cockpit start [--ticket <id>] [--topic <label>] [--topic-summary <summary>] [--intent <intent>] [--phase <phase>] [--intent-confidence <0..1>] [--workspace <path>] [--branch <name>] [--json]",
|
|
142
142
|
"",
|
|
143
143
|
"Starts local ambient capture. Parent folders start each child git worktree.",
|
|
144
144
|
"Add --ticket only when the work already has a visible ticket.",
|
|
145
|
-
"
|
|
145
|
+
"Use --topic/--intent/--phase for planning, discovery, and learning work that has no ticket yet.",
|
|
146
|
+
"Supported intents: implementation, bug_fix, root_cause_analysis, planning, discovery, review, testing, documentation, release, learning, coordination, maintenance, analysis, unknown, other.",
|
|
147
|
+
"Supported phases: planning, discovery, implementation, debugging, review, testing, documentation, release, handoff, analysis, unknown, other.",
|
|
148
|
+
"`--repo <path>` remains supported as a backward-compatible alias; use --workspace in agent guidance.",
|
|
146
149
|
],
|
|
147
150
|
],
|
|
148
151
|
[
|
|
@@ -209,13 +212,12 @@ function localSubcommandHelp(command) {
|
|
|
209
212
|
[
|
|
210
213
|
"agent-rules",
|
|
211
214
|
[
|
|
212
|
-
"Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--json]",
|
|
215
|
+
"Usage: cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
213
216
|
"",
|
|
214
217
|
"Installs a managed Cockpit Ticket Binding block into ~/.codex/AGENTS.md",
|
|
215
218
|
"and ~/.claude/CLAUDE.md by default. Pass --host to manage only one.",
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"once when the ticket ID is missing.",
|
|
219
|
+
"The block is scoped to --workspace, or the current directory when omitted,",
|
|
220
|
+
"so Codex and Claude only run Cockpit ticket binding inside that onboarded folder.",
|
|
219
221
|
"Action defaults to `install`.",
|
|
220
222
|
],
|
|
221
223
|
],
|
|
@@ -314,19 +316,23 @@ async function maybeOfferAutostart(command, io) {
|
|
|
314
316
|
async function maybeOfferAgentRules(command, io) {
|
|
315
317
|
if (command.json || !isInteractiveStdin(io))
|
|
316
318
|
return;
|
|
317
|
-
const
|
|
319
|
+
const scopePath = path.resolve(command.repoRoot ?? process.cwd());
|
|
320
|
+
const current = await inspectAgentRules({
|
|
321
|
+
homeDir: command.homeDir,
|
|
322
|
+
scopePath,
|
|
323
|
+
});
|
|
318
324
|
if (current.installed) {
|
|
319
325
|
writeLine(io.stdout, onboardAgentRulesAlreadyInstalledLine(current));
|
|
320
326
|
return;
|
|
321
327
|
}
|
|
322
|
-
const answer = (await readLine(io,
|
|
328
|
+
const answer = (await readLine(io, `Add Cockpit ticket-binding rules scoped to ${scopePath} to AGENTS.md and CLAUDE.md? [Y/n] `))
|
|
323
329
|
.trim()
|
|
324
330
|
.toLowerCase();
|
|
325
331
|
if (answer === "n" || answer === "no") {
|
|
326
|
-
writeLine(io.stdout, "Skipped agent rules. Run `cockpit agent-rules install` anytime.");
|
|
332
|
+
writeLine(io.stdout, "Skipped agent rules. Run `cockpit agent-rules install --workspace \"$PWD\"` anytime.");
|
|
327
333
|
return;
|
|
328
334
|
}
|
|
329
|
-
const result = await installAgentRules({ homeDir: command.homeDir });
|
|
335
|
+
const result = await installAgentRules({ homeDir: command.homeDir, scopePath });
|
|
330
336
|
writeLine(io.stdout, `Agent rules: ${onboardAgentRulesInstallLine(result)}`);
|
|
331
337
|
for (const target of result.targets) {
|
|
332
338
|
writeLine(io.stdout, `${agentRuleHostLabel(target.host)}: ${target.rules_file}`);
|
|
@@ -776,7 +782,7 @@ function nextStepForOnboardBlocker(blocker) {
|
|
|
776
782
|
case "install":
|
|
777
783
|
return "Rerun `cockpit onboard` from the repo root; it will reinstall local config.";
|
|
778
784
|
case "work_context":
|
|
779
|
-
return "Run `cockpit start --ticket <id> --
|
|
785
|
+
return "Run `cockpit start --ticket <id> --workspace \"$PWD\"`, then retry `cockpit sync`.";
|
|
780
786
|
default:
|
|
781
787
|
return "Run `cockpit status --json` and report the blocker label plus last failure reason.";
|
|
782
788
|
}
|
|
@@ -800,6 +806,12 @@ async function runStart(command, io) {
|
|
|
800
806
|
repoRoot: worktree.repo_root,
|
|
801
807
|
branch: command.branch,
|
|
802
808
|
activeTicketId: command.activeTicketId,
|
|
809
|
+
topicLabel: command.topicLabel,
|
|
810
|
+
topicSummaryRedacted: command.topicSummaryRedacted,
|
|
811
|
+
workIntent: command.workIntent,
|
|
812
|
+
workPhase: command.workPhase,
|
|
813
|
+
intentSource: command.intentSource,
|
|
814
|
+
intentConfidence: command.intentConfidence,
|
|
803
815
|
operatorId: command.operatorId,
|
|
804
816
|
sessionId: command.sessionId,
|
|
805
817
|
})));
|
|
@@ -822,6 +834,9 @@ async function runStart(command, io) {
|
|
|
822
834
|
writeLine(io.stdout, `Repo: ${context.repo}`);
|
|
823
835
|
writeLine(io.stdout, `Branch: ${context.branch}`);
|
|
824
836
|
writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
|
|
837
|
+
if (context.topic_label || context.work_intent || context.work_phase) {
|
|
838
|
+
writeLine(io.stdout, `Topic: ${context.topic_label ?? "unlabeled"} · ${context.work_intent ?? "unknown"} · ${context.work_phase ?? "unknown"}`);
|
|
839
|
+
}
|
|
825
840
|
writeLine(io.stdout, `Context: ${context.work_context_id}`);
|
|
826
841
|
return 0;
|
|
827
842
|
}
|
|
@@ -1081,11 +1096,12 @@ async function runAutostart(command, io) {
|
|
|
1081
1096
|
}
|
|
1082
1097
|
async function runAgentRules(command, io) {
|
|
1083
1098
|
const hosts = agentRuleHosts(command.host);
|
|
1099
|
+
const scopePath = path.resolve(command.repoRoot ?? process.cwd());
|
|
1084
1100
|
const result = command.action === "install"
|
|
1085
|
-
? await installAgentRules({ homeDir: command.homeDir, hosts })
|
|
1101
|
+
? await installAgentRules({ homeDir: command.homeDir, hosts, scopePath })
|
|
1086
1102
|
: command.action === "uninstall"
|
|
1087
1103
|
? await uninstallAgentRules({ homeDir: command.homeDir, hosts })
|
|
1088
|
-
: await inspectAgentRules({ homeDir: command.homeDir, hosts });
|
|
1104
|
+
: await inspectAgentRules({ homeDir: command.homeDir, hosts, scopePath });
|
|
1089
1105
|
if (command.json) {
|
|
1090
1106
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
1091
1107
|
return 0;
|
|
@@ -25,9 +25,9 @@ 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: interactive `cockpit onboard` offers AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install` for repair/headless setup.",
|
|
28
|
+
"Agent setup: interactive `cockpit onboard` offers AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install --workspace \"$PWD\"` 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
|
-
"Manual collector path: `install`, `login`, `start [--ticket <id>]`, `sync`, `status`, `agent-rules`.",
|
|
30
|
+
"Manual collector path: `install`, `login`, `start [--ticket <id>] [--topic <label>] [--intent <intent>] [--phase <phase>]`, `sync`, `status`, `agent-rules`.",
|
|
31
31
|
].join("\n");
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -120,7 +120,9 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
120
120
|
local_path: result.file_path,
|
|
121
121
|
codex_session_id: result.codex_session_id,
|
|
122
122
|
})),
|
|
123
|
+
codexAttributionScan: codexAttribution,
|
|
123
124
|
claudeSessionFiles,
|
|
125
|
+
claudeAttributionScan: claudeAttribution,
|
|
124
126
|
rawEvidenceBudget,
|
|
125
127
|
fetch: options.fetchImpl,
|
|
126
128
|
};
|
|
@@ -480,8 +482,20 @@ function buildAgentSessionSummary(options) {
|
|
|
480
482
|
function emptyClaudeScan() {
|
|
481
483
|
return {
|
|
482
484
|
results: [],
|
|
485
|
+
discovered_session_count: 0,
|
|
483
486
|
scanned_session_count: 0,
|
|
487
|
+
since_minutes: 0,
|
|
488
|
+
session_limit: 0,
|
|
489
|
+
session_limit_applied: false,
|
|
490
|
+
max_file_bytes: 0,
|
|
491
|
+
max_sidecar_files: 0,
|
|
492
|
+
max_line_buffer_bytes: 0,
|
|
484
493
|
project_dirs_skipped: 0,
|
|
494
|
+
project_dir_read_failed_count: 0,
|
|
495
|
+
session_stat_failed_count: 0,
|
|
496
|
+
sidecar_dir_read_failed_count: 0,
|
|
497
|
+
sidecar_stat_failed_count: 0,
|
|
498
|
+
disabled_reason: "claude_collection_disabled_by_config",
|
|
485
499
|
counts: {
|
|
486
500
|
attributed: 0,
|
|
487
501
|
ambiguous: 0,
|
package/dist/local-state.js
CHANGED
|
@@ -170,6 +170,12 @@ export async function startLocalWorkContext(options = {}) {
|
|
|
170
170
|
updated_at: now.toISOString(),
|
|
171
171
|
active_ticket_id: options.activeTicketId ?? undefined,
|
|
172
172
|
ticket_binding_candidates: ticketBindingCandidates,
|
|
173
|
+
topic_label: options.topicLabel,
|
|
174
|
+
topic_summary_redacted: options.topicSummaryRedacted,
|
|
175
|
+
work_intent: options.workIntent,
|
|
176
|
+
work_phase: options.workPhase,
|
|
177
|
+
intent_source: options.intentSource,
|
|
178
|
+
intent_confidence: options.intentConfidence,
|
|
173
179
|
pull_request_url: existingContext?.pull_request_url,
|
|
174
180
|
provenance: {
|
|
175
181
|
capture_source: "collector_runtime",
|
|
@@ -285,7 +291,7 @@ export async function readLocalWorkContextForRepo(paths, repoRoot) {
|
|
|
285
291
|
path.resolve(active.repo) === identity.repo_root) {
|
|
286
292
|
return active;
|
|
287
293
|
}
|
|
288
|
-
throw new Error(`Active work context missing for ${identity.worktree_label}. Run \`cockpit start --
|
|
294
|
+
throw new Error(`Active work context missing for ${identity.worktree_label}. Run \`cockpit start --workspace "${identity.repo_root}"\`.`);
|
|
289
295
|
}
|
|
290
296
|
async function readLocalWorkContextByFingerprint(paths, worktreeFingerprint) {
|
|
291
297
|
return LocalWorkContextSchema.parse(await readJsonFile(workContextFile(paths, worktreeFingerprint)));
|
package/dist/upload.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentImageArtifactReportRequestSchema, TelemetryIngestEnvelopeSchema, TelemetryIngestEventDtoSchema, } from "@bli-cockpit/telemetry-core";
|
|
1
|
+
import { AgentImageArtifactReportRequestSchema, EvidenceCompletenessPayloadSchema, TelemetryIngestEnvelopeSchema, TelemetryIngestEventDtoSchema, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContextForRepo, } from "./local-state.js";
|
|
4
4
|
import { runLocalSourceCollectors } from "./adapters/local-sources.js";
|
|
@@ -62,7 +62,9 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
62
62
|
rawEvidenceIncludeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
|
|
63
63
|
rawEvidenceIncludeClaudeJsonl: options.rawEvidenceIncludeClaudeJsonl,
|
|
64
64
|
rawEvidenceCodexSessionFiles: options.codexSessionFiles,
|
|
65
|
+
rawEvidenceCodexAttributionScan: options.codexAttributionScan,
|
|
65
66
|
rawEvidenceClaudeSessionFiles: options.claudeSessionFiles,
|
|
67
|
+
rawEvidenceClaudeAttributionScan: options.claudeAttributionScan,
|
|
66
68
|
rawEvidenceSkipContentHashes: skipContentHashes,
|
|
67
69
|
rawEvidenceByteBudget: options.rawEvidenceByteBudget,
|
|
68
70
|
rawEvidenceObjectBudget: options.rawEvidenceObjectBudget,
|
|
@@ -534,12 +536,29 @@ function pruneUndurablePointers(envelope, outcomes) {
|
|
|
534
536
|
.filter((outcome) => outcome.upload_state === "upload_failed" &&
|
|
535
537
|
!durablePointerIds.has(outcome.pointer.raw_evidence_pointer_id))
|
|
536
538
|
.map((outcome) => outcome.pointer.raw_evidence_pointer_id));
|
|
539
|
+
const failedOutcomes = outcomes.filter((outcome) => failedPointerIds.has(outcome.pointer.raw_evidence_pointer_id));
|
|
537
540
|
if (failedPointerIds.size === 0)
|
|
538
541
|
return envelope;
|
|
539
542
|
return {
|
|
540
543
|
...envelope,
|
|
541
544
|
events: envelope.events.map((event) => ({
|
|
542
545
|
...event,
|
|
546
|
+
metrics: {
|
|
547
|
+
...event.metrics,
|
|
548
|
+
evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
|
|
549
|
+
failedOutcomes.length,
|
|
550
|
+
},
|
|
551
|
+
attributes: event.evidence_completeness
|
|
552
|
+
? {
|
|
553
|
+
...event.attributes,
|
|
554
|
+
evidence_completeness_schema_version: event.evidence_completeness.schema_version,
|
|
555
|
+
evidence_completeness_status: "partial",
|
|
556
|
+
evidence_incomplete: true,
|
|
557
|
+
}
|
|
558
|
+
: event.attributes,
|
|
559
|
+
evidence_completeness: event.evidence_completeness
|
|
560
|
+
? markCompletenessUploadFailures(event.evidence_completeness, failedOutcomes)
|
|
561
|
+
: event.evidence_completeness,
|
|
543
562
|
raw_evidence_pointers: event.raw_evidence_pointers.filter((pointer) => !failedPointerIds.has(pointer.raw_evidence_pointer_id)),
|
|
544
563
|
redaction: {
|
|
545
564
|
...event.redaction,
|
|
@@ -548,6 +567,71 @@ function pruneUndurablePointers(envelope, outcomes) {
|
|
|
548
567
|
})),
|
|
549
568
|
};
|
|
550
569
|
}
|
|
570
|
+
function markCompletenessUploadFailures(completeness, failedOutcomes) {
|
|
571
|
+
const failureCounts = new Map();
|
|
572
|
+
for (const outcome of failedOutcomes) {
|
|
573
|
+
const source = outcome.kind ?? "raw_evidence";
|
|
574
|
+
failureCounts.set(source, (failureCounts.get(source) ?? 0) + 1);
|
|
575
|
+
}
|
|
576
|
+
const totalFailures = [...failureCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
577
|
+
const sourceCounts = [...completeness.source_counts];
|
|
578
|
+
for (const [source, count] of failureCounts) {
|
|
579
|
+
const existingIndex = sourceCounts.findIndex((entry) => entry.source === source);
|
|
580
|
+
if (existingIndex === -1) {
|
|
581
|
+
sourceCounts.push({
|
|
582
|
+
source,
|
|
583
|
+
scanned_count: 0,
|
|
584
|
+
included_count: 0,
|
|
585
|
+
skipped_count: 0,
|
|
586
|
+
truncated_count: 0,
|
|
587
|
+
deferred_count: 0,
|
|
588
|
+
reused_count: 0,
|
|
589
|
+
failed_count: count,
|
|
590
|
+
});
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const existing = sourceCounts[existingIndex];
|
|
594
|
+
if (!existing)
|
|
595
|
+
continue;
|
|
596
|
+
sourceCounts[existingIndex] = {
|
|
597
|
+
...existing,
|
|
598
|
+
failed_count: existing.failed_count + count,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const failureReasons = [...completeness.failure_reasons];
|
|
602
|
+
for (const [source, count] of failureCounts) {
|
|
603
|
+
const reason = "upload_failed";
|
|
604
|
+
const existingIndex = failureReasons.findIndex((entry) => entry.source === source && entry.reason === reason);
|
|
605
|
+
if (existingIndex === -1) {
|
|
606
|
+
failureReasons.push({ source, reason, count });
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const existing = failureReasons[existingIndex];
|
|
610
|
+
if (existing) {
|
|
611
|
+
failureReasons[existingIndex] = {
|
|
612
|
+
...existing,
|
|
613
|
+
count: existing.count + count,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return EvidenceCompletenessPayloadSchema.parse({
|
|
619
|
+
...completeness,
|
|
620
|
+
status: "partial",
|
|
621
|
+
source_counts: sourceCounts,
|
|
622
|
+
totals: {
|
|
623
|
+
...completeness.totals,
|
|
624
|
+
failed_count: completeness.totals.failed_count + totalFailures,
|
|
625
|
+
},
|
|
626
|
+
failure_reasons: failureReasons.sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
627
|
+
notes: [
|
|
628
|
+
...new Set([
|
|
629
|
+
...completeness.notes,
|
|
630
|
+
"Some collected evidence did not become durable; downstream analysis should lower confidence.",
|
|
631
|
+
]),
|
|
632
|
+
],
|
|
633
|
+
});
|
|
634
|
+
}
|
|
551
635
|
function makeUploadWorkContext(options) {
|
|
552
636
|
const provenance = makeCollectorProvenance({
|
|
553
637
|
context: options.activeContext,
|
|
@@ -575,6 +659,7 @@ function makeSourceScanCompletedEvent(options) {
|
|
|
575
659
|
throw new Error("Upload work context is missing provenance.");
|
|
576
660
|
}
|
|
577
661
|
const rawEvidencePointers = options.rawEvidenceFacts?.pointers ?? [];
|
|
662
|
+
const evidenceCompleteness = options.rawEvidenceFacts?.evidence_completeness;
|
|
578
663
|
const hasRawEvidence = rawEvidencePointers.length > 0;
|
|
579
664
|
const eventPrivacyClassification = hasRawEvidence
|
|
580
665
|
? "redacted_summary"
|
|
@@ -617,6 +702,13 @@ function makeSourceScanCompletedEvent(options) {
|
|
|
617
702
|
raw_evidence_byte_size: options.rawEvidenceFacts?.byte_size ?? 0,
|
|
618
703
|
raw_evidence_skipped_count: options.rawEvidenceFacts?.skipped_count ?? 0,
|
|
619
704
|
raw_evidence_reused_count: options.rawEvidenceFacts?.reused_count ?? 0,
|
|
705
|
+
evidence_scanned_count: evidenceCompleteness?.totals.scanned_count ?? 0,
|
|
706
|
+
evidence_included_count: evidenceCompleteness?.totals.included_count ?? 0,
|
|
707
|
+
evidence_skipped_count: evidenceCompleteness?.totals.skipped_count ?? 0,
|
|
708
|
+
evidence_truncated_count: evidenceCompleteness?.totals.truncated_count ?? 0,
|
|
709
|
+
evidence_deferred_count: evidenceCompleteness?.totals.deferred_count ?? 0,
|
|
710
|
+
evidence_reused_count: evidenceCompleteness?.totals.reused_count ?? 0,
|
|
711
|
+
evidence_failed_count: evidenceCompleteness?.totals.failed_count ?? 0,
|
|
620
712
|
},
|
|
621
713
|
attributes: {
|
|
622
714
|
repo_label: options.context.repo,
|
|
@@ -634,7 +726,31 @@ function makeSourceScanCompletedEvent(options) {
|
|
|
634
726
|
? "remote_durable_raw_evidence"
|
|
635
727
|
: "metadata_only",
|
|
636
728
|
raw_payload_included: false,
|
|
729
|
+
evidence_completeness_schema_version: evidenceCompleteness?.schema_version ?? "evidence-completeness.v1",
|
|
730
|
+
evidence_completeness_status: evidenceCompleteness?.status ?? "unknown",
|
|
731
|
+
evidence_incomplete: evidenceCompleteness
|
|
732
|
+
? evidenceCompleteness.status !== "complete"
|
|
733
|
+
: true,
|
|
734
|
+
...(options.context.topic_label
|
|
735
|
+
? { topic_label: options.context.topic_label }
|
|
736
|
+
: {}),
|
|
737
|
+
...(options.context.topic_summary_redacted
|
|
738
|
+
? { topic_summary_redacted: options.context.topic_summary_redacted }
|
|
739
|
+
: {}),
|
|
740
|
+
...(options.context.work_intent
|
|
741
|
+
? { work_intent: options.context.work_intent }
|
|
742
|
+
: {}),
|
|
743
|
+
...(options.context.work_phase
|
|
744
|
+
? { work_phase: options.context.work_phase }
|
|
745
|
+
: {}),
|
|
746
|
+
...(options.context.intent_source
|
|
747
|
+
? { intent_source: options.context.intent_source }
|
|
748
|
+
: {}),
|
|
749
|
+
...(options.context.intent_confidence !== undefined
|
|
750
|
+
? { intent_confidence: options.context.intent_confidence }
|
|
751
|
+
: {}),
|
|
637
752
|
},
|
|
753
|
+
evidence_completeness: evidenceCompleteness,
|
|
638
754
|
ticket_binding: options.ticketBinding ?? undefined,
|
|
639
755
|
risk_flags: options.riskFlags,
|
|
640
756
|
raw_evidence_pointers: rawEvidencePointers,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
29
|
+
"@bli-cockpit/telemetry-core": "0.1.7"
|
|
30
30
|
}
|
|
31
31
|
}
|