@roodriigoooo/pi-docket 0.4.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +21 -0
  3. package/README.md +241 -0
  4. package/assets/docket_logo.jpeg +0 -0
  5. package/docs/adr/0001-bundle-first-checkpoints.md +21 -0
  6. package/docs/adr/0002-rename-to-docket.md +44 -0
  7. package/docs/architecture.md +101 -0
  8. package/docs/bundle-guidelines.md +39 -0
  9. package/docs/configuration.md +191 -0
  10. package/docs/releases/0.4.0.md +93 -0
  11. package/extensions/artifact-catalog.ts +467 -0
  12. package/extensions/background-work.ts +510 -0
  13. package/extensions/checkpoint-commands.ts +147 -0
  14. package/extensions/checkpoint-lifecycle.ts +195 -0
  15. package/extensions/checkpoint-selector.ts +162 -0
  16. package/extensions/checkpoint-store.ts +230 -0
  17. package/extensions/checkpoint-summarizer.ts +141 -0
  18. package/extensions/docket-command-grammar.ts +319 -0
  19. package/extensions/docket-command-router.ts +626 -0
  20. package/extensions/docket-config.ts +88 -0
  21. package/extensions/docket-extension-surface.ts +43 -0
  22. package/extensions/docket-navigator.ts +585 -0
  23. package/extensions/docket.README.md +46 -0
  24. package/extensions/docket.ts +2965 -0
  25. package/extensions/event-log.ts +121 -0
  26. package/extensions/git-context.ts +44 -0
  27. package/extensions/loaded-artifact-context.ts +228 -0
  28. package/extensions/search-index.ts +140 -0
  29. package/extensions/types.ts +40 -0
  30. package/extensions/worker-activity.ts +402 -0
  31. package/extensions/worker-changes.ts +180 -0
  32. package/extensions/worker-commands.ts +251 -0
  33. package/extensions/worker-dock-cache.ts +147 -0
  34. package/extensions/worker-events.ts +87 -0
  35. package/extensions/worker-eviction.ts +55 -0
  36. package/extensions/worker-guardrails.md +125 -0
  37. package/extensions/worker-kinds/patcher.md +23 -0
  38. package/extensions/worker-kinds/scout.md +17 -0
  39. package/extensions/worker-kinds.ts +280 -0
  40. package/extensions/worker-result.ts +193 -0
  41. package/extensions/worker-store.ts +621 -0
  42. package/extensions/worker-summary-embed.ts +98 -0
  43. package/package.json +53 -0
