@drkmttr/adhere 0.18.1 → 0.23.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/README.md +62 -1575
  2. package/package.json +18 -15
  3. package/presets/README.md +140 -0
  4. package/presets/alchemy/runtime/bind-tag-not-implementation/RULE.md +1 -0
  5. package/presets/effect/data/brand-meaningful-primitives/RULE.md +1 -0
  6. package/presets/effect/data/variants-are-tagged-unions/RULE.md +1 -0
  7. package/presets/typescript/types/derive-dont-restate/RULE.md +1 -0
  8. package/skills/adhere/SKILL.md +194 -29
  9. package/skills/adhere-fix/SKILL.md +1 -1
  10. package/skills/adhere-setup/SKILL.md +60 -26
  11. package/src/cli.ts +334 -36
  12. package/src/config.ts +173 -142
  13. package/src/create/agents.ts +149 -0
  14. package/src/create/ask.ts +363 -0
  15. package/src/create/examples.ts +94 -0
  16. package/src/create/look.ts +191 -0
  17. package/src/create/oxlint-trial.ts +464 -0
  18. package/src/create/oxlint.ts +1012 -0
  19. package/src/create/trial-review.ts +562 -0
  20. package/src/create/trial.ts +611 -0
  21. package/src/create/wizard.ts +466 -0
  22. package/src/index.ts +2 -0
  23. package/src/init.ts +171 -41
  24. package/src/install.ts +296 -94
  25. package/src/instructions.ts +98 -0
  26. package/src/markdown.ts +15 -11
  27. package/src/oxlint/catalog.json +7844 -0
  28. package/src/oxlint/catalog.ts +72 -0
  29. package/src/oxlint/shared.ts +140 -0
  30. package/src/readme-guide.ts +431 -0
  31. package/src/readme.ts +291 -0
  32. package/src/rules.ts +42 -45
  33. package/src/services/AdhereConfig.ts +67 -21
  34. package/src/services/AuditCache.ts +176 -169
  35. package/src/services/Credentials.ts +14 -8
  36. package/src/services/Jev.reads.test.ts +22 -0
  37. package/src/services/Jev.ts +35 -13
  38. package/src/services/SourceWalker.ts +12 -5
  39. package/src/wording.ts +4 -4
  40. package/src/workflows/audit.ts +85 -14
  41. package/src/workflows/format.ts +98 -1
  42. package/src/workspaces.test.ts +37 -1
  43. package/src/workspaces.ts +121 -12
