@bli-cockpit/cli 0.1.19 → 0.1.20
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/raw-evidence.js +278 -19
- 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/local-state.js +7 -1
- package/dist/upload.js +31 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -46,19 +46,21 @@ On machines where Codex or Claude agents will do ticketed work, interactive
|
|
|
46
46
|
`cockpit onboard` checks `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` after
|
|
47
47
|
harvest proof. If equivalent Cockpit ticket-binding guidance already exists, it
|
|
48
48
|
leaves the files alone. If guidance is missing or clearly stale, it asks whether
|
|
49
|
-
to install or replace it.
|
|
49
|
+
to install or replace it. The managed guidance is scoped to the workspace path
|
|
50
|
+
used for onboarding, so agents should ignore it in private chats or unrelated
|
|
51
|
+
repos.
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
# Repair/manual path, or headless/json onboarding where Cockpit cannot prompt.
|
|
53
|
-
cockpit agent-rules install
|
|
55
|
+
cockpit agent-rules install --workspace "$PWD"
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
The direct command updates `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` with
|
|
57
59
|
the Cockpit rule to bind known Linear tickets before edits, or ask once when
|
|
58
|
-
the ticket ID is missing. That binding starts attributing
|
|
59
|
-
the specific ticket in Cockpit. It checks for managed or
|
|
60
|
-
first, so current files are left unchanged and stale Cockpit
|
|
61
|
-
sections are replaced.
|
|
60
|
+
the ticket ID is missing inside that workspace. That binding starts attributing
|
|
61
|
+
the session's work to the specific ticket in Cockpit. It checks for managed or
|
|
62
|
+
equivalent guidance first, so current files are left unchanged and stale Cockpit
|
|
63
|
+
ticket-binding sections are replaced.
|
|
62
64
|
|
|
63
65
|
Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
|
|
64
66
|
repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
|
|
@@ -113,6 +115,30 @@ cockpit sync \
|
|
|
113
115
|
No ticket is required for setup, chatting, planning, or general ambient capture.
|
|
114
116
|
Only pass `--ticket` when the work really belongs to a visible ticket.
|
|
115
117
|
|
|
118
|
+
When the work is important but ticketless, label it explicitly before syncing so
|
|
119
|
+
later analysis does not have to guess the topic:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cockpit start \
|
|
123
|
+
--workspace "$PWD" \
|
|
124
|
+
--topic "lead ingestion rewrite planning" \
|
|
125
|
+
--intent planning \
|
|
126
|
+
--phase discovery \
|
|
127
|
+
--intent-confidence 0.9
|
|
128
|
+
|
|
129
|
+
cockpit sync \
|
|
130
|
+
--workspace "$PWD" \
|
|
131
|
+
--json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Supported `--intent` values are `implementation`, `bug_fix`,
|
|
135
|
+
`root_cause_analysis`, `planning`, `discovery`, `review`, `testing`,
|
|
136
|
+
`documentation`, `release`, `learning`, `coordination`, `maintenance`,
|
|
137
|
+
`analysis`, `unknown`, and `other`. Supported `--phase` values are `planning`,
|
|
138
|
+
`discovery`, `implementation`, `debugging`, `review`, `testing`,
|
|
139
|
+
`documentation`, `release`, `handoff`, `analysis`, `unknown`, and `other`.
|
|
140
|
+
Use `--topic-summary` only for short redacted summaries, not transcript text.
|
|
141
|
+
|
|
116
142
|
## What gets saved
|
|
117
143
|
|
|
118
144
|
Local files:
|
|
@@ -180,3 +206,5 @@ This public package intentionally excludes Cockpit admin bootstrap commands,
|
|
|
180
206
|
service-role credential handling, source maps, tests, and internal runbooks.
|
|
181
207
|
Clean `npm pack` and `npm publish` run the public CLI build before packaging so
|
|
182
208
|
`dist/cli.js` is present in emergency releases.
|
|
209
|
+
When collector changes depend on new telemetry-core exports, publish
|
|
210
|
+
`@bli-cockpit/telemetry-core` first, then publish `@bli-cockpit/cli`.
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { SECRET_FILE_SEGMENT_PATTERN, SourceScanResultSchema, containsSecretLikeContent, } from "@bli-cockpit/telemetry-core";
|
|
2
|
-
import {
|
|
1
|
+
import { EvidenceCompletenessPayloadSchema, SECRET_FILE_SEGMENT_PATTERN, SourceScanResultSchema, containsSecretLikeContent, } from "@bli-cockpit/telemetry-core";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
7
|
import { makeSourceAdapterIdentity, } from "./common.js";
|
|
9
8
|
import { collectAgentImageEvidenceFromJsonlFile, } from "./agent-image-evidence.js";
|
|
10
|
-
const execFileAsync = promisify(execFile);
|
|
11
9
|
const DEFAULT_SINCE_MINUTES = 24 * 60;
|
|
12
10
|
const DEFAULT_SESSION_LIMIT = 50;
|
|
13
11
|
const MAX_GIT_DIFF_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const GIT_DIFF_TIMEOUT_MS = 3_000;
|
|
14
13
|
export const RAW_EVIDENCE_BUCKET = "ambient-raw-evidence";
|
|
15
14
|
export const RAW_EVIDENCE_RETENTION_MODE = "remote_durable";
|
|
16
15
|
// Per-sync upload budgets enforced at COLLECTION time (D7b). With sidecars a
|
|
@@ -28,18 +27,65 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
28
27
|
const filesDir = path.join(evidenceDir, "files");
|
|
29
28
|
const entries = [];
|
|
30
29
|
const skipped = [];
|
|
30
|
+
const truncated = [];
|
|
31
31
|
const reused = [];
|
|
32
|
+
const sinceMinutes = options.sinceMinutes ?? DEFAULT_SINCE_MINUTES;
|
|
33
|
+
const sessionLimit = options.sessionLimit ?? DEFAULT_SESSION_LIMIT;
|
|
34
|
+
const byteBudget = options.byteBudget ?? RAW_EVIDENCE_DEFAULT_BYTE_BUDGET;
|
|
35
|
+
const objectBudget = options.objectBudget ?? RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET;
|
|
32
36
|
const collection = {
|
|
33
37
|
context,
|
|
34
38
|
filesDir,
|
|
35
39
|
packId,
|
|
36
40
|
entries,
|
|
37
41
|
skipped,
|
|
42
|
+
truncated,
|
|
38
43
|
reused,
|
|
44
|
+
scanned: new Map(),
|
|
45
|
+
caps: [
|
|
46
|
+
{
|
|
47
|
+
source: "raw_evidence",
|
|
48
|
+
cap_type: "byte_budget",
|
|
49
|
+
limit: byteBudget,
|
|
50
|
+
observed: options.budget?.remainingBytes ?? byteBudget,
|
|
51
|
+
applied: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
source: "raw_evidence",
|
|
55
|
+
cap_type: "object_budget",
|
|
56
|
+
limit: objectBudget,
|
|
57
|
+
observed: options.budget?.remainingObjects ?? objectBudget,
|
|
58
|
+
applied: false,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
source: "git_diff",
|
|
62
|
+
cap_type: "max_bytes_per_diff",
|
|
63
|
+
limit: MAX_GIT_DIFF_BYTES,
|
|
64
|
+
applied: false,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
source: "git_diff",
|
|
68
|
+
cap_type: "timeout_ms",
|
|
69
|
+
limit: GIT_DIFF_TIMEOUT_MS,
|
|
70
|
+
applied: false,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
source: "claude_jsonl",
|
|
74
|
+
cap_type: "max_file_bytes",
|
|
75
|
+
limit: CLAUDE_MAX_COLLECT_FILE_BYTES,
|
|
76
|
+
applied: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
source: "claude_jsonl_sidecar",
|
|
80
|
+
cap_type: "max_file_bytes",
|
|
81
|
+
limit: CLAUDE_MAX_COLLECT_FILE_BYTES,
|
|
82
|
+
applied: false,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
39
85
|
skipContentHashes: options.skipContentHashes ?? new Set(),
|
|
40
86
|
budget: options.budget ?? {
|
|
41
|
-
remainingBytes:
|
|
42
|
-
remainingObjects:
|
|
87
|
+
remainingBytes: byteBudget,
|
|
88
|
+
remainingObjects: objectBudget,
|
|
43
89
|
},
|
|
44
90
|
index: { value: 0 },
|
|
45
91
|
};
|
|
@@ -50,8 +96,8 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
50
96
|
await collectCodexJsonlFiles(collection, {
|
|
51
97
|
codexSessionFiles: options.codexSessionFiles,
|
|
52
98
|
sessionsDir: options.sessionsDir,
|
|
53
|
-
sinceMinutes
|
|
54
|
-
limit:
|
|
99
|
+
sinceMinutes,
|
|
100
|
+
limit: sessionLimit,
|
|
55
101
|
});
|
|
56
102
|
}
|
|
57
103
|
if (options.includeClaudeJsonl !== false && options.claudeSessionFiles) {
|
|
@@ -61,6 +107,11 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
61
107
|
const deferredByteBudgetCount = skipped.filter((entry) => entry.reason === "deferred_byte_budget").length;
|
|
62
108
|
const deferredObjectBudgetCount = skipped.filter((entry) => entry.reason === "deferred_object_budget").length;
|
|
63
109
|
if (entries.length === 0) {
|
|
110
|
+
const evidenceCompleteness = makeEvidenceCompleteness(collection, {
|
|
111
|
+
startedAt,
|
|
112
|
+
finishedAt: context.now.toISOString(),
|
|
113
|
+
sinceMinutes,
|
|
114
|
+
});
|
|
64
115
|
const facts = {
|
|
65
116
|
pack_id: packId,
|
|
66
117
|
manifest_path: path.join(evidenceDir, "manifest.json"),
|
|
@@ -73,6 +124,7 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
73
124
|
deferred_byte_budget_count: deferredByteBudgetCount,
|
|
74
125
|
deferred_object_budget_count: deferredObjectBudgetCount,
|
|
75
126
|
content_kinds: [],
|
|
127
|
+
evidence_completeness: evidenceCompleteness,
|
|
76
128
|
pointers: [],
|
|
77
129
|
upload_files: [],
|
|
78
130
|
reused,
|
|
@@ -109,6 +161,11 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
109
161
|
bytes: manifestBytes,
|
|
110
162
|
});
|
|
111
163
|
entries.push(manifestEntry);
|
|
164
|
+
const evidenceCompleteness = makeEvidenceCompleteness(collection, {
|
|
165
|
+
startedAt,
|
|
166
|
+
finishedAt: context.now.toISOString(),
|
|
167
|
+
sinceMinutes,
|
|
168
|
+
});
|
|
112
169
|
const facts = {
|
|
113
170
|
pack_id: packId,
|
|
114
171
|
manifest_path: manifestPath,
|
|
@@ -121,6 +178,7 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
121
178
|
deferred_byte_budget_count: deferredByteBudgetCount,
|
|
122
179
|
deferred_object_budget_count: deferredObjectBudgetCount,
|
|
123
180
|
content_kinds: [...new Set(entries.map((entry) => entry.kind))],
|
|
181
|
+
evidence_completeness: evidenceCompleteness,
|
|
124
182
|
pointers: entries.map(pointerFromEntry),
|
|
125
183
|
upload_files: entries.map((entry) => ({
|
|
126
184
|
pointer: pointerFromEntry(entry),
|
|
@@ -185,20 +243,33 @@ function makeRawEvidenceScan(options) {
|
|
|
185
243
|
`bytes:${options.facts.byte_size}`,
|
|
186
244
|
`skipped:${options.facts.skipped_count}`,
|
|
187
245
|
`reused:${options.facts.reused_count}`,
|
|
246
|
+
`completeness:${options.facts.evidence_completeness.status}`,
|
|
247
|
+
`truncated:${options.facts.evidence_completeness.totals.truncated_count}`,
|
|
248
|
+
`deferred:${options.facts.evidence_completeness.totals.deferred_count}`,
|
|
188
249
|
...options.facts.content_kinds.map((kind) => `kind:${kind}`),
|
|
189
250
|
],
|
|
190
251
|
});
|
|
191
252
|
}
|
|
192
253
|
async function collectCodexJsonlFiles(collection, options) {
|
|
193
|
-
const
|
|
254
|
+
const resolvedCandidates = options.codexSessionFiles
|
|
194
255
|
? options.codexSessionFiles.map((file) => ({
|
|
195
256
|
filePath: file.local_path,
|
|
196
257
|
codexSessionId: file.codex_session_id,
|
|
197
258
|
}))
|
|
198
259
|
: (await walkJsonlFiles(options.sessionsDir ?? path.join(os.homedir(), ".codex", "sessions"), collection.context.now.getTime() - options.sinceMinutes * 60 * 1000))
|
|
199
|
-
.slice(0, options.limit)
|
|
200
260
|
.map((filePath) => ({ filePath, codexSessionId: null }));
|
|
261
|
+
collection.caps.push({
|
|
262
|
+
source: "codex_jsonl",
|
|
263
|
+
cap_type: "session_limit",
|
|
264
|
+
limit: options.limit,
|
|
265
|
+
observed: resolvedCandidates.length,
|
|
266
|
+
applied: !options.codexSessionFiles && resolvedCandidates.length > options.limit,
|
|
267
|
+
});
|
|
268
|
+
const candidates = options.codexSessionFiles
|
|
269
|
+
? resolvedCandidates
|
|
270
|
+
: resolvedCandidates.slice(0, options.limit);
|
|
201
271
|
for (const candidate of candidates) {
|
|
272
|
+
recordScanned(collection, "codex_jsonl");
|
|
202
273
|
const codexSessionId = candidate.codexSessionId ?? shortHash(candidate.filePath);
|
|
203
274
|
const transcriptAccepted = await collectOneEvidenceFile(collection, {
|
|
204
275
|
filePath: candidate.filePath,
|
|
@@ -220,6 +291,7 @@ async function collectCodexJsonlFiles(collection, options) {
|
|
|
220
291
|
}
|
|
221
292
|
}
|
|
222
293
|
async function collectClaudeJsonlFiles(collection, sessions) {
|
|
294
|
+
recordScanned(collection, "claude_jsonl", sessions.reduce((count, session) => count + 1 + session.sidecar_files.length, 0));
|
|
223
295
|
for (const session of sessions) {
|
|
224
296
|
const sessionId = session.claude_session_id;
|
|
225
297
|
if (session.main_file_oversized) {
|
|
@@ -300,6 +372,7 @@ async function collectAgentImagesFromTranscript(collection, options) {
|
|
|
300
372
|
sessionId: options.sessionId,
|
|
301
373
|
sidecarId: options.sidecarId,
|
|
302
374
|
});
|
|
375
|
+
recordScanned(collection, options.kind, result.images.length + result.skipped.length);
|
|
303
376
|
for (const skipped of result.skipped) {
|
|
304
377
|
collection.skipped.push({
|
|
305
378
|
kind: options.kind,
|
|
@@ -336,6 +409,7 @@ async function collectOneAgentImageFile(collection, options) {
|
|
|
336
409
|
}
|
|
337
410
|
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
338
411
|
if (deferReason) {
|
|
412
|
+
markBudgetCapApplied(collection, deferReason);
|
|
339
413
|
collection.skipped.push({
|
|
340
414
|
kind: options.kind,
|
|
341
415
|
label: options.image.label,
|
|
@@ -392,6 +466,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
392
466
|
return false;
|
|
393
467
|
}
|
|
394
468
|
if (options.maxFileBytes && raw.byteLength > options.maxFileBytes) {
|
|
469
|
+
markCapApplied(collection, options.kind, "max_file_bytes");
|
|
395
470
|
collection.skipped.push({
|
|
396
471
|
kind: options.kind,
|
|
397
472
|
label: fileName,
|
|
@@ -419,6 +494,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
419
494
|
}
|
|
420
495
|
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
421
496
|
if (deferReason) {
|
|
497
|
+
markBudgetCapApplied(collection, deferReason);
|
|
422
498
|
collection.skipped.push({
|
|
423
499
|
kind: options.kind,
|
|
424
500
|
label: fileName,
|
|
@@ -465,10 +541,23 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
465
541
|
{ label: "staged", args: ["diff", "--cached", "--no-ext-diff", "--"] },
|
|
466
542
|
];
|
|
467
543
|
for (const target of diffTargets) {
|
|
544
|
+
recordScanned(collection, "git_diff");
|
|
468
545
|
const diff = await runGitDiff(target.args, repoRoot);
|
|
469
|
-
if (
|
|
546
|
+
if (diff.truncated) {
|
|
547
|
+
markCapApplied(collection, "git_diff", diff.truncationCapType);
|
|
548
|
+
collection.truncated.push({
|
|
549
|
+
kind: "git_diff",
|
|
550
|
+
reason: diff.truncationReason,
|
|
551
|
+
...(diff.truncationCapType === "max_bytes_per_diff"
|
|
552
|
+
? { max_bytes: MAX_GIT_DIFF_BYTES }
|
|
553
|
+
: {}),
|
|
554
|
+
observed_bytes: diff.observedBytes,
|
|
555
|
+
included_bytes: Buffer.byteLength(diff.stdout, "utf8"),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
if (!diff.stdout.trim())
|
|
470
559
|
continue;
|
|
471
|
-
if (containsSecretLikeContent(diff)) {
|
|
560
|
+
if (containsSecretLikeContent(diff.stdout)) {
|
|
472
561
|
collection.skipped.push({
|
|
473
562
|
kind: "git_diff",
|
|
474
563
|
label: target.label,
|
|
@@ -476,7 +565,7 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
476
565
|
});
|
|
477
566
|
continue;
|
|
478
567
|
}
|
|
479
|
-
const raw = Buffer.from(diff.
|
|
568
|
+
const raw = Buffer.from(diff.stdout, "utf8");
|
|
480
569
|
const contentHash = sha256(raw);
|
|
481
570
|
if (collection.skipContentHashes.has(contentHash)) {
|
|
482
571
|
collection.reused.push({
|
|
@@ -489,6 +578,7 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
489
578
|
}
|
|
490
579
|
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
491
580
|
if (deferReason) {
|
|
581
|
+
markBudgetCapApplied(collection, deferReason);
|
|
492
582
|
collection.skipped.push({
|
|
493
583
|
kind: "git_diff",
|
|
494
584
|
label: target.label,
|
|
@@ -507,7 +597,9 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
507
597
|
localPath: destination,
|
|
508
598
|
relativePath,
|
|
509
599
|
mediaType: "text/x-diff",
|
|
510
|
-
redactedSummary:
|
|
600
|
+
redactedSummary: diff.truncated
|
|
601
|
+
? `Raw git ${target.label} diff truncated to the capture cap and preserved locally with env/secret paths excluded.`
|
|
602
|
+
: `Raw git ${target.label} diff preserved locally with env/secret paths excluded.`,
|
|
511
603
|
bytes: raw,
|
|
512
604
|
contentAddress: `git-diff/${target.label}-${contentHash.slice(0, 16)}.diff`,
|
|
513
605
|
}));
|
|
@@ -526,12 +618,179 @@ async function runGitDiff(args, repoRoot) {
|
|
|
526
618
|
":(exclude)**/*.pem",
|
|
527
619
|
":(exclude)**/*.key",
|
|
528
620
|
];
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
621
|
+
return new Promise((resolve, reject) => {
|
|
622
|
+
const child = spawn("git", [...args, ...pathspec], {
|
|
623
|
+
cwd: repoRoot,
|
|
624
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
625
|
+
});
|
|
626
|
+
const stdoutChunks = [];
|
|
627
|
+
const stderrChunks = [];
|
|
628
|
+
let observedBytes = 0;
|
|
629
|
+
let includedBytes = 0;
|
|
630
|
+
let truncated = false;
|
|
631
|
+
let timedOut = false;
|
|
632
|
+
const timeout = setTimeout(() => {
|
|
633
|
+
timedOut = true;
|
|
634
|
+
truncated = true;
|
|
635
|
+
child.kill("SIGTERM");
|
|
636
|
+
}, GIT_DIFF_TIMEOUT_MS);
|
|
637
|
+
child.stdout.on("data", (chunk) => {
|
|
638
|
+
observedBytes += chunk.byteLength;
|
|
639
|
+
if (includedBytes < MAX_GIT_DIFF_BYTES) {
|
|
640
|
+
const remaining = MAX_GIT_DIFF_BYTES - includedBytes;
|
|
641
|
+
const next = chunk.subarray(0, remaining);
|
|
642
|
+
stdoutChunks.push(next);
|
|
643
|
+
includedBytes += next.byteLength;
|
|
644
|
+
}
|
|
645
|
+
if (observedBytes > MAX_GIT_DIFF_BYTES) {
|
|
646
|
+
truncated = true;
|
|
647
|
+
child.kill("SIGTERM");
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
child.stderr.on("data", (chunk) => {
|
|
651
|
+
if (stderrChunks.reduce((sum, item) => sum + item.byteLength, 0) < 4096) {
|
|
652
|
+
stderrChunks.push(chunk.subarray(0, 4096));
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
child.on("error", (error) => {
|
|
656
|
+
clearTimeout(timeout);
|
|
657
|
+
reject(error);
|
|
658
|
+
});
|
|
659
|
+
child.on("close", (code, signal) => {
|
|
660
|
+
clearTimeout(timeout);
|
|
661
|
+
if (code === 0 || truncated || signal === "SIGTERM") {
|
|
662
|
+
resolve({
|
|
663
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
664
|
+
truncated,
|
|
665
|
+
observedBytes,
|
|
666
|
+
truncationReason: timedOut
|
|
667
|
+
? "git_diff_timeout"
|
|
668
|
+
: "max_git_diff_bytes",
|
|
669
|
+
truncationCapType: timedOut ? "timeout_ms" : "max_bytes_per_diff",
|
|
670
|
+
});
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
674
|
+
reject(new Error(stderr || `git diff failed with code ${code ?? signal}`));
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function recordScanned(collection, source, count = 1) {
|
|
679
|
+
collection.scanned.set(source, (collection.scanned.get(source) ?? 0) + count);
|
|
680
|
+
}
|
|
681
|
+
function markBudgetCapApplied(collection, reason) {
|
|
682
|
+
markCapApplied(collection, "raw_evidence", reason === "deferred_object_budget" ? "object_budget" : "byte_budget");
|
|
683
|
+
}
|
|
684
|
+
function markCapApplied(collection, source, capType) {
|
|
685
|
+
const cap = collection.caps.find((candidate) => candidate.source === source && candidate.cap_type === capType);
|
|
686
|
+
if (cap)
|
|
687
|
+
cap.applied = true;
|
|
688
|
+
}
|
|
689
|
+
function makeEvidenceCompleteness(collection, options) {
|
|
690
|
+
const sources = new Set(collection.scanned.keys());
|
|
691
|
+
for (const entry of collection.entries)
|
|
692
|
+
sources.add(entry.kind);
|
|
693
|
+
for (const entry of collection.skipped)
|
|
694
|
+
sources.add(entry.kind);
|
|
695
|
+
for (const entry of collection.reused)
|
|
696
|
+
sources.add(entry.kind);
|
|
697
|
+
for (const entry of collection.truncated)
|
|
698
|
+
sources.add(entry.kind);
|
|
699
|
+
const sourceCounts = [...sources].sort().map((source) => {
|
|
700
|
+
const skipped = collection.skipped.filter((entry) => entry.kind === source);
|
|
701
|
+
return {
|
|
702
|
+
source,
|
|
703
|
+
scanned_count: collection.scanned.get(source) ?? 0,
|
|
704
|
+
included_count: collection.entries.filter((entry) => entry.kind === source).length,
|
|
705
|
+
skipped_count: skipped.length,
|
|
706
|
+
truncated_count: collection.truncated.filter((entry) => entry.kind === source).length,
|
|
707
|
+
deferred_count: skipped.filter((entry) => entry.reason.startsWith("deferred_")).length,
|
|
708
|
+
reused_count: collection.reused.filter((entry) => entry.kind === source).length,
|
|
709
|
+
};
|
|
710
|
+
});
|
|
711
|
+
const totals = sourceCounts.reduce((sum, count) => ({
|
|
712
|
+
scanned_count: sum.scanned_count + count.scanned_count,
|
|
713
|
+
included_count: sum.included_count + count.included_count,
|
|
714
|
+
skipped_count: sum.skipped_count + count.skipped_count,
|
|
715
|
+
truncated_count: sum.truncated_count + count.truncated_count,
|
|
716
|
+
deferred_count: sum.deferred_count + count.deferred_count,
|
|
717
|
+
reused_count: sum.reused_count + count.reused_count,
|
|
718
|
+
}), {
|
|
719
|
+
scanned_count: 0,
|
|
720
|
+
included_count: 0,
|
|
721
|
+
skipped_count: 0,
|
|
722
|
+
truncated_count: 0,
|
|
723
|
+
deferred_count: 0,
|
|
724
|
+
reused_count: 0,
|
|
725
|
+
});
|
|
726
|
+
const skipReasonCounts = new Map();
|
|
727
|
+
for (const skipped of collection.skipped) {
|
|
728
|
+
const key = `${skipped.kind}:${skipped.reason}`;
|
|
729
|
+
const existing = skipReasonCounts.get(key);
|
|
730
|
+
if (existing) {
|
|
731
|
+
existing.count += 1;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
skipReasonCounts.set(key, {
|
|
735
|
+
source: skipped.kind,
|
|
736
|
+
reason: skipped.reason,
|
|
737
|
+
count: 1,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const truncationCounts = new Map();
|
|
742
|
+
for (const truncated of collection.truncated) {
|
|
743
|
+
const key = `${truncated.kind}:${truncated.reason}`;
|
|
744
|
+
const existing = truncationCounts.get(key);
|
|
745
|
+
if (existing) {
|
|
746
|
+
existing.count += 1;
|
|
747
|
+
existing.observed_bytes = Math.max(existing.observed_bytes ?? 0, truncated.observed_bytes ?? 0);
|
|
748
|
+
existing.included_bytes = Math.max(existing.included_bytes ?? 0, truncated.included_bytes ?? 0);
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
truncationCounts.set(key, {
|
|
752
|
+
source: truncated.kind,
|
|
753
|
+
reason: truncated.reason,
|
|
754
|
+
count: 1,
|
|
755
|
+
...(truncated.max_bytes !== undefined
|
|
756
|
+
? { max_bytes: truncated.max_bytes }
|
|
757
|
+
: {}),
|
|
758
|
+
...(truncated.observed_bytes !== undefined
|
|
759
|
+
? { observed_bytes: truncated.observed_bytes }
|
|
760
|
+
: {}),
|
|
761
|
+
...(truncated.included_bytes !== undefined
|
|
762
|
+
? { included_bytes: truncated.included_bytes }
|
|
763
|
+
: {}),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const hasGaps = totals.skipped_count > 0 ||
|
|
768
|
+
totals.truncated_count > 0 ||
|
|
769
|
+
totals.deferred_count > 0 ||
|
|
770
|
+
collection.caps.some((cap) => cap.applied);
|
|
771
|
+
const status = totals.included_count + totals.reused_count === 0 && !hasGaps
|
|
772
|
+
? "empty"
|
|
773
|
+
: hasGaps
|
|
774
|
+
? "partial"
|
|
775
|
+
: "complete";
|
|
776
|
+
return EvidenceCompletenessPayloadSchema.parse({
|
|
777
|
+
schema_version: "evidence-completeness.v1",
|
|
778
|
+
status,
|
|
779
|
+
generated_at: options.finishedAt,
|
|
780
|
+
scan_window: {
|
|
781
|
+
started_at: options.startedAt,
|
|
782
|
+
finished_at: options.finishedAt,
|
|
783
|
+
since_minutes: options.sinceMinutes,
|
|
784
|
+
},
|
|
785
|
+
source_counts: sourceCounts,
|
|
786
|
+
totals,
|
|
787
|
+
caps: collection.caps,
|
|
788
|
+
skip_reasons: [...skipReasonCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
789
|
+
truncation_markers: [...truncationCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
790
|
+
notes: hasGaps
|
|
791
|
+
? ["Evidence is incomplete; downstream analysis should lower confidence."]
|
|
792
|
+
: [],
|
|
533
793
|
});
|
|
534
|
-
return stdout;
|
|
535
794
|
}
|
|
536
795
|
async function walkJsonlFiles(dir, cutoffMs) {
|
|
537
796
|
const out = [];
|
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
|
|
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
|
@@ -575,6 +575,7 @@ function makeSourceScanCompletedEvent(options) {
|
|
|
575
575
|
throw new Error("Upload work context is missing provenance.");
|
|
576
576
|
}
|
|
577
577
|
const rawEvidencePointers = options.rawEvidenceFacts?.pointers ?? [];
|
|
578
|
+
const evidenceCompleteness = options.rawEvidenceFacts?.evidence_completeness;
|
|
578
579
|
const hasRawEvidence = rawEvidencePointers.length > 0;
|
|
579
580
|
const eventPrivacyClassification = hasRawEvidence
|
|
580
581
|
? "redacted_summary"
|
|
@@ -617,6 +618,12 @@ function makeSourceScanCompletedEvent(options) {
|
|
|
617
618
|
raw_evidence_byte_size: options.rawEvidenceFacts?.byte_size ?? 0,
|
|
618
619
|
raw_evidence_skipped_count: options.rawEvidenceFacts?.skipped_count ?? 0,
|
|
619
620
|
raw_evidence_reused_count: options.rawEvidenceFacts?.reused_count ?? 0,
|
|
621
|
+
evidence_scanned_count: evidenceCompleteness?.totals.scanned_count ?? 0,
|
|
622
|
+
evidence_included_count: evidenceCompleteness?.totals.included_count ?? 0,
|
|
623
|
+
evidence_skipped_count: evidenceCompleteness?.totals.skipped_count ?? 0,
|
|
624
|
+
evidence_truncated_count: evidenceCompleteness?.totals.truncated_count ?? 0,
|
|
625
|
+
evidence_deferred_count: evidenceCompleteness?.totals.deferred_count ?? 0,
|
|
626
|
+
evidence_reused_count: evidenceCompleteness?.totals.reused_count ?? 0,
|
|
620
627
|
},
|
|
621
628
|
attributes: {
|
|
622
629
|
repo_label: options.context.repo,
|
|
@@ -634,7 +641,31 @@ function makeSourceScanCompletedEvent(options) {
|
|
|
634
641
|
? "remote_durable_raw_evidence"
|
|
635
642
|
: "metadata_only",
|
|
636
643
|
raw_payload_included: false,
|
|
644
|
+
evidence_completeness_schema_version: evidenceCompleteness?.schema_version ?? "evidence-completeness.v1",
|
|
645
|
+
evidence_completeness_status: evidenceCompleteness?.status ?? "unknown",
|
|
646
|
+
evidence_incomplete: evidenceCompleteness
|
|
647
|
+
? evidenceCompleteness.status !== "complete"
|
|
648
|
+
: true,
|
|
649
|
+
...(options.context.topic_label
|
|
650
|
+
? { topic_label: options.context.topic_label }
|
|
651
|
+
: {}),
|
|
652
|
+
...(options.context.topic_summary_redacted
|
|
653
|
+
? { topic_summary_redacted: options.context.topic_summary_redacted }
|
|
654
|
+
: {}),
|
|
655
|
+
...(options.context.work_intent
|
|
656
|
+
? { work_intent: options.context.work_intent }
|
|
657
|
+
: {}),
|
|
658
|
+
...(options.context.work_phase
|
|
659
|
+
? { work_phase: options.context.work_phase }
|
|
660
|
+
: {}),
|
|
661
|
+
...(options.context.intent_source
|
|
662
|
+
? { intent_source: options.context.intent_source }
|
|
663
|
+
: {}),
|
|
664
|
+
...(options.context.intent_confidence !== undefined
|
|
665
|
+
? { intent_confidence: options.context.intent_confidence }
|
|
666
|
+
: {}),
|
|
637
667
|
},
|
|
668
|
+
evidence_completeness: evidenceCompleteness,
|
|
638
669
|
ticket_binding: options.ticketBinding ?? undefined,
|
|
639
670
|
risk_flags: options.riskFlags,
|
|
640
671
|
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.20",
|
|
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.6"
|
|
30
30
|
}
|
|
31
31
|
}
|