@@ -0,0 +1,626 @@
1
+ import { deriveWorkerState, workerQuestions, workerSourceLabel, workerStatusArtifact, workerSummaryName, type WorkerStatus } from "./background-work.js";
2
+ import { workerResultArtifact, workerResultText } from "./worker-result.js";
3
+ import { isSharedSessionTarget, SHARED_TMUX_SESSION, workerInProject } from "./worker-store.js";
4
+ import type { CheckpointCommands } from "./checkpoint-commands.js";
5
+ import type { CheckpointStore, CheckpointSummary } from "./checkpoint-store.js";
6
+ import type { ArtifactCatalog } from "./artifact-catalog.js";
7
+ import type { LoadedArtifactContext, LoadResult } from "./loaded-artifact-context.js";
8
+ import type { NavigatorMode } from "./docket-navigator.js";
9
+ import type { CheckpointCreateOptions, DocketIntent } from "./docket-command-grammar.js";
10
+ import type { Artifact, CheckpointIndexEntry } from "./types.js";
11
+ import type { WorkerCommands } from "./worker-commands.js";
12
+ import { workerChangeSetArtifact } from "./worker-changes.js";
13
+ import type { WorkerStore } from "./worker-store.js";
14
+
15
+ export type DocketBrowserAction = { action: "inspect" | "openFile" | "promoteWorker" | "reference" | "injectFull" | "copy" | "save" | "search" | "tellWorker" | "verdict"; artifact?: Artifact };
16
+
17
+ export type DocketVerdictAction = {
18
+ verb: "accept" | "reject" | "rejectStop" | "chat" | "diff" | "send";
19
+ worker: WorkerStatus;
20
+ changeSet?: Artifact;
21
+ text?: string;
22
+ };
23
+
24
+ export type ParallelWorkEntry = {
25
+ worker: WorkerStatus;
26
+ artifact: Artifact;
27
+ };
28
+
29
+ export type ParallelWorkAction =
30
+ | { action: "peek"; entry: ParallelWorkEntry }
31
+ | { action: "details" | "load" | "copyAttach" | "answers" | "tell" | "stop"; worker: WorkerStatus }
32
+ | null;
33
+
34
+ export type LoadPickerMode = "checkpoint" | "worker";
35
+ export type LoadPickerSelection =
36
+ | { kind: "checkpoint"; action: "load" | "preview"; summary: CheckpointSummary }
37
+ | { kind: "worker"; action: "load"; worker: WorkerStatus }
38
+ | null;
39
+
40
+ type NotifyLevel = "info" | "warning" | "error";
41
+ type DocketMessageKind = "notice" | "success" | "error" | "usage" | "list" | "action" | "help";
42
+
43
+ export type DocketCommandRouterDeps = {
44
+ hasUI: boolean;
45
+ workerId?: string;
46
+ projectRoot?: string;
47
+ workerCommands: WorkerCommands;
48
+ checkpointCommands: CheckpointCommands;
49
+ loadedArtifacts: LoadedArtifactContext;
50
+ workerStore: WorkerStore;
51
+ checkpointStore: CheckpointStore;
52
+ notify(text: string, level: NotifyLevel): void;
53
+ emitText(text: string, kind: DocketMessageKind, heading?: string): void;
54
+ announce(subject: string, detail?: string, kind?: DocketMessageKind): void;
55
+ docketUsage(advanced?: boolean): string;
56
+ renderArtifactList(artifacts: Artifact[]): string;
57
+ renderParallelWorkList(workers: WorkerStatus[], artifactsByWorker: Map<string, Artifact[]>, options?: { groupByProject?: boolean }): string;
58
+ formatArtifact(artifact: Artifact): string;
59
+ refreshChipWidget(): void;
60
+ refreshWorkerDockWidget(): Promise<void>;
61
+ refreshWorkerCarryoverForReview(): Promise<void>;
62
+ showWorkerResult(worker: WorkerStatus, artifacts: Artifact[], expanded: boolean): void;
63
+ clearWorkerResult(): boolean;
64
+ markArtifactDone(artifact: Artifact): void;
65
+ promoteWorkerChangeSet(artifact: Artifact): Promise<boolean>;
66
+ applyWorkerState(state: "needs_input" | "ready" | "failed", text?: string): Promise<void>;
67
+ createCheckpoint(options: CheckpointCreateOptions): Promise<void>;
68
+ createHandoffCheckpoint(): Promise<void>;
69
+ catalog(): Promise<ArtifactCatalog>;
70
+ readWorkersWithArtifacts(options?: { allProjects?: boolean }): Promise<{ workers: WorkerStatus[]; artifactsByWorker: Map<string, Artifact[]> }>;
71
+ showParallelWorkDashboard(workers: WorkerStatus[], artifactsByWorker: Map<string, Artifact[]>, options?: { groupByProject?: boolean }): Promise<ParallelWorkAction>;
72
+ showLoadPicker(summaries: CheckpointSummary[], workers: WorkerStatus[], initialMode: LoadPickerMode): Promise<LoadPickerSelection>;
73
+ showText(title: string, text: string): Promise<void>;
74
+ showDocketBrowser(catalog: ArtifactCatalog, artifacts: Artifact[], initialMode: NavigatorMode): Promise<DocketBrowserAction | null>;
75
+ showVerdict(worker: WorkerStatus, remaining?: number): Promise<DocketVerdictAction | null>;
76
+ showArtifact(catalog: ArtifactCatalog, artifact: Artifact): Promise<void>;
77
+ openFileOrArtifact(catalog: ArtifactCatalog, artifact: Artifact): Promise<void>;
78
+ input(title: string, placeholder: string): Promise<string | undefined>;
79
+ confirmDeleteWorker(worker: WorkerStatus): Promise<boolean>;
80
+ copyText(text: string): Promise<boolean>;
81
+ announceChipChange(artifact: Artifact, mode: "ref" | "full", result: ReturnType<LoadedArtifactContext["toggleChip"]>): void;
82
+ parallelKindLabel(kind: Artifact["kind"]): string;
83
+ };
84
+
85
+ export function buildAttachCommand(target: string): string {
86
+ if (isSharedSessionTarget(target)) {
87
+ const window = target.split(":")[1] ?? "";
88
+ return window ? `tmux attach -t ${SHARED_TMUX_SESSION} \\; select-window -t ${window}` : `tmux attach -t ${SHARED_TMUX_SESSION}`;
89
+ }
90
+ return `tmux attach -t ${target}`;
91
+ }
92
+
93
+ function docketMetaString(artifact: Artifact, key: string): string | undefined {
94
+ const value = artifact.meta?.[key];
95
+ return typeof value === "string" && value.length > 0 ? value : undefined;
96
+ }
97
+
98
+ function artifactWorkerRef(artifact: Artifact): string | undefined {
99
+ const label = artifact.meta?.workerLabel;
100
+ if (typeof label === "string" && label.length > 0) return label;
101
+ return artifact.source;
102
+ }
103
+
104
+ function loadResultSubject(result: LoadResult): string {
105
+ const slot = result.slot;
106
+ return `loaded ${slot.slot} · ${slot.artifacts.length} artifact${slot.artifacts.length === 1 ? "" : "s"}`;
107
+ }
108
+
109
+ function loadResultDetail(result: LoadResult): string {
110
+ const slot = result.slot;
111
+ if (result.source.kind === "worker") return `${workerSummaryName(result.source.worker)}\nattach: @${slot.slot}.<id>`;
112
+ const checkpoint = result.source.checkpoint;
113
+ const tag = result.queuedConsume ? "consume on session end" : `${checkpoint.mode} bundle`;
114
+ return `${checkpoint.id}\n${tag}\nrefs: @${slot.slot}.<id>`;
115
+ }
116
+
117
+ export function createDocketCommandRouter(deps: DocketCommandRouterDeps) {
118
+ const announceLoadResult = (result: LoadResult): void => deps.announce(loadResultSubject(result), loadResultDetail(result), "success");
119
+
120
+ const showWorkerResult = async (ref: string, action: "show" | "use"): Promise<void> => {
121
+ const worker = await deps.workerStore.find(ref);
122
+ if (!worker) {
123
+ deps.notify("Docket worker not found", "error");
124
+ return;
125
+ }
126
+ if (action === "show") {
127
+ const artifacts = await deps.workerStore.readArtifacts(worker.id);
128
+ if (deps.hasUI) deps.showWorkerResult(worker, artifacts, true);
129
+ else deps.emitText(workerResultText(worker, artifacts), "list", `docket · ${workerSourceLabel(worker)}`);
130
+ return;
131
+ }
132
+ const result = await deps.loadedArtifacts.loadSource({ kind: "worker", worker });
133
+ const artifact = workerResultArtifact(worker, result.slot.artifacts);
134
+ if (!artifact) {
135
+ deps.notify(`No result yet for ${workerSourceLabel(worker)}`, "warning");
136
+ return;
137
+ }
138
+ const chipResult = deps.loadedArtifacts.toggleChip(artifact, "ref");
139
+ deps.refreshChipWidget();
140
+ deps.showWorkerResult(worker, result.slot.artifacts, false);
141
+ deps.announceChipChange(artifact, "ref", chipResult);
142
+ await deps.refreshWorkerDockWidget();
143
+ };
144
+
145
+ const tellWorker = async (ref: string, text?: string, artifact?: Artifact): Promise<void> => {
146
+ const trimmed = text?.trim();
147
+ if (trimmed) {
148
+ await deps.workerCommands.tell(ref, trimmed);
149
+ await deps.refreshWorkerDockWidget();
150
+ return;
151
+ }
152
+ if (!deps.hasUI) {
153
+ deps.notify("Usage: /docket tell w<N> <text>", "error");
154
+ return;
155
+ }
156
+ const worker = await deps.workerStore.find(ref);
157
+ const label = worker ? workerSourceLabel(worker) : ref;
158
+ const questions = worker ? workerQuestions(worker).map((question, index) => `${index + 1}. ${question.text}`).join("\n") : undefined;
159
+ const placeholder = artifact ? docketMetaString(artifact, "question") ?? artifact.title : questions;
160
+ const message = (await deps.input(`Tell ${label}`, placeholder ?? "instruction, answer, or follow-up"))?.trim();
161
+ if (!message) return;
162
+ await deps.workerCommands.tell(ref, message);
163
+ await deps.refreshWorkerDockWidget();
164
+ };
165
+
166
+ const projectWorker = (worker: WorkerStatus): boolean => !deps.projectRoot || workerInProject(worker, deps.projectRoot);
167
+
168
+ const workerHasChangeSet = (worker: WorkerStatus): Artifact | undefined => {
169
+ const state = deriveWorkerState(worker);
170
+ if (state !== "ready" && state !== "ready_open_todos") return undefined;
171
+ return workerChangeSetArtifact(worker);
172
+ };
173
+
174
+ const verdictCandidateRank = (worker: WorkerStatus): number => {
175
+ // Rank on cheap derived state only — never stage/diff a worktree here. The change set is
176
+ // computed lazily for the single chosen worker when the card opens (showWorkerVerdict),
177
+ // so ranking N ready workers costs zero git calls instead of one stage+diff per worker.
178
+ const state = deriveWorkerState(worker);
179
+ if (state === "needs_input") return 0;
180
+ if (state === "failed") return 1;
181
+ if (state === "ready" || state === "ready_open_todos") return 2;
182
+ return 100;
183
+ };
184
+
185
+ const rankedVerdictWorkers = async (exclude?: Set<string>): Promise<WorkerStatus[]> => {
186
+ const workers = await deps.workerStore.list({ ...(deps.projectRoot ? { projectRoot: deps.projectRoot } : {}) });
187
+ return workers
188
+ .filter((worker) => !exclude?.has(worker.id))
189
+ .map((worker) => ({ worker, rank: verdictCandidateRank(worker) }))
190
+ .filter((entry) => entry.rank < 100)
191
+ .sort((a, b) => a.rank - b.rank || Date.parse(b.worker.updatedAt) - Date.parse(a.worker.updatedAt))
192
+ .map((entry) => entry.worker);
193
+ };
194
+
195
+ const findVerdictWorker = async (ref?: string): Promise<WorkerStatus | undefined> => {
196
+ if (ref) {
197
+ const worker = await deps.workerStore.find(ref);
198
+ return worker && projectWorker(worker) ? worker : undefined;
199
+ }
200
+ return (await rankedVerdictWorkers())[0];
201
+ };
202
+
203
+ const runVerdict = async (worker: WorkerStatus, remaining = 0): Promise<"advance" | "stop"> => {
204
+ if (!deps.hasUI) {
205
+ deps.notify("Docket verdict needs UI. Use /docket tell, /docket load, or /docket delete.", "error");
206
+ return "stop";
207
+ }
208
+ while (true) {
209
+ const result = await deps.showVerdict(worker, remaining);
210
+ if (!result) return "stop";
211
+ const latest = await deps.workerStore.find(result.worker.id) ?? result.worker;
212
+ const label = workerSourceLabel(latest);
213
+ const state = deriveWorkerState(latest);
214
+ const changeSet = result.changeSet ?? workerHasChangeSet(latest);
215
+ const statusArtifact = workerStatusArtifact(latest);
216
+ if (result.verb === "diff") {
217
+ if (changeSet) await deps.showText(`${label} · full diff`, deps.formatArtifact(changeSet));
218
+ continue;
219
+ }
220
+ if (result.verb === "send") {
221
+ if (result.text) await deps.workerCommands.tell(label, result.text);
222
+ await deps.refreshWorkerDockWidget();
223
+ return "advance";
224
+ }
225
+ if (result.verb === "rejectStop") {
226
+ if (!(await deps.confirmDeleteWorker(latest))) continue;
227
+ await deps.workerCommands.delete(label);
228
+ await deps.refreshWorkerDockWidget();
229
+ return "advance";
230
+ }
231
+ if (result.verb === "chat") {
232
+ const text = (await deps.input(`Chat ${label}`, "message to worker"))?.trim();
233
+ if (!text) continue;
234
+ await deps.workerCommands.tell(label, changeSet ? `revise: ${text}` : text);
235
+ await deps.refreshWorkerDockWidget();
236
+ return "advance";
237
+ }
238
+ if (result.verb === "accept") {
239
+ if (state === "needs_input") await deps.workerCommands.tell(label, "Approved. Proceed.");
240
+ else if (state === "failed") await deps.workerCommands.respawn(label);
241
+ else if (changeSet) {
242
+ if (await deps.promoteWorkerChangeSet(changeSet)) deps.markArtifactDone(changeSet);
243
+ } else if (statusArtifact) deps.markArtifactDone(statusArtifact);
244
+ await deps.refreshWorkerDockWidget();
245
+ return "advance";
246
+ }
247
+ if (result.verb === "reject") {
248
+ if (state === "needs_input") {
249
+ const text = (await deps.input(`Reject ${label}`, "what should the worker do instead?"))?.trim();
250
+ if (!text) continue;
251
+ await deps.workerCommands.tell(label, text);
252
+ } else if (changeSet) deps.markArtifactDone(changeSet);
253
+ else if (statusArtifact) deps.markArtifactDone(statusArtifact);
254
+ await deps.refreshWorkerDockWidget();
255
+ return "advance";
256
+ }
257
+ }
258
+ };
259
+
260
+ const runVerdictQueue = async (first: WorkerStatus): Promise<void> => {
261
+ const resolved = new Set<string>();
262
+ let current: WorkerStatus | undefined = first;
263
+ while (current) {
264
+ const others = (await rankedVerdictWorkers(resolved)).filter((entry) => entry.id !== current!.id);
265
+ const outcome = await runVerdict(current, others.length);
266
+ if (outcome === "stop") return;
267
+ resolved.add(current.id);
268
+ current = (await rankedVerdictWorkers(resolved))[0];
269
+ }
270
+ };
271
+
272
+ return {
273
+ async handle(intent: DocketIntent): Promise<void> {
274
+ if (intent.kind === "help") {
275
+ deps.emitText(deps.docketUsage(intent.advanced === true), "help", intent.advanced === true ? "docket · help advanced" : "docket · help");
276
+ return;
277
+ }
278
+
279
+ if (intent.kind === "clear") {
280
+ const had = deps.loadedArtifacts.clearChips();
281
+ deps.refreshChipWidget();
282
+ const hadWorkerResult = deps.clearWorkerResult();
283
+ deps.notify(had || hadWorkerResult ? "Docket cleared" : "Docket had no chips", "info");
284
+ return;
285
+ }
286
+
287
+ if (intent.kind === "tell") {
288
+ await tellWorker(intent.worker, intent.text);
289
+ return;
290
+ }
291
+
292
+ if (intent.kind === "verdict") {
293
+ const worker = await findVerdictWorker(intent.worker);
294
+ if (!worker) {
295
+ deps.notify("Docket worker needing verdict not found", "warning");
296
+ return;
297
+ }
298
+ if (intent.worker) await runVerdict(worker);
299
+ else await runVerdictQueue(worker);
300
+ return;
301
+ }
302
+
303
+ if (intent.kind === "attach") {
304
+ let target = `${SHARED_TMUX_SESSION}:`;
305
+ if (intent.worker) {
306
+ const worker = await deps.workerStore.find(intent.worker);
307
+ if (!worker) {
308
+ deps.notify("Docket worker not found", "error");
309
+ return;
310
+ }
311
+ target = worker.tmuxSession;
312
+ }
313
+ const command = buildAttachCommand(target);
314
+ const copied = await deps.copyText(command);
315
+ deps.notify(copied ? `Copied: ${command}` : command, copied ? "info" : "warning");
316
+ return;
317
+ }
318
+
319
+ if (intent.kind === "worker-state") {
320
+ if (!deps.workerId) {
321
+ deps.notify("Worker state commands only run inside a Docket worker", "warning");
322
+ return;
323
+ }
324
+ await deps.applyWorkerState(intent.state, intent.text);
325
+ return;
326
+ }
327
+
328
+ if (intent.kind === "save") {
329
+ await deps.createCheckpoint(intent.options);
330
+ return;
331
+ }
332
+
333
+ if (intent.kind === "delete") {
334
+ if (intent.targetKind === "worker") {
335
+ await deps.workerCommands.delete(intent.target);
336
+ await deps.refreshWorkerDockWidget();
337
+ } else await deps.checkpointCommands.delete(intent.target);
338
+ return;
339
+ }
340
+
341
+ if (intent.kind === "list") {
342
+ if (intent.workers === true) await deps.workerCommands.list({ allProjects: intent.allProjects === true });
343
+ else await deps.checkpointCommands.list(intent.includeConsumed === true);
344
+ return;
345
+ }
346
+
347
+ if (intent.kind === "spawn") {
348
+ await deps.workerCommands.spawn(intent.task, { worktree: intent.worktree === true, fresh: intent.fresh === true, ...(intent.as ? { as: intent.as } : {}) });
349
+ await deps.refreshWorkerDockWidget();
350
+ return;
351
+ }
352
+
353
+ if (intent.kind === "kinds") {
354
+ await deps.workerCommands.listKinds();
355
+ return;
356
+ }
357
+
358
+ if (intent.kind === "respawn") {
359
+ await deps.workerCommands.respawn(intent.target);
360
+ await deps.refreshWorkerDockWidget();
361
+ return;
362
+ }
363
+
364
+ if (intent.kind === "workers") {
365
+ const { workers, artifactsByWorker } = await deps.readWorkersWithArtifacts({ allProjects: intent.allProjects === true });
366
+ const groupByProject = intent.allProjects === true;
367
+ if (!deps.hasUI) {
368
+ deps.emitText(deps.renderParallelWorkList(workers, artifactsByWorker, { groupByProject }), "list", "docket · parallel work");
369
+ return;
370
+ }
371
+ while (true) {
372
+ const result = await deps.showParallelWorkDashboard(workers, artifactsByWorker, { groupByProject });
373
+ if (!result) return;
374
+ if (result.action === "peek") {
375
+ await deps.showText(`${workerSourceLabel(result.entry.worker)} · ${deps.parallelKindLabel(result.entry.artifact.kind)}`, deps.formatArtifact(result.entry.artifact));
376
+ continue;
377
+ }
378
+ if (result.action === "details") {
379
+ await deps.showText(`${workerSourceLabel(result.worker)} · details`, workerResultText(result.worker, artifactsByWorker.get(result.worker.id) ?? []));
380
+ continue;
381
+ }
382
+ if (result.action === "load") {
383
+ announceLoadResult(await deps.loadedArtifacts.loadSource({ kind: "worker", worker: result.worker }));
384
+ await deps.refreshWorkerDockWidget();
385
+ return;
386
+ }
387
+ if (result.action === "copyAttach") {
388
+ const command = buildAttachCommand(result.worker.tmuxSession);
389
+ const copied = await deps.copyText(command);
390
+ deps.notify(copied ? `Copied: ${command}` : command, copied ? "info" : "warning");
391
+ return;
392
+ }
393
+ if (result.action === "tell") {
394
+ await tellWorker(workerSourceLabel(result.worker));
395
+ return;
396
+ }
397
+ if (result.action === "stop") {
398
+ await deps.workerCommands.delete(workerSourceLabel(result.worker));
399
+ await deps.refreshWorkerDockWidget();
400
+ return;
401
+ }
402
+ if (result.action === "answers") {
403
+ const loadResult = await deps.loadedArtifacts.loadSource({ kind: "worker", worker: result.worker });
404
+ await deps.refreshWorkerDockWidget();
405
+ const answers = loadResult.slot.artifacts.filter((artifact) => artifact.kind === "response");
406
+ if (answers.length === 0) {
407
+ deps.notify(`No answers yet for ${workerSourceLabel(result.worker)}`, "info");
408
+ return;
409
+ }
410
+ await deps.showDocketBrowser(await deps.catalog(), answers, "answers");
411
+ return;
412
+ }
413
+ }
414
+ }
415
+
416
+ if (intent.kind === "load") {
417
+ if (intent.refKind === "worker") {
418
+ await deps.workerCommands.load(intent.ref);
419
+ await deps.refreshWorkerDockWidget();
420
+ return;
421
+ }
422
+
423
+ const opts = { includeConsumed: intent.includeConsumed === true };
424
+ let source: Parameters<LoadedArtifactContext["loadSource"]>[0] | undefined;
425
+ if (intent.ref) {
426
+ const checkpoint = await deps.checkpointStore.find(intent.ref, opts);
427
+ if (!checkpoint) {
428
+ deps.notify("Docket bundle not found", "error");
429
+ return;
430
+ }
431
+ source = { kind: "checkpoint", checkpoint };
432
+ } else {
433
+ const [summaries, workers] = await Promise.all([
434
+ deps.checkpointStore.listSummaries(opts),
435
+ deps.workerStore.list({ ...(deps.projectRoot ? { projectRoot: deps.projectRoot } : {}) }),
436
+ ]);
437
+ if (summaries.length === 0 && workers.length === 0) {
438
+ deps.notify("Docket has nothing to load — try /docket save or /docket spawn", "error");
439
+ return;
440
+ }
441
+ if (!deps.hasUI) {
442
+ source = deps.loadedArtifacts.defaultLoadSource({ checkpoints: summaries.map((summary) => summary.entry), workers });
443
+ } else {
444
+ const initial: LoadPickerMode = summaries.length > 0 ? "checkpoint" : "worker";
445
+ while (true) {
446
+ const selected = await deps.showLoadPicker(summaries, workers, initial);
447
+ if (!selected) {
448
+ deps.notify("Docket load cancelled", "info");
449
+ return;
450
+ }
451
+ if (selected.kind === "worker") {
452
+ source = { kind: "worker", worker: selected.worker };
453
+ break;
454
+ }
455
+ if (selected.action === "preview") {
456
+ const md = await deps.checkpointStore.readMarkdown(selected.summary.entry);
457
+ await deps.showText(`Docket checkpoint ${selected.summary.entry.id}`, md);
458
+ continue;
459
+ }
460
+ source = { kind: "checkpoint", checkpoint: selected.summary.entry };
461
+ break;
462
+ }
463
+ }
464
+ }
465
+ if (!source) return;
466
+ try {
467
+ const result = await deps.loadedArtifacts.loadSource(source);
468
+ announceLoadResult(result);
469
+ if (source.kind === "worker") await deps.refreshWorkerDockWidget();
470
+ } catch (err) {
471
+ deps.notify(`Docket load failed: ${String(err)}`, "error");
472
+ }
473
+ return;
474
+ }
475
+
476
+ if (intent.kind === "unload") {
477
+ if (intent.targetKind === "all") {
478
+ const slots = deps.loadedArtifacts.slots().map((entry) => entry.slot);
479
+ for (const slot of slots) deps.loadedArtifacts.unloadSlot(slot);
480
+ if (slots.length) deps.announce(`unloaded ${slots.length} slot${slots.length === 1 ? "" : "s"}`, slots.join(", "));
481
+ else deps.notify("Docket had no loaded slots", "info");
482
+ return;
483
+ }
484
+ if (intent.targetKind === "worker") {
485
+ await deps.workerCommands.unload(intent.target);
486
+ await deps.refreshWorkerDockWidget();
487
+ return;
488
+ }
489
+ const checkpoint = await deps.checkpointStore.find(intent.target, { includeConsumed: true });
490
+ const targetId = checkpoint?.id ?? intent.target;
491
+ const removed = deps.loadedArtifacts.unloadSource("checkpoint", targetId);
492
+ if (removed) deps.announce(`unloaded ${removed.slot}`, removed.sourceId);
493
+ else deps.notify("Docket checkpoint not loaded", "warning");
494
+ return;
495
+ }
496
+
497
+ const shouldBrowse = intent.kind === "browse" || intent.kind === "answers" || intent.kind === "search";
498
+ if (shouldBrowse) await deps.refreshWorkerCarryoverForReview();
499
+ const catalog = await deps.catalog();
500
+ let artifacts = catalog.list();
501
+ let initialMode: NavigatorMode = intent.kind === "browse" && intent.mode ? intent.mode : "review";
502
+
503
+ if (intent.kind === "answers") {
504
+ initialMode = "answers";
505
+ if (intent.query) artifacts = (await catalog.search(intent.query)).filter((artifact) => artifact.kind === "response");
506
+ else artifacts = artifacts.filter((artifact) => artifact.kind === "response");
507
+ if (artifacts.length === 0) {
508
+ deps.notify(intent.query ? `Docket answers found no matches for: ${intent.query}` : "Docket has no answers yet", "info");
509
+ return;
510
+ }
511
+ if (!deps.hasUI) {
512
+ deps.emitText(deps.renderArtifactList(artifacts), "list", intent.query ? `docket · answers "${intent.query}"` : "docket · answers");
513
+ return;
514
+ }
515
+ }
516
+
517
+ if (intent.kind === "search") {
518
+ initialMode = "log";
519
+ artifacts = await catalog.search(intent.query);
520
+ if (artifacts.length === 0) {
521
+ deps.notify(`Docket search found no artifacts for: ${intent.query}`, "info");
522
+ return;
523
+ }
524
+ if (!deps.hasUI) {
525
+ deps.emitText(deps.renderArtifactList(artifacts), "list", `docket · search "${intent.query}"`);
526
+ return;
527
+ }
528
+ }
529
+
530
+ if (intent.kind === "artifact") {
531
+ const artifact = catalog.find(intent.idOrRef);
532
+ if (!artifact) {
533
+ deps.notify("Docket artifact not found", "error");
534
+ return;
535
+ }
536
+ deps.markArtifactDone(artifact);
537
+ if (intent.action === "ref") {
538
+ const r = deps.loadedArtifacts.toggleChip(artifact, "ref");
539
+ deps.refreshChipWidget();
540
+ deps.announceChipChange(artifact, "ref", r);
541
+ } else if (intent.action === "inject-full") {
542
+ const r = deps.loadedArtifacts.toggleChip(artifact, "full");
543
+ deps.refreshChipWidget();
544
+ deps.announceChipChange(artifact, "full", r);
545
+ } else {
546
+ const ok = await deps.copyText(catalog.fullText(artifact));
547
+ deps.notify(ok ? `Docket copied ${artifact.id}` : "No clipboard command found", ok ? "info" : "warning");
548
+ }
549
+ return;
550
+ }
551
+
552
+ if (!deps.hasUI) {
553
+ deps.emitText(deps.renderArtifactList(artifacts), "list", `docket · ${initialMode}`);
554
+ return;
555
+ }
556
+
557
+ while (true) {
558
+ const result = await deps.showDocketBrowser(catalog, artifacts, initialMode);
559
+ if (!result) return;
560
+ if (result.action === "save") {
561
+ await deps.createHandoffCheckpoint();
562
+ return;
563
+ }
564
+ if (result.action === "search") {
565
+ const query = (await deps.input("Search Docket", "commands, errors, files, answers..."))?.trim();
566
+ if (!query) continue;
567
+ const matches = await catalog.search(query);
568
+ if (matches.length === 0) {
569
+ deps.notify(`Docket search found no artifacts for: ${query}`, "info");
570
+ continue;
571
+ }
572
+ artifacts = matches;
573
+ initialMode = "log";
574
+ continue;
575
+ }
576
+ if (result.action === "tellWorker" && result.artifact) {
577
+ const workerRef = artifactWorkerRef(result.artifact);
578
+ if (!workerRef) {
579
+ deps.notify("Docket worker not found for this item", "error");
580
+ continue;
581
+ }
582
+ await tellWorker(workerRef, undefined, result.artifact);
583
+ return;
584
+ }
585
+ if (result.action === "verdict" && result.artifact) {
586
+ const workerId = docketMetaString(result.artifact, "workerId") ?? artifactWorkerRef(result.artifact);
587
+ const worker = workerId ? await findVerdictWorker(workerId) : undefined;
588
+ if (!worker) {
589
+ deps.notify("Docket worker not found for this item", "error");
590
+ continue;
591
+ }
592
+ await runVerdict(worker);
593
+ return;
594
+ }
595
+ if (!result.artifact) return;
596
+ deps.markArtifactDone(result.artifact);
597
+ if (result.action === "inspect") {
598
+ await deps.showArtifact(catalog, result.artifact);
599
+ continue;
600
+ }
601
+ if (result.action === "openFile") {
602
+ await deps.openFileOrArtifact(catalog, result.artifact);
603
+ continue;
604
+ }
605
+ if (result.action === "promoteWorker") {
606
+ if (await deps.promoteWorkerChangeSet(result.artifact)) deps.markArtifactDone(result.artifact);
607
+ return;
608
+ }
609
+ const artifact = result.artifact;
610
+ if (result.action === "reference") {
611
+ const r = deps.loadedArtifacts.toggleChip(artifact, "ref");
612
+ deps.refreshChipWidget();
613
+ deps.announceChipChange(artifact, "ref", r);
614
+ } else if (result.action === "injectFull") {
615
+ const r = deps.loadedArtifacts.toggleChip(artifact, "full");
616
+ deps.refreshChipWidget();
617
+ deps.announceChipChange(artifact, "full", r);
618
+ } else if (result.action === "copy") {
619
+ const ok = await deps.copyText(catalog.fullText(artifact));
620
+ deps.notify(ok ? `Docket copied ${artifact.id}` : "No clipboard command found", ok ? "info" : "warning");
621
+ }
622
+ return;
623
+ }
624
+ },
625
+ };
626
+ }