pi-anchorloop 2.1.1 → 2.2.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.
@@ -1,94 +1,112 @@
1
1
  /**
2
- * pi extension: before a run that changed something settles, asks Jev whether the
3
- * final reply shows the request met, what review it needs, and how good it is.
4
- * Only what the user typed counts as the request: messages that extensions send
5
- * (intercom, monitor wakes) do not replace it. A run they start is sent with
6
- * `trigger: wake` and the message that woke it, and is judged on what that message asks.
7
- * A run that arranged a wake (a monitor, a loop, a subagent) is waiting, not finished,
8
- * and is not judged. Jev also says whether the reply is a
9
- * checkpoint, a point that hands work back to the user; a reply that only acknowledges,
10
- * reports progress, or waits gets no reminder. A wake run is asked a stricter checkpoint
11
- * question, since with the general one Jev counts a reply to a routine liveness or health
12
- * check as handing work back. A wrong review tier or low quality gets one
13
- * advisory continuation per run; a low done score is only shown. The agent is never
14
- * blocked. Only the request, the trigger, the wake message, and the final reply are sent.
15
- * A subagent session registers nothing: the agent that spawned it judges its result.
2
+ * pi extension: inside an open loop, asks Jev to check the phase machine's reading of the loop book. The
3
+ * program computes the next phase from the book's evidence; Jev sees the kind, intent, anchor, diff
4
+ * summary, phase (current, next, missing), verify state, open todos, tasks, and friction, and judges only
5
+ * whether the current phase's completion condition truly holds and whether to advance, go back, or ask
6
+ * the user. It runs when a run settles and the book gained an entry since the last judgment, and at a
7
+ * `close` declaration. The advisory is one line leading with `next → <phase>`; it continues the run once
8
+ * when Jev agrees to advance and the next phase is a verify, review, or fix. A run that arranged a wake
9
+ * (a monitor, a loop, a subagent) is waiting, not finished, and is not judged. Outside a loop nothing
10
+ * runs, nor in a `quick` loop, whose only prompt is the close reminder; a subagent session registers
11
+ * nothing, since the agent that spawned it judges its result.
16
12
  */
17
13
 
18
- import { type ExtensionAPI, parseSkillBlock } from "@earendil-works/pi-coding-agent";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import {
16
+ type Book,
17
+ allFriction,
18
+ childLines,
19
+ currentIntent,
20
+ currentKind,
21
+ diffAgainstBases,
22
+ diffText,
23
+ guarded,
24
+ isOpenTask,
25
+ latestAnchor,
26
+ openLoop,
27
+ replayTodos,
28
+ reviewClearAfterLastAct,
29
+ reviewTrend,
30
+ stagesDone,
31
+ TASK_LEGEND,
32
+ taskLines,
33
+ verifyAfterLastAct,
34
+ } from "./support/book.ts";
35
+ import { type Answers, classify } from "./support/jev.ts";
36
+ import { computePhases } from "./support/phases.ts";
37
+ import { isSubagent } from "./support/subagent.ts";
38
+
39
+ export { isSubagent };
19
40
 
20
41
  export const ENTRY_TYPE = "anchorloop-second-thought";
21
- const WORK_TOOLS = new Set(["write", "edit", "bash"]);
22
42
  const WAIT_TOOLS = new Set(["MonitorCreate", "LoopCreate", "goal_wait", "subagent", "subagent_resume", "subagent_send"]);
23
- const REQUEST_CHARS = 1500;
24
- const WAKE_CHARS = 1500;
25
- const REPLY_CHARS = 3000;
26
- const TIMEOUT_MS = 15_000;
27
43
 
28
- export const THRESHOLDS = { checkpointBelow: 0.5, doneBelow: 0.25, qualityBelow: 1.5, reviewTier: "full" };
44
+ export const THRESHOLDS = { completeBelow: 0.5, doneBelow: 0.25, qualityBelow: 1.5, reviewTier: "full" };
45
+
46
+ export const MOVES = ["advance", "back", "ask-user"] as const;
47
+ export type Move = (typeof MOVES)[number];
48
+
49
+ /** The phases whose missing evidence the agent can supply by acting, so Jev's agreement continues the run. */
50
+ const ACTIONABLE = new Set(["verify", "check", "review", "fix"]);
29
51
 
