@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.
@@ -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
- "- 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.",
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 --repo \"$PWD\" --json` so Cockpit has fresh ticket/session binding metadata.",
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 { next: upsertManagedBlock(contents, block), state: "managed" };
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
- if (hasEquivalentUnmanagedTicketBinding(contents))
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 hasEquivalentUnmanagedTicketBinding(contents) {
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 findStaleUnmanagedTicketBindingBlock(contents) {
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) && !hasEquivalentUnmanagedTicketBinding(candidate)) {
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)
@@ -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
- "`--repo <path>` remains supported and is still the canonical agent-rule spelling.",
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
- "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.",
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 current = await inspectAgentRules({ homeDir: command.homeDir });
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, "Add Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md? [Y/n] "))
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> --repo \"$PWD\"`, then retry `cockpit sync`.";
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,
@@ -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 --repo "${identity.repo_root}"\`.`);
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.19",
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.5"
29
+ "@bli-cockpit/telemetry-core": "0.1.7"
30
30
  }
31
31
  }