dsh-plugin-jules 0.1.0

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/lib/views.js ADDED
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Projections from Jules wire objects to the canonical values the tools return,
3
+ * and the text those values render to.
4
+ *
5
+ * Every projected object carries a value for each declared member: an absent
6
+ * optional scalar becomes `''` and an absent list becomes `[]`. The canonical
7
+ * value therefore stays lossless JSON with a fixed shape, and the renderer — not
8
+ * the model — decides what an empty value means.
9
+ *
10
+ * @module dsh-plugin-jules/views
11
+ */
12
+ import { activityKind, shortSourceName } from "./types.js";
13
+ /**
14
+ * Join a plan's steps into display lines.
15
+ * @param plan - the plan, when one was generated.
16
+ * @returns one line per step.
17
+ */
18
+ function planLines(plan) {
19
+ const steps = plan?.steps ?? [];
20
+ return steps.map((step, index) => {
21
+ const ordinal = step.index ?? index + 1;
22
+ const title = step.title ?? 'untitled step';
23
+ return step.description === undefined || step.description.length === 0
24
+ ? `${ordinal}. ${title}`
25
+ : `${ordinal}. ${title} — ${step.description}`;
26
+ });
27
+ }
28
+ /**
29
+ * Describe one artifact without reproducing its payload.
30
+ * @param artifact - one artifact from an activity.
31
+ * @returns a short descriptor.
32
+ */
33
+ function artifactSummary(artifact) {
34
+ if (artifact.changeSet !== undefined) {
35
+ const patch = artifact.changeSet.gitPatch?.unidiffPatch ?? '';
36
+ const files = patch.length === 0 ? 0 : patch.split('\n').filter(line => line.startsWith('diff --git ')).length;
37
+ const bytes = patch.length;
38
+ return `changeSet: ${files} file(s), ${bytes} byte(s) of unified diff`;
39
+ }
40
+ if (artifact.bashOutput !== undefined) {
41
+ const command = artifact.bashOutput.command ?? 'command';
42
+ const exit = artifact.bashOutput.exitCode;
43
+ return `bashOutput: ${command}${exit === undefined ? '' : ` (exit ${exit})`}`;
44
+ }
45
+ if (artifact.media !== undefined) {
46
+ return `media: ${artifact.media.mimeType ?? 'application/octet-stream'}`;
47
+ }
48
+ return 'unknown artifact';
49
+ }
50
+ /**
51
+ * Read the one human-readable line an activity carries.
52
+ * @param activity - one activity from the log.
53
+ * @returns the message, progress text, or failure reason.
54
+ */
55
+ export function activitySummary(activity) {
56
+ if (activity.planGenerated !== undefined) {
57
+ const steps = planLines(activity.planGenerated.plan);
58
+ return steps.length === 0 ? 'Plan generated.' : `Plan generated with ${steps.length} step(s).`;
59
+ }
60
+ if (activity.planApproved !== undefined)
61
+ return `Plan ${activity.planApproved.planId ?? ''} approved.`.trim();
62
+ if (activity.userMessaged !== undefined)
63
+ return activity.userMessaged.userMessage ?? 'User message.';
64
+ if (activity.agentMessaged !== undefined)
65
+ return activity.agentMessaged.agentMessage ?? 'Agent message.';
66
+ if (activity.progressUpdated !== undefined) {
67
+ const title = activity.progressUpdated.title ?? '';
68
+ const description = activity.progressUpdated.description ?? '';
69
+ if (title.length > 0 && description.length > 0)
70
+ return `${title}: ${description}`;
71
+ return title.length > 0 ? title : description;
72
+ }
73
+ if (activity.sessionCompleted !== undefined)
74
+ return 'Session completed.';
75
+ if (activity.sessionFailed !== undefined)
76
+ return `Session failed: ${activity.sessionFailed.reason ?? 'no reason reported'}`;
77
+ return activity.description ?? 'Unrecognized activity.';
78
+ }
79
+ /**
80
+ * Project one session into a list row.
81
+ * @param session - the wire session.
82
+ * @returns the row value.
83
+ */
84
+ export function sessionRow(session) {
85
+ return {
86
+ id: session.id ?? '',
87
+ title: session.title ?? '',
88
+ state: session.state ?? 'STATE_UNSPECIFIED',
89
+ url: session.url ?? '',
90
+ source: shortSourceName(session.sourceContext?.source),
91
+ branch: session.sourceContext?.githubRepoContext?.startingBranch ?? '',
92
+ createTime: session.createTime ?? '',
93
+ updateTime: session.updateTime ?? '',
94
+ pullRequests: (session.outputs ?? []).flatMap(output => output.pullRequest?.url ?? []),
95
+ };
96
+ }
97
+ /**
98
+ * Project one session into the full detail view.
99
+ * @param session - the wire session.
100
+ * @param activities - the session's event log, when it was read.
101
+ * @returns the detail value.
102
+ */
103
+ export function sessionDetail(session, activities = [], options = {}) {
104
+ const latestPlan = [...activities].reverse()
105
+ .find(activity => activity.planGenerated !== undefined)?.planGenerated?.plan;
106
+ const approved = activities.some(activity => activity.planApproved !== undefined);
107
+ // A plan is pending while the newest plan event is its generation.
108
+ const lastPlanIndex = activities.findLastIndex(activity => activity.planGenerated !== undefined);
109
+ const lastApprovalIndex = activities.findLastIndex(activity => activity.planApproved !== undefined);
110
+ const planPending = lastPlanIndex >= 0 && lastApprovalIndex < lastPlanIndex;
111
+ const latestCommand = [...activities].reverse()
112
+ .flatMap(activity => activity.artifacts ?? [])
113
+ .find(artifact => artifact.bashOutput?.command !== undefined)?.bashOutput?.command ?? '';
114
+ const pullRequests = (session.outputs ?? []).flatMap(output => output.pullRequest?.url ?? []);
115
+ const progress = [...activities].reverse()
116
+ .find(activity => activity.progressUpdated !== undefined)?.progressUpdated;
117
+ // A session that stops to ask something produces no plan and no progress, so
118
+ // without this the projection reads as an empty success.
119
+ const lastMessage = [...activities].reverse()
120
+ .find(activity => activity.agentMessaged !== undefined)?.agentMessaged?.agentMessage ?? '';
121
+ return {
122
+ id: session.id ?? '',
123
+ title: session.title ?? '',
124
+ state: session.state ?? 'STATE_UNSPECIFIED',
125
+ url: session.url ?? '',
126
+ prompt: session.prompt ?? '',
127
+ source: shortSourceName(session.sourceContext?.source),
128
+ branch: session.sourceContext?.githubRepoContext?.startingBranch ?? '',
129
+ // Evidence first, echo second. The session object omits these inputs on
130
+ // read, so 'unknown' beats a fabricated 'no'.
131
+ requirePlanApproval: session.requirePlanApproval === true
132
+ ? 'yes'
133
+ : approved || (latestPlan !== undefined && session.state === 'AWAITING_PLAN_APPROVAL')
134
+ ? 'yes'
135
+ : session.requirePlanApproval === false
136
+ ? 'no'
137
+ : 'unknown',
138
+ autoCreatePr: session.automationMode === 'AUTO_CREATE_PR' || pullRequests.length > 0
139
+ ? 'yes'
140
+ : session.automationMode === undefined || session.automationMode === 'AUTOMATION_MODE_UNSPECIFIED'
141
+ ? 'unknown'
142
+ : 'no',
143
+ archived: session.archived === true,
144
+ createTime: session.createTime ?? '',
145
+ updateTime: session.updateTime ?? '',
146
+ planId: latestPlan?.id ?? '',
147
+ planSteps: planLines(latestPlan),
148
+ planPending,
149
+ planApproved: approved,
150
+ pullRequests,
151
+ latestCommand,
152
+ generatedFiles: (session.generatedFiles ?? []).map(file => ({
153
+ path: file.path ?? '',
154
+ changeType: file.changeType ?? '',
155
+ bytes: (file.content ?? '').length,
156
+ })),
157
+ approvalConfirmed: options.approvalConfirmed === true,
158
+ logRead: options.logRead !== false,
159
+ lastMessage,
160
+ unchangedForMs: options.unchangedForMs ?? 0,
161
+ lastProgress: progress === undefined
162
+ ? ''
163
+ : [progress.title, progress.description].filter(part => part !== undefined && part.length > 0).join(': '),
164
+ };
165
+ }
166
+ /**
167
+ * Project one source into a row.
168
+ * @param source - the wire source.
169
+ * @returns the row value.
170
+ */
171
+ export function sourceRow(source) {
172
+ const repo = source.githubRepo;
173
+ return {
174
+ name: source.name ?? '',
175
+ owner: repo?.owner ?? '',
176
+ repo: repo?.repo ?? '',
177
+ isPrivate: repo?.isPrivate === true,
178
+ defaultBranch: repo?.defaultBranch?.displayName ?? '',
179
+ branches: (repo?.branches ?? []).flatMap(branch => branch.displayName ?? []),
180
+ };
181
+ }
182
+ /**
183
+ * Project one activity into a row.
184
+ * @param activity - the wire activity.
185
+ * @returns the row value.
186
+ */
187
+ export function activityRow(activity) {
188
+ return {
189
+ id: activity.id ?? '',
190
+ time: activity.createTime ?? '',
191
+ originator: activity.originator ?? '',
192
+ kind: activityKind(activity),
193
+ summary: activitySummary(activity),
194
+ artifacts: (activity.artifacts ?? []).map(artifactSummary),
195
+ };
196
+ }
197
+ /**
198
+ * Find the newest unified diff in an activity log.
199
+ *
200
+ * Activities are append-only, so later pages hold later patches; the last
201
+ * `changeSet` artifact is the state the session finished in.
202
+ * @param activities - the session's event log, oldest first.
203
+ * @returns the newest patch and its provenance, or undefined.
204
+ */
205
+ export function latestPatch(activities) {
206
+ for (let index = activities.length - 1; index >= 0; index -= 1) {
207
+ const changeSet = activities[index]?.artifacts?.find(artifact => artifact.changeSet !== undefined)?.changeSet;
208
+ if (changeSet !== undefined)
209
+ return changeSet.gitPatch;
210
+ }
211
+ return undefined;
212
+ }
213
+ /** Longest agent message the rendered text repeats before pointing at the log. */
214
+ const MESSAGE_DISPLAY_LIMIT = 1200;
215
+ /**
216
+ * Bound an agent message for display. The canonical value always keeps the full
217
+ * text; only the rendered card is clipped, because a scoping question can run to
218
+ * several paragraphs.
219
+ * @param text - the message to show.
220
+ * @returns the message, clipped with a pointer to the full log when long.
221
+ */
222
+ function clipForDisplay(text) {
223
+ return text.length <= MESSAGE_DISPLAY_LIMIT
224
+ ? text
225
+ : text.slice(0, MESSAGE_DISPLAY_LIMIT) + ' [clipped; read the full log with jules_activities]';
226
+ }
227
+ /**
228
+ * List the paths a unified diff touches.
229
+ *
230
+ * The service's `generatedFiles` manifest was empty on every session observed,
231
+ * so the diff itself is the only reliable statement of what changed.
232
+ * @param unidiff - the complete unified diff.
233
+ * @returns one entry per touched path, in diff order.
234
+ */
235
+ export function patchFiles(unidiff) {
236
+ const files = [];
237
+ for (const line of unidiff.split('\n')) {
238
+ if (!line.startsWith('diff --git '))
239
+ continue;
240
+ const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
241
+ const path = match?.[2];
242
+ if (path !== undefined && !files.includes(path))
243
+ files.push(path);
244
+ }
245
+ return files;
246
+ }
247
+ /** Render a session state with the action it asks for, when it asks for one. */
248
+ function stateLabel(state) {
249
+ if (state === 'AWAITING_PLAN_APPROVAL')
250
+ return 'AWAITING_PLAN_APPROVAL (approve or reject the plan)';
251
+ if (state === 'AWAITING_USER_FEEDBACK')
252
+ return 'AWAITING_USER_FEEDBACK (the agent asked a question)';
253
+ return state;
254
+ }
255
+ /**
256
+ * Render the source list.
257
+ * @param sources - projected rows.
258
+ * @returns the model-facing text.
259
+ */
260
+ export function renderSources(sources) {
261
+ if (sources.length === 0) {
262
+ return 'No repositories are connected to Jules. Install the Jules GitHub App at https://jules.google.com first.';
263
+ }
264
+ return sources.map((source) => {
265
+ const visibility = source.isPrivate ? 'private' : 'public';
266
+ const branch = source.defaultBranch.length > 0 ? ` default=${source.defaultBranch}` : '';
267
+ return `${source.owner}/${source.repo} (${visibility},${branch}) -> jules_create source=${source.owner}/${source.repo}`;
268
+ }).join('\n');
269
+ }
270
+ /**
271
+ * Render session rows as one line each.
272
+ * @param rows - projected rows.
273
+ * @param nextPageToken - continuation token, when the page was truncated.
274
+ * @returns the model-facing text.
275
+ */
276
+ export function renderSessionRows(rows, nextPageToken) {
277
+ const head = rows.length === 0 ? 'No Jules sessions matched.' : rows.map((row) => {
278
+ const parts = [`${row.id} ${row.state}`];
279
+ if (row.title.length > 0)
280
+ parts.push(row.title);
281
+ if (row.source.length > 0)
282
+ parts.push(shortSourceName(`sources/github/${row.source}`));
283
+ if (row.pullRequests.length > 0)
284
+ parts.push(row.pullRequests.join(' '));
285
+ if (row.updateTime.length > 0)
286
+ parts.push(`updated ${row.updateTime}`);
287
+ return parts.join(' | ');
288
+ }).join('\n');
289
+ return nextPageToken.length === 0 ? head : `${head}\n(nextPageToken: ${nextPageToken})`;
290
+ }
291
+ /**
292
+ * Render one session in full.
293
+ * @param detail - the projected detail.
294
+ * @returns the model-facing text.
295
+ */
296
+ export function renderSessionDetail(detail) {
297
+ const lines = [
298
+ `${detail.title.length > 0 ? detail.title : '(untitled session)'} [${detail.id}]`,
299
+ `state: ${stateLabel(detail.state)}`,
300
+ ];
301
+ if (!detail.logRead) {
302
+ // Without this the reader sees an empty plan, no approval, and no progress,
303
+ // and concludes the session is idle — the opposite of "we could not look".
304
+ lines.push('warning: the activity log could not be read, so plan, approval, latest command, and messages below are unknown rather than empty. Retry, or read it directly with jules_activities.');
305
+ }
306
+ if (detail.approvalConfirmed) {
307
+ // The phase and the approval disagree in both directions, and the event log
308
+ // is the one that is right.
309
+ lines.push('approval confirmed by the planApproved event in the log.');
310
+ if (detail.state === 'AWAITING_PLAN_APPROVAL') {
311
+ lines.push('note: the service still reports AWAITING_PLAN_APPROVAL — that field lags its own approval, so trust the event, not the phase.');
312
+ }
313
+ }
314
+ if (detail.unchangedForMs > 0) {
315
+ // Meets a polling agent where it actually is: the answer it just got is the
316
+ // one it already had.
317
+ lines.push(`note: nothing has changed since your last check ${Math.round(detail.unchangedForMs / 1000)}s ago.`
318
+ + ' Do not keep polling — a jules_watch notice is what tells you something happened.');
319
+ }
320
+ if (detail.url.length > 0)
321
+ lines.push(`url: ${detail.url}`);
322
+ if (detail.source.length > 0) {
323
+ lines.push(`repo: ${detail.source}${detail.branch.length > 0 ? ` (branch ${detail.branch})` : ''}`);
324
+ }
325
+ else {
326
+ lines.push('repo: none (repoless session)');
327
+ }
328
+ lines.push(`plan approval required: ${detail.requirePlanApproval}`);
329
+ lines.push(`auto-create PR: ${detail.autoCreatePr}`);
330
+ if (detail.createTime.length > 0)
331
+ lines.push(`created: ${detail.createTime}`);
332
+ if (detail.updateTime.length > 0)
333
+ lines.push(`updated: ${detail.updateTime}`);
334
+ if (detail.planSteps.length > 0) {
335
+ lines.push(`plan${detail.planPending ? ' (awaiting your approval)' : detail.planApproved ? ' (approved)' : ''}:`);
336
+ lines.push(...detail.planSteps.map(step => ` ${step}`));
337
+ }
338
+ if (detail.latestCommand.length > 0)
339
+ lines.push(`latest command: ${detail.latestCommand}`);
340
+ if (detail.pullRequests.length > 0)
341
+ lines.push(...detail.pullRequests.map(url => `pull request: ${url}`));
342
+ if (detail.generatedFiles.length > 0) {
343
+ lines.push(`generated files (${detail.generatedFiles.length}):`);
344
+ lines.push(...detail.generatedFiles.map(file => ` ${file.changeType.length > 0 ? file.changeType : 'changed'} ${file.path}`));
345
+ }
346
+ if (detail.lastMessage.length > 0) {
347
+ lines.push('latest agent message:');
348
+ lines.push(...clipForDisplay(detail.lastMessage).split('\n').map(line => ` ${line}`));
349
+ }
350
+ if (detail.lastProgress.length > 0)
351
+ lines.push(`latest progress: ${detail.lastProgress}`);
352
+ if (detail.prompt.length > 0)
353
+ lines.push(`prompt:\n ${detail.prompt.replaceAll('\n', '\n ')}`);
354
+ return lines.join('\n');
355
+ }
356
+ /**
357
+ * Render an activity page.
358
+ * @param rows - projected rows.
359
+ * @param nextPageToken - continuation token, when the page was truncated.
360
+ * @returns the model-facing text.
361
+ */
362
+ export function renderActivities(rows, nextPageToken) {
363
+ if (rows.length === 0)
364
+ return 'This session has no activities yet.';
365
+ const lines = rows.map((row) => {
366
+ const head = [row.time, row.kind, row.originator].filter(part => part.length > 0).join(' ');
367
+ const artifacts = row.artifacts.length === 0 ? '' : `\n artifacts: ${row.artifacts.join('; ')}`;
368
+ return `${head}\n ${row.summary.replaceAll('\n', '\n ')}${artifacts}`;
369
+ });
370
+ const body = lines.join('\n');
371
+ return nextPageToken.length === 0 ? body : `${body}\n(nextPageToken: ${nextPageToken})`;
372
+ }
373
+ /**
374
+ * Render a wait result.
375
+ * @param result - the canonical wait value.
376
+ * @returns the model-facing text.
377
+ */
378
+ export function renderWait(result) {
379
+ const outcome = result.timedOut
380
+ ? `still ${result.state} after ${Math.round(result.waitedMs / 1000)}s`
381
+ : `reached ${stateLabel(result.state)} after ${Math.round(result.waitedMs / 1000)}s`;
382
+ const lines = [`session ${result.id}: ${outcome}`];
383
+ if (result.lastMessage.length > 0) {
384
+ lines.push('latest agent message:');
385
+ lines.push(...clipForDisplay(result.lastMessage).split('\n').map(line => ` ${line}`));
386
+ }
387
+ if (result.lastProgress.length > 0)
388
+ lines.push(`latest progress: ${result.lastProgress}`);
389
+ if (result.planSteps.length > 0) {
390
+ lines.push('plan:');
391
+ lines.push(...result.planSteps.map(step => ` ${step}`));
392
+ }
393
+ if (result.pullRequests.length > 0)
394
+ lines.push(...result.pullRequests.map(url => `pull request: ${url}`));
395
+ if (result.state === 'AWAITING_PLAN_APPROVAL') {
396
+ lines.push(`next: call jules_approve_plan with session "${result.id}" to let the agent start, or jules_send_message to change the plan.`);
397
+ }
398
+ else if (result.state === 'AWAITING_USER_FEEDBACK') {
399
+ lines.push(`next: read jules_activities for session "${result.id}" and answer with jules_send_message.`);
400
+ }
401
+ else if (result.state === 'FAILED') {
402
+ lines.push(`next: read jules_activities for session "${result.id}" to see the failure reason.`);
403
+ }
404
+ else if (result.lastMessage.length > 0 && !result.settled) {
405
+ lines.push(`next: the agent posted a message — answer it with jules_send_message for session "${result.id}", and watch again if the work is still running.`);
406
+ }
407
+ else if (result.timedOut) {
408
+ lines.push(`next: call jules_wait or jules_status again for session "${result.id}".`);
409
+ }
410
+ else if (result.pullRequests.length === 0) {
411
+ lines.push(`next: call jules_patch with session "${result.id}" to read the change, or jules_status for the full report.`);
412
+ }
413
+ return lines.join('\n');
414
+ }
415
+ /**
416
+ * Render the newest patch, or explain why there is none.
417
+ * @param result - the canonical patch value.
418
+ * @returns the model-facing text.
419
+ */
420
+ export function renderPatch(result) {
421
+ if (!result.found) {
422
+ return `Session ${result.id} has no code change yet. Call jules_wait or jules_status first; a changeSet artifact appears only once the agent edits files.`;
423
+ }
424
+ const lines = [];
425
+ if (result.baseCommitId.length > 0)
426
+ lines.push(`base commit: ${result.baseCommitId}`);
427
+ if (result.suggestedCommitMessage.length > 0)
428
+ lines.push(`suggested commit message: ${result.suggestedCommitMessage}`);
429
+ if (result.files.length > 0) {
430
+ lines.push(`files (${result.files.length}):`);
431
+ lines.push(...result.files.map(file => ` ${file}`));
432
+ }
433
+ if (result.truncated) {
434
+ lines.push(`(showing bytes ${result.offset}-${result.offset + result.patch.length} of ${result.bytes}; continue with offset=${result.nextOffset})`);
435
+ }
436
+ lines.push(result.patch);
437
+ return lines.join('\n');
438
+ }
package/lib/watch.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Background watching for Jules sessions.
3
+ *
4
+ * A Jules session runs for minutes in Google's cloud, so a turn that blocks on
5
+ * one wastes the model's time. `jules_watch` instead registers the wait with
6
+ * `ctx.jobs`: the call returns a job id immediately, the harness keeps polling
7
+ * out of band, and `dsh-tool-jobs` delivers a completion notice to the owning
8
+ * agent when the session finishes, fails, or stops to ask something. The model
9
+ * reads the report with `job_output` on a later step.
10
+ *
11
+ * The session state alone is not a completion signal. Jules can post its final
12
+ * answer as an `agentMessaged` activity and then leave `state` sitting at
13
+ * `IN_PROGRESS` indefinitely, so the watcher treats the append-only activity
14
+ * log as authoritative and settles on what the agent did, not only on what the
15
+ * state field says.
16
+ *
17
+ * @module dsh-plugin-jules/watch
18
+ */
19
+ import type { Context } from '@deepseek-ai/cordis';
20
+ import type { JulesClient } from './client.ts';
21
+ import type { WatchJournal } from './journal.ts';
22
+ import type { WaitResult } from './views.ts';
23
+ declare module '@deepseek-ai/dsh-jobs' {
24
+ interface JobKindMap {
25
+ jules: 'jules';
26
+ }
27
+ }
28
+ /** Bounds the watcher applies to its own work. */
29
+ export interface JulesWatchConfig {
30
+ /** Delay between two status polls. */
31
+ pollIntervalMs: number;
32
+ /** Watch budget used when the caller does not choose one. */
33
+ defaultMs: number;
34
+ /** Largest watch budget a caller may request. */
35
+ maxMs: number;
36
+ /** Activity pages one report may walk. */
37
+ maxActivityPages: number;
38
+ /** End the watch when the agent posts a message, not only on a state change. */
39
+ settleOnMessage: boolean;
40
+ }
41
+ /** Why a watch stopped. */
42
+ export type WatchTrigger = 'state' | 'message' | 'completed' | 'failed' | 'timeout';
43
+ /** What one finished watch observed. */
44
+ export interface WatchOutcome {
45
+ /** Why the watch stopped. */
46
+ trigger: WatchTrigger;
47
+ /** Short status detail for the job snapshot status line. */
48
+ detail: string;
49
+ /** The full report the model reads back with `job_output`. */
50
+ report: string;
51
+ /**
52
+ * The structured result. `jules_wait` returns this directly, so the
53
+ * foreground and background paths can never drift apart in what they decide
54
+ * or in what they report.
55
+ */
56
+ wait: WaitResult;
57
+ }
58
+ /** Polling rules for one watch. */
59
+ export type WatchOptions = Pick<JulesWatchConfig, 'pollIntervalMs' | 'maxActivityPages' | 'settleOnMessage'>;
60
+ /**
61
+ * Poll one session until the agent hands something back, then describe it.
62
+ *
63
+ * Separated from the job adapter so the settling rules — which states end a
64
+ * watch, which activities end it, when the budget expires, and what the report
65
+ * contains — are testable without a live agent owner or a real job registry.
66
+ * @param client - the configured Jules client.
67
+ * @param session - bare session id.
68
+ * @param targets - states that end the watch.
69
+ * @param budgetMs - total watch budget.
70
+ * @param config - polling rules.
71
+ * @param signal - task-owned cancellation signal.
72
+ * @returns why the watch stopped and the rendered report.
73
+ * @throws whatever the client throws, including cancellation.
74
+ */
75
+ export declare function runWatch(client: JulesClient, session: string, targets: ReadonlySet<string>, budgetMs: number, config: WatchOptions, signal: AbortSignal): Promise<WatchOutcome>;
76
+ /**
77
+ * Register `jules_watch`.
78
+ *
79
+ * Registered through `ctx.inject(['jobs'])` rather than a plugin-level
80
+ * injection, so a composition without the job registry still gets the rest of
81
+ * the tool family instead of holding the whole plugin pending forever.
82
+ * @param ctx - plugin context supplying the job registry.
83
+ * @param client - the configured Jules client.
84
+ * @param config - watch budgets and polling bounds.
85
+ * @param journal - durable record of armed watches, when storage is mounted.
86
+ */
87
+ export declare function registerJulesWatch(ctx: Context, client: JulesClient, config: JulesWatchConfig, journal: () => WatchJournal | undefined, live: Set<string>): void;