@@ -0,0 +1,562 @@
1
+ /**
2
+ * The last step of `adhere rules create`: run the draft on real files before
3
+ * anything is written, let the authoring agent read the saved report, and let
4
+ * the user accept the draft, revise it from the evidence, or cancel.
5
+ *
6
+ * The trial is draft-only: nothing is installed, no rule is written, and the
7
+ * cache is read but never pruned. A skipped or failed trial leaves the draft
8
+ * explicitly untested; it never counts as validation.
9
+ */
10
+ import { type Agent } from "#create/agents.ts";
11
+ import { RuleNotCreated, asking, inspectionPrompt } from "#create/ask.ts";
12
+ import { line, note, said, spinning, warn } from "#create/look.ts";
13
+ import {
14
+ type TrialDraft,
15
+ type TrialFinding,
16
+ type TrialPreview,
17
+ type TrialRun,
18
+ type TrialScope,
19
+ previewTrial,
20
+ runTrial,
21
+ } from "#create/trial.ts";
22
+ import { Effect, type PlatformError, Terminal } from "effect";
23
+ import { Prompt } from "effect/cli";
24
+
25
+ /** How many files the trial reads unless the user picks another number. */
26
+ export const DEFAULT_TRIAL_FILES = 100;
27
+
28
+ /** A custom trial size: a positive whole number of files, or nothing. */
29
+ export const parseTrialFiles = (input: string): number | undefined => {
30
+ const count = Number(input);
31
+ return Number.isInteger(count) && count > 0 ? count : undefined;
32
+ };
33
+
34
+ /** What the user made of one finding. */
35
+ export type FindingVerdict = "correct" | "false-positive" | "unsure";
36
+
37
+ /** One finding the user judged, with the note they added when they did. */
38
+ export interface FindingReview {
39
+ readonly where: string;
40
+ readonly probability?: number;
41
+ readonly verdict: FindingVerdict;
42
+ readonly note?: string;
43
+ }
44
+
45
+ /**
46
+ * Whether a draft may run. A `RULE.ts` and a plugin are code, so the user
47
+ * approves them before the trial imports or loads them; a plugin's own tests
48
+ * run again after every revision.
49
+ */
50
+ export type Approval<D> =
51
+ | { readonly _tag: "Approved"; readonly draft: D }
52
+ | { readonly _tag: "Declined" }
53
+ /** The draft could not be checked here; the trial can be skipped. */
54
+ | { readonly _tag: "Unavailable"; readonly problems: ReadonlyArray<string> }
55
+ /** The draft failed its own checks; nothing should be written. */
56
+ | {
57
+ readonly _tag: "Failed";
58
+ readonly problems: ReadonlyArray<string>;
59
+ readonly reason?: string;
60
+ };
61
+
62
+ /**
63
+ * A draft the trial can run, and what the wizard needs to show, approve, and
64
+ * revise it. The caller owns the draft's real form; this module owns the loop.
65
+ */
66
+ export type TrialFailure = RuleNotCreated | PlatformError.PlatformError | Terminal.QuitError;
67
+
68
+ export interface TrialSubject<D, R = never> {
69
+ readonly agent: Agent;
70
+ readonly root: string;
71
+ readonly id: string;
72
+ readonly draft: D;
73
+ readonly toDraft: (draft: D) => TrialDraft;
74
+ readonly approve: (draft: D) => Effect.Effect<Approval<D>, TrialFailure, R>;
75
+ readonly revise: (draft: D, evidence: string) => Effect.Effect<D, TrialFailure, R>;
76
+ readonly show: (draft: D) => Effect.Effect<void>;
77
+ }
78
+
79
+ /** What the trial left the wizard with. Only `Accepted` carries fresh evidence. */
80
+ export type TrialOutcome<D> =
81
+ | {
82
+ readonly _tag: "Accepted";
83
+ readonly draft: D;
84
+ readonly run: TrialRun;
85
+ readonly reviews: ReadonlyArray<FindingReview>;
86
+ }
87
+ | { readonly _tag: "Untested"; readonly draft: D; readonly reason: string }
88
+ | { readonly _tag: "Cancelled"; readonly draft: D; readonly reason: string };
89
+
90
+ const messageOf = (cause: unknown): string => {
91
+ if (typeof cause === "object" && cause !== null && "message" in cause) {
92
+ const message = (cause as { readonly message: unknown }).message;
93
+ if (typeof message === "string") return message;
94
+ }
95
+ return cause instanceof Error ? cause.message : String(cause);
96
+ };
97
+
98
+ const filesWord = (count: number) => `${count} file${count === 1 ? "" : "s"}`;
99
+
100
+ const whereOf = (finding: TrialFinding) => {
101
+ const at = finding.lines ?? finding.section;
102
+ if (at === undefined) return finding.file;
103
+ return `${finding.file}:${at.first === at.last ? at.first : `${at.first}-${at.last}`}`;
104
+ };
105
+
106
+ /** What a preview costs, with only the numbers adhere actually knows. */
107
+ const describePreview = (preview: TrialPreview) => {
108
+ const parts = [filesWord(preview.selected)];
109
+ if (preview.requests > 0) {
110
+ parts.push(`${preview.requests} request${preview.requests === 1 ? "" : "s"}`);
111
+ }
112
+ if (preview.tokens > 0) parts.push(`about ${preview.tokens} input tokens`);
113
+ if (preview.price !== undefined && preview.tokens > 0) {
114
+ const dollars = (preview.tokens / 1_000_000) * preview.price;
115
+ parts.push(`about $${dollars < 0.01 ? dollars.toFixed(4) : dollars.toFixed(2)}`);
116
+ }
117
+ return parts.join(", ");
118
+ };
119
+
120
+ const showPreview = (scope: TrialScope, preview: TrialPreview) =>
121
+ Effect.gen(function* () {
122
+ const label = scope.kind === "all" ? "The entire repo" : `The first ${scope.limit} files`;
123
+ yield* note(`${label}: ${describePreview(preview)}.`);
124
+ if (preview.eligible > preview.selected) {
125
+ yield* note(`${preview.eligible} eligible files; ${preview.selected} selected.`);
126
+ }
127
+ if (preview.cached > 0) yield* note(`${preview.cached} cached judgments.`);
128
+ if (preview.deferred > 0) {
129
+ yield* note(`${filesWord(preview.deferred)} wait for a later run.`);
130
+ }
131
+ if (preview.skipped > 0) yield* note(`${filesWord(preview.skipped)} skipped as too long.`);
132
+ if (preview.requests === 0) {
133
+ yield* note("Nothing to send: the selected files have no pending requests.");
134
+ }
135
+ if (preview.provider !== undefined) yield* note(`Requests go to ${preview.provider}.`);
136
+ yield* note("Plan only: no requests sent.");
137
+ });
138
+
139
+ const optionalNote = (message: string) =>
140
+ Effect.map(Prompt.run(Prompt.String({ message, default: "" })), (value) =>
141
+ value.trim() === "" ? undefined : value.trim(),
142
+ );
143
+
144
+ /** Asks whether to go on without the trial, and never claims another judge fills in. */
145
+ const skipOrCancel = (reason: string) =>
146
+ Effect.gen(function* () {
147
+ yield* warn(reason);
148
+ yield* note("adhere will not substitute another judge.");
149
+ return yield* Prompt.run(
150
+ Prompt.Confirm({ message: "Skip the trial and continue without it?", initial: true }),
151
+ );
152
+ });
153
+
154
+ type Previewed =
155
+ | { readonly _tag: "Preview"; readonly preview: TrialPreview }
156
+ | { readonly _tag: "Failed"; readonly message: string };
157
+
158
+ const previewOf = <D, R>(
159
+ subject: TrialSubject<D, R>,
160
+ draft: D,
161
+ scope: TrialScope,
162
+ files?: ReadonlyArray<string>,
163
+ ) =>
164
+ previewTrial(subject.toDraft(draft), scope, files === undefined ? {} : { files }).pipe(
165
+ Effect.map((preview): Previewed => ({ _tag: "Preview", preview })),
166
+ Effect.catch((cause): Effect.Effect<Previewed> =>
167
+ Effect.succeed({ _tag: "Failed", message: messageOf(cause) }),
168
+ ),
169
+ );
170
+
171
+ type ScopeChoice = TrialScope | "custom" | "skip";
172
+
173
+ /** The trial's scope, with the default's real cost when it could be worked out. */
174
+ const chooseScope = <D, R>(subject: TrialSubject<D, R>, draft: D) =>
175
+ Effect.gen(function* () {
176
+ const base = yield* previewOf(subject, draft, {
177
+ kind: "files",
178
+ limit: DEFAULT_TRIAL_FILES,
179
+ });
180
+ if (base._tag === "Failed") {
181
+ yield* note(`Could not estimate the trial: ${base.message}`);
182
+ }
183
+ const eligible = base._tag === "Preview" ? base.preview.eligible : undefined;
184
+ const picked = yield* Prompt.run(
185
+ Prompt.Select<ScopeChoice>({
186
+ message: "Trial the draft on real files before writing it?",
187
+ choices: [
188
+ {
189
+ title: `${DEFAULT_TRIAL_FILES} files`,
190
+ value: { kind: "files", limit: DEFAULT_TRIAL_FILES } as const,
191
+ description:
192
+ base._tag === "Preview"
193
+ ? describePreview(base.preview)
194
+ : "adhere's default; the cost is shown before it runs.",
195
+ },
196
+ {
197
+ title:
198
+ eligible === undefined
199
+ ? "The entire repo"
200
+ : `The entire repo (${eligible} eligible files)`,
201
+ value: { kind: "all" } as const,
202
+ description: "Every eligible file; the cost is shown before it runs.",
203
+ },
204
+ {
205
+ title: "Another number of files",
206
+ value: "custom" as const,
207
+ description: "A positive whole number, cached files included.",
208
+ },
209
+ {
210
+ title: "Skip the trial",
211
+ value: "skip" as const,
212
+ description: "Write the draft untested.",
213
+ },
214
+ ],
215
+ }),
216
+ );
217
+ if (picked === "skip") return { _tag: "Skip" } as const;
218
+ if (picked === "custom") {
219
+ const value = yield* Prompt.run(
220
+ Prompt.String({
221
+ message: "How many files?",
222
+ validate: (input) => {
223
+ const count = parseTrialFiles(input);
224
+ return count === undefined
225
+ ? Effect.fail("A positive whole number.")
226
+ : Effect.succeed(String(count));
227
+ },
228
+ }),
229
+ );
230
+ return { _tag: "Scope", scope: { kind: "files", limit: Number(value) } } as const;
231
+ }
232
+ return {
233
+ _tag: "Scope",
234
+ scope: picked,
235
+ ...(base._tag === "Preview" && picked.kind === "files" && picked.limit === DEFAULT_TRIAL_FILES
236
+ ? { preview: base.preview }
237
+ : {}),
238
+ } as const;
239
+ });
240
+
241
+ type Ran =
242
+ | { readonly _tag: "Run"; readonly run: TrialRun }
243
+ | { readonly _tag: "Failed"; readonly message: string };
244
+
245
+ const runOnce = <D, R>(
246
+ subject: TrialSubject<D, R>,
247
+ draft: D,
248
+ scope: TrialScope,
249
+ files?: ReadonlyArray<string>,
250
+ ) =>
251
+ spinning(
252
+ `Trialling ${subject.id}`,
253
+ runTrial(subject.toDraft(draft), scope, files === undefined ? {} : { files }),
254
+ ).pipe(
255
+ Effect.map((run): Ran => ({ _tag: "Run", run })),
256
+ Effect.catch((cause): Effect.Effect<Ran> =>
257
+ Effect.succeed({ _tag: "Failed", message: messageOf(cause) }),
258
+ ),
259
+ );
260
+
261
+ const showRun = (run: TrialRun) =>
262
+ Effect.gen(function* () {
263
+ yield* note(
264
+ `Trialled ${filesWord(run.files)}: ${run.judged} judged, ${run.cached} cached, ${run.skipped} skipped, ${run.waiting} waiting.`,
265
+ );
266
+ yield* note(`${run.findings.length} finding${run.findings.length === 1 ? "" : "s"}.`);
267
+ if (run.judged === 0 && run.files > 0) {
268
+ yield* note("Every selected file was cached; no request was sent.");
269
+ }
270
+ yield* note(`Report: ${run.reportPath}`);
271
+ for (const limitation of run.notes) yield* note(limitation);
272
+ });
273
+
274
+ /** The authoring agent reads the saved report and the source it names, every run. */
275
+ const inspect = <D, R>(subject: TrialSubject<D, R>, run: TrialRun) =>
276
+ spinning(
277
+ `Asking ${subject.agent} to read the trial report`,
278
+ asking(
279
+ subject.agent,
280
+ "to inspect the trial report",
281
+ subject.root,
282
+ inspectionPrompt(run.reportPath, subject.id),
283
+ ),
284
+ ).pipe(
285
+ Effect.map((text) => ({ _tag: "Text", text }) as const),
286
+ Effect.catch((cause) => Effect.succeed({ _tag: "Failed", message: messageOf(cause) } as const)),
287
+ );
288
+
289
+ const MAX_SHOWN = 40;
290
+
291
+ const showFindings = (run: TrialRun) =>
292
+ Effect.gen(function* () {
293
+ let shown = 0;
294
+ for (const [file, findings] of Map.groupBy(run.findings, (finding) => finding.file)) {
295
+ yield* line(`${file}: ${findings.length} finding${findings.length === 1 ? "" : "s"}`);
296
+ for (const finding of findings) {
297
+ if (shown >= MAX_SHOWN) continue;
298
+ shown += 1;
299
+ const at = finding.lines ?? finding.section;
300
+ const score =
301
+ finding.probability === undefined
302
+ ? ""
303
+ : ` ${(finding.probability * 100).toFixed(0)}%${
304
+ finding.threshold === undefined
305
+ ? ""
306
+ : ` (threshold ${(finding.threshold * 100).toFixed(0)}%)`
307
+ }`;
308
+ yield* note(
309
+ ` ${at === undefined ? "" : `${at.first}-${at.last}`}${score}${
310
+ finding.message === undefined ? "" : ` ${finding.message}`
311
+ }`,
312
+ );
313
+ }
314
+ }
315
+ if (run.findings.length > shown) {
316
+ yield* note(`… ${run.findings.length - shown} more in the report.`);
317
+ }
318
+ });
319
+
320
+ const drill = (findings: ReadonlyArray<TrialFinding>) =>
321
+ Effect.gen(function* () {
322
+ const reviews: Array<FindingReview> = [];
323
+ for (const [index, finding] of findings.entries()) {
324
+ yield* line("");
325
+ yield* line(`${whereOf(finding)} (${index + 1} of ${findings.length})`);
326
+ if (finding.excerpt !== undefined) yield* line(finding.excerpt);
327
+ if (finding.probability !== undefined) {
328
+ yield* said(
329
+ `probability ${(finding.probability * 100).toFixed(0)}%${
330
+ finding.threshold === undefined
331
+ ? ""
332
+ : `, threshold ${(finding.threshold * 100).toFixed(0)}%`
333
+ }`,
334
+ );
335
+ }
336
+ if (finding.message !== undefined) yield* said(finding.message);
337
+ const verdict = yield* Prompt.run(
338
+ Prompt.Select({
339
+ message: "Is this finding right?",
340
+ choices: [
341
+ { title: "Correct", value: "correct" as const },
342
+ { title: "False positive", value: "false-positive" as const },
343
+ { title: "Unsure", value: "unsure" as const },
344
+ ],
345
+ }),
346
+ );
347
+ const note = yield* optionalNote("Note for the agent (optional)");
348
+ reviews.push({
349
+ where: whereOf(finding),
350
+ ...(finding.probability === undefined ? {} : { probability: finding.probability }),
351
+ verdict,
352
+ ...(note === undefined ? {} : { note }),
353
+ });
354
+ }
355
+ return reviews;
356
+ });
357
+
358
+ const reviewFindings = (run: TrialRun) =>
359
+ Effect.gen(function* () {
360
+ if (run.findings.length === 0) {
361
+ const choice = yield* Prompt.run(
362
+ Prompt.Select({
363
+ message: "No findings. What next?",
364
+ choices: [
365
+ { title: "Accept the draft", value: "accept" as const },
366
+ { title: "Ask the agent to revise it anyway", value: "revise" as const },
367
+ { title: "Cancel", value: "cancel" as const },
368
+ ],
369
+ }),
370
+ );
371
+ return choice === "accept"
372
+ ? ({ _tag: "Accept", reviews: [] } as const)
373
+ : choice === "revise"
374
+ ? ({ _tag: "Revise", reviews: [] } as const)
375
+ : ({ _tag: "Cancel" } as const);
376
+ }
377
+ yield* showFindings(run);
378
+ const choice = yield* Prompt.run(
379
+ Prompt.Select({
380
+ message: "What do you make of these findings?",
381
+ choices: [
382
+ { title: "Accept all as correct", value: "accept" as const },
383
+ { title: "Review them one by one", value: "drill" as const },
384
+ { title: "Ask the agent to revise with this evidence", value: "revise" as const },
385
+ { title: "Cancel", value: "cancel" as const },
386
+ ],
387
+ }),
388
+ );
389
+ if (choice === "accept") {
390
+ const note = yield* optionalNote("Note for the agent about these findings (optional)");
391
+ return {
392
+ _tag: "Accept",
393
+ reviews: run.findings.map((finding) => ({
394
+ where: whereOf(finding),
395
+ ...(finding.probability === undefined ? {} : { probability: finding.probability }),
396
+ verdict: "correct" as const,
397
+ ...(note === undefined ? {} : { note }),
398
+ })),
399
+ } as const;
400
+ }
401
+ if (choice === "drill") {
402
+ const reviews = yield* drill(run.findings);
403
+ const next = yield* Prompt.run(
404
+ Prompt.Select({
405
+ message: "Now what?",
406
+ choices: [
407
+ { title: "Accept the draft", value: "accept" as const },
408
+ { title: "Ask the agent to revise with these verdicts", value: "revise" as const },
409
+ { title: "Cancel", value: "cancel" as const },
410
+ ],
411
+ }),
412
+ );
413
+ return next === "accept"
414
+ ? ({ _tag: "Accept", reviews } as const)
415
+ : next === "revise"
416
+ ? ({ _tag: "Revise", reviews } as const)
417
+ : ({ _tag: "Cancel" } as const);
418
+ }
419
+ return choice === "revise"
420
+ ? ({ _tag: "Revise", reviews: [] } as const)
421
+ : ({ _tag: "Cancel" } as const);
422
+ });
423
+
424
+ /** What the agent reads when revising: the report, the counts, and the user's verdicts. */
425
+ export const trialEvidence = (run: TrialRun, reviews: ReadonlyArray<FindingReview>) => {
426
+ const lines = [
427
+ `Trial report: ${run.reportPath}`,
428
+ `Scope: ${run.scope.kind === "all" ? "the entire repo" : `the first ${run.scope.limit} files`}; ${filesWord(run.selectedFiles.length)} selected.`,
429
+ `Counts: ${run.files} files, ${run.judged} judged, ${run.cached} cached, ${run.skipped} skipped, ${run.waiting} waiting.`,
430
+ `Findings: ${run.findings.length}.`,
431
+ ];
432
+ if (reviews.length > 0) {
433
+ lines.push("The user's verdicts:");
434
+ for (const review of reviews) {
435
+ lines.push(
436
+ `- ${review.verdict}${review.note === undefined ? "" : ` — ${review.note}`}: ${review.where}`,
437
+ );
438
+ }
439
+ }
440
+ return lines.join("\n");
441
+ };
442
+
443
+ const approved = <D, R>(subject: TrialSubject<D, R>, draft: D) =>
444
+ Effect.gen(function* () {
445
+ const approval = yield* subject.approve(draft);
446
+ if (approval._tag === "Approved" || approval._tag === "Declined") return approval;
447
+ for (const problem of approval.problems) yield* warn(problem);
448
+ if (approval._tag === "Failed") {
449
+ return {
450
+ _tag: "Stop",
451
+ reason: approval.reason ?? "The draft did not pass its own checks.",
452
+ } as const;
453
+ }
454
+ const skip = yield* skipOrCancel("The draft could not be trialled as it is.");
455
+ return skip
456
+ ? ({ _tag: "Skip", reason: "The draft could not be trialled as it is." } as const)
457
+ : ({ _tag: "Declined" } as const);
458
+ });
459
+
460
+ /**
461
+ * Runs the draft, then loops on the user's review: accept it, revise it from
462
+ * the evidence and run the same files again, or cancel. Nothing is written
463
+ * here; the caller writes only on `Accepted` or an explicit `Untested`.
464
+ */
465
+ export const trialAndReview = <D, R>(subject: TrialSubject<D, R>) =>
466
+ Effect.gen(function* () {
467
+ const first = yield* approved(subject, subject.draft);
468
+ if (first._tag === "Declined") {
469
+ return {
470
+ _tag: "Cancelled",
471
+ draft: subject.draft,
472
+ reason: "The draft was not approved to run.",
473
+ } as const;
474
+ }
475
+ if (first._tag === "Stop") {
476
+ return { _tag: "Cancelled", draft: subject.draft, reason: first.reason } as const;
477
+ }
478
+ if (first._tag === "Skip") {
479
+ return { _tag: "Untested", draft: subject.draft, reason: first.reason } as const;
480
+ }
481
+ let draft = first.draft;
482
+ const chosen = yield* chooseScope(subject, draft);
483
+ if (chosen._tag === "Skip") {
484
+ return { _tag: "Untested", draft, reason: "The trial was skipped." } as const;
485
+ }
486
+ const scope = chosen.scope;
487
+ const preview =
488
+ chosen.preview === undefined
489
+ ? yield* previewOf(subject, draft, scope)
490
+ : ({ _tag: "Preview", preview: chosen.preview } as const);
491
+ if (preview._tag === "Failed") {
492
+ const skip = yield* skipOrCancel(`The trial could not be planned: ${preview.message}`);
493
+ return skip
494
+ ? ({ _tag: "Untested", draft, reason: preview.message } as const)
495
+ : ({ _tag: "Cancelled", draft, reason: preview.message } as const);
496
+ }
497
+ yield* showPreview(scope, preview.preview);
498
+
499
+ let pinned: ReadonlyArray<string> | undefined;
500
+ for (;;) {
501
+ const result = yield* runOnce(subject, draft, scope, pinned);
502
+ if (result._tag === "Failed") {
503
+ const skip = yield* skipOrCancel(`The trial did not run: ${result.message}`);
504
+ return skip
505
+ ? ({ _tag: "Untested", draft, reason: result.message } as const)
506
+ : ({ _tag: "Cancelled", draft, reason: result.message } as const);
507
+ }
508
+ const run = result.run;
509
+ pinned = run.selectedFiles;
510
+ yield* showRun(run);
511
+ const inspection = yield* inspect(subject, run);
512
+ if (inspection._tag === "Text") {
513
+ if (inspection.text.trim() !== "") yield* line(inspection.text.trim());
514
+ } else {
515
+ yield* warn(`The agent could not read the report: ${inspection.message}`);
516
+ }
517
+ const decision = yield* reviewFindings(run);
518
+ if (decision._tag === "Accept") {
519
+ return { _tag: "Accepted", draft, run, reviews: decision.reviews } as const;
520
+ }
521
+ if (decision._tag === "Cancel") {
522
+ return { _tag: "Cancelled", draft, reason: "The trial was cancelled." } as const;
523
+ }
524
+ const revised = yield* subject.revise(draft, trialEvidence(run, decision.reviews));
525
+ yield* subject.show(revised);
526
+ const again = yield* approved(subject, revised);
527
+ if (again._tag === "Declined") {
528
+ return {
529
+ _tag: "Cancelled",
530
+ draft: revised,
531
+ reason: "The revised draft was not approved to run.",
532
+ } as const;
533
+ }
534
+ if (again._tag === "Stop") {
535
+ return { _tag: "Cancelled", draft: revised, reason: again.reason } as const;
536
+ }
537
+ if (again._tag === "Skip") {
538
+ return { _tag: "Untested", draft: revised, reason: again.reason } as const;
539
+ }
540
+ draft = again.draft;
541
+ const next = yield* previewOf(subject, draft, scope, pinned);
542
+ if (next._tag === "Failed") {
543
+ const skip = yield* skipOrCancel(`The revised draft could not be planned: ${next.message}`);
544
+ return skip
545
+ ? ({ _tag: "Untested", draft, reason: next.message } as const)
546
+ : ({ _tag: "Cancelled", draft, reason: next.message } as const);
547
+ }
548
+ yield* showPreview(scope, next.preview);
549
+ }
550
+ });
551
+
552
+ /** The write prompt's reminder when the draft was never trialled. */
553
+ export const trialWriteSuffix = <D>(outcome: TrialOutcome<D>) =>
554
+ outcome._tag === "Untested" ? " (not trialled)" : "";
555
+
556
+ /** Whether a draft was trialled, for the write prompt and the closing line. */
557
+ export const trialStatus = <D>(outcome: TrialOutcome<D>) =>
558
+ outcome._tag === "Accepted"
559
+ ? `Trialled on ${filesWord(outcome.run.selectedFiles.length)}: ${outcome.run.findings.length} finding${outcome.run.findings.length === 1 ? "" : "s"}.`
560
+ : outcome._tag === "Untested"
561
+ ? `Not trialled: ${outcome.reason}`
562
+ : "Not written.";