30
52
  export const QUESTIONS = {
31
- checkpoint: {
32
- type: "bool" as const,
53
+ move: {
54
+ type: "choice" as const,
33
55
  instructions:
34
- "Is `reply` a point where the agent hands work back to the user? `trigger` is `user` when the user's message started this run and `wake` when an automated message (a monitor, a loop, another agent) did.",
56
+ "The program computed `phase` from the loop book: the last complete phase, the next one, and what the next lacks. Given `kind`, `intent`, `anchor`, `diff`, `verify` (check evidence and reviews), `todos`, `tasks`, and `friction`, should the agent move on to the next phase, go back because the evidence of an earlier phase does not hold, or ask the user?",
57
+ criteria: {
58
+ advance: "the recorded evidence supports the last complete phase and the next phase can proceed",
59
+ back: "evidence recorded for an earlier phase is wrong, stale, or contradicted by a later result",
60
+ "ask-user": "a decision or judgment that belongs to the user blocks progress",
61
+ },
62
+ },
63
+ complete: {
64
+ type: "bool" as const,
65
+ instructions: "Does the evidence in `diff`, `verify`, and `phase` show that the completion condition of the last complete phase (`phase` current) truly holds, beyond the program's mechanical check?",
35
66
  criteria: {
36
- true: "the reply delivers a result, reports the request finished or blocked, or asks the user to decide",
37
- false: "the reply only acknowledges a message, reports interim progress, or says the agent is waiting on something it started",
67
+ true: "the evidence observably meets what the phase requires",
68
+ false: "the phase counts as complete on a mechanical signal, but its content is empty, stale, or off the intent",
38
69
  },
39
70
  },
40
71
  done: {
41
72
  type: "bool" as const,
42
- instructions:
43
- "Does `reply` show, with evidence, that the work this run took on is fully met? That work is `request`, or, when `trigger` is `wake`, what `wake` asks in service of `request`.",
73
+ instructions: "Does the `verify` evidence show, with observed results, that `intent` is fully met?",
44
74
  criteria: {
45
- true: "every part of the request has observed evidence in the reply: command output, a test result, or an artifact",
75
+ true: "every part of the intent has observed evidence: command output, a test result, a review verdict, or an artifact",
46
76
  false: "some part failed, was skipped, is left for later, or rests only on a claim",
47
77
  },
48
78
  },
49
79
  review: {
50
80
  type: "choice" as const,
51
- instructions: "What review should the change described in `reply` get before it is merged or released?",
81
+ instructions: "What review should the `diff` still get before it is merged or released, given the reviews recorded in `verify`?",
52
82
  criteria: {
53
- none: "trivial, fully covered by a passing check",
83
+ none: "trivial, fully covered by a passing check, or already reviewed with no blocking finding",
54
84
  focused: "one independent reviewer on the changed lines",
55
85
  full: "independent review plus an adversarial pass: security, data, permissions, migration, or a shared contract",
56
86
  },
57
87
  },
58
88
  quality: {
59
89
  type: "score" as const,
60
- instructions: "How acceptable is the delivered work against the work this run took on: `request`, or `wake` when `trigger` is `wake`?",
90
+ instructions: "How acceptable is the work recorded in the loop against its `intent`?",
61
91
  criteria: [
62
92
  "unacceptable: wrong or broken",
63
93
  "weak: works but misses requirements",
64
- "acceptable: meets the request",
94
+ "acceptable: meets the intent",
65
95
  "strong: meets it with direct evidence and no excess",
66
96
  ],
67
97
  },
68
98
  };
69
99
 
70
- // Replayed on the 26 wake-run advisories of one long session, this question let through 1 of
71
- // the 21 status or liveness replies instead of all 21, and 3 of the 5 delivered results.
72
- export const WAKE_CHECKPOINT = {
73
- type: "bool" as const,
74
- instructions:
75
- "Is `reply` a point where the agent hands work back to the user? `trigger` is `user` when the user's message started this run and `wake` when an automated message (a monitor, a loop, another agent) did; `wake` is that message. A reply to a routine check that `wake` asked for is not a checkpoint.",
76
- criteria: {
77
- true: "the reply delivers a finished result or artifact, reports the user's request finished or blocked, or asks the user to decide",
78
- false: "the reply acknowledges a message, reports the status, health, load, or progress of work that is still running, or says the agent is waiting on something",
79
- },
80
- };
81
-
82
- type Block = { type: string; text?: string };
83
- type Answers = Record<string, { type: string; probability?: number; choice?: string; probabilities?: Record<string, number>; score?: number }>;
84
- export type Verdict = { checkpoint: number; done: number; review: string; reviewProbability: number; quality: number };
100
+ export type Verdict = { move: Move; complete: number; done: number; review: string; reviewProbability: number; quality: number };
85
101
 
86
102
  export function readVerdict(answers: Answers): Verdict | undefined {
87
- const { checkpoint, done, review, quality } = answers;
88
- if (checkpoint?.type !== "bool" || done?.type !== "bool" || review?.type !== "choice" || quality?.type !== "score") return undefined;
103
+ const { move, complete, done, review, quality } = answers;
104
+ if (move?.type !== "choice" || complete?.type !== "bool" || done?.type !== "bool" || review?.type !== "choice" || quality?.type !== "score") return undefined;
105
+ if (!(MOVES as readonly string[]).includes(move.choice ?? "")) return undefined;
89
106
  const choice = review.choice ?? "";
90
107
  return {
91
- checkpoint: checkpoint.probability ?? 0,
108
+ move: move.choice as Move,
109
+ complete: complete.probability ?? 0,
92
110
  done: done.probability ?? 0,
93
111
  review: choice,
94
112
  reviewProbability: review.probabilities?.[choice] ?? 0,
@@ -96,147 +114,160 @@ export function readVerdict(answers: Answers): Verdict | undefined {
96
114
  };
97
115
  }
98
116
 
99
- export function concerns(verdict: Verdict): string[] {
100
- const found: string[] = [];
101
- if (verdict.checkpoint < THRESHOLDS.checkpointBelow) return found;
102
- if (verdict.done < THRESHOLDS.doneBelow) found.push("request not shown met");
103
- if (verdict.review === THRESHOLDS.reviewTier) found.push("needs full independent review");
104
- if (verdict.quality < THRESHOLDS.qualityBelow) found.push("result below the request");
105
- return found;
106
- }
117
+ export type Snapshot = {
118
+ state: Record<string, string>;
119
+ /** The program's next phase and what it lacks, in a few words. */
120
+ next: string;
121
+ missing: string;
122
+ /** Advisory hints that are not phases, such as an unrecorded friction. */
123
+ hints: string[];
124
+ /** The last phase the book shows complete. */
125
+ current: string;
126
+ /** A review recorded after the last change with no blocking finding on it. */
127
+ reviewClear: boolean;
128
+ };
107
129
 
108
- export function needsRecheck(verdict: Verdict): boolean {
109
- return verdict.review === THRESHOLDS.reviewTier || verdict.quality < THRESHOLDS.qualityBelow;
130
+ export function snapshot(book: Book, ctx: { cwd: string; sessionManager: { getBranch(): unknown[] } }): Snapshot {
131
+ const { entries } = book;
132
+ const todos = replayTodos(ctx.sessionManager.getBranch());
133
+ const open = todos.filter(isOpenTask).length;
134
+ const phases = computePhases(book, { cwd: ctx.cwd, todos });
135
+ const check = verifyAfterLastAct(entries);
136
+ const verify = [
137
+ `stages: ${stagesDone(entries).join(", ") || "none"}`,
138
+ `last check: ${check ? `${check.declared ? `declared ${check.result}` : `exit ${check.exit ?? "unknown"}`}${check.last ? ` — ${check.last}` : ""}` : "none after the last change"}`,
139
+ `reviews: ${reviewTrend(entries)}`,
140
+ ...childLines(entries).map((line) => `child ${line}`),
141
+ ].join("; ");
142
+ const friction = allFriction(entries).map((item) => `${item.ref}: ${item.tried} → ${item.found} [${item.reflected ? item.class : "unreflected"}]`);
143
+ return {
144
+ state: {
145
+ kind: currentKind(entries) ?? "not declared",
146
+ intent: (currentIntent(entries) ?? "not declared").slice(0, 500),
147
+ anchor: (latestAnchor(entries) ?? "none").slice(0, 500),
148
+ diff: diffText(diffAgainstBases(entries)),
149
+ phase: `current: ${phases.current}; next: ${phases.next}${phases.missing ? `; missing: ${phases.missing}` : ""}${phases.skipped.length ? `; skipped: ${phases.skipped.join(", ")}` : ""}`.slice(0, 600),
150
+ verify: verify.slice(0, 1200),
151
+ todos: `${open} open of ${todos.length}`,
152
+ tasks: [`legend: ${TASK_LEGEND}`, ...taskLines(entries, todos, ctx.cwd)].join("\n").slice(0, 1500),
153
+ friction: (friction.join("\n") || "none").slice(0, 800),
154
+ },
155
+ next: phases.next,
156
+ missing: phases.missing,
157
+ hints: phases.hints,
158
+ current: phases.current,
159
+ reviewClear: reviewClearAfterLastAct(entries),
160
+ };
110
161
  }
111
162
 
112
- export function message(verdict: Verdict, found: string[], recheck: boolean): string {
113
- return (
114
- `Second thought (Jev, advisory): done ${verdict.done.toFixed(2)}, review ${verdict.review}, ` +
115
- `quality ${verdict.quality.toFixed(2)}/3; ${found.join("; ")}.` +
116
- (recheck ? " Recheck your evidence; continue only if it fails." : "") +
117
- " No reply needed. Clear finished or stale todos; continue any unfinished work."
118
- );
163
+ export function assess(verdict: Verdict, snap: Snapshot): { found: string[]; notes: string[] } {
164
+ const found: string[] = [];
165
+ const notes: string[] = [];
166
+ if (verdict.complete < THRESHOLDS.completeBelow && snap.current !== "start") found.push(`${snap.current} not shown to be complete`);
167
+ if (verdict.done < THRESHOLDS.doneBelow) found.push("evidence not shown to cover the intent");
168
+ if (verdict.review === THRESHOLDS.reviewTier) {
169
+ if (snap.reviewClear) notes.push("review rated full, but a review recorded after the last change found no blocking finding");
170
+ else found.push("needs full independent review");
171
+ }
172
+ if (verdict.quality < THRESHOLDS.qualityBelow) found.push("result below the intent");
173
+ return { found, notes };
119
174
  }
120
175
 
121
- function text(content: unknown): string {
122
- if (typeof content === "string") return content.trim();
123
- if (!Array.isArray(content)) return "";
124
- return (content as Block[])
125
- .filter((block) => block.type === "text")
126
- .map((block) => block.text ?? "")
127
- .join("\n")
128
- .trim();
176
+ /** True when Jev agrees to advance and the next phase is one the agent can act on now. */
177
+ export function continues(verdict: Verdict, snap: Snapshot): boolean {
178
+ return verdict.move === "advance" && ACTIONABLE.has(snap.next);
129
179
  }
130
180
 
131
- // pi-herdr-agents marks its children with PI_SUBAGENT_ID, pi-subagents with PI_SUBAGENT_CHILD,
132
- // and avtc's pi-subagent with PI_SUBAGENT_CHILD_AGENT plus PI_SUBAGENT_PARENT_PID.
133
- export function isSubagent(env: NodeJS.ProcessEnv = process.env): boolean {
134
- return Boolean(env.PI_SUBAGENT_ID) || env.PI_SUBAGENT_CHILD === "1" || Boolean(env.PI_SUBAGENT_CHILD_AGENT && env.PI_SUBAGENT_PARENT_PID);
181
+ /** One line: the program's next phase and its gap, Jev's move and scores, and, away from close, the call to act. */
182
+ export function message(verdict: Verdict, snap: Snapshot, found: string[], notes: string[], atClose = false): string {
183
+ const extra = [...found, ...notes, ...snap.hints];
184
+ return (
185
+ `Second thought (Jev, advisory): next → ${snap.next}${snap.missing ? ` (missing: ${snap.missing})` : ""}; ` +
186
+ `${verdict.move}; complete ${verdict.complete.toFixed(2)}, done ${verdict.done.toFixed(2)}, review ${verdict.review}, quality ${verdict.quality.toFixed(2)}/3` +
187
+ `${extra.length ? `; ${extra.join("; ")}` : ""}.` +
188
+ (!atClose && continues(verdict, snap) ? ` Do ${snap.next} now, or say why it does not apply.` : "")
189
+ );
135
190
  }
136
191
 
137
192
  export default function (pi: ExtensionAPI) {
138
193
  if (isSubagent()) return;
139
- let request = "";
140
- let reply = "";
141
- let worked = false;
142
194
  let waiting = false;
143
- let asked = false;
144
- // A run is `user` once a message the user typed lands in it; otherwise a wake started it.
145
- let trigger: "user" | "wake" = "wake";
146
- // The automated message that started a wake run: the last one to land before the run's first reply.
147
- let wake = "";
148
- // Texts extensions sent as user messages that have not landed yet; matched by content,
149
- // since another handler may swallow an input or a queue may be cleared before it lands.
150
- const sent = new Map<string, number>();
195
+ let continued = false;
196
+ // Entries each open book held when it was last judged, and the advisory it last received.
197
+ const judged = new Map<string, number>();
198
+ const told = new Map<string, string>();
199
+ // A close deletes its book, so what Jev needs is read when the call starts.
200
+ const closing = new Map<string, Snapshot>();
151
201
 
152
202
  pi.on("session_start", () => {
153
- sent.clear();
154
- });
155
-
156
- pi.on("input", (event) => {
157
- if (event.source !== "extension") return;
158
- const said = event.text.trim();
159
- sent.set(said, (sent.get(said) ?? 0) + 1);
203
+ judged.clear();
204
+ told.clear();
205
+ closing.clear();
160
206
  });
161
207
 
162
- // Each run is judged once, on its own work. State resets once the session settles, so a
163
- // continuation (this extension's recheck, or one after compaction) stays in the same run.
164
- // An extension text still pending then was swallowed or dropped and will not land.
165
208
  pi.on("agent_settled", () => {
166
- sent.clear();
167
- reply = "";
168
- worked = false;
169
209
  waiting = false;
170
- asked = false;
171
- trigger = "wake";
172
- wake = "";
210
+ continued = false;
173
211
  });
174
212
 
175
- pi.on("tool_call", (event) => {
176
- if (WORK_TOOLS.has(event.toolName)) worked = true;
177
- if (WAIT_TOOLS.has(event.toolName)) waiting = true;
178
- });
213
+ pi.on(
214
+ "tool_call",
215
+ guarded((event, ctx) => {
216
+ if (WAIT_TOOLS.has(event.toolName)) waiting = true;
217
+ if (event.toolName !== "anchorloop_stage" || (event.input as { stage?: unknown }).stage !== "close") return;
218
+ const book = openLoop(ctx);
219
+ if (book && currentKind(book.entries) !== "quick") closing.set(event.toolCallId, snapshot(book, ctx));
220
+ }),
221
+ );
179
222
 
180
- // A message the user typed, including one that steers a running loop, starts a new request.
181
- pi.on("message_end", (event) => {
182
- const msg = event.message as { role?: string; content?: unknown; customType?: string };
183
- const said = text(msg.content);
184
- if (msg.role === "custom") {
185
- if (msg.customType !== ENTRY_TYPE && !reply && said) wake = said;
186
- } else if (msg.role === "user") {
187
- const pending = sent.get(said) ?? 0;
188
- if (pending > 0) {
189
- if (pending === 1) sent.delete(said);
190
- else sent.set(said, pending - 1);
191
- if (!reply) wake = said;
192
- return;
193
- }
194
- const skill = parseSkillBlock(said);
195
- request = skill ? (skill.userMessage ?? `/skill:${skill.name}`) : said;
196
- reply = "";
197
- worked = false;
198
- asked = false;
199
- trigger = "user";
200
- wake = "";
201
- } else if (msg.role === "assistant" && said) {
202
- reply = said;
203
- }
204
- });
223
+ async function judge(ctx: Parameters<typeof classify>[0], snap: Snapshot) {
224
+ const { answers, reason } = await classify(ctx, snap.state, QUESTIONS);
225
+ if (reason === "no-auth") return undefined;
226
+ return { verdict: answers ? readVerdict(answers) : undefined, reason };
227
+ }
205
228
 
206
- pi.on("agent_before_settle", async (event, ctx) => {
207
- if (asked || !worked || waiting || !reply || !request || event.outcome !== "completed") return;
208
- asked = true;
209
- const record = (data: Record<string, unknown>) => ({ type: "custom" as const, customType: ENTRY_TYPE, data });
210
- const jev = ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest");
211
- type AuthModel = Parameters<typeof ctx.modelRegistry.hasConfiguredAuth>[0];
212
- // SAFETY: hasConfiguredAuth is typed for chat models but reads only `provider`, which a classifier has too.
213
- if (!jev || !ctx.modelRegistry.hasConfiguredAuth(jev as unknown as AuthModel)) return;
214
- let verdict: Verdict | undefined;
215
- let reason = "";
216
- try {
217
- const result = await ctx.modelRegistry.classify(
218
- jev,
219
- {
220
- state: { request: request.slice(0, REQUEST_CHARS), trigger, wake: wake.slice(0, WAKE_CHARS), reply: reply.slice(-REPLY_CHARS) },
221
- questions: trigger === "wake" ? { ...QUESTIONS, checkpoint: WAKE_CHECKPOINT } : QUESTIONS,
222
- },
223
- { signal: AbortSignal.any([AbortSignal.timeout(TIMEOUT_MS), ...(ctx.signal ? [ctx.signal] : [])]) },
224
- );
225
- verdict = result.stopReason === "stop" ? readVerdict(result.answers as Answers) : undefined;
226
- reason = result.stopReason;
227
- } catch (error) {
228
- reason = error instanceof Error ? error.message : String(error);
229
- }
230
- if (!verdict) return { entries: [record({ verdict: "unknown", reason })] };
231
- const found = concerns(verdict);
232
- if (found.length === 0) return { entries: [record({ ...verdict, concerns: [] })] };
233
- const recheck = needsRecheck(verdict);
234
- return {
235
- entries: [
236
- record({ ...verdict, concerns: found }),
237
- { type: "custom_message" as const, customType: ENTRY_TYPE, content: message(verdict, found, recheck), display: true },
238
- ],
239
- ...(recheck ? { continue: true } : {}),
240
- };
241
- });
229
+ pi.on(
230
+ "tool_result",
231
+ guarded(async (event, ctx) => {
232
+ const snap = closing.get(event.toolCallId);
233
+ if (!snap) return;
234
+ closing.delete(event.toolCallId);
235
+ if (event.isError) return;
236
+ const judgment = await judge(ctx, snap);
237
+ if (!judgment?.verdict) return;
238
+ const { found, notes } = assess(judgment.verdict, snap);
239
+ pi.appendEntry(ENTRY_TYPE, { ...judgment.verdict, at: "close", concerns: found });
240
+ return { content: [...event.content, { type: "text" as const, text: message(judgment.verdict, snap, found, notes, true) }] };
241
+ }),
242
+ );
243
+
244
+ pi.on(
245
+ "agent_before_settle",
246
+ guarded(async (event, ctx) => {
247
+ if (waiting || event.outcome !== "completed") return;
248
+ const book = openLoop(ctx);
249
+ // A quick loop has no prompts but the close reminder.
250
+ if (!book || currentKind(book.entries) === "quick") return;
251
+ const id = String(book.header.id);
252
+ if (book.entries.length <= (judged.get(id) ?? 0)) return;
253
+ judged.set(id, book.entries.length);
254
+ const snap = snapshot(book, ctx);
255
+ const judgment = await judge(ctx, snap);
256
+ if (!judgment) return;
257
+ const record = (data: Record<string, unknown>) => ({ type: "custom" as const, customType: ENTRY_TYPE, data });
258
+ // A later handler's boundary result replaces the entries an earlier one returned, so carry them along.
259
+ const earlier = event.entries ?? [];
260
+ if (!judgment.verdict) return { entries: [...earlier, record({ verdict: "unknown", reason: judgment.reason })] };
261
+ const { found, notes } = assess(judgment.verdict, snap);
262
+ const text = message(judgment.verdict, snap, found, notes);
263
+ if (told.get(id) === text) return;
264
+ told.set(id, text);
265
+ const proceed = !continued && continues(judgment.verdict, snap);
266
+ if (proceed) continued = true;
267
+ return {
268
+ entries: [...earlier, record({ ...judgment.verdict, concerns: found }), { type: "custom_message" as const, customType: ENTRY_TYPE, content: text, display: true }],
269
+ ...(proceed ? { continue: true } : {}),
270
+ };
271
+ }),
272
+ );
242
273
  }