@bli-cockpit/cli 0.2.236 → 0.2.238
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/dist/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +106 -0
- package/dist/commands/cal.js +40 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/doctor-usage-telemetry.js +2 -1
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +54 -48
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +56 -188
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-msg.js +149 -0
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +11 -4
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/mail.js +6 -4
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/msg.js +51 -9
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +2 -2
|
@@ -91,40 +91,39 @@ export async function showIssue(command, door) {
|
|
|
91
91
|
results.push(await fetchOneIssue(door, ref));
|
|
92
92
|
// BLI-10450: one directory call for the whole batch, never one per issue.
|
|
93
93
|
const directory = await resolveAssigneeDirectory(door, results.filter((result) => result.ok).map((result) => result.issue.assignee_id));
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
94
|
+
if (refs.length === 1)
|
|
95
|
+
return reportSingleIssue(door, results[0], directory);
|
|
96
|
+
return reportSeveralIssues(door, results, directory);
|
|
97
|
+
}
|
|
98
|
+
/** The JSON body for one issue that was read: the issue, its comments and any Linear link. */
|
|
99
|
+
function readIssueJson(result, directory) {
|
|
100
|
+
return {
|
|
101
|
+
issue: withSizeLabel(withAssigneeIdentity(result.issue, directory)),
|
|
102
|
+
comments: result.comments,
|
|
103
|
+
...(result.linearTicketId ? { linear_ticket_id: result.linearTicketId } : {}),
|
|
104
|
+
...(result.commentsUnreadReason ? { comments_unread_reason: result.commentsUnreadReason } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// A single ref keeps the pre-BLI-10450 shape and failure path exactly.
|
|
108
|
+
function reportSingleIssue(door, result, directory) {
|
|
109
|
+
if (!result || !result.ok) {
|
|
110
|
+
return failAgentDoor(door, TAG, result?.reason ?? "issue_not_found_or_unreadable", result?.detail ?? "That issue does not exist, or you cannot read it.");
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
if (door.json)
|
|
113
|
+
return emitAgentDoor(door, { ok: true, ...readIssueJson(result, directory) });
|
|
114
|
+
renderOneIssue(door, result, directory);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
// Several refs: `{ ok, issues: [...] }`, one entry per ref in the order
|
|
118
|
+
// given, each carrying `ref` so a caller can match an answer back to what
|
|
119
|
+
// it asked for. One bad id never loses the rest of the batch.
|
|
120
|
+
function reportSeveralIssues(door, results, directory) {
|
|
115
121
|
const failed = results.filter((result) => !result.ok);
|
|
116
122
|
if (door.json) {
|
|
117
123
|
return emitAgentDoor(door, {
|
|
118
124
|
ok: failed.length === 0,
|
|
119
125
|
issues: results.map((result) => result.ok
|
|
120
|
-
? {
|
|
121
|
-
ref: result.ref,
|
|
122
|
-
ok: true,
|
|
123
|
-
issue: withSizeLabel(withAssigneeIdentity(result.issue, directory)),
|
|
124
|
-
comments: result.comments,
|
|
125
|
-
...(result.linearTicketId ? { linear_ticket_id: result.linearTicketId } : {}),
|
|
126
|
-
...(result.commentsUnreadReason ? { comments_unread_reason: result.commentsUnreadReason } : {}),
|
|
127
|
-
}
|
|
126
|
+
? { ref: result.ref, ok: true, ...readIssueJson(result, directory) }
|
|
128
127
|
: { ref: result.ref, ok: false, error: result.reason, detail: result.detail }),
|
|
129
128
|
}, failed.length === 0 ? 0 : 1);
|
|
130
129
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
/**
|
|
2
3
|
* The verbs that CHANGE something: `cockpit issue create|update|move|comment`
|
|
3
4
|
* (BLI-3716). The reads and the router live in `issue.ts`, the shared shapes
|
|
@@ -19,12 +20,14 @@ export async function createIssue(command, door) {
|
|
|
19
20
|
const body = await readBody(command, door.io);
|
|
20
21
|
if (!body.ok)
|
|
21
22
|
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
22
|
-
const
|
|
23
|
+
const requestId = command.requestId ?? randomUUID();
|
|
24
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
25
|
+
const answer = await askTicketWrite(door, {
|
|
23
26
|
path: "/api/work/issues",
|
|
24
27
|
method: "POST",
|
|
25
28
|
label: "issue create",
|
|
26
29
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
27
|
-
body: {
|
|
30
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision,
|
|
28
31
|
title: command.title,
|
|
29
32
|
...(body.text ? { description: body.text } : {}),
|
|
30
33
|
...(refs.projectId ? { project_id: refs.projectId } : {}),
|
|
@@ -35,11 +38,12 @@ export async function createIssue(command, door) {
|
|
|
35
38
|
},
|
|
36
39
|
});
|
|
37
40
|
if (!answer.ok)
|
|
38
|
-
return
|
|
41
|
+
return refuseWrite(door, answer, requestId);
|
|
39
42
|
const issue = maybeWithSizeLabel(answer.body.issue);
|
|
40
43
|
writeLine(door.io.stderr, `${TAG} created ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null, description_bytes: Buffer.byteLength(body.text, "utf8") })}`);
|
|
44
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
41
45
|
if (door.json)
|
|
42
|
-
return emitAgentDoor(door, { ok: true, issue });
|
|
46
|
+
return emitAgentDoor(door, { ok: true, issue, mutation: answer.body.mutation });
|
|
43
47
|
writeLine(door.io.stdout, `Created ${issue?.identifier ?? ""} — ${issue?.title ?? ""} (${issue?.id ?? ""}).`);
|
|
44
48
|
return 0;
|
|
45
49
|
}
|
|
@@ -55,21 +59,17 @@ export async function updateIssue(command, door) {
|
|
|
55
59
|
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
56
60
|
description = body.text;
|
|
57
61
|
}
|
|
58
|
-
if (description
|
|
59
|
-
&& command.title === undefined
|
|
60
|
-
&& command.priority === undefined
|
|
61
|
-
&& command.estimate === undefined
|
|
62
|
-
&& command.assignee === undefined
|
|
63
|
-
&& refs.projectId === undefined
|
|
64
|
-
&& refs.parentId === undefined) {
|
|
62
|
+
if (!changesAnything(command, description, refs)) {
|
|
65
63
|
return failAgentDoor(door, TAG, "invalid_body", "issue update needs at least one of --title, --priority, --size, --assignee, --project, --parent, or a description on --body-stdin/--file.");
|
|
66
64
|
}
|
|
67
|
-
const
|
|
65
|
+
const requestId = command.requestId ?? randomUUID();
|
|
66
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
67
|
+
const answer = await askTicketWrite(door, {
|
|
68
68
|
path: `/api/work/issues/${encodeURIComponent(ref)}`,
|
|
69
69
|
method: "PATCH",
|
|
70
70
|
label: "issue update",
|
|
71
71
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
72
|
-
body: {
|
|
72
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision,
|
|
73
73
|
...(command.title !== undefined ? { title: command.title } : {}),
|
|
74
74
|
...(description !== undefined ? { description } : {}),
|
|
75
75
|
...(command.priority === undefined ? {} : { priority: command.priority }),
|
|
@@ -80,29 +80,33 @@ export async function updateIssue(command, door) {
|
|
|
80
80
|
},
|
|
81
81
|
});
|
|
82
82
|
if (!answer.ok)
|
|
83
|
-
return
|
|
83
|
+
return refuseWrite(door, answer, requestId);
|
|
84
84
|
const issue = maybeWithSizeLabel(answer.body.issue);
|
|
85
85
|
writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null })}`);
|
|
86
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
86
87
|
if (door.json)
|
|
87
|
-
return emitAgentDoor(door, { ok: true, issue });
|
|
88
|
+
return emitAgentDoor(door, { ok: true, issue, mutation: answer.body.mutation });
|
|
88
89
|
writeLine(door.io.stdout, `Updated ${issue?.identifier ?? ref} — ${issue?.title ?? ""}.`);
|
|
89
90
|
return 0;
|
|
90
91
|
}
|
|
91
92
|
export async function moveIssue(command, door) {
|
|
92
93
|
const ref = command.issueRef ?? "";
|
|
93
|
-
const
|
|
94
|
+
const requestId = command.requestId ?? randomUUID();
|
|
95
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
96
|
+
const answer = await askTicketWrite(door, {
|
|
94
97
|
path: `/api/work/issues/${encodeURIComponent(ref)}/state`,
|
|
95
98
|
method: "POST",
|
|
96
99
|
label: "issue move",
|
|
97
100
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
98
|
-
body: { state: command.moveState },
|
|
101
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision, state: command.moveState },
|
|
99
102
|
});
|
|
100
103
|
if (!answer.ok)
|
|
101
|
-
return
|
|
104
|
+
return refuseWrite(door, answer, requestId);
|
|
102
105
|
const issue = answer.body.issue;
|
|
103
106
|
writeLine(door.io.stderr, `${TAG} moved ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null, to_state: command.moveState })}`);
|
|
107
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
104
108
|
if (door.json)
|
|
105
|
-
return emitAgentDoor(door, { ok: true, issue });
|
|
109
|
+
return emitAgentDoor(door, { ok: true, issue, mutation: answer.body.mutation });
|
|
106
110
|
writeLine(door.io.stdout, `${issue?.identifier ?? ref} is now ${issue?.state ?? command.moveState}.`);
|
|
107
111
|
return 0;
|
|
108
112
|
}
|
|
@@ -114,19 +118,62 @@ export async function commentIssue(command, door) {
|
|
|
114
118
|
if (body.text.trim() === "") {
|
|
115
119
|
return failAgentDoor(door, TAG, "invalid_body", 'A comment is never taken on the command line — pipe it in: `echo "..." | cockpit issue comment BLI-3654`.');
|
|
116
120
|
}
|
|
117
|
-
const
|
|
121
|
+
const requestId = command.requestId ?? randomUUID();
|
|
122
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
123
|
+
const answer = await askTicketWrite(door, {
|
|
118
124
|
path: `/api/work/issues/${encodeURIComponent(ref)}/comments`,
|
|
119
125
|
method: "POST",
|
|
120
126
|
label: "issue comment",
|
|
121
127
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
122
|
-
body: { body_markdown: body.text },
|
|
128
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision, body_markdown: body.text },
|
|
123
129
|
});
|
|
124
130
|
if (!answer.ok)
|
|
125
|
-
return
|
|
131
|
+
return refuseWrite(door, answer, requestId);
|
|
126
132
|
const comment = answer.body.comment;
|
|
127
133
|
writeLine(door.io.stderr, `${TAG} commented ${JSON.stringify({ comment_id: comment?.id ?? null, byte_size: Buffer.byteLength(body.text, "utf8") })}`);
|
|
134
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
128
135
|
if (door.json)
|
|
129
|
-
return emitAgentDoor(door, { ok: true, comment });
|
|
136
|
+
return emitAgentDoor(door, { ok: true, comment, mutation: answer.body.mutation });
|
|
130
137
|
writeLine(door.io.stdout, `Commented on ${ref} (${comment?.id ?? ""}).`);
|
|
131
138
|
return 0;
|
|
139
|
+
}
|
|
140
|
+
/** A truncated or missing response body cannot be called a successful ticket write. */
|
|
141
|
+
async function askTicketWrite(door, options) {
|
|
142
|
+
try {
|
|
143
|
+
const answer = await askAgentDoor(door, options);
|
|
144
|
+
if (!answer.ok)
|
|
145
|
+
return answer;
|
|
146
|
+
const body = answer.body;
|
|
147
|
+
if (body && (body.issue || body.comment))
|
|
148
|
+
return answer;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
console.error(`${TAG} response lost`, JSON.stringify({ reason: "mutation_unknown" }));
|
|
152
|
+
}
|
|
153
|
+
const requestId = options.body.request_id;
|
|
154
|
+
return { ok: false, reason: "mutation_unknown", detail: "Result unknown. Check this request ID before retrying with identical input.",
|
|
155
|
+
body: { mutation: { request_id: requestId, outcome: "unknown_check_again" } } };
|
|
156
|
+
}
|
|
157
|
+
/** Did the person ask for at least one field to change? */
|
|
158
|
+
function changesAnything(command, description, refs) {
|
|
159
|
+
return description !== undefined
|
|
160
|
+
|| command.title !== undefined
|
|
161
|
+
|| command.priority !== undefined
|
|
162
|
+
|| command.estimate !== undefined
|
|
163
|
+
|| command.assignee !== undefined
|
|
164
|
+
|| refs.projectId !== undefined
|
|
165
|
+
|| refs.parentId !== undefined;
|
|
166
|
+
}
|
|
167
|
+
/** Says where the local commit stands, then fails with the door's own reason. */
|
|
168
|
+
function refuseWrite(door, answer, requestId) {
|
|
169
|
+
writeLine(door.io.stderr, `${TAG} check local commit with cockpit ticket mutation-status ${requestId}; retry unchanged input with --request-id ${requestId}.`);
|
|
170
|
+
const mutation = answer.body?.mutation ?? { request_id: requestId, outcome: "unknown_check_again" };
|
|
171
|
+
if (door.json) {
|
|
172
|
+
writeLine(door.io.stderr, `${TAG} refused ${JSON.stringify({ reason: answer.reason, request_id: requestId })}`);
|
|
173
|
+
return emitAgentDoor(door, { ok: false, error: answer.reason, detail: answer.detail, mutation }, 1);
|
|
174
|
+
}
|
|
175
|
+
const receipt = mutation;
|
|
176
|
+
if (receipt.current)
|
|
177
|
+
writeLine(door.io.stderr, `${TAG} conflict ${JSON.stringify({ request_id: requestId, outcome: receipt.outcome, current_state: receipt.current.state, current_revision: receipt.current.revision })}`);
|
|
178
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
132
179
|
}
|
package/dist/commands/issue.js
CHANGED
|
@@ -36,6 +36,16 @@ import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write
|
|
|
36
36
|
export async function runIssue(command, io) {
|
|
37
37
|
const door = await openAgentDoor("issue", command, io);
|
|
38
38
|
switch (command.action) {
|
|
39
|
+
case "mutation-status": {
|
|
40
|
+
const answer = await askAgentDoor(door, { path: `/api/work/mutations/${encodeURIComponent(command.issueRef ?? "")}`,
|
|
41
|
+
method: "GET", label: "ticket mutation status", timeoutMs: READ_DEADLINE_MS });
|
|
42
|
+
if (!answer.ok)
|
|
43
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
44
|
+
if (door.json)
|
|
45
|
+
return emitAgentDoor(door, { ok: true, ...answer.body });
|
|
46
|
+
writeLine(door.io.stdout, JSON.stringify(answer.body));
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
39
49
|
case "board":
|
|
40
50
|
return readTicketBoard(command, door);
|
|
41
51
|
case "list":
|
|
@@ -92,23 +102,10 @@ export function listGapLines(body, requestedLimit) {
|
|
|
92
102
|
return lines;
|
|
93
103
|
}
|
|
94
104
|
async function listIssues(command, door) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
query.set("assignee_id", command.assignee);
|
|
100
|
-
if (command.limit !== undefined)
|
|
101
|
-
query.set("limit", String(command.limit));
|
|
102
|
-
if (command.project) {
|
|
103
|
-
const resolved = await resolveProjectId(door, command.project);
|
|
104
|
-
if (resolved.status === "list_failed")
|
|
105
|
-
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
106
|
-
if (resolved.status === "not_found") {
|
|
107
|
-
return failAgentDoor(door, TAG, "project_not_found_or_unreadable", `No project here is called "${command.project}" — run \`cockpit project list\` for the names.`);
|
|
108
|
-
}
|
|
109
|
-
query.set("project_id", resolved.id);
|
|
110
|
-
}
|
|
111
|
-
const suffix = query.toString();
|
|
105
|
+
const built = await buildIssueListQuery(command, door);
|
|
106
|
+
if (!built.ok)
|
|
107
|
+
return built.exitCode;
|
|
108
|
+
const suffix = built.query.toString();
|
|
112
109
|
// BLI-10579: the assignee directory (`/api/team/members`) is the whole org
|
|
113
110
|
// roster regardless of which issues come back — it never actually depended
|
|
114
111
|
// on the issue list — so start it now, alongside the issues request,
|
|
@@ -116,26 +113,7 @@ async function listIssues(command, door) {
|
|
|
116
113
|
// `work list` p50 was exactly this serial second call).
|
|
117
114
|
const directoryFetch = beginAssigneeDirectoryFetch(door);
|
|
118
115
|
const listPath = `/api/work/issues${suffix ? `?${suffix}` : ""}`;
|
|
119
|
-
const answer = await
|
|
120
|
-
io: door.io,
|
|
121
|
-
homeDir: door.homeDir,
|
|
122
|
-
deviceToken: door.deviceToken,
|
|
123
|
-
surface: "work",
|
|
124
|
-
label: "issue list",
|
|
125
|
-
path: listPath,
|
|
126
|
-
json: door.json,
|
|
127
|
-
ask: () => askAgentDoor(door, {
|
|
128
|
-
path: listPath,
|
|
129
|
-
method: "GET",
|
|
130
|
-
label: "issue list",
|
|
131
|
-
timeoutMs: READ_DEADLINE_MS,
|
|
132
|
-
// BLI-10746: `work list` was the slowest of the six reads measured
|
|
133
|
-
// from Vietnam (1,682 ms p50) — route it through the resident memory
|
|
134
|
-
// daemon's warm connection so this command skips a fresh TCP+TLS
|
|
135
|
-
// handshake.
|
|
136
|
-
warmRoute: true,
|
|
137
|
-
}),
|
|
138
|
-
});
|
|
116
|
+
const answer = await askIssueList(door, listPath);
|
|
139
117
|
if (!answer.ok) {
|
|
140
118
|
// Not awaited for its own sake — `finishAssigneeDirectory` always awaits
|
|
141
119
|
// the in-flight fetch so it never leaks past this command's exit.
|
|
@@ -176,6 +154,51 @@ async function listIssues(command, door) {
|
|
|
176
154
|
writeLine(door.io.stdout, `${issues.length} issue(s).`);
|
|
177
155
|
return 0;
|
|
178
156
|
}
|
|
157
|
+
/** The list query for the filters given, or the exit code of the project lookup that failed. */
|
|
158
|
+
async function buildIssueListQuery(command, door) {
|
|
159
|
+
const query = new URLSearchParams();
|
|
160
|
+
if (command.stateFilter)
|
|
161
|
+
query.set("state", command.stateFilter);
|
|
162
|
+
if (command.assignee)
|
|
163
|
+
query.set("assignee_id", command.assignee);
|
|
164
|
+
if (command.limit !== undefined)
|
|
165
|
+
query.set("limit", String(command.limit));
|
|
166
|
+
if (command.project) {
|
|
167
|
+
const resolved = await resolveProjectId(door, command.project);
|
|
168
|
+
if (resolved.status === "list_failed")
|
|
169
|
+
return { ok: false, exitCode: failAgentDoor(door, TAG, resolved.reason, resolved.detail) };
|
|
170
|
+
if (resolved.status === "not_found") {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
exitCode: failAgentDoor(door, TAG, "project_not_found_or_unreadable", `No project here is called "${command.project}" — run \`cockpit project list\` for the names.`),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
query.set("project_id", resolved.id);
|
|
177
|
+
}
|
|
178
|
+
return { ok: true, query };
|
|
179
|
+
}
|
|
180
|
+
function askIssueList(door, listPath) {
|
|
181
|
+
return readThroughCache({
|
|
182
|
+
io: door.io,
|
|
183
|
+
homeDir: door.homeDir,
|
|
184
|
+
deviceToken: door.deviceToken,
|
|
185
|
+
surface: "work",
|
|
186
|
+
label: "issue list",
|
|
187
|
+
path: listPath,
|
|
188
|
+
json: door.json,
|
|
189
|
+
ask: () => askAgentDoor(door, {
|
|
190
|
+
path: listPath,
|
|
191
|
+
method: "GET",
|
|
192
|
+
label: "issue list",
|
|
193
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
194
|
+
// BLI-10746: `work list` was the slowest of the six reads measured
|
|
195
|
+
// from Vietnam (1,682 ms p50) — route it through the resident memory
|
|
196
|
+
// daemon's warm connection so this command skips a fresh TCP+TLS
|
|
197
|
+
// handshake.
|
|
198
|
+
warmRoute: true,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
179
202
|
/**
|
|
180
203
|
* What a history row actually says. A row imported from Linear's issue
|
|
181
204
|
* TIMESTAMPS (created/started/completed/canceled) carries no state names at
|
|
@@ -187,6 +210,14 @@ async function listIssues(command, door) {
|
|
|
187
210
|
* reads a real gap as a broken command.
|
|
188
211
|
*/
|
|
189
212
|
function historyChange(row) {
|
|
213
|
+
if (row.source === "native" && row.detail?.operation) {
|
|
214
|
+
if (row.detail.operation === "create")
|
|
215
|
+
return "created the ticket";
|
|
216
|
+
if (row.detail.operation === "comment")
|
|
217
|
+
return "added a comment";
|
|
218
|
+
if (row.change_type === "other")
|
|
219
|
+
return `updated ${(row.detail.fields ?? []).join(", ")}`;
|
|
220
|
+
}
|
|
190
221
|
if (row.from_value === null && row.to_value === null) {
|
|
191
222
|
return `(no state names recorded — imported from ${row.source})`;
|
|
192
223
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { colorEnabled, dim, writeFragment, writeLine } from "./cli-io.js";
|
|
2
|
+
import { ANSWER_REVISED_LINE, writeReply, writeReplyContinuation } from "./jarvis-render.js";
|
|
3
|
+
/**
|
|
4
|
+
* The answer as it is written, in a terminal (BLI-3517).
|
|
5
|
+
*
|
|
6
|
+
* A terminal cannot unprint. That single fact decides everything here:
|
|
7
|
+
*
|
|
8
|
+
* - The words stream out under the usual `jarvis> ` prefix, printed the moment
|
|
9
|
+
* they arrive rather than after the whole turn (a measured 4-16 s of nothing
|
|
10
|
+
* before this ticket).
|
|
11
|
+
* - A `reset` — the server taking back a draft the model wrote before deciding
|
|
12
|
+
* to call a tool — cannot erase what is on screen, so it SAYS so on its own
|
|
13
|
+
* line and the replacement follows. Never a retracted sentence left standing
|
|
14
|
+
* with nothing marking it.
|
|
15
|
+
* - The settled reply is the authority. The grounding gate runs on the whole
|
|
16
|
+
* text once the stream ends, so the answer can grow (citations, a fallback
|
|
17
|
+
* notice) or change. A pure continuation is printed as the remainder; a
|
|
18
|
+
* genuine change is reprinted whole under a line saying it was revised.
|
|
19
|
+
* - `--json` never streams a fragment: that contract is exactly one object on
|
|
20
|
+
* stdout, and every method here is a no-op for it.
|
|
21
|
+
*/
|
|
22
|
+
export function createLiveAnswer(command, io) {
|
|
23
|
+
const styled = colorEnabled(io);
|
|
24
|
+
const quiet = command.json === true;
|
|
25
|
+
const screen = { printed: "", opened: false };
|
|
26
|
+
return {
|
|
27
|
+
token(event) {
|
|
28
|
+
if (quiet)
|
|
29
|
+
return;
|
|
30
|
+
const text = event.text ?? "";
|
|
31
|
+
if (event.reset) {
|
|
32
|
+
if (screen.printed) {
|
|
33
|
+
writeLine(io.stdout, "");
|
|
34
|
+
writeLine(io.stdout, dim(" — that draft was replaced —", styled));
|
|
35
|
+
screen.opened = false;
|
|
36
|
+
}
|
|
37
|
+
screen.printed = "";
|
|
38
|
+
}
|
|
39
|
+
if (!text)
|
|
40
|
+
return;
|
|
41
|
+
openAnswerLine(io, screen, "");
|
|
42
|
+
screen.printed += text;
|
|
43
|
+
writeFragment(io.stdout, text);
|
|
44
|
+
},
|
|
45
|
+
interrupt() {
|
|
46
|
+
if (quiet || !screen.opened)
|
|
47
|
+
return;
|
|
48
|
+
writeLine(io.stdout, "");
|
|
49
|
+
screen.opened = false;
|
|
50
|
+
},
|
|
51
|
+
settle(reply, subject, revised) {
|
|
52
|
+
if (quiet)
|
|
53
|
+
return;
|
|
54
|
+
settleLiveAnswer(io, styled, screen, { reply, subject, revised });
|
|
55
|
+
},
|
|
56
|
+
abandon() {
|
|
57
|
+
// The turn died with words already on screen. End the line so the
|
|
58
|
+
// failure sentence does not run on from a half-written answer.
|
|
59
|
+
if (!quiet && screen.opened)
|
|
60
|
+
writeLine(io.stdout, "");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function openAnswerLine(io, screen, subject) {
|
|
65
|
+
if (screen.opened)
|
|
66
|
+
return;
|
|
67
|
+
screen.opened = true;
|
|
68
|
+
writeFragment(io.stdout, `jarvis${subject}> `);
|
|
69
|
+
}
|
|
70
|
+
function settleLiveAnswer(io, styled, screen, settled) {
|
|
71
|
+
const { reply, subject, revised } = settled;
|
|
72
|
+
const { printed } = screen;
|
|
73
|
+
if (!printed) {
|
|
74
|
+
// Nothing streamed: a non-streaming dashboard, `--no-stream`, or a
|
|
75
|
+
// turn whose words never arrived. Exactly the pre-ticket line, dim
|
|
76
|
+
// citation link and all (BLI-3570).
|
|
77
|
+
writeReply(io, `jarvis${subject}> `, reply);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (reply === printed) {
|
|
81
|
+
if (screen.opened)
|
|
82
|
+
writeLine(io.stdout, "");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (reply.startsWith(printed)) {
|
|
86
|
+
// The gate only ADDED — citations going back under the answer, a
|
|
87
|
+
// fallback notice. Print the tail and leave the words alone; the
|
|
88
|
+
// citation block is exactly the thing BLI-3570 puts its link under, and
|
|
89
|
+
// it is arriving here rather than in the stream.
|
|
90
|
+
if (!screen.opened)
|
|
91
|
+
writeFragment(io.stdout, `jarvis${subject}> `);
|
|
92
|
+
writeReplyContinuation(io, reply.slice(printed.length));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (screen.opened)
|
|
96
|
+
writeLine(io.stdout, "");
|
|
97
|
+
writeLine(io.stdout, dim(revised ? ` — ${ANSWER_REVISED_LINE} —` : " — corrected —", styled));
|
|
98
|
+
writeReply(io, `jarvis${subject}> `, reply);
|
|
99
|
+
}
|
|
@@ -228,7 +228,7 @@ export function writeReplyContinuation(io, tail) {
|
|
|
228
228
|
* here in the same register rather than shared through a dependency that does
|
|
229
229
|
* not exist.
|
|
230
230
|
*/
|
|
231
|
-
const ANSWER_REVISED_LINE = "revised: I checked that against its sources and changed what they did not back up";
|
|
231
|
+
export const ANSWER_REVISED_LINE = "revised: I checked that against its sources and changed what they did not back up";
|
|
232
232
|
/**
|
|
233
233
|
* The three lines a correction's rewrite prints (BLI-3567).
|
|
234
234
|
*
|
|
@@ -251,99 +251,6 @@ const ANSWER_CORRECTED_NOTE_LINE = "The paragraph printed above is unchanged —
|
|
|
251
251
|
"printed. This is what it says now.";
|
|
252
252
|
const ANSWER_REWRITE_UNVERIFIED_LINE = "Your correction is recorded. I could not verify a rewrite of that paragraph against its " +
|
|
253
253
|
"sources, so it stays as it was.";
|
|
254
|
-
/**
|
|
255
|
-
* The answer as it is written, in a terminal (BLI-3517).
|
|
256
|
-
*
|
|
257
|
-
* A terminal cannot unprint. That single fact decides everything here:
|
|
258
|
-
*
|
|
259
|
-
* - The words stream out under the usual `jarvis> ` prefix, printed the moment
|
|
260
|
-
* they arrive rather than after the whole turn (a measured 4-16 s of nothing
|
|
261
|
-
* before this ticket).
|
|
262
|
-
* - A `reset` — the server taking back a draft the model wrote before deciding
|
|
263
|
-
* to call a tool — cannot erase what is on screen, so it SAYS so on its own
|
|
264
|
-
* line and the replacement follows. Never a retracted sentence left standing
|
|
265
|
-
* with nothing marking it.
|
|
266
|
-
* - The settled reply is the authority. The grounding gate runs on the whole
|
|
267
|
-
* text once the stream ends, so the answer can grow (citations, a fallback
|
|
268
|
-
* notice) or change. A pure continuation is printed as the remainder; a
|
|
269
|
-
* genuine change is reprinted whole under a line saying it was revised.
|
|
270
|
-
* - `--json` never streams a fragment: that contract is exactly one object on
|
|
271
|
-
* stdout, and every method here is a no-op for it.
|
|
272
|
-
*/
|
|
273
|
-
export function createLiveAnswer(command, io) {
|
|
274
|
-
const styled = colorEnabled(io);
|
|
275
|
-
const quiet = command.json === true;
|
|
276
|
-
let printed = "";
|
|
277
|
-
let opened = false;
|
|
278
|
-
const open = (subject) => {
|
|
279
|
-
if (opened)
|
|
280
|
-
return;
|
|
281
|
-
opened = true;
|
|
282
|
-
writeFragment(io.stdout, `jarvis${subject}> `);
|
|
283
|
-
};
|
|
284
|
-
return {
|
|
285
|
-
token(event) {
|
|
286
|
-
if (quiet)
|
|
287
|
-
return;
|
|
288
|
-
const text = event.text ?? "";
|
|
289
|
-
if (event.reset) {
|
|
290
|
-
if (printed) {
|
|
291
|
-
writeLine(io.stdout, "");
|
|
292
|
-
writeLine(io.stdout, dim(" — that draft was replaced —", styled));
|
|
293
|
-
opened = false;
|
|
294
|
-
}
|
|
295
|
-
printed = "";
|
|
296
|
-
}
|
|
297
|
-
if (!text)
|
|
298
|
-
return;
|
|
299
|
-
open("");
|
|
300
|
-
printed += text;
|
|
301
|
-
writeFragment(io.stdout, text);
|
|
302
|
-
},
|
|
303
|
-
interrupt() {
|
|
304
|
-
if (quiet || !opened)
|
|
305
|
-
return;
|
|
306
|
-
writeLine(io.stdout, "");
|
|
307
|
-
opened = false;
|
|
308
|
-
},
|
|
309
|
-
settle(reply, subject, revised) {
|
|
310
|
-
if (quiet)
|
|
311
|
-
return;
|
|
312
|
-
if (!printed) {
|
|
313
|
-
// Nothing streamed: a non-streaming dashboard, `--no-stream`, or a
|
|
314
|
-
// turn whose words never arrived. Exactly the pre-ticket line, dim
|
|
315
|
-
// citation link and all (BLI-3570).
|
|
316
|
-
writeReply(io, `jarvis${subject}> `, reply);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (reply === printed) {
|
|
320
|
-
if (opened)
|
|
321
|
-
writeLine(io.stdout, "");
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (reply.startsWith(printed)) {
|
|
325
|
-
// The gate only ADDED — citations going back under the answer, a
|
|
326
|
-
// fallback notice. Print the tail and leave the words alone; the
|
|
327
|
-
// citation block is exactly the thing BLI-3570 puts its link under, and
|
|
328
|
-
// it is arriving here rather than in the stream.
|
|
329
|
-
if (!opened)
|
|
330
|
-
writeFragment(io.stdout, `jarvis${subject}> `);
|
|
331
|
-
writeReplyContinuation(io, reply.slice(printed.length));
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (opened)
|
|
335
|
-
writeLine(io.stdout, "");
|
|
336
|
-
writeLine(io.stdout, dim(revised ? ` — ${ANSWER_REVISED_LINE} —` : " — corrected —", styled));
|
|
337
|
-
writeReply(io, `jarvis${subject}> `, reply);
|
|
338
|
-
},
|
|
339
|
-
abandon() {
|
|
340
|
-
// The turn died with words already on screen. End the line so the
|
|
341
|
-
// failure sentence does not run on from a half-written answer.
|
|
342
|
-
if (!quiet && opened)
|
|
343
|
-
writeLine(io.stdout, "");
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
254
|
/**
|
|
348
255
|
* Print what a correction did to the answer above, once the turn has settled.
|
|
349
256
|
*
|
|
@@ -84,28 +84,10 @@ export async function readRememberedTurn(homeDir) {
|
|
|
84
84
|
}
|
|
85
85
|
export async function runJarvisTrace(context, io) {
|
|
86
86
|
const log = (line) => writeLine(io.stderr, line);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!remembered) {
|
|
92
|
-
return refuse(context, io, "no_remembered_turn", "This machine has not asked JARVIS anything since traces were turned on, so there is no last turn to open. Ask something, then run this again — or pass the trace id from the turn's receipt.");
|
|
93
|
-
}
|
|
94
|
-
if (remembered.traceId) {
|
|
95
|
-
traceId = remembered.traceId;
|
|
96
|
-
resolvedFrom = "bookmark";
|
|
97
|
-
}
|
|
98
|
-
else if (remembered.threadId) {
|
|
99
|
-
// The bookmark knew the conversation but not the turn (an older receipt).
|
|
100
|
-
// Ask the server for that conversation's newest turn rather than nothing.
|
|
101
|
-
const newest = await newestTraceForThread(context, remembered.threadId, io);
|
|
102
|
-
if (!newest) {
|
|
103
|
-
return refuse(context, io, "no_traces_in_thread", "That conversation has no recorded turns inside the 30-day retention. Pass a trace id, or ask something new.");
|
|
104
|
-
}
|
|
105
|
-
traceId = newest;
|
|
106
|
-
resolvedFrom = "thread";
|
|
107
|
-
}
|
|
108
|
-
}
|
|
87
|
+
const resolution = await resolveRequestedTrace(context, io);
|
|
88
|
+
if (!resolution.ok)
|
|
89
|
+
return resolution.exitCode;
|
|
90
|
+
const { traceId, resolvedFrom } = resolution;
|
|
109
91
|
const result = await towerJsonRequest({
|
|
110
92
|
dashboardUrl: context.dashboardUrl,
|
|
111
93
|
path: `/api/ops/trace/${encodeURIComponent(traceId)}`,
|
|
@@ -139,6 +121,32 @@ export async function runJarvisTrace(context, io) {
|
|
|
139
121
|
})}`);
|
|
140
122
|
return 0;
|
|
141
123
|
}
|
|
124
|
+
/** Turns `last` into the remembered turn's trace id; any other argument is already a trace id. */
|
|
125
|
+
async function resolveRequestedTrace(context, io) {
|
|
126
|
+
if (context.requested !== "last")
|
|
127
|
+
return { ok: true, traceId: context.requested, resolvedFrom: "argument" };
|
|
128
|
+
const remembered = await readRememberedTurn(context.homeDir);
|
|
129
|
+
if (!remembered) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
exitCode: refuse(context, io, "no_remembered_turn", "This machine has not asked JARVIS anything since traces were turned on, so there is no last turn to open. Ask something, then run this again — or pass the trace id from the turn's receipt."),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (remembered.traceId)
|
|
136
|
+
return { ok: true, traceId: remembered.traceId, resolvedFrom: "bookmark" };
|
|
137
|
+
if (!remembered.threadId)
|
|
138
|
+
return { ok: true, traceId: context.requested, resolvedFrom: "argument" };
|
|
139
|
+
// The bookmark knew the conversation but not the turn (an older receipt).
|
|
140
|
+
// Ask the server for that conversation's newest turn rather than nothing.
|
|
141
|
+
const newest = await newestTraceForThread(context, remembered.threadId, io);
|
|
142
|
+
if (!newest) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
exitCode: refuse(context, io, "no_traces_in_thread", "That conversation has no recorded turns inside the 30-day retention. Pass a trace id, or ask something new."),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, traceId: newest, resolvedFrom: "thread" };
|
|
149
|
+
}
|
|
142
150
|
async function newestTraceForThread(context, threadId, io) {
|
|
143
151
|
const result = await towerJsonRequest({
|
|
144
152
|
dashboardUrl: context.dashboardUrl,
|