@bli-cockpit/cli 0.1.28 → 0.1.29
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 +3 -0
- package/dist/commands/local-args.js +8 -1
- package/dist/commands/local.js +5 -3
- package/dist/commands/session-sync.js +18 -0
- package/dist/local-state.js +29 -10
- package/dist/repo-identity.js +15 -10
- package/dist/upload.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -137,6 +137,9 @@ cockpit sync \
|
|
|
137
137
|
|
|
138
138
|
No ticket is required for setup, chatting, planning, or general ambient capture.
|
|
139
139
|
Only pass `--ticket` when the work really belongs to a visible ticket.
|
|
140
|
+
Omitting `--ticket` preserves any existing ticket binding; run
|
|
141
|
+
`cockpit start --clear-ticket --workspace "$PWD"` when you intentionally want
|
|
142
|
+
to return a previously bound work context to general ambient capture.
|
|
140
143
|
|
|
141
144
|
When the work is important but ticketless, label it explicitly before syncing so
|
|
142
145
|
later analysis does not have to guess the topic:
|
|
@@ -206,6 +206,7 @@ function parseStartArgs(args) {
|
|
|
206
206
|
"--workspace",
|
|
207
207
|
"--branch",
|
|
208
208
|
"--ticket",
|
|
209
|
+
"--clear-ticket",
|
|
209
210
|
"--topic",
|
|
210
211
|
"--topic-summary",
|
|
211
212
|
"--intent",
|
|
@@ -237,6 +238,11 @@ function parseStartArgs(args) {
|
|
|
237
238
|
],
|
|
238
239
|
});
|
|
239
240
|
assertNoPositionals(values.positionals, "start");
|
|
241
|
+
const activeTicketId = optionalNonEmpty(values.flags.get("--ticket"));
|
|
242
|
+
const clearTicket = values.booleans.has("--clear-ticket");
|
|
243
|
+
if (activeTicketId && clearTicket) {
|
|
244
|
+
throw new Error("--ticket and --clear-ticket cannot be combined.");
|
|
245
|
+
}
|
|
240
246
|
const topicLabel = optionalNonEmpty(values.flags.get("--topic"));
|
|
241
247
|
const topicSummaryRedacted = optionalNonEmpty(values.flags.get("--topic-summary"));
|
|
242
248
|
const workIntent = optionalSchemaValue(WorkIntentSchema, values.flags.get("--intent"), "--intent");
|
|
@@ -252,7 +258,8 @@ function parseStartArgs(args) {
|
|
|
252
258
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
253
259
|
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
254
260
|
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
255
|
-
activeTicketId
|
|
261
|
+
activeTicketId,
|
|
262
|
+
clearTicket,
|
|
256
263
|
topicLabel,
|
|
257
264
|
topicSummaryRedacted,
|
|
258
265
|
workIntent,
|
package/dist/commands/local.js
CHANGED
|
@@ -91,7 +91,7 @@ export function localCommandHelp(command) {
|
|
|
91
91
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
92
92
|
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
93
93
|
" cockpit logout",
|
|
94
|
-
" cockpit start [--ticket <id
|
|
94
|
+
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
95
95
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
96
96
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
97
97
|
" cockpit sessions [--source codex|claude] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -169,10 +169,11 @@ function localSubcommandHelp(command) {
|
|
|
169
169
|
[
|
|
170
170
|
"start",
|
|
171
171
|
[
|
|
172
|
-
"Usage: cockpit start [--ticket <id
|
|
172
|
+
"Usage: cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--topic-summary <summary>] [--intent <intent>] [--phase <phase>] [--intent-confidence <0..1>] [--workspace <path>] [--branch <name>] [--json]",
|
|
173
173
|
"",
|
|
174
174
|
"Starts local ambient capture. Parent folders start each child git worktree.",
|
|
175
|
-
"Add --ticket only when the work already has a visible ticket.",
|
|
175
|
+
"Add --ticket only when the work already has a visible ticket; omit it to preserve an existing binding.",
|
|
176
|
+
"Use --clear-ticket to intentionally return the context to general ambient capture.",
|
|
176
177
|
"Use --topic/--intent/--phase for planning, discovery, and learning work that has no ticket yet.",
|
|
177
178
|
"Supported intents: implementation, bug_fix, root_cause_analysis, planning, discovery, review, testing, documentation, release, learning, coordination, maintenance, analysis, unknown, other.",
|
|
178
179
|
"Supported phases: planning, discovery, implementation, debugging, review, testing, documentation, release, handoff, analysis, unknown, other.",
|
|
@@ -1186,6 +1187,7 @@ async function runStart(command, io) {
|
|
|
1186
1187
|
repoRoot: worktree.repo_root,
|
|
1187
1188
|
branch: command.branch,
|
|
1188
1189
|
activeTicketId: command.activeTicketId,
|
|
1190
|
+
clearTicket: command.clearTicket,
|
|
1189
1191
|
topicLabel: command.topicLabel,
|
|
1190
1192
|
topicSummaryRedacted: command.topicSummaryRedacted,
|
|
1191
1193
|
workIntent: command.workIntent,
|
|
@@ -114,6 +114,7 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
114
114
|
homeDir: options.homeDir,
|
|
115
115
|
repoRoot: worktree.repo_root,
|
|
116
116
|
dashboardUrl: options.dashboardUrl,
|
|
117
|
+
worktreeInventory: worktreeInventoryForRepo(worktree, options.worktrees),
|
|
117
118
|
codexSessionFiles: codexAttribution.results
|
|
118
119
|
.filter((result) => result.state === "attributed" &&
|
|
119
120
|
result.worktree?.worktree_fingerprint ===
|
|
@@ -510,6 +511,23 @@ function emptyClaudeScan() {
|
|
|
510
511
|
},
|
|
511
512
|
};
|
|
512
513
|
}
|
|
514
|
+
function worktreeInventoryForRepo(current, worktrees) {
|
|
515
|
+
return worktrees
|
|
516
|
+
.filter((worktree) => worktree.repo_fingerprint
|
|
517
|
+
? worktree.repo_fingerprint === current.repo_fingerprint
|
|
518
|
+
: worktree.repo_label === current.repo_label)
|
|
519
|
+
.map((worktree) => ({
|
|
520
|
+
repo: worktree.repo_root,
|
|
521
|
+
repo_label: worktree.repo_label,
|
|
522
|
+
repo_fingerprint: worktree.repo_fingerprint,
|
|
523
|
+
repo_origin_url: worktree.repo_origin_url ?? undefined,
|
|
524
|
+
head_sha: worktree.head_sha ?? undefined,
|
|
525
|
+
worktree_label: worktree.worktree_label,
|
|
526
|
+
worktree_fingerprint: worktree.worktree_fingerprint,
|
|
527
|
+
worktree_is_primary: worktree.worktree_is_primary,
|
|
528
|
+
branch: worktree.branch,
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
513
531
|
async function isClaudeCollectionEnabled(paths) {
|
|
514
532
|
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
515
533
|
return config?.collect_claude_jsonl !== false;
|
package/dist/local-state.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { resolveRepoWorktreeIdentity, } from "./repo-identity.js";
|
|
7
|
+
import { resolveRepoWorktreeIdentity, stableWorktreeFingerprint, stableWorktreeRoot, } from "./repo-identity.js";
|
|
8
8
|
import { normalizeCollectionRoots } from "./root-normalization.js";
|
|
9
9
|
import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
|
|
10
10
|
const localCollectorPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -138,6 +138,9 @@ export async function logoutLocalCollector(options = {}) {
|
|
|
138
138
|
return { removed, session_file: paths.session_file };
|
|
139
139
|
}
|
|
140
140
|
export async function startLocalWorkContext(options = {}) {
|
|
141
|
+
if (options.activeTicketId && options.clearTicket) {
|
|
142
|
+
throw new Error("--ticket and --clear-ticket cannot be combined.");
|
|
143
|
+
}
|
|
141
144
|
const now = options.now ?? new Date();
|
|
142
145
|
const homeDir = options.homeDir ?? os.homedir();
|
|
143
146
|
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
@@ -161,6 +164,9 @@ export async function startLocalWorkContext(options = {}) {
|
|
|
161
164
|
worktreeFingerprint: identity.worktree_fingerprint,
|
|
162
165
|
});
|
|
163
166
|
const existingContext = await readLocalWorkContextByFingerprint(paths, identity.worktree_fingerprint).catch(() => null);
|
|
167
|
+
const activeTicketId = options.clearTicket
|
|
168
|
+
? undefined
|
|
169
|
+
: (options.activeTicketId ?? existingContext?.active_ticket_id);
|
|
164
170
|
const ticketBindingCandidates = options.activeTicketId
|
|
165
171
|
? [
|
|
166
172
|
{
|
|
@@ -170,7 +176,9 @@ export async function startLocalWorkContext(options = {}) {
|
|
|
170
176
|
evidence_labels: ["cockpit_start_ticket"],
|
|
171
177
|
},
|
|
172
178
|
]
|
|
173
|
-
:
|
|
179
|
+
: options.clearTicket
|
|
180
|
+
? []
|
|
181
|
+
: (existingContext?.ticket_binding_candidates ?? []);
|
|
174
182
|
const context = LocalWorkContextSchema.parse({
|
|
175
183
|
work_context_id: workContextId,
|
|
176
184
|
repo: identity.repo_root,
|
|
@@ -186,7 +194,7 @@ export async function startLocalWorkContext(options = {}) {
|
|
|
186
194
|
session_id: sessionId,
|
|
187
195
|
started_at: existingContext?.started_at ?? now.toISOString(),
|
|
188
196
|
updated_at: now.toISOString(),
|
|
189
|
-
active_ticket_id:
|
|
197
|
+
active_ticket_id: activeTicketId,
|
|
190
198
|
ticket_binding_candidates: ticketBindingCandidates,
|
|
191
199
|
topic_label: options.topicLabel,
|
|
192
200
|
topic_summary_redacted: options.topicSummaryRedacted,
|
|
@@ -229,8 +237,8 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
229
237
|
const identity = await resolveIdentityOrFallback(repoRoot, options.branch);
|
|
230
238
|
const context = await readLocalWorkContextForRepo(paths, repoRoot).catch(() => null);
|
|
231
239
|
const branch = options.branch ?? identity.branch;
|
|
232
|
-
const freshness = classifyCollectorFreshness(context, now);
|
|
233
240
|
const uploadSpool = await summarizeLocalUploadSpool(paths);
|
|
241
|
+
const freshness = classifyCollectorFreshness(context, uploadSpool.last_upload_success_at, now);
|
|
234
242
|
const uploadState = !config
|
|
235
243
|
? "not_installed"
|
|
236
244
|
: uploadSpool.pending_upload_count > 0
|
|
@@ -381,14 +389,14 @@ function workContextFile(paths, worktreeFingerprint) {
|
|
|
381
389
|
return path.join(paths.work_contexts_dir, `${worktreeFingerprint}.json`);
|
|
382
390
|
}
|
|
383
391
|
async function resolveIdentityOrFallback(repoRoot, branchOverride) {
|
|
384
|
-
const resolvedRoot =
|
|
392
|
+
const resolvedRoot = await stableWorktreeRoot(repoRoot);
|
|
385
393
|
const identity = await resolveRepoWorktreeIdentity(resolvedRoot).catch(() => null);
|
|
386
394
|
if (identity) {
|
|
387
395
|
return branchOverride ? { ...identity, branch: branchOverride } : identity;
|
|
388
396
|
}
|
|
389
397
|
const repoLabel = path.basename(resolvedRoot) || "workspace";
|
|
390
398
|
const repoFingerprint = `repo-${sha256(`local:${resolvedRoot}`).slice(0, 24)}`;
|
|
391
|
-
const worktreeFingerprint =
|
|
399
|
+
const worktreeFingerprint = stableWorktreeFingerprint(resolvedRoot);
|
|
392
400
|
const branch = branchOverride ?? (await resolveGitBranch(resolvedRoot));
|
|
393
401
|
return {
|
|
394
402
|
requested_path: resolvedRoot,
|
|
@@ -426,13 +434,24 @@ async function writeJsonFile(filePath, value) {
|
|
|
426
434
|
await fs.chmod(filePath, 0o600).catch(() => undefined);
|
|
427
435
|
}
|
|
428
436
|
}
|
|
429
|
-
function classifyCollectorFreshness(context, now) {
|
|
437
|
+
export function classifyCollectorFreshness(context, lastUploadSuccessAt, now) {
|
|
430
438
|
if (!context)
|
|
431
439
|
return "missing";
|
|
432
|
-
const
|
|
433
|
-
|
|
440
|
+
const latestActivityAt = latestTimestamp([
|
|
441
|
+
context.updated_at ?? context.started_at,
|
|
442
|
+
lastUploadSuccessAt,
|
|
443
|
+
]);
|
|
444
|
+
if (latestActivityAt === null)
|
|
434
445
|
return "stale";
|
|
435
|
-
return now.getTime() -
|
|
446
|
+
return now.getTime() - latestActivityAt <= 5 * 60 * 1000 ? "fresh" : "stale";
|
|
447
|
+
}
|
|
448
|
+
function latestTimestamp(values) {
|
|
449
|
+
const timestamps = values
|
|
450
|
+
.map((value) => Date.parse(value ?? ""))
|
|
451
|
+
.filter((value) => Number.isFinite(value));
|
|
452
|
+
if (timestamps.length === 0)
|
|
453
|
+
return null;
|
|
454
|
+
return Math.max(...timestamps);
|
|
436
455
|
}
|
|
437
456
|
function normalizeDashboardUrl(value) {
|
|
438
457
|
const normalized = value.trim().replace(/\/+$/, "");
|
package/dist/repo-identity.js
CHANGED
|
@@ -18,7 +18,7 @@ const SKIPPED_DIR_NAMES = new Set([
|
|
|
18
18
|
export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
19
19
|
const requestedPath = path.resolve(repoRoot);
|
|
20
20
|
const gitRoot = await runGit(["rev-parse", "--show-toplevel"], requestedPath);
|
|
21
|
-
const resolvedRoot =
|
|
21
|
+
const resolvedRoot = await stableWorktreeRoot(gitRoot.trim() || requestedPath);
|
|
22
22
|
const branch = await resolveGitBranchWithGit(resolvedRoot);
|
|
23
23
|
const headSha = await runGit(["rev-parse", "HEAD"], resolvedRoot).then((value) => value.trim() || null, () => null);
|
|
24
24
|
const absoluteGitDir = await runGit(["rev-parse", "--absolute-git-dir"], resolvedRoot)
|
|
@@ -36,11 +36,6 @@ export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
|
36
36
|
? `origin:${repoOriginUrl}`
|
|
37
37
|
: `local:${sha256(commonGitDir ?? resolvedRoot)}`;
|
|
38
38
|
const repoFingerprint = `repo-${sha256(repoMaterial).slice(0, 24)}`;
|
|
39
|
-
const worktreeMaterial = [
|
|
40
|
-
repoFingerprint,
|
|
41
|
-
resolvedRoot,
|
|
42
|
-
commonGitDir ?? "",
|
|
43
|
-
].join("\n");
|
|
44
39
|
const gitFilePath = path.join(resolvedRoot, ".git");
|
|
45
40
|
const worktreeIsPrimary = await fs.stat(gitFilePath).then((stat) => stat.isDirectory(), () => false);
|
|
46
41
|
return {
|
|
@@ -52,7 +47,7 @@ export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
|
52
47
|
branch,
|
|
53
48
|
head_sha: headSha,
|
|
54
49
|
worktree_label: path.basename(resolvedRoot),
|
|
55
|
-
worktree_fingerprint:
|
|
50
|
+
worktree_fingerprint: stableWorktreeFingerprint(resolvedRoot),
|
|
56
51
|
worktree_is_primary: worktreeIsPrimary,
|
|
57
52
|
};
|
|
58
53
|
}
|
|
@@ -144,10 +139,9 @@ async function hasGitMarker(dir) {
|
|
|
144
139
|
return fs.stat(path.join(dir, ".git")).then((stat) => stat.isDirectory() || stat.isFile(), () => false);
|
|
145
140
|
}
|
|
146
141
|
async function fallbackFilesystemIdentity(repoRoot) {
|
|
147
|
-
const resolvedRoot =
|
|
142
|
+
const resolvedRoot = await stableWorktreeRoot(repoRoot);
|
|
148
143
|
const repoLabel = path.basename(resolvedRoot) || "repo";
|
|
149
144
|
const repoFingerprint = `repo-${sha256(`local:${resolvedRoot}`).slice(0, 24)}`;
|
|
150
|
-
const worktreeFingerprint = `wt-${sha256(`${repoFingerprint}:${resolvedRoot}`).slice(0, 24)}`;
|
|
151
145
|
return {
|
|
152
146
|
requested_path: resolvedRoot,
|
|
153
147
|
repo_root: resolvedRoot,
|
|
@@ -157,10 +151,17 @@ async function fallbackFilesystemIdentity(repoRoot) {
|
|
|
157
151
|
branch: await resolveBranchFromHead(resolvedRoot),
|
|
158
152
|
head_sha: null,
|
|
159
153
|
worktree_label: repoLabel,
|
|
160
|
-
worktree_fingerprint:
|
|
154
|
+
worktree_fingerprint: stableWorktreeFingerprint(resolvedRoot),
|
|
161
155
|
worktree_is_primary: true,
|
|
162
156
|
};
|
|
163
157
|
}
|
|
158
|
+
export async function stableWorktreeRoot(repoRoot) {
|
|
159
|
+
const resolvedRoot = path.resolve(repoRoot);
|
|
160
|
+
return fs.realpath(resolvedRoot).catch(() => resolvedRoot);
|
|
161
|
+
}
|
|
162
|
+
export function stableWorktreeFingerprint(repoRoot) {
|
|
163
|
+
return `wt-${sha256(`worktree:${normalizeFingerprintPath(repoRoot)}`).slice(0, 24)}`;
|
|
164
|
+
}
|
|
164
165
|
async function resolveBranchFromHead(repoRoot) {
|
|
165
166
|
try {
|
|
166
167
|
const gitPath = path.join(repoRoot, ".git");
|
|
@@ -244,6 +245,10 @@ async function runGit(args, cwd) {
|
|
|
244
245
|
});
|
|
245
246
|
return stdout;
|
|
246
247
|
}
|
|
248
|
+
function normalizeFingerprintPath(repoRoot) {
|
|
249
|
+
const resolved = path.resolve(repoRoot);
|
|
250
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
251
|
+
}
|
|
247
252
|
function sha256(value) {
|
|
248
253
|
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
249
254
|
}
|
package/dist/upload.js
CHANGED
|
@@ -101,6 +101,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
101
101
|
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
102
102
|
session_reference: sanitizeSessionReference(session),
|
|
103
103
|
work_context: uploadWorkContext,
|
|
104
|
+
worktree_inventory: options.worktreeInventory ?? [],
|
|
104
105
|
source_scan_results: sanitizeSourceScanResults(sourceCollection.scans, repoLabel),
|
|
105
106
|
events,
|
|
106
107
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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.9"
|
|
30
30
|
}
|
|
31
31
|
}
|