pi-firecode 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import { fileURLToPath } from "node:url";
5
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
6
 
7
7
  // jsonc.ts
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
8
11
  function parseJsonc(text) {
9
12
  let output = "";
10
13
  let inString = false;
@@ -60,22 +63,15 @@ function seedConfig() {
60
63
  }
61
64
  }
62
65
  function readConfigFile() {
63
- let text;
66
+ let parsed;
64
67
  try {
65
- text = readFileSync(CONFIG_PATH, "utf8");
68
+ parsed = parseJsonc(readFileSync(CONFIG_PATH, "utf8"));
66
69
  } catch (error) {
67
70
  if (error.code === "ENOENT")
68
71
  return { raw: {}, fault: { kind: "missing" } };
69
72
  return { raw: {}, fault: { kind: "parse", message: error instanceof Error ? error.message : String(error) } };
70
73
  }
71
- try {
72
- const parsed = parseJsonc(text);
73
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
74
- return { raw: parsed };
75
- return { raw: {}, fault: { kind: "notObject" } };
76
- } catch (error) {
77
- return { raw: {}, fault: { kind: "parse", message: error instanceof Error ? error.message : String(error) } };
78
- }
74
+ return isRecord(parsed) ? { raw: parsed } : { raw: {}, fault: { kind: "notObject" } };
79
75
  }
80
76
 
81
77
  // i18n.ts
@@ -123,7 +119,6 @@ var msg = defineMessages({
123
119
  modelSegment: (value) => `模型段不是 provider/model:${value}`,
124
120
  thinkingLevel: (value) => `思考档无效:${value}`,
125
121
  modelAtomFaults: (shape, faults) => `${shape}(${faults.join(";")})`,
126
- reviewBackground: "review.background 已随审查子进程层删除,请直接移除该键",
127
122
  reviewIncomplete: "请显式完整配置 review",
128
123
  reviewReviewers: "review.reviewers 必须包含 1–5 个模型原子",
129
124
  reviewToolsArray: "review.tools 必须是字符串数组",
@@ -172,7 +167,6 @@ var msg = defineMessages({
172
167
  modelSegment: (value) => `model segment is not provider/model: ${value}`,
173
168
  thinkingLevel: (value) => `invalid thinking level: ${value}`,
174
169
  modelAtomFaults: (shape, faults) => `${shape} (${faults.join("; ")})`,
175
- reviewBackground: "review.background was removed together with the review subprocess layer; delete this key",
176
170
  reviewIncomplete: "Configure review explicitly and completely",
177
171
  reviewReviewers: "review.reviewers must contain 1–5 model atoms",
178
172
  reviewToolsArray: "review.tools must be an array of strings",
@@ -202,6 +196,7 @@ var msg = defineMessages({
202
196
  });
203
197
 
204
198
  // config.ts
199
+ var THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
205
200
  var FEATURES = [
206
201
  "header",
207
202
  "statusbar",
@@ -233,11 +228,8 @@ function readFile({ raw, fault }, problems) {
233
228
  return {};
234
229
  }
235
230
  }
236
- function isPlainObject(value) {
237
- return typeof value === "object" && value !== null && !Array.isArray(value);
238
- }
239
231
  function asRecord(value) {
240
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
232
+ return isRecord(value) ? value : {};
241
233
  }
242
234
  function rejectUnknownKeys(record, allowed, field, problems) {
243
235
  for (const key of Object.keys(record))
@@ -295,7 +287,7 @@ function loadConfig() {
295
287
  return cached;
296
288
  const blocking = [];
297
289
  const raw = readFile(readConfigFile(), blocking);
298
- const featuresBroken = raw.features !== undefined && !isPlainObject(raw.features);
290
+ const featuresBroken = raw.features !== undefined && !isRecord(raw.features);
299
291
  if (featuresBroken)
300
292
  blocking.push(msg.config.featuresNotObject);
301
293
  const features = featuresBroken ? Object.fromEntries(FEATURES.map((feature) => [feature, false])) : asRecord(raw.features);
@@ -315,7 +307,7 @@ function loadConfig() {
315
307
  problems.push(msg.config.language);
316
308
  const section = (name, refused, parse, incomplete) => {
317
309
  const own = [];
318
- if (raw[name] !== undefined && !isPlainObject(raw[name]))
310
+ if (raw[name] !== undefined && !isRecord(raw[name]))
319
311
  own.push(msg.config.mustBeObject(name));
320
312
  const config = parse(asRecord(raw[name]), own);
321
313
  if (features[name] !== false)
@@ -341,15 +333,6 @@ function loadConfig() {
341
333
  };
342
334
  return cached;
343
335
  }
344
- var THINKING_LEVELS = new Set([
345
- "off",
346
- "minimal",
347
- "low",
348
- "medium",
349
- "high",
350
- "xhigh",
351
- "max"
352
- ]);
353
336
  var FALLBACK_THINKING = "medium";
354
337
  function parseModelAtom(value, field, problems) {
355
338
  const shape = msg.config.modelAtomShape(field);
@@ -361,7 +344,7 @@ function parseModelAtom(value, field, problems) {
361
344
  const model = slash > 0 ? value.slice(0, slash) : "";
362
345
  const thinking = slash > 0 ? value.slice(slash + 1) : value;
363
346
  const providerSlash = model.indexOf("/");
364
- const valid = THINKING_LEVELS.has(thinking);
347
+ const valid = THINKING_LEVELS.includes(thinking);
365
348
  const faults = [];
366
349
  if (providerSlash <= 0 || providerSlash === model.length - 1)
367
350
  faults.push(msg.config.modelSegment(model || value));
@@ -375,14 +358,14 @@ var PRESET_KEYS = ["model", "tools", "instructions", "key"];
375
358
  function parsePresets(value, problems) {
376
359
  if (value === undefined)
377
360
  return {};
378
- if (!isPlainObject(value)) {
361
+ if (!isRecord(value)) {
379
362
  problems.push(msg.config.mustBeObject("presets"));
380
363
  return {};
381
364
  }
382
365
  return Object.fromEntries(Object.entries(value).map(([name, raw]) => [name, parsePreset(raw, `presets.${name}`, problems)]));
383
366
  }
384
367
  function parsePreset(value, field, problems) {
385
- if (!isPlainObject(value)) {
368
+ if (!isRecord(value)) {
386
369
  problems.push(msg.config.mustBeObject(field));
387
370
  return {};
388
371
  }
@@ -394,21 +377,10 @@ function parsePreset(value, field, problems) {
394
377
  ...value.key === undefined ? {} : { key: stringValue(value.key, `${field}.key`, problems) }
395
378
  };
396
379
  }
397
- var REVIEW_KEYS = new Set([
398
- "advisor",
399
- "reviewers",
400
- "maxRounds",
401
- "advisorAfterFailures",
402
- "timeoutMinutes",
403
- "tools"
404
- ]);
380
+ var REVIEW_KEYS = ["advisor", "reviewers", "maxRounds", "advisorAfterFailures", "timeoutMinutes", "tools"];
405
381
  var DEFAULT_TOOLS = ["read", "grep", "find", "ls", "bash"];
406
382
  function parseReviewConfig(raw, problems) {
407
- for (const key of Object.keys(raw)) {
408
- if (REVIEW_KEYS.has(key))
409
- continue;
410
- problems.push(key === "background" ? msg.config.reviewBackground : msg.config.unknownField(`review.${key}`));
411
- }
383
+ rejectUnknownKeys(raw, REVIEW_KEYS, "review", problems);
412
384
  for (const key of REVIEW_KEYS)
413
385
  if (key !== "advisor" && key !== "reviewers" && !(key in raw))
414
386
  problems.push(msg.config.mustSet(`review.${key}`));
@@ -457,22 +429,17 @@ function parseMasterConfig(raw, problems) {
457
429
  problems.push(msg.config.unknownField(`master.${key}`));
458
430
  const exclusions = stringArray(raw.workerExcludeExtensions, "master.workerExcludeExtensions", problems);
459
431
  const autoActivate = booleanValue(raw.autoActivate, "master.autoActivate", true, problems);
460
- if (raw.roles === undefined)
461
- return { roles: [], workerExcludeExtensions: exclusions, autoActivate };
462
- if (!isPlainObject(raw.roles) || Object.keys(raw.roles).length === 0) {
432
+ const table = raw.roles;
433
+ if (table !== undefined && (!isRecord(table) || Object.keys(table).length === 0))
463
434
  problems.push(msg.config.masterRoles);
464
- return { roles: [], workerExcludeExtensions: exclusions, autoActivate };
465
- }
466
- const roles = Object.entries(raw.roles).map(([role, value]) => masterRole(value, `master.roles.${role}`, role, problems));
435
+ const roles = isRecord(table) ? Object.entries(table).map(([role, value]) => masterRole(value, `master.roles.${role}`, role, problems)) : [];
467
436
  return { roles, workerExcludeExtensions: exclusions, autoActivate };
468
437
  }
469
438
  function masterRole(value, field, role, problems) {
470
439
  const record = asRecord(value);
471
440
  rejectUnknownKeys(record, ["model", "use", "fallback"], field, problems);
472
441
  const atom = parseModelAtom(record.model, `${field}.model`, problems);
473
- const use = typeof record.use === "string" && record.use ? record.use : "";
474
- if (!use)
475
- problems.push(msg.config.mustBeString(`${field}.use`));
442
+ const use = stringValue(record.use, `${field}.use`, problems) ?? "";
476
443
  const fallback = masterFallback(record.fallback, `${field}.fallback`, problems);
477
444
  return { role, ...atom, use, fallback };
478
445
  }
@@ -606,8 +573,8 @@ function flame(cells, phase) {
606
573
  return out;
607
574
  }
608
575
  function settleMark(kind, sinceMs) {
609
- const k = easeOut(sinceMs / SETTLE_MS);
610
- return kind === "done" ? paint(mix(HEAT_COLORS.orange, HEAT_COLORS.green, k), "✓") : paint(mix(HEAT_COLORS.orange, HEAT_COLORS.fail, k), "✗");
576
+ const done = kind === "done";
577
+ return paint(mix(HEAT_COLORS.orange, done ? HEAT_COLORS.green : HEAT_COLORS.fail, easeOut(sinceMs / SETTLE_MS)), done ? "✓" : "✗");
611
578
  }
612
579
  var settling = (sinceMs) => sinceMs < SETTLE_MS;
613
580
  var SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
@@ -732,8 +699,6 @@ var FADE_FROM = [20, 19, 18];
732
699
  var RULE2 = [60, 57, 53];
733
700
  var TINY_SUBTITLE_MIN = 8;
734
701
  var TINY_RULE_MIN = 3;
735
- var clamp2 = (value) => Math.min(1, Math.max(0, value));
736
- var ease = (k) => 1 - (1 - clamp2(k)) ** 3;
737
702
  var FLAME = [
738
703
  "......#......",
739
704
  "......##.....",
@@ -785,12 +750,12 @@ function flameHeat(y) {
785
750
  return v < FLAME_RED_END ? mix(HEAT_COLORS.red, HEAT_COLORS.orange, v / FLAME_RED_END) : mix(HEAT_COLORS.orange, HEAT_COLORS.gold, (v - FLAME_RED_END) / (1 - FLAME_RED_END));
786
751
  }
787
752
  function logo(t) {
788
- const flameLine = ease(t / IGNITE_END) * (FLAME.length + 4);
753
+ const flameLine = easeOut(t / IGNITE_END) * (FLAME.length + 4);
789
754
  const wordWidth = WORD_ROWS[0].length;
790
755
  const wordEdge = (t - REVEAL_START) / (SETTLE_END - REVEAL_START) * wordWidth;
791
756
  return FLAME.map((row, y) => {
792
757
  const burnt = flameLine - (FLAME.length - y);
793
- const color = mix(HEAT_COLORS.white, flameHeat(y), clamp2(burnt / 4));
758
+ const color = mix(HEAT_COLORS.white, flameHeat(y), clamp(burnt / 4));
794
759
  const pixels = [...row].map((dot) => dot === "#" && burnt >= 0 ? color : undefined);
795
760
  pixels.push(...Array(LOGO_GAP).fill(undefined));
796
761
  const word = WORD_ROWS[y - WORD_TOP] ?? "";
@@ -812,7 +777,7 @@ function fitSubtitle({ version, cwd }, width) {
812
777
  return clip(segments.at(-1) ?? cwd, width, "start");
813
778
  }
814
779
  function subtitleLine(subtitle, width, t) {
815
- return paint(mix(FADE_FROM, HEAT_COLORS.ash, ease((t - 0.4) / 0.6)), fitSubtitle(subtitle, width));
780
+ return paint(mix(FADE_FROM, HEAT_COLORS.ash, easeOut((t - 0.4) / 0.6)), fitSubtitle(subtitle, width));
816
781
  }
817
782
  function large(t, subtitle) {
818
783
  const rows = halfBlocks(logo(t));
@@ -826,10 +791,10 @@ function tiny(width, t, glyph, subtitle) {
826
791
  const room = width - visibleWidth2(word) - 1 - TINY_RULE_MIN;
827
792
  const head = room >= TINY_SUBTITLE_MIN ? `${word}${subtitleLine(subtitle, room, t)} ` : word;
828
793
  const length = width - visibleWidth2(head);
829
- const reveal = clamp2((t - REVEAL_START) / (SETTLE_END - REVEAL_START));
794
+ const reveal = clamp((t - REVEAL_START) / (SETTLE_END - REVEAL_START));
830
795
  let rule = "";
831
796
  for (let index = 0;index < length; index++)
832
- rule += index / length <= reveal ? paint(mix(HEAT_COLORS.gold, RULE2, ease(index / Math.max(8, length * 0.7))), "─") : " ";
797
+ rule += index / length <= reveal ? paint(mix(HEAT_COLORS.gold, RULE2, easeOut(index / Math.max(8, length * 0.7))), "─") : " ";
833
798
  return clip(head + rule, width, "end", "");
834
799
  }
835
800
  function center(lines, width) {
@@ -882,8 +847,6 @@ function registerHeader(pi) {
882
847
  // provider/claude-sub.ts
883
848
  import { execFileSync } from "node:child_process";
884
849
  import { createHash } from "node:crypto";
885
- import { appendFileSync, mkdirSync as mkdirSync2 } from "node:fs";
886
- import { dirname as dirname2, resolve } from "node:path";
887
850
 
888
851
  // provider/messages.ts
889
852
  var msg2 = defineMessages({
@@ -912,13 +875,9 @@ var msg2 = defineMessages({
912
875
  // provider/claude-sub.ts
913
876
  var BILLING_PREFIX = "x-anthropic-billing-header:";
914
877
  var FALLBACK_CLAUDE_CODE_VERSION = "2.1.281";
915
- var DEFAULT_ENTRYPOINT = "cli";
916
878
  var BILLING_SALT = "59cf53e54c78";
917
- function isObject(value) {
918
- return typeof value === "object" && value !== null;
919
- }
920
879
  function isTextBlock(value) {
921
- return isObject(value) && value.type === "text" && typeof value.text === "string";
880
+ return isRecord(value) && value.type === "text" && typeof value.text === "string";
922
881
  }
923
882
  function shouldApply(ctx) {
924
883
  const model = ctx.model;
@@ -943,33 +902,16 @@ function detectClaudeCodeVersion() {
943
902
  var claudeCodeVersion = detectClaudeCodeVersion();
944
903
  function firstUserText(messages) {
945
904
  const list = Array.isArray(messages) ? messages : [];
946
- const firstUser = list.find((message) => isObject(message) && message.role === "user");
947
- return isObject(firstUser) ? textOf(firstUser.content) : "";
905
+ const firstUser = list.find((message) => isRecord(message) && message.role === "user");
906
+ return isRecord(firstUser) ? textOf(firstUser.content) : "";
948
907
  }
949
908
  function versionSuffix(messageText) {
950
- const explicit = process.env.PI_CLAUDE_CODE_VERSION_SUFFIX;
951
- if (explicit)
952
- return explicit;
953
909
  const sampled = [4, 7, 20].map((index) => messageText[index] ?? "0").join("");
954
910
  return createHash("sha256").update(`${BILLING_SALT}${sampled}${claudeCodeVersion}`).digest("hex").slice(0, 3);
955
911
  }
956
912
  function buildBillingHeader(messages) {
957
913
  const version = `${claudeCodeVersion}.${versionSuffix(firstUserText(messages))}`;
958
- const entrypoint = process.env.PI_CLAUDE_CODE_ENTRYPOINT ?? process.env.CLAUDE_CODE_ENTRYPOINT ?? DEFAULT_ENTRYPOINT;
959
- const workload = process.env.PI_CLAUDE_CODE_WORKLOAD ?? process.env.CLAUDE_CODE_WORKLOAD;
960
- const workloadPart = workload ? ` cc_workload=${workload};` : "";
961
- return `${BILLING_PREFIX} cc_version=${version}; cc_entrypoint=${entrypoint}; cch=00000;${workloadPart}`;
962
- }
963
- function log(details) {
964
- const logFile = process.env.PI_CLAUDE_OAUTH_LOG_FILE;
965
- if (!logFile)
966
- return;
967
- const path = resolve(logFile);
968
- try {
969
- mkdirSync2(dirname2(path), { recursive: true });
970
- appendFileSync(path, `${JSON.stringify({ timestamp: new Date().toISOString(), ...details })}
971
- `, "utf8");
972
- } catch {}
914
+ return `${BILLING_PREFIX} cc_version=${version}; cc_entrypoint=cli; cch=00000;`;
973
915
  }
974
916
  function isRevokedTokenFailure(entry) {
975
917
  if (entry?.type !== "message" || entry.message.role !== "assistant")
@@ -997,14 +939,13 @@ function registerClaudeSub(pi) {
997
939
  });
998
940
  pi.on("before_provider_request", (event, ctx) => {
999
941
  const payload = event.payload;
1000
- if (!shouldApply(ctx) || !isObject(payload))
942
+ if (!shouldApply(ctx) || !isRecord(payload))
1001
943
  return;
1002
944
  const { system, messages } = payload;
1003
945
  const blocks = Array.isArray(system) ? system : [];
1004
946
  if (blocks.some((block) => isTextBlock(block) && block.text.startsWith(BILLING_PREFIX)))
1005
947
  return;
1006
948
  const header = { type: "text", text: buildBillingHeader(messages) };
1007
- log({ event: "billing_header_injected", header: header.text });
1008
949
  return { ...payload, system: [header, ...blocks] };
1009
950
  });
1010
951
  pi.on("agent_before_settle", (event, ctx) => {
@@ -1021,9 +962,6 @@ function registerClaudeSub(pi) {
1021
962
  // provider/openai-native/src/config.ts
1022
963
  import { closeSync, openSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
1023
964
  var TEXT_VERBOSITIES = ["low", "medium", "high"];
1024
- function isRecord(value) {
1025
- return value !== null && typeof value === "object" && !Array.isArray(value);
1026
- }
1027
965
  function isTextVerbosity(value) {
1028
966
  return typeof value === "string" && TEXT_VERBOSITIES.includes(value);
1029
967
  }
@@ -1083,11 +1021,8 @@ function replaceKeyedObject(text, key, value) {
1083
1021
  }
1084
1022
  return;
1085
1023
  }
1086
- function openaiRoot(root) {
1087
- return isRecord(root.openai) ? root.openai : root;
1088
- }
1089
- function isFirecodeConfig(root) {
1090
- return "openai" in root || "presets" in root || "keys" in root || "features" in root;
1024
+ function openaiSection(root) {
1025
+ return isRecord(root.openai) ? root.openai : {};
1091
1026
  }
1092
1027
  function parseProviderSettings(provider, value, warnings) {
1093
1028
  if (!isRecord(value)) {
@@ -1181,17 +1116,10 @@ function acquireConfigLock(lockPath) {
1181
1116
  rmSync(lockPath, { force: true });
1182
1117
  return openSync(lockPath, "wx", 384);
1183
1118
  }
1184
- function writeConfig(configPath, settings, firecodeRoot) {
1119
+ function writeConfig(configPath, settings, root) {
1185
1120
  const serialized = serializeSettings(settings);
1186
- let text;
1187
- if (firecodeRoot) {
1188
- const original = readFileSync3(configPath, "utf8");
1189
- text = replaceKeyedObject(original, "openai", serialized) ?? `${JSON.stringify({ ...firecodeRoot, openai: serialized }, null, "\t")}
1190
- `;
1191
- } else {
1192
- text = `${JSON.stringify(serialized, null, 2)}
1121
+ const text = replaceKeyedObject(readFileSync3(configPath, "utf8"), "openai", serialized) ?? `${JSON.stringify({ ...root, openai: serialized }, null, "\t")}
1193
1122
  `;
1194
- }
1195
1123
  const temporaryPath = `${configPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
1196
1124
  try {
1197
1125
  writeFileSync2(temporaryPath, text, {
@@ -1217,11 +1145,11 @@ function withConfigLock(configPath, action) {
1217
1145
  }
1218
1146
  function loadOpenAINativeSettings(configPath) {
1219
1147
  try {
1220
- return parseSettings(openaiRoot(readConfig(configPath)));
1148
+ return parseSettings(openaiSection(readConfig(configPath)));
1221
1149
  } catch (error) {
1222
1150
  return {
1223
1151
  settings: createDefaultSettings(),
1224
- warnings: [`config.json: ${error instanceof Error ? error.message : String(error)}`]
1152
+ warnings: [`config.jsonc: ${error instanceof Error ? error.message : String(error)}`]
1225
1153
  };
1226
1154
  }
1227
1155
  }
@@ -1231,7 +1159,7 @@ function togglePriority(provider, configPath) {
1231
1159
  }
1232
1160
  return withConfigLock(configPath, () => {
1233
1161
  const root = readConfig(configPath);
1234
- const loaded = parseSettings(openaiRoot(root));
1162
+ const loaded = parseSettings(openaiSection(root));
1235
1163
  const providerSettings = loaded.settings.providers[provider] ?? {};
1236
1164
  const enabled = providerSettings.priority !== true;
1237
1165
  const providers = {
@@ -1251,16 +1179,13 @@ function togglePriority(provider, configPath) {
1251
1179
  nativeCompaction: loaded.settings.nativeCompaction,
1252
1180
  providers
1253
1181
  };
1254
- writeConfig(configPath, settings, isFirecodeConfig(root) ? root : undefined);
1182
+ writeConfig(configPath, settings, root);
1255
1183
  return {
1256
1184
  enabled,
1257
1185
  loaded: { settings, warnings: loaded.warnings }
1258
1186
  };
1259
1187
  });
1260
1188
  }
1261
- function providerSettings(settings, provider) {
1262
- return provider ? settings.providers[provider] : undefined;
1263
- }
1264
1189
 
1265
1190
  // provider/openai-native/src/native-compaction.ts
1266
1191
  import { buildSessionContext } from "@earendil-works/pi-coding-agent";
@@ -1269,9 +1194,6 @@ import { buildSessionContext } from "@earendil-works/pi-coding-agent";
1269
1194
  var JSON_CONTENT_TYPE = "application/json";
1270
1195
  var COMPACTION_TRIGGER = { type: "compaction_trigger" };
1271
1196
  var RETAINED_MESSAGE_TOKEN_BUDGET = 64000;
1272
- function isRecord2(value) {
1273
- return value !== null && typeof value === "object" && !Array.isArray(value);
1274
- }
1275
1197
  function isAbortError(error) {
1276
1198
  return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR");
1277
1199
  }
@@ -1287,34 +1209,34 @@ function normalizeResponseTimestamp(value) {
1287
1209
  return Number.isNaN(parsed) ? value.trim() : new Date(parsed).toISOString();
1288
1210
  }
1289
1211
  function errorMessage(value) {
1290
- if (!isRecord2(value)) {
1212
+ if (!isRecord(value)) {
1291
1213
  return;
1292
1214
  }
1293
1215
  if (typeof value.message === "string" && value.message.trim()) {
1294
1216
  return value.message.trim();
1295
1217
  }
1296
- if (isRecord2(value.error)) {
1218
+ if (isRecord(value.error)) {
1297
1219
  return errorMessage(value.error);
1298
1220
  }
1299
- if (isRecord2(value.response)) {
1221
+ if (isRecord(value.response)) {
1300
1222
  return errorMessage(value.response);
1301
1223
  }
1302
1224
  return;
1303
1225
  }
1304
1226
  function providerError(value) {
1305
- if (!isRecord2(value)) {
1227
+ if (!isRecord(value)) {
1306
1228
  return;
1307
1229
  }
1308
- if (isRecord2(value.error)) {
1230
+ if (isRecord(value.error)) {
1309
1231
  return value.error;
1310
1232
  }
1311
1233
  return providerError(value.response);
1312
1234
  }
1313
1235
  function parseFailedResponse(value) {
1314
- if (!isRecord2(value)) {
1236
+ if (!isRecord(value)) {
1315
1237
  return;
1316
1238
  }
1317
- const response = isRecord2(value.response) ? value.response : value;
1239
+ const response = isRecord(value.response) ? value.response : value;
1318
1240
  if (value.type !== "response.failed" && value.type !== "error" && response.status !== "failed") {
1319
1241
  return;
1320
1242
  }
@@ -1334,10 +1256,10 @@ function parseErrorDetail(responseText) {
1334
1256
  }
1335
1257
  }
1336
1258
  function isOutputArray(value) {
1337
- return Array.isArray(value) && value.every(isRecord2);
1259
+ return Array.isArray(value) && value.every(isRecord);
1338
1260
  }
1339
1261
  function parseJsonOutput(value) {
1340
- if (!isRecord2(value) || !isOutputArray(value.output)) {
1262
+ if (!isRecord(value) || !isOutputArray(value.output)) {
1341
1263
  return;
1342
1264
  }
1343
1265
  return { output: value.output, createdAt: value.created_at };
@@ -1358,17 +1280,17 @@ function parseSseOutput(responseText) {
1358
1280
  } catch {
1359
1281
  continue;
1360
1282
  }
1361
- if (!isRecord2(event)) {
1283
+ if (!isRecord(event)) {
1362
1284
  continue;
1363
1285
  }
1364
- if (event.type === "response.output_item.done" && isRecord2(event.item)) {
1286
+ if (event.type === "response.output_item.done" && isRecord(event.item)) {
1365
1287
  streamedItems.push(event.item);
1366
1288
  continue;
1367
1289
  }
1368
1290
  if (event.type === "response.completed") {
1369
1291
  completed = parseJsonOutput(event.response) ?? {
1370
1292
  output: [],
1371
- createdAt: isRecord2(event.response) ? event.response.created_at : undefined
1293
+ createdAt: isRecord(event.response) ? event.response.created_at : undefined
1372
1294
  };
1373
1295
  continue;
1374
1296
  }
@@ -1408,7 +1330,7 @@ function parseResponseOutput(responseText) {
1408
1330
  return sse ? { ok: false, ...sse } : { ok: false, reason: "malformed-response" };
1409
1331
  }
1410
1332
  function isRetainedInputItem(value) {
1411
- return isRecord2(value) && (value.role === "user" || value.role === "developer" || value.role === "system");
1333
+ return isRecord(value) && (value.role === "user" || value.role === "developer" || value.role === "system");
1412
1334
  }
1413
1335
  function itemText(item) {
1414
1336
  if (typeof item.content === "string") {
@@ -1417,7 +1339,7 @@ function itemText(item) {
1417
1339
  if (!Array.isArray(item.content)) {
1418
1340
  return "";
1419
1341
  }
1420
- return item.content.filter(isRecord2).map((part) => typeof part.text === "string" ? part.text : "").join("");
1342
+ return item.content.filter(isRecord).map((part) => typeof part.text === "string" ? part.text : "").join("");
1421
1343
  }
1422
1344
  function estimateTokens(item) {
1423
1345
  return Math.max(1, Math.ceil(itemText(item).length / 4));
@@ -1447,14 +1369,14 @@ function decodeJwtPayload(token) {
1447
1369
  }
1448
1370
  try {
1449
1371
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1450
- return isRecord2(payload) ? payload : undefined;
1372
+ return isRecord(payload) ? payload : undefined;
1451
1373
  } catch {
1452
1374
  return;
1453
1375
  }
1454
1376
  }
1455
1377
  function codexAccountId(token) {
1456
1378
  const authClaims = decodeJwtPayload(token)?.["https://api.openai.com/auth"];
1457
- if (!isRecord2(authClaims) || typeof authClaims.chatgpt_account_id !== "string") {
1379
+ if (!isRecord(authClaims) || typeof authClaims.chatgpt_account_id !== "string") {
1458
1380
  return;
1459
1381
  }
1460
1382
  return authClaims.chatgpt_account_id.trim() || undefined;
@@ -1540,9 +1462,6 @@ async function executeNativeCompaction(args) {
1540
1462
  // provider/openai-native/src/native-details.ts
1541
1463
  var NATIVE_COMPACTION_STRATEGY = "openai-native-compact";
1542
1464
  var NATIVE_COMPACTION_SUMMARY = "[OpenAI native compaction checkpoint]";
1543
- function isRecord3(value) {
1544
- return value !== null && typeof value === "object" && !Array.isArray(value);
1545
- }
1546
1465
  function isNonEmptyString(value) {
1547
1466
  return typeof value === "string" && value.trim().length > 0;
1548
1467
  }
@@ -1553,10 +1472,10 @@ function isStructuredValue(value) {
1553
1472
  if (Array.isArray(value)) {
1554
1473
  return value.every(isStructuredValue);
1555
1474
  }
1556
- return isRecord3(value) && Object.values(value).every(isStructuredValue);
1475
+ return isRecord(value) && Object.values(value).every(isStructuredValue);
1557
1476
  }
1558
1477
  function isCompactedWindowItem(value) {
1559
- return isRecord3(value) && Object.values(value).every(isStructuredValue);
1478
+ return isRecord(value) && Object.values(value).every(isStructuredValue);
1560
1479
  }
1561
1480
  function hasNativeCompactionIdentity(value) {
1562
1481
  return isNonEmptyString(value.provider) && isNonEmptyString(value.api) && isNonEmptyString(value.model) && isNonEmptyString(value.baseUrl);
@@ -1568,13 +1487,13 @@ function cloneStructuredValue(value) {
1568
1487
  if (Array.isArray(value)) {
1569
1488
  return value.map(cloneStructuredValue);
1570
1489
  }
1571
- if (isRecord3(value)) {
1490
+ if (isRecord(value)) {
1572
1491
  return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [key, cloneStructuredValue(nestedValue)]));
1573
1492
  }
1574
1493
  throw new Error(`Unsupported structured value: ${typeof value}`);
1575
1494
  }
1576
1495
  function isNativeCompactionDetails(value) {
1577
- if (!isRecord3(value) || value.strategy !== NATIVE_COMPACTION_STRATEGY || !hasNativeCompactionIdentity(value)) {
1496
+ if (!isRecord(value) || value.strategy !== NATIVE_COMPACTION_STRATEGY || !hasNativeCompactionIdentity(value)) {
1578
1497
  return false;
1579
1498
  }
1580
1499
  return Array.isArray(value.compactedWindow) && value.compactedWindow.every(isCompactedWindowItem) && isNonEmptyString(value.createdAt);
@@ -1930,11 +1849,8 @@ function isToolCallBlock(block) {
1930
1849
  }
1931
1850
 
1932
1851
  // provider/openai-native/src/native-replay.ts
1933
- function isRecord4(value) {
1934
- return value !== null && typeof value === "object" && !Array.isArray(value);
1935
- }
1936
1852
  function isResponsesInputContentItem(value) {
1937
- if (!isRecord4(value) || typeof value.type !== "string") {
1853
+ if (!isRecord(value) || typeof value.type !== "string") {
1938
1854
  return false;
1939
1855
  }
1940
1856
  if (value.type === "input_text") {
@@ -1943,7 +1859,7 @@ function isResponsesInputContentItem(value) {
1943
1859
  return value.type === "input_image" && value.detail === "auto" && typeof value.image_url === "string";
1944
1860
  }
1945
1861
  function isResponsesInputMessageItem(value) {
1946
- if (!isRecord4(value) || value.role !== "user" && value.role !== "developer" && value.role !== "system") {
1862
+ if (!isRecord(value) || value.role !== "user" && value.role !== "developer" && value.role !== "system") {
1947
1863
  return false;
1948
1864
  }
1949
1865
  return typeof value.content === "string" || Array.isArray(value.content) && value.content.every(isResponsesInputContentItem);
@@ -1981,7 +1897,7 @@ function areEquivalentValues(left, right) {
1981
1897
  if (Array.isArray(left) || Array.isArray(right)) {
1982
1898
  return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => areEquivalentValues(item, right[index]));
1983
1899
  }
1984
- if (!isRecord4(left) || !isRecord4(right)) {
1900
+ if (!isRecord(left) || !isRecord(right)) {
1985
1901
  return false;
1986
1902
  }
1987
1903
  const leftKeys = Object.keys(left).sort();
@@ -2306,12 +2222,6 @@ var PRIORITY_MODEL_IDS = new Set([
2306
2222
  "gpt-6.1-sol",
2307
2223
  "gpt-6-luna"
2308
2224
  ]);
2309
- function isRecord5(value) {
2310
- return value !== null && typeof value === "object" && !Array.isArray(value);
2311
- }
2312
- function isTextVerbosity2(value) {
2313
- return value === "low" || value === "medium" || value === "high";
2314
- }
2315
2225
  function isOpenAIResponsesModel(model) {
2316
2226
  return model !== undefined && OPENAI_RESPONSES_APIS.has(model.api);
2317
2227
  }
@@ -2326,24 +2236,24 @@ function fastModeEnabled(model, settings) {
2326
2236
  if (!model || !supportsFastMode(model)) {
2327
2237
  return false;
2328
2238
  }
2329
- return providerSettings(settings, model.provider)?.priority === true;
2239
+ return settings.providers[model.provider]?.priority === true;
2330
2240
  }
2331
2241
  function resolveTextVerbosity(model, settings, verbosityOverride) {
2332
2242
  if (verbosityOverride !== undefined) {
2333
- return isTextVerbosity2(verbosityOverride) ? verbosityOverride : undefined;
2243
+ return isTextVerbosity(verbosityOverride) ? verbosityOverride : undefined;
2334
2244
  }
2335
- return providerSettings(settings, model.provider)?.textVerbosity;
2245
+ return settings.providers[model.provider]?.textVerbosity;
2336
2246
  }
2337
2247
  function applyOpenAIOptions(payload, model, settings, verbosityOverride) {
2338
- const configuredProvider = model ? providerSettings(settings, model.provider) : undefined;
2248
+ const configuredProvider = model ? settings.providers[model.provider] : undefined;
2339
2249
  let nextPayload = payload;
2340
2250
  if (model && isOpenAIResponsesModel(model)) {
2341
2251
  const textVerbosity = resolveTextVerbosity(model, settings, verbosityOverride);
2342
- if (textVerbosity && (!isRecord5(payload.text) || payload.text.verbosity !== textVerbosity)) {
2252
+ if (textVerbosity && (!isRecord(payload.text) || payload.text.verbosity !== textVerbosity)) {
2343
2253
  nextPayload = {
2344
2254
  ...nextPayload,
2345
2255
  text: {
2346
- ...isRecord5(nextPayload.text) ? nextPayload.text : {},
2256
+ ...isRecord(nextPayload.text) ? nextPayload.text : {},
2347
2257
  verbosity: textVerbosity
2348
2258
  }
2349
2259
  };
@@ -2357,12 +2267,9 @@ function applyOpenAIOptions(payload, model, settings, verbosityOverride) {
2357
2267
  }
2358
2268
 
2359
2269
  // provider/openai-native/src/request-pipeline.ts
2360
- function isRecord6(value) {
2361
- return value !== null && typeof value === "object" && !Array.isArray(value);
2362
- }
2363
2270
  function rewriteOpenAIProviderRequest(payload, ctx, settings, verbosityOverride) {
2364
2271
  let nextPayload = settings.nativeCompaction ? replayOpenAINative(payload, ctx) ?? payload : payload;
2365
- if (isRecord6(nextPayload)) {
2272
+ if (isRecord(nextPayload)) {
2366
2273
  nextPayload = applyOpenAIOptions(nextPayload, ctx.model, settings, verbosityOverride);
2367
2274
  }
2368
2275
  return nextPayload;
@@ -2607,20 +2514,21 @@ function describe(preset) {
2607
2514
  return parts.join(" | ");
2608
2515
  }
2609
2516
  function registerPresets(pi) {
2610
- let presets = {};
2517
+ const { presets } = loadConfig().config;
2611
2518
  let activeName;
2612
- let activePreset;
2613
2519
  let originalState;
2614
2520
  let applying = false;
2615
2521
  pi.registerFlag("preset", {
2616
2522
  description: msg3.presets.flagDescription,
2617
2523
  type: "string"
2618
2524
  });
2619
- const updateStatus = (ctx) => ctx.ui.setStatus("preset", activeName && (activePreset?.tools?.length || activePreset?.instructions) ? ctx.ui.theme.fg("accent", title(activeName)) : undefined);
2620
- const noPresetsHint = (ctx) => ctx.ui.notify(msg3.presets.noneDefined, "warning");
2525
+ const updateStatus = (ctx) => {
2526
+ const active = activePreset();
2527
+ ctx.ui.setStatus("preset", activeName && (active?.tools?.length || active?.instructions) ? ctx.ui.theme.fg("accent", title(activeName)) : undefined);
2528
+ };
2529
+ const activePreset = () => activeName === undefined ? undefined : presets[activeName];
2621
2530
  function setActive(name, ctx) {
2622
2531
  activeName = name;
2623
- activePreset = name === undefined ? undefined : presets[name];
2624
2532
  if (name === undefined)
2625
2533
  originalState = undefined;
2626
2534
  pi.appendEntry(STATE_ENTRY, { name: name ?? null });
@@ -2637,7 +2545,8 @@ function registerPresets(pi) {
2637
2545
  if (valid.length)
2638
2546
  pi.setActiveTools(valid);
2639
2547
  }
2640
- async function applyPreset(name, preset, ctx) {
2548
+ async function applyPreset(name, ctx) {
2549
+ const preset = presets[name];
2641
2550
  const snapshot = activeName === undefined ? { model: ctx.model, thinkingLevel: pi.getThinkingLevel(), tools: pi.getActiveTools() } : originalState;
2642
2551
  applying = true;
2643
2552
  const failure = await applyModel(preset, ctx).finally(() => {
@@ -2665,18 +2574,13 @@ function registerPresets(pi) {
2665
2574
  return;
2666
2575
  }
2667
2576
  function dropIfDiverged(ctx) {
2668
- if (!activeName || !activePreset || holds(activePreset, ctx.model))
2577
+ const active = activePreset();
2578
+ if (!activeName || !active || holds(active, ctx.model))
2669
2579
  return;
2670
2580
  const name = activeName;
2671
2581
  setActive(undefined, ctx);
2672
2582
  ctx.ui.notify(msg3.presets.diverged(name), "info");
2673
2583
  }
2674
- async function activate(name, ctx) {
2675
- const preset = presets[name];
2676
- if (!preset)
2677
- return;
2678
- await applyPreset(name, preset, ctx);
2679
- }
2680
2584
  async function clearPreset(ctx) {
2681
2585
  const original = originalState;
2682
2586
  setActive(undefined, ctx);
@@ -2691,7 +2595,7 @@ function registerPresets(pi) {
2691
2595
  async function showSelector(ctx) {
2692
2596
  const names = Object.keys(presets);
2693
2597
  if (names.length === 0) {
2694
- noPresetsHint(ctx);
2598
+ ctx.ui.notify(msg3.presets.noneDefined, "warning");
2695
2599
  return;
2696
2600
  }
2697
2601
  const items = names.map((name) => ({
@@ -2734,15 +2638,14 @@ function registerPresets(pi) {
2734
2638
  if (choice === CLEAR_ITEM)
2735
2639
  await clearPreset(ctx);
2736
2640
  else
2737
- await activate(choice, ctx);
2641
+ await applyPreset(choice, ctx);
2738
2642
  }
2739
- const { presets: configured } = loadConfig().config;
2740
- for (const [name, preset] of Object.entries(configured)) {
2643
+ for (const [name, preset] of Object.entries(presets)) {
2741
2644
  if (!preset.key)
2742
2645
  continue;
2743
2646
  pi.registerShortcut(preset.key, {
2744
2647
  description: msg3.presets.shortcutDescription(name),
2745
- handler: (ctx) => activate(name, ctx)
2648
+ handler: (ctx) => applyPreset(name, ctx)
2746
2649
  });
2747
2650
  }
2748
2651
  pi.registerCommand("preset", {
@@ -2762,7 +2665,7 @@ function registerPresets(pi) {
2762
2665
  ctx.ui.notify(msg3.presets.unknown(name, available), "error");
2763
2666
  return;
2764
2667
  }
2765
- await activate(name, ctx);
2668
+ await applyPreset(name, ctx);
2766
2669
  }
2767
2670
  });
2768
2671
  pi.on("model_select", (_event, ctx) => {
@@ -2771,15 +2674,15 @@ function registerPresets(pi) {
2771
2674
  });
2772
2675
  pi.on("before_agent_start", async (event, ctx) => {
2773
2676
  dropIfDiverged(ctx);
2774
- if (!activePreset?.instructions)
2677
+ const instructions = activePreset()?.instructions;
2678
+ if (!instructions)
2775
2679
  return;
2776
2680
  return { systemPrompt: `${event.systemPrompt}
2777
2681
 
2778
- ${activePreset.instructions}` };
2682
+ ${instructions}` };
2779
2683
  });
2780
2684
  const reset = () => {
2781
2685
  activeName = undefined;
2782
- activePreset = undefined;
2783
2686
  originalState = undefined;
2784
2687
  };
2785
2688
  pi.on("session_tree", (_event, ctx) => {
@@ -2789,11 +2692,10 @@ ${activePreset.instructions}` };
2789
2692
  });
2790
2693
  pi.on("session_start", async (_event, ctx) => {
2791
2694
  reset();
2792
- presets = loadConfig().config.presets;
2793
2695
  const flag = pi.getFlag("preset");
2794
2696
  if (typeof flag === "string" && flag) {
2795
2697
  if (presets[flag]) {
2796
- await applyPreset(flag, presets[flag], ctx);
2698
+ await applyPreset(flag, ctx);
2797
2699
  } else {
2798
2700
  const available = Object.keys(presets).join(", ") || msg3.presets.noneAvailable;
2799
2701
  ctx.ui.notify(msg3.presets.unknown(flag, available), "warning");
@@ -2814,7 +2716,6 @@ ${activePreset.instructions}` };
2814
2716
  return;
2815
2717
  }
2816
2718
  activeName = name;
2817
- activePreset = preset;
2818
2719
  originalState = { model: undefined, thinkingLevel: pi.getThinkingLevel(), tools: pi.getActiveTools() };
2819
2720
  }
2820
2721
  }
@@ -2982,11 +2883,12 @@ var SOURCE = "firecode";
2982
2883
  var AGENT = "pi";
2983
2884
  var RETRY_TIMEOUT_MS = 1500;
2984
2885
  var UNSAFE_ARGUMENT = /['\u0000-\u001f\u007f]/;
2985
- var seq = Date.now() * 1000;
2986
- var nextSeq = () => seq += 1;
2886
+ var seq = 0;
2887
+ var nextSeq = () => seq = Math.max(seq + 1, Date.now() * 1000);
2987
2888
  var agentKey = (p) => `${p.working}\x00${p.resume?.join("\x00") ?? ""}`;
2988
2889
  var metaKey = (p) => `${p.title}\x00${p.agent}\x00${p.label ?? ""}`;
2989
- function identityOf(ctx, thinking) {
2890
+ function identityOf(ctx, pi) {
2891
+ const thinking = ctx.model?.reasoning ? pi.getThinkingLevel() : undefined;
2990
2892
  const level = thinking && thinking !== "off" ? `/${thinking}` : "";
2991
2893
  return { title: ctx.sessionManager.getSessionName() ?? "", agent: `pi·${formatModelName(ctx.model?.id)}${level}` };
2992
2894
  }
@@ -2999,9 +2901,9 @@ function labelOf(view) {
2999
2901
  return msg3.herdr.review;
3000
2902
  return !view.agentRunning && view.inFlight > 0 ? msg3.herdr.workers : undefined;
3001
2903
  }
3002
- function registerHerdrProjection(pi, subsession = false) {
2904
+ function registerHerdrProjection(pi) {
3003
2905
  const env = herdrPaneEnv();
3004
- if (!env || subsession)
2906
+ if (!env)
3005
2907
  return () => Promise.resolve();
3006
2908
  const paneId = env.paneId;
3007
2909
  let enabled = false;
@@ -3072,14 +2974,14 @@ function registerHerdrProjection(pi, subsession = false) {
3072
2974
  });
3073
2975
  const syncIdentity = (ctx) => {
3074
2976
  if (ctx.mode === "tui")
3075
- update(identityOf(ctx, ctx.model?.reasoning ? pi.getThinkingLevel() : undefined));
2977
+ update(identityOf(ctx, pi));
3076
2978
  };
3077
2979
  pi.on("session_start", (_event, ctx) => {
3078
2980
  if (ctx.mode !== "tui")
3079
2981
  return;
3080
2982
  carriedRun = !ctx.isIdle();
3081
2983
  enabled = true;
3082
- desired = { ...desired, ...identityOf(ctx, ctx.model?.reasoning ? pi.getThinkingLevel() : undefined), resume: resumeOf(ctx) };
2984
+ desired = { ...desired, ...identityOf(ctx, pi), resume: resumeOf(ctx) };
3083
2985
  project();
3084
2986
  });
3085
2987
  pi.on("session_info_changed", (_event, ctx) => syncIdentity(ctx));
@@ -3796,6 +3698,9 @@ class Shell {
3796
3698
  this.stopClock?.();
3797
3699
  this.stopClock = undefined;
3798
3700
  this.requestRender = () => {};
3701
+ this.busy = IDLE;
3702
+ this.settled = undefined;
3703
+ this.review = undefined;
3799
3704
  }
3800
3705
  sync(view) {
3801
3706
  this.busy = view;
@@ -3895,9 +3800,7 @@ class ShellEditor extends CustomEditor {
3895
3800
  return hiddenLineCount > 0 ? super.renderBottomBorder(width, hiddenLineCount) : bottomBorder(width, this.bottomParts(), this.borderColor);
3896
3801
  }
3897
3802
  }
3898
- function registerStatusBar(pi, subsession = false) {
3899
- if (subsession)
3900
- return;
3803
+ function registerStatusBar(pi) {
3901
3804
  const shell = new Shell;
3902
3805
  const updateTitle = (ctx, incoming) => {
3903
3806
  shell.title = displayTitle(ctx, incoming);
@@ -3948,9 +3851,6 @@ function registerStatusBar(pi, subsession = false) {
3948
3851
  pi.on("model_select", () => shell.requestRender());
3949
3852
  pi.on("session_shutdown", (_event, ctx) => {
3950
3853
  shell.dispose();
3951
- shell.busy = IDLE;
3952
- shell.settled = undefined;
3953
- shell.review = undefined;
3954
3854
  ctx.ui.setFooter(undefined);
3955
3855
  ctx.ui.setEditorComponent(undefined);
3956
3856
  });
@@ -4160,7 +4060,6 @@ function genericArgsParts(args) {
4160
4060
  }
4161
4061
 
4162
4062
  // tools/actions.ts
4163
- var LABEL = msg4.labels;
4164
4063
  var argPath = (args) => args?.file_path ?? args?.path ?? "";
4165
4064
  function rangeSuffix(args) {
4166
4065
  if (args.offset === undefined && args.limit === undefined)
@@ -4193,9 +4092,10 @@ function toolTarget(tool, args, cwd) {
4193
4092
  return { value: genericArgsParts(args), clip: "end" };
4194
4093
  }
4195
4094
  }
4095
+ var toolTargetText = (tool, args, cwd) => toolTarget(tool, args, cwd).value.map((part) => part.text).join("").trim();
4196
4096
  function toolActionText(tool, args, cwd) {
4197
- const word = LABEL[tool] ?? tool;
4198
- const target = toolTarget(tool, args, cwd).value.map((part) => part.text).join("");
4097
+ const word = msg4.labels[tool] ?? tool;
4098
+ const target = toolTargetText(tool, args, cwd);
4199
4099
  return target ? `${word} ${target}` : word;
4200
4100
  }
4201
4101
 
@@ -4511,7 +4411,11 @@ function entryShell(marker) {
4511
4411
  shell.hasContent = () => true;
4512
4412
  return shell;
4513
4413
  }
4514
- var ABORTED_TEXT = "Operation aborted";
4414
+ function failureResult(message) {
4415
+ const text = message.stopReason === "aborted" ? "Operation aborted" : message.errorMessage || "Error";
4416
+ return { content: [{ type: "text", text }], isError: true };
4417
+ }
4418
+ var failed = (message) => message.stopReason === "aborted" || message.stopReason === "error";
4515
4419
 
4516
4420
  class ChatMirror {
4517
4421
  sources;
@@ -4537,8 +4441,8 @@ class ChatMirror {
4537
4441
  if (call.type !== "toolCall")
4538
4442
  continue;
4539
4443
  const row = this.toolRow(call.name, call.id, call.arguments);
4540
- if (message.stopReason === "aborted" || message.stopReason === "error")
4541
- row.updateResult({ content: [{ type: "text", text: message.stopReason === "aborted" ? ABORTED_TEXT : message.errorMessage || "Error" }], isError: true });
4444
+ if (failed(message))
4445
+ row.updateResult(failureResult(message));
4542
4446
  else
4543
4447
  this.pending.set(call.id, row);
4544
4448
  }
@@ -4554,20 +4458,15 @@ class ChatMirror {
4554
4458
  handle(event) {
4555
4459
  switch (event.type) {
4556
4460
  case "message_start":
4557
- if (event.message.role === "assistant") {
4558
- this.streaming = this.assistant(event.message, true);
4559
- this.chat.addChild(this.streaming);
4560
- } else
4461
+ if (event.message.role === "assistant")
4462
+ this.stream(event.message);
4463
+ else
4561
4464
  this.add(event.message);
4562
4465
  return true;
4563
4466
  case "message_update":
4564
4467
  if (event.message.role !== "assistant")
4565
4468
  return false;
4566
- if (!this.streaming) {
4567
- this.streaming = this.assistant(event.message, true);
4568
- this.chat.addChild(this.streaming);
4569
- }
4570
- this.streaming.updateContent(event.message, true);
4469
+ (this.streaming ?? this.stream(event.message)).updateContent(event.message, true);
4571
4470
  for (const call of event.message.content) {
4572
4471
  if (call.type !== "toolCall")
4573
4472
  continue;
@@ -4582,10 +4481,9 @@ class ChatMirror {
4582
4481
  if (!this.streaming || event.message.role !== "assistant")
4583
4482
  return false;
4584
4483
  this.streaming.updateContent(event.message, false);
4585
- if (event.message.stopReason === "aborted" || event.message.stopReason === "error") {
4586
- const text = event.message.stopReason === "aborted" ? ABORTED_TEXT : event.message.errorMessage || "Error";
4484
+ if (failed(event.message)) {
4587
4485
  for (const row of this.pending.values())
4588
- row.updateResult({ content: [{ type: "text", text }], isError: true });
4486
+ row.updateResult(failureResult(event.message));
4589
4487
  this.pending.clear();
4590
4488
  } else
4591
4489
  for (const row of this.pending.values())
@@ -4651,6 +4549,11 @@ class ChatMirror {
4651
4549
  this.chat.addChild(new Spacer(1));
4652
4550
  this.chat.addChild(new UserMessageComponent(text, getMarkdownTheme2()));
4653
4551
  }
4552
+ stream(message) {
4553
+ this.streaming = this.assistant(message, true);
4554
+ this.chat.addChild(this.streaming);
4555
+ return this.streaming;
4556
+ }
4654
4557
  assistant(message, streaming = false) {
4655
4558
  const component = new AssistantMessageComponent(message, true, getMarkdownTheme2());
4656
4559
  if (streaming)
@@ -4758,9 +4661,10 @@ var enTerms = {
4758
4661
  };
4759
4662
  var ALL_TERMS = [zhTerms, enTerms];
4760
4663
  var escapeRegExp = (literal) => literal.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
4761
- function termPattern(pick, ...extra) {
4762
- return `(?:${[...ALL_TERMS.map(pick), ...extra].map(escapeRegExp).join("|")})`;
4664
+ function termPattern(pick) {
4665
+ return `(?:${ALL_TERMS.map(pick).map(escapeRegExp).join("|")})`;
4763
4666
  }
4667
+ var SUGGESTIONS_HEADING = new RegExp(`^##\\s+${termPattern((terms) => terms.suggestions)}\\s*$`, "iu");
4764
4668
  var REDUNDANT_VERDICT_LINES = new Set([
4765
4669
  "PASS",
4766
4670
  "FAIL",
@@ -4808,9 +4712,7 @@ var msg6 = defineMessages({
4808
4712
  passed: zhTerms.passed,
4809
4713
  failed: zhTerms.failed,
4810
4714
  round: (round, title) => `第 ${round} 轮${title}`,
4811
- advisorNote: "顾问建议",
4812
4715
  stoppedByAdvisor: "审查已由顾问终止",
4813
- cancelled: "审查已取消",
4814
4716
  incomplete: "审查未完成",
4815
4717
  timeoutBlocker: `${zhTerms.blocker}:审查超时`,
4816
4718
  timeoutReason: `${zhTerms.reason}:超过总体时限`,
@@ -4819,8 +4721,6 @@ var msg6 = defineMessages({
4819
4721
  advisorGuidance: (decision) => `顾问指引 · ${decision}`,
4820
4722
  modelLine: (model) => `**${zhTerms.model} · ${model}**`,
4821
4723
  elapsed: (elapsed, total) => total === undefined ? `${zhTerms.elapsed}:${elapsed}` : `${zhTerms.elapsed}:${elapsed} / ${zhTerms.total} ${total}`,
4822
- stoppedBy: { user: "已按你的操作停止", shutdown: "会话关闭时停止", other: "已停止" },
4823
- stopReason: { advisor: "顾问建议停止", maxRounds: "已达到最大审查轮数" },
4824
4724
  decision: { continue: "继续修复", narrow: "收窄范围", stop: "停止修复" }
4825
4725
  },
4826
4726
  summary: {
@@ -4850,11 +4750,11 @@ var msg6 = defineMessages({
4850
4750
  advisorScope: "顾问收窄后的范围(以此为准):",
4851
4751
  advisorAdvice: "顾问建议:",
4852
4752
  materialTruncated: (omitted) => `[材料截断:省略 ${omitted} 字]`,
4853
- materialLabel: { passed: "末轮审查结论:", maxRounds: "末轮未通过的发现:", advisorStop: "顾问裁决:" },
4753
+ materialLabel: { passed: "末轮审查结论:", max_rounds: "末轮未通过的发现:", advisor_stop: "顾问裁决:" },
4854
4754
  summaryInstruction: {
4855
4755
  passed: (rounds) => `对抗审查已通过(共 ${rounds} 轮)。请给用户一个简洁的人话收尾总结:1) 各轮审查发现了什么、你修了什么;2) 最终靠什么通过(关键修复与验证证据);3) 审查中提到但未阻塞通过的建议——逐条列出,并给出你建议处理还是不处理及理由。只做总结:不要修改代码、不要运行工具,直接以最终回复结束本回合。`,
4856
- maxRounds: (rounds) => `对抗审查在 ${rounds} 轮内未能通过,已按上限终止。请如实向用户总结(人话):1) 各轮分别卡在什么发现上、你做了哪些修复尝试;2) 你认为无法收敛的根因;3) 当前代码的真实状态与剩余风险;4) 建议用户下一步怎么办。只做总结:不要再继续修改代码或运行工具,直接以最终回复结束本回合。`,
4857
- advisorStop: (rounds) => `对抗审查被顾问裁定终止(第 ${rounds} 轮)。请结合下方顾问裁决向用户总结:1) 审查循环走到了哪一步、修了什么;2) 顾问为什么叫停;3) 当前状态与你建议的下一步。只做总结:不要再修改代码或运行工具,直接以最终回复结束本回合。`
4756
+ max_rounds: (rounds) => `对抗审查在 ${rounds} 轮内未能通过,已按上限终止。请如实向用户总结(人话):1) 各轮分别卡在什么发现上、你做了哪些修复尝试;2) 你认为无法收敛的根因;3) 当前代码的真实状态与剩余风险;4) 建议用户下一步怎么办。只做总结:不要再继续修改代码或运行工具,直接以最终回复结束本回合。`,
4757
+ advisor_stop: (rounds) => `对抗审查被顾问裁定终止(第 ${rounds} 轮)。请结合下方顾问裁决向用户总结:1) 审查循环走到了哪一步、修了什么;2) 顾问为什么叫停;3) 当前状态与你建议的下一步。只做总结:不要再修改代码或运行工具,直接以最终回复结束本回合。`
4858
4758
  }
4859
4759
  },
4860
4760
  reviewer: {
@@ -4929,9 +4829,7 @@ Stopped by user`
4929
4829
  passed: enTerms.passed,
4930
4830
  failed: enTerms.failed,
4931
4831
  round: (round, title) => `Round ${round} ${title}`,
4932
- advisorNote: "Advisor note",
4933
4832
  stoppedByAdvisor: "Review stopped by advisor",
4934
- cancelled: "Review cancelled",
4935
4833
  incomplete: "Review incomplete",
4936
4834
  timeoutBlocker: `${enTerms.blocker}: review timed out`,
4937
4835
  timeoutReason: `${enTerms.reason}: overall time limit exceeded`,
@@ -4940,8 +4838,6 @@ Stopped by user`
4940
4838
  advisorGuidance: (decision) => `Advisor guidance · ${decision}`,
4941
4839
  modelLine: (model) => `**${enTerms.model} · ${model}**`,
4942
4840
  elapsed: (elapsed, total) => total === undefined ? `${enTerms.elapsed}: ${elapsed}` : `${enTerms.elapsed}: ${elapsed} / ${enTerms.total} ${total}`,
4943
- stoppedBy: { user: "Stopped by user", shutdown: "Stopped when the session closed", other: "Stopped" },
4944
- stopReason: { advisor: "Advisor recommends stopping", maxRounds: "Maximum review rounds reached" },
4945
4841
  decision: { continue: "Continue fixing", narrow: "Narrow scope", stop: "Stop fixing" }
4946
4842
  },
4947
4843
  summary: {
@@ -4971,11 +4867,11 @@ Stopped by user`
4971
4867
  advisorScope: "Advisor scope (authoritative):",
4972
4868
  advisorAdvice: "Advisor note:",
4973
4869
  materialTruncated: (omitted) => `[material truncated: ${omitted} characters omitted]`,
4974
- materialLabel: { passed: "Final review verdict:", maxRounds: "Final round findings:", advisorStop: "Advisor ruling:" },
4870
+ materialLabel: { passed: "Final review verdict:", max_rounds: "Final round findings:", advisor_stop: "Advisor ruling:" },
4975
4871
  summaryInstruction: {
4976
4872
  passed: (rounds) => `The adversarial review passed after ${rounds} round(s). Give the user a concise plain-language wrap-up: 1) what the review rounds found and what you fixed; 2) how it finally passed (key fixes and verification evidence); 3) non-blocking suggestions raised during review — list each and say whether you recommend addressing it and why. Summary only: do not change code or run tools; end this turn with the final reply.`,
4977
- maxRounds: (rounds) => `The adversarial review did not pass within ${rounds} round(s) and stopped at the limit. Summarize honestly for the user in plain language: 1) what each round got stuck on and what fixes you attempted; 2) your root-cause read on why it would not converge; 3) the real current state of the code and remaining risks; 4) what you recommend the user do next. Summary only: do not keep changing code or run tools; end this turn with the final reply.`,
4978
- advisorStop: (rounds) => `The adversarial review was stopped by the advisor (round ${rounds}). Using the advisor ruling below, summarize for the user: 1) how far the review loop got and what was fixed; 2) why the advisor called it off; 3) the current state and your recommended next step. Summary only: do not change code or run tools; end this turn with the final reply.`
4873
+ max_rounds: (rounds) => `The adversarial review did not pass within ${rounds} round(s) and stopped at the limit. Summarize honestly for the user in plain language: 1) what each round got stuck on and what fixes you attempted; 2) your root-cause read on why it would not converge; 3) the real current state of the code and remaining risks; 4) what you recommend the user do next. Summary only: do not keep changing code or run tools; end this turn with the final reply.`,
4874
+ advisor_stop: (rounds) => `The adversarial review was stopped by the advisor (round ${rounds}). Using the advisor ruling below, summarize for the user: 1) how far the review loop got and what was fixed; 2) why the advisor called it off; 3) the current state and your recommended next step. Summary only: do not change code or run tools; end this turn with the final reply.`
4979
4875
  }
4980
4876
  },
4981
4877
  reviewer: {
@@ -5173,7 +5069,6 @@ class Line {
5173
5069
  }
5174
5070
 
5175
5071
  // tools/group-view.ts
5176
- var rowData = toolFacts;
5177
5072
  function actionLine(component) {
5178
5073
  return component && "actionWord" in component && typeof component.actionWord === "string" ? component : undefined;
5179
5074
  }
@@ -5253,8 +5148,8 @@ function scan(segment, activity, env) {
5253
5148
  const age = settled ? env.clock.arrivalAge(item) : Infinity;
5254
5149
  if (settled && age < ARRIVAL_FLASH_MS)
5255
5150
  arrival = { text: settled.title, failed: settled.alarm, age };
5256
- if (item instanceof ToolExecutionComponent2 && rowData(item).isPartial)
5257
- running.push(rowData(item));
5151
+ if (item instanceof ToolExecutionComponent2 && toolFacts(item).isPartial)
5152
+ running.push(toolFacts(item));
5258
5153
  }
5259
5154
  const record = combineRounds(rounds);
5260
5155
  return { notice, arrival, round: record?.round, earlier: record?.earlier, failures: failed.size, action: actionOf(running.at(-1), activity, env) };
@@ -5262,7 +5157,7 @@ function scan(segment, activity, env) {
5262
5157
  function actionOf(tool, activity, env) {
5263
5158
  if (tool) {
5264
5159
  const line = actionLine(tool.callRendererComponent);
5265
- return line ? { word: line.actionWord, target: line.actionTarget } : { word: tool.toolDefinition?.label ?? tool.toolName, target: toolTarget(tool.toolName, tool.args, tool.cwd).value.map((part) => part.text).join("").trim() };
5160
+ return line ? { word: line.actionWord, target: line.actionTarget } : { word: tool.toolDefinition?.label ?? tool.toolName, target: toolTargetText(tool.toolName, tool.args, tool.cwd) };
5266
5161
  }
5267
5162
  if (activity)
5268
5163
  return { word: msg4.activity[activity] };
@@ -5319,7 +5214,7 @@ class ToolItem {
5319
5214
  }
5320
5215
  invalidate() {}
5321
5216
  render(width) {
5322
- const data = rowData(this.row);
5217
+ const data = toolFacts(this.row);
5323
5218
  if (!data.expanded) {
5324
5219
  const renderer = actionLine(data.callRendererComponent) ?? compactLine(data, this.ui.theme);
5325
5220
  return renderer.render(width);
@@ -5329,9 +5224,9 @@ class ToolItem {
5329
5224
  return lines.slice(this.leadingRows);
5330
5225
  }
5331
5226
  handleMouse(event) {
5332
- if (rowData(this.row).expanded)
5227
+ if (toolFacts(this.row).expanded)
5333
5228
  return this.row.handleMouse({ ...event, y: event.y + this.leadingRows, height: event.height + this.leadingRows });
5334
- if (event.type !== "click" || event.button !== "left" || !rowData(this.row).result)
5229
+ if (event.type !== "click" || event.button !== "left" || !toolFacts(this.row).result)
5335
5230
  return;
5336
5231
  this.toggle(this.row);
5337
5232
  return { handled: true };
@@ -5517,7 +5412,7 @@ function processList(segment, env) {
5517
5412
  return list;
5518
5413
  }
5519
5414
  function toggleToolDetails(row, setExpanded) {
5520
- setExpanded.call(row, !rowData(row).expanded);
5415
+ setExpanded.call(row, !toolFacts(row).expanded);
5521
5416
  }
5522
5417
 
5523
5418
  // tools/grouping.ts
@@ -5672,21 +5567,14 @@ function attach(tui, ui, options) {
5672
5567
  }
5673
5568
 
5674
5569
  // tools/index.ts
5675
- var cache = new Map;
5676
5570
  function createTools(cwd) {
5677
- return {
5678
- read: createReadTool(cwd),
5679
- bash: createBashTool(cwd),
5680
- edit: createEditTool(cwd),
5681
- write: createWriteTool(cwd)
5682
- };
5571
+ return { read: createReadTool(cwd), bash: createBashTool(cwd), edit: createEditTool(cwd), write: createWriteTool(cwd) };
5683
5572
  }
5573
+ var cache = new Map;
5684
5574
  function tools(cwd) {
5685
5575
  let value = cache.get(cwd);
5686
- if (!value) {
5687
- value = createTools(cwd);
5688
- cache.set(cwd, value);
5689
- }
5576
+ if (!value)
5577
+ cache.set(cwd, value = createTools(cwd));
5690
5578
  return value;
5691
5579
  }
5692
5580
  function lineCount(text) {
@@ -5697,8 +5585,13 @@ function lineCount(text) {
5697
5585
  return text.endsWith(`
5698
5586
  `) ? lines - 1 : lines;
5699
5587
  }
5700
- function invoke(execute, args) {
5701
- return Reflect.apply(execute, undefined, args);
5588
+ function shell(name, meta) {
5589
+ return {
5590
+ label: msg4.labels[name],
5591
+ renderShell: "self",
5592
+ execute: (id, params, signal, update, ctx) => executeTimed(id, () => tools(ctx.cwd)[name].execute(id, params, signal, update, ctx)),
5593
+ renderCall: (args, theme, ctx) => new ToolLine({ label: msg4.labels[name], ...toolTarget(name, args, ctx.cwd), meta: meta?.(args), theme, ctx })
5594
+ };
5702
5595
  }
5703
5596
  var EDIT_RESULT = makeResultRenderer(false);
5704
5597
  var definitions;
@@ -5708,43 +5601,11 @@ function toolDefinitions() {
5708
5601
  function buildDefinitions() {
5709
5602
  const initial = tools(process.cwd());
5710
5603
  return {
5711
- read: defineTool({
5712
- ...initial.read,
5713
- label: LABEL.read,
5714
- renderShell: "self",
5715
- execute: (id, params, signal, update, ctx) => executeTimed(id, () => invoke(tools(ctx.cwd).read.execute, [id, params, signal, update, ctx])),
5716
- renderCall: (args, theme, ctx) => new ToolLine({
5717
- label: LABEL.read,
5718
- ...toolTarget("read", args, ctx.cwd),
5719
- theme,
5720
- ctx
5721
- }),
5722
- renderResult: makeResultRenderer(true)
5723
- }),
5724
- bash: defineTool({
5725
- ...initial.bash,
5726
- label: LABEL.bash,
5727
- renderShell: "self",
5728
- execute: (id, params, signal, update, ctx) => executeTimed(id, () => invoke(tools(ctx.cwd).bash.execute, [id, params, signal, update, ctx])),
5729
- renderCall: (args, theme, ctx) => new ToolLine({
5730
- label: LABEL.bash,
5731
- ...toolTarget("bash", args, ctx.cwd),
5732
- theme,
5733
- ctx
5734
- }),
5735
- renderResult: makeResultRenderer(true)
5736
- }),
5604
+ read: defineTool({ ...initial.read, ...shell("read"), renderResult: makeResultRenderer(true) }),
5605
+ bash: defineTool({ ...initial.bash, ...shell("bash"), renderResult: makeResultRenderer(true) }),
5737
5606
  edit: defineTool({
5738
5607
  ...initial.edit,
5739
- label: LABEL.edit,
5740
- renderShell: "self",
5741
- execute: (id, params, signal, update, ctx) => executeTimed(id, () => invoke(tools(ctx.cwd).edit.execute, [id, params, signal, update, ctx])),
5742
- renderCall: (args, theme, ctx) => new ToolLine({
5743
- label: LABEL.edit,
5744
- ...toolTarget("edit", args, ctx.cwd),
5745
- theme,
5746
- ctx
5747
- }),
5608
+ ...shell("edit"),
5748
5609
  renderResult(result, options, theme, ctx) {
5749
5610
  const details = result.details;
5750
5611
  const diff = !ctx.isError && typeof details?.diff === "string" ? details.diff : undefined;
@@ -5755,16 +5616,7 @@ function buildDefinitions() {
5755
5616
  }),
5756
5617
  write: defineTool({
5757
5618
  ...initial.write,
5758
- label: LABEL.write,
5759
- renderShell: "self",
5760
- execute: (id, params, signal, update, ctx) => executeTimed(id, () => invoke(tools(ctx.cwd).write.execute, [id, params, signal, update, ctx])),
5761
- renderCall: (args, theme, ctx) => new ToolLine({
5762
- label: LABEL.write,
5763
- ...toolTarget("write", args, ctx.cwd),
5764
- meta: [{ text: ` +${lineCount(args.content ?? "")}`, color: "toolDiffAdded" }],
5765
- theme,
5766
- ctx
5767
- }),
5619
+ ...shell("write", (args) => [{ text: " +" + lineCount(args.content ?? ""), color: "toolDiffAdded" }]),
5768
5620
  renderResult: makeResultRenderer(false)
5769
5621
  })
5770
5622
  };
@@ -5805,7 +5657,7 @@ import { randomUUID } from "node:crypto";
5805
5657
 
5806
5658
  // master/spawn.ts
5807
5659
  import { existsSync as existsSync2 } from "node:fs";
5808
- import { basename, dirname as dirname3 } from "node:path";
5660
+ import { basename, dirname as dirname2 } from "node:path";
5809
5661
  import {
5810
5662
  createAgentSession,
5811
5663
  createCodemodeExtension,
@@ -5830,8 +5682,7 @@ var msg7 = defineMessages({
5830
5682
  thinking: "可选思考档覆盖;省略时使用角色原子档或当前档。",
5831
5683
  cwd: "Worker 工作目录的绝对路径;start 默认当前目录,send 给空闲 Worker 换检出时带上(同一会话重开)。",
5832
5684
  review: "按审查纪律为 start/send 记录义务;true 不自动开审。",
5833
- onlyInMaster: (tool) => `${tool} 只在 Master 中可用`,
5834
- unknownAction: (action) => `未知 subagents action:${action}`
5685
+ onlyInMaster: (tool) => `${tool} 只在 Master 中可用`
5835
5686
  },
5836
5687
  command: {
5837
5688
  description: "翻转当前会话的指挥官模式;status 查看状态",
@@ -5931,7 +5782,6 @@ var msg7 = defineMessages({
5931
5782
  },
5932
5783
  guard: (path) => `子代理只能修改当前 checkout 或系统临时目录:${path}`,
5933
5784
  state: {
5934
- legacy: (version, current) => `Master Worker Pool 状态是旧版 v${version}(当前 v${current}),不再读取;重新启动指挥官模式会从空池重建,旧运行时进程不会纳入新池,需要手动清理`,
5935
5785
  invalidJson: (path) => `Master Worker Pool 状态不是合法 JSON:${path}`,
5936
5786
  invalidShape: (path) => `Master Worker Pool 状态结构无效:${path}`,
5937
5787
  missing: (name) => `子代理不存在:${name}`,
@@ -5995,8 +5845,7 @@ var msg7 = defineMessages({
5995
5845
  thinking: "Optional thinking-level override; defaults to the role atom's level or the current level.",
5996
5846
  cwd: "Absolute path of the Worker's working directory; start defaults to the current directory, and send passes it to move an idle Worker to another checkout (the same session is reopened).",
5997
5847
  review: "Record a review obligation for start/send per the review discipline; true does not start a review automatically.",
5998
- onlyInMaster: (tool) => `${tool} is only available in Master mode`,
5999
- unknownAction: (action) => `Unknown subagents action: ${action}`
5848
+ onlyInMaster: (tool) => `${tool} is only available in Master mode`
6000
5849
  },
6001
5850
  command: {
6002
5851
  description: "Toggle Master mode for the current session; status shows the state",
@@ -6096,7 +5945,6 @@ var msg7 = defineMessages({
6096
5945
  },
6097
5946
  guard: (path) => `Workers may only modify the current checkout or the system temp directory: ${path}`,
6098
5947
  state: {
6099
- legacy: (version, current) => `The Master Worker Pool state is legacy v${version} (current v${current}) and is no longer read; restarting Master mode rebuilds from an empty pool, old runtime processes are not adopted into the new pool and need manual cleanup`,
6100
5948
  invalidJson: (path) => `The Master Worker Pool state is not valid JSON: ${path}`,
6101
5949
  invalidShape: (path) => `The Master Worker Pool state has an invalid structure: ${path}`,
6102
5950
  missing: (name) => `Worker does not exist: ${name}`,
@@ -6237,7 +6085,6 @@ class InProcessSessionPool {
6237
6085
  this.held.set(key, held);
6238
6086
  return {
6239
6087
  session: created,
6240
- sessionPath,
6241
6088
  prompt: (text) => created.prompt(text),
6242
6089
  dispose: () => this.release(held)
6243
6090
  };
@@ -6304,7 +6151,7 @@ class InProcessSessionPool {
6304
6151
  }
6305
6152
  }
6306
6153
  function preallocateWorkerSession(mainSessionPath, cwd) {
6307
- const sessionPath = SessionManager.create(cwd, `${dirname3(mainSessionPath)}/subagents`).getSessionFile();
6154
+ const sessionPath = SessionManager.create(cwd, `${dirname2(mainSessionPath)}/subagents`).getSessionFile();
6308
6155
  if (!sessionPath)
6309
6156
  throw new Error(msg7.spawn.noSessionPath);
6310
6157
  return sessionPath;
@@ -6312,7 +6159,7 @@ function preallocateWorkerSession(mainSessionPath, cwd) {
6312
6159
  function makeSessionManager(persistence, cwd) {
6313
6160
  if (persistence.type === "memory")
6314
6161
  return SessionManager.inMemory(cwd);
6315
- return SessionManager.open(persistence.sessionPath, dirname3(persistence.sessionPath), cwd);
6162
+ return SessionManager.open(persistence.sessionPath, dirname2(persistence.sessionPath), cwd);
6316
6163
  }
6317
6164
  function matchesExtension(path, exclusions) {
6318
6165
  return exclusions.some((excluded) => excluded === path || excluded === basename(path));
@@ -6321,257 +6168,603 @@ function matchesExtension(path, exclusions) {
6321
6168
  // review/card.ts
6322
6169
  import { getMarkdownTheme as getMarkdownTheme4 } from "@earendil-works/pi-coding-agent";
6323
6170
  import { Box, Markdown as Markdown3, Spacer as Spacer4, Text as Text5 } from "@earendil-works/pi-tui";
6324
- var CARD_TYPE = "firecode-review-card";
6325
- var VERSION2 = 1;
6326
- var CARD_KINDS = new Set([
6327
- "queued",
6328
- "start",
6329
- "pass",
6330
- "fail",
6331
- "stop",
6332
- "cancel",
6333
- "timeout",
6334
- "error",
6335
- "advisor"
6336
- ]);
6337
- var CARD_TONES = new Set(["success", "warning", "error", "neutral", "accent"]);
6338
- var MARKS = {
6339
- start: { glyph: "⠿", color: "review" },
6340
- advisor: { glyph: "⠿", color: "review" },
6341
- pass: { glyph: "✓", color: "success" },
6342
- fail: { glyph: "✗", color: "error" },
6343
- stop: { glyph: "✗", color: "error" },
6344
- timeout: { glyph: "◌", color: "error" },
6345
- error: { glyph: "◌", color: "error" },
6346
- cancel: { glyph: "‖", color: "muted" }
6347
- };
6348
- function paintMark(details, theme) {
6349
- const mark = MARKS[details.kind];
6350
- if (!mark)
6351
- return details.icon;
6352
- return mark.color === "review" ? paint(HEAT_COLORS.gold, details.icon) : theme.fg(mark.color, details.icon);
6353
- }
6354
- function isValidCardDetails(value) {
6355
- if (typeof value !== "object" || value === null || Array.isArray(value))
6356
- return false;
6357
- const record = value;
6358
- if (Object.keys(record).length !== 6)
6359
- return false;
6360
- if (record.version !== VERSION2)
6361
- return false;
6362
- if (typeof record.kind !== "string" || !CARD_KINDS.has(record.kind))
6363
- return false;
6364
- if (typeof record.title !== "string")
6365
- return false;
6366
- if (!Array.isArray(record.lines) || !record.lines.every((line) => typeof line === "string"))
6367
- return false;
6368
- if (typeof record.tone !== "string" || !CARD_TONES.has(record.tone))
6369
- return false;
6370
- return typeof record.icon === "string";
6371
- }
6372
- function registerCardRenderer(pi) {
6373
- pi.registerMessageRenderer(CARD_TYPE, (message, _options, theme) => new ReviewCard(message.details, message.content, theme));
6374
- }
6375
6171
 
6376
- class ReviewCard {
6377
- card;
6378
- fallback;
6379
- constructor(details, content, theme) {
6380
- this.fallback = new Text5(plainContent(content), 0, 0);
6381
- let card;
6382
- try {
6383
- card = isValidCardDetails(details) ? nativeCard(details, theme) : undefined;
6384
- } catch {
6385
- card = undefined;
6386
- }
6387
- this.card = card;
6388
- }
6389
- render(width) {
6390
- try {
6391
- return (this.card ?? this.fallback).render(Math.max(1, width));
6392
- } catch {
6393
- try {
6394
- return this.fallback.render(Math.max(1, width));
6395
- } catch {
6396
- return [];
6397
- }
6398
- }
6399
- }
6400
- invalidate() {
6401
- this.card?.invalidate?.();
6402
- this.fallback.invalidate?.();
6403
- }
6172
+ // review/state.ts
6173
+ var ISSUE_LINE = new RegExp(`^[-*+]\\s*(?:\\*\\*)?${termPattern((terms) => terms.field.issue)}(?:\\*\\*)?\\s*[::]\\s*(.+)$`, "iu");
6174
+ function initialState(runId) {
6175
+ return {
6176
+ runId,
6177
+ phase: "idle",
6178
+ round: 0,
6179
+ focus: "",
6180
+ history: [],
6181
+ active: null,
6182
+ pending: null,
6183
+ repair: null,
6184
+ summary: null,
6185
+ consecutiveFailures: 0,
6186
+ startedAt: 0,
6187
+ roundStartedAt: 0,
6188
+ updatedAt: 0
6189
+ };
6404
6190
  }
6405
- function nativeCard(details, theme) {
6406
- const box = new Box(1, 0, (text) => theme.bg(backgroundFor(details.tone), text));
6407
- box.addChild(new Text5(`${paintMark(details, theme)} ${details.title}`, 0, 0));
6408
- box.addChild(new Spacer4(1));
6409
- box.addChild(new Markdown3(details.lines.join(`
6410
- `), 0, 0, getMarkdownTheme4()));
6411
- return box;
6191
+ function reduce(state, event, limits, now) {
6192
+ switch (event.type) {
6193
+ case "START":
6194
+ return onStart(state, event, now);
6195
+ case "RECOVER":
6196
+ return onRecover(state);
6197
+ case "REVIEWER_SETTLED":
6198
+ return onReviewerSettled(state, event, limits, now);
6199
+ case "ADVISOR_SETTLED":
6200
+ return onAdvisorSettled(state, event, limits, now);
6201
+ case "ADVISOR_SKIPPED":
6202
+ return onAdvisorSkipped(state, now);
6203
+ case "INFRASTRUCTURE_ERROR":
6204
+ return onInfrastructureError(state, event.details, now);
6205
+ case "ADVANCE":
6206
+ return onAdvance(state, limits, now);
6207
+ case "FEEDBACK_DISPATCHED":
6208
+ return updateRepairStatus(state, "pending", "awaiting_start", now);
6209
+ case "REPAIR_STARTED":
6210
+ return updateRepairStatus(state, "awaiting_start", "running", now);
6211
+ case "REPAIR_COMPLETED":
6212
+ return updateRepairStatus(state, "running", "completed", now);
6213
+ case "SUMMARY_DISPATCHED":
6214
+ return updateSummaryStatus(state, "pending", "awaiting_start", now);
6215
+ case "SUMMARY_STARTED":
6216
+ return updateSummaryStatus(state, "awaiting_start", "running", now);
6217
+ case "SUMMARY_SETTLED":
6218
+ return onSummarySettled(state, now);
6219
+ case "CANCEL":
6220
+ return onInterrupted(state, now, "cancelled", event.reason, event.reason === "user" ? [{ kind: "notify_cancelled" }] : []);
6221
+ case "TIMEOUT":
6222
+ return onInterrupted(state, now, "timed_out", "timeout", [{ kind: "send_card", card: { kind: "timeout" } }]);
6223
+ }
6412
6224
  }
6413
- function backgroundFor(tone) {
6414
- if (tone === "success")
6415
- return "toolSuccessBg";
6416
- if (tone === "warning" || tone === "error")
6417
- return "toolErrorBg";
6418
- return "customMessageBg";
6225
+ function onStart(state, event, now) {
6226
+ return {
6227
+ state: { ...initialState(state.runId), phase: "queued", focus: event.focus.trim(), startedAt: now, updatedAt: now },
6228
+ effects: [{ kind: "advance" }]
6229
+ };
6419
6230
  }
6420
- function plainContent(content) {
6421
- if (typeof content === "string")
6422
- return content;
6423
- if (!Array.isArray(content))
6424
- return "";
6425
- return content.map(plainPart).filter(Boolean).join(`
6426
- `);
6231
+ function onRecover(state) {
6232
+ if (state.phase === "idle" || state.phase === "settled")
6233
+ return { state, effects: [] };
6234
+ const repair = state.phase === "awaiting_fix" && state.repair && state.repair.status !== "completed" ? { ...state.repair, status: "pending" } : state.repair;
6235
+ const summary = state.phase === "summarizing" && state.summary ? { ...state.summary, status: "pending" } : state.summary;
6236
+ return { state: { ...state, repair, summary }, effects: [] };
6427
6237
  }
6428
- function plainPart(part) {
6429
- return typeof part === "object" && part !== null && "text" in part ? String(part.text) : "";
6238
+ function updateRepairStatus(state, expected, status, now) {
6239
+ if (state.phase !== "awaiting_fix" || state.repair?.status !== expected)
6240
+ return { state, effects: [] };
6241
+ return {
6242
+ state: { ...state, repair: { ...state.repair, status }, updatedAt: now },
6243
+ effects: []
6244
+ };
6430
6245
  }
6431
- function buildCard(card) {
6432
- switch (card.kind) {
6433
- case "start":
6434
- return started(card);
6435
- case "pass":
6436
- return passed(card);
6437
- case "fail":
6438
- return failed(card);
6439
- case "stop":
6440
- return stopped(card);
6441
- case "cancel":
6442
- return cancelled(card);
6443
- case "timeout":
6444
- return timedOut(card);
6445
- case "error":
6446
- return errored(card);
6447
- case "advisor":
6448
- return advisorCard(card);
6449
- }
6246
+ function updateSummaryStatus(state, expected, status, now) {
6247
+ if (state.phase !== "summarizing" || state.summary?.status !== expected)
6248
+ return { state, effects: [] };
6249
+ return {
6250
+ state: { ...state, summary: { ...state.summary, status }, updatedAt: now },
6251
+ effects: []
6252
+ };
6450
6253
  }
6451
- function started(card) {
6452
- return spec("start", msg6.card.started, [msg6.card.startedModels(card.models.map(shortModel))], "neutral");
6254
+ function onSummarySettled(state, now) {
6255
+ if (state.phase !== "summarizing")
6256
+ return { state, effects: [] };
6257
+ return {
6258
+ state: { ...state, phase: "settled", summary: null, updatedAt: now },
6259
+ effects: []
6260
+ };
6453
6261
  }
6454
- function shortModel(model) {
6455
- return model.split("/").at(-1) || model;
6262
+ function summarizing(kind) {
6263
+ return { phase: "summarizing", summary: { kind, status: "pending" } };
6456
6264
  }
6457
- function passed(card) {
6458
- const title = qualityTitle(card.round, msg6.card.passed);
6459
- const lines = withFooter(formatReviewResultLines(card.summary), [
6460
- elapsedLine(card.elapsedMs, card.totalElapsedMs, card.round > 1)
6461
- ]);
6462
- return spec("pass", title, lines, "success");
6265
+ function onAdvance(state, limits, now) {
6266
+ if (state.phase === "queued")
6267
+ return {
6268
+ state: beginRound(state, 1, limits, now),
6269
+ effects: [
6270
+ { kind: "send_card", card: { kind: "start", models: limits.reviewers.map((item) => item.model) } },
6271
+ { kind: "advance" }
6272
+ ]
6273
+ };
6274
+ if (state.phase !== "awaiting_fix" || state.repair?.status !== "completed")
6275
+ return { state, effects: [] };
6276
+ return {
6277
+ state: beginRound(state, state.round + 1, limits, now),
6278
+ effects: [{ kind: "advance" }]
6279
+ };
6463
6280
  }
6464
- function failed(card) {
6465
- const title = qualityTitle(card.round, msg6.card.failed);
6466
- const footer = [
6467
- ...card.advisor?.advice ? [msg6.card.advisorNote, ...adviceLines(card.advisor.advice)] : [],
6468
- ...card.elapsedMs === undefined ? [] : [elapsedLine(card.elapsedMs, card.totalElapsedMs, false)]
6469
- ];
6470
- return spec("fail", title, withFooter(formatReviewResultLines(card.details), footer), "warning");
6281
+ function onReviewerSettled(state, event, limits, now) {
6282
+ if (state.phase !== "reviewing" || !state.active)
6283
+ return { state, effects: [] };
6284
+ const active = state.active;
6285
+ const reviewers = active.reviewers.map((item) => item.index === event.index ? { ...item, status: event.result.status, result: event.result } : item);
6286
+ const settledCount = active.settledCount + 1;
6287
+ const nextActive = { ...active, reviewers, settledCount };
6288
+ if (settledCount < reviewers.length)
6289
+ return { state: { ...state, active: nextActive, updatedAt: now }, effects: [] };
6290
+ return settleRound(state, nextActive, resultsOf(reviewers), limits, now);
6471
6291
  }
6472
- function stopped(card) {
6473
- const footer = card.elapsedMs === undefined ? [] : [elapsedLine(card.elapsedMs, card.totalElapsedMs, false)];
6474
- if (card.reason === "advisor") {
6475
- const title = qualityTitle(card.round, msg6.card.stoppedByAdvisor);
6476
- const body = [advisorModelLine(card.advisorModel), "", ...adviceLines(card.advisor.advice)];
6477
- return spec("stop", title, withFooter(body, footer), "warning");
6292
+ function settleRound(state, active, settled, limits, now) {
6293
+ const { result, details, feedbackDetails, summary } = aggregate(settled);
6294
+ const base = { ...state, active: null, updatedAt: now };
6295
+ const round = closeRound(state, now, { round: active.round, result, details, reviewers: settled });
6296
+ const history = [...state.history, round];
6297
+ if (result === "passed")
6298
+ return {
6299
+ state: { ...base, ...summarizing("passed"), history },
6300
+ effects: [
6301
+ {
6302
+ kind: "send_card",
6303
+ card: {
6304
+ kind: "pass",
6305
+ round: active.round,
6306
+ summary: appendClosedFindings(summary, state.history, active.round),
6307
+ elapsedMs: round.elapsedMs,
6308
+ totalElapsedMs: Math.max(0, now - state.startedAt)
6309
+ }
6310
+ },
6311
+ { kind: "advance" }
6312
+ ]
6313
+ };
6314
+ if (result === "error")
6315
+ return {
6316
+ state: { ...base, phase: "settled", history },
6317
+ effects: [{ kind: "send_card", card: { kind: "error", message: details, elapsedMs: round.elapsedMs } }]
6318
+ };
6319
+ const consecutiveFailures = state.consecutiveFailures + 1;
6320
+ if (active.round >= limits.maxRounds)
6321
+ return {
6322
+ state: { ...base, ...summarizing("max_rounds"), consecutiveFailures, history },
6323
+ effects: [
6324
+ { kind: "send_card", card: { kind: "stop", reason: "max_rounds", round: active.round, details, elapsedMs: round.elapsedMs } },
6325
+ { kind: "advance" }
6326
+ ]
6327
+ };
6328
+ const failCard = {
6329
+ kind: "send_card",
6330
+ card: { kind: "fail", round: active.round, details, elapsedMs: round.elapsedMs }
6331
+ };
6332
+ if (consecutiveFailures >= limits.advisorAfterFailures)
6333
+ return {
6334
+ state: {
6335
+ ...base,
6336
+ phase: "needs_fix",
6337
+ pending: { round: active.round, reviewers: settled, details: feedbackDetails },
6338
+ consecutiveFailures
6339
+ },
6340
+ effects: [failCard, { kind: "advance" }]
6341
+ };
6342
+ return {
6343
+ state: {
6344
+ ...base,
6345
+ phase: "awaiting_fix",
6346
+ pending: null,
6347
+ repair: { details: feedbackDetails, advisor: null, status: "pending" },
6348
+ consecutiveFailures,
6349
+ history
6350
+ },
6351
+ effects: [failCard, { kind: "advance" }]
6352
+ };
6353
+ }
6354
+ function onAdvisorSettled(state, event, limits, now) {
6355
+ if (state.phase !== "needs_fix" || !state.pending)
6356
+ return { state, effects: [] };
6357
+ const pending = state.pending;
6358
+ const advisor = event.result;
6359
+ const consultMs = Math.max(0, now - state.updatedAt);
6360
+ if (advisor.verdict === "stop") {
6361
+ const round = closeRound(state, now, { round: pending.round, result: "stopped", details: advisor.advice, reviewers: pending.reviewers, advisor });
6362
+ return {
6363
+ state: { ...state, ...summarizing("advisor_stop"), pending: null, history: [...state.history, round], updatedAt: now },
6364
+ effects: [
6365
+ {
6366
+ kind: "send_card",
6367
+ card: { kind: "stop", reason: "advisor", round: pending.round, details: advisor.advice, advisor, advisorModel: limits.advisorModel, elapsedMs: consultMs }
6368
+ },
6369
+ { kind: "advance" }
6370
+ ]
6371
+ };
6478
6372
  }
6479
- const title = qualityTitle(card.round, msg6.card.failed);
6480
- const body = formatReviewResultLines(card.details || stopReason(card.reason));
6481
- return spec("stop", title, withFooter(body, footer), "warning");
6373
+ const round = closeRound(state, now, { round: pending.round, result: "failed", details: pending.details, reviewers: pending.reviewers, advisor });
6374
+ return {
6375
+ state: {
6376
+ ...state,
6377
+ phase: "awaiting_fix",
6378
+ pending: null,
6379
+ repair: { details: pending.details, advisor, status: "pending" },
6380
+ history: [...state.history, round],
6381
+ updatedAt: now
6382
+ },
6383
+ effects: [
6384
+ { kind: "send_card", card: { kind: "advisor", advisor, advisorModel: limits.advisorModel, elapsedMs: consultMs } },
6385
+ { kind: "advance" }
6386
+ ]
6387
+ };
6482
6388
  }
6483
- function cancelled(card) {
6484
- return spec("cancel", msg6.card.cancelled, [reasonText(card.reason)], "neutral");
6389
+ function onAdvisorSkipped(state, now) {
6390
+ if (state.phase !== "needs_fix" || !state.pending)
6391
+ return { state, effects: [] };
6392
+ const pending = state.pending;
6393
+ const round = closeRound(state, now, { round: pending.round, result: "failed", details: pending.details, reviewers: pending.reviewers });
6394
+ return {
6395
+ state: {
6396
+ ...state,
6397
+ phase: "awaiting_fix",
6398
+ pending: null,
6399
+ repair: { details: pending.details, advisor: null, status: "pending" },
6400
+ history: [...state.history, round],
6401
+ updatedAt: now
6402
+ },
6403
+ effects: [{ kind: "advance" }]
6404
+ };
6485
6405
  }
6486
- function timedOut(_card) {
6487
- return spec("timeout", msg6.card.incomplete, [msg6.card.timeoutBlocker, msg6.card.timeoutReason], "warning");
6406
+ function onInterrupted(state, now, result, reason, effects) {
6407
+ if (state.phase === "idle" || state.phase === "settled")
6408
+ return { state, effects: [] };
6409
+ if (state.phase === "summarizing")
6410
+ return onSummarySettled(state, now);
6411
+ const round = interruptedRound(state, now, result, reason);
6412
+ return {
6413
+ state: {
6414
+ ...state,
6415
+ phase: "settled",
6416
+ active: null,
6417
+ pending: null,
6418
+ repair: null,
6419
+ history: round ? [...state.history, round] : state.history,
6420
+ updatedAt: now
6421
+ },
6422
+ effects
6423
+ };
6488
6424
  }
6489
- function reasonText(reason) {
6490
- if (reason === "user")
6491
- return msg6.card.stoppedBy.user;
6492
- if (reason === "shutdown")
6493
- return msg6.card.stoppedBy.shutdown;
6494
- return msg6.card.stoppedBy.other;
6425
+ function onInfrastructureError(state, details, now) {
6426
+ if (state.phase === "idle" || state.phase === "settled")
6427
+ return { state, effects: [] };
6428
+ if (state.phase === "summarizing")
6429
+ return onSummarySettled(state, now);
6430
+ const reviewers = state.active ? resultsOf(state.active.reviewers) : state.pending?.reviewers ?? [];
6431
+ const round = state.round > 0 ? closeRound(state, now, { round: state.round, result: "error", details, reviewers }) : undefined;
6432
+ return {
6433
+ state: {
6434
+ ...state,
6435
+ phase: "settled",
6436
+ active: null,
6437
+ pending: null,
6438
+ repair: null,
6439
+ history: round ? [...state.history, round] : state.history,
6440
+ updatedAt: now
6441
+ },
6442
+ effects: [{ kind: "send_card", card: { kind: "error", message: details } }]
6443
+ };
6495
6444
  }
6496
- function stopReason(reason) {
6497
- if (reason === "advisor")
6498
- return msg6.card.stopReason.advisor;
6499
- if (reason === "max_rounds")
6500
- return msg6.card.stopReason.maxRounds;
6501
- return reasonText(reason);
6445
+ function beginRound(state, round, limits, now) {
6446
+ const reviewers = limits.reviewers.map((item, index) => ({
6447
+ index,
6448
+ model: item.model,
6449
+ thinking: item.thinking,
6450
+ status: "running",
6451
+ result: null
6452
+ }));
6453
+ return {
6454
+ ...state,
6455
+ phase: "reviewing",
6456
+ round,
6457
+ active: { round, reviewers, settledCount: 0 },
6458
+ pending: null,
6459
+ repair: null,
6460
+ summary: null,
6461
+ roundStartedAt: now,
6462
+ updatedAt: now
6463
+ };
6502
6464
  }
6503
- function errored(card) {
6504
- const lines = [
6505
- msg6.card.errorBlocker,
6506
- msg6.card.errorReason(card.message),
6507
- ...card.elapsedMs === undefined ? [] : ["", elapsedLine(card.elapsedMs, card.totalElapsedMs, false)]
6508
- ];
6509
- return spec("error", msg6.card.incomplete, lines, "warning");
6465
+ function resultsOf(reviewers) {
6466
+ return reviewers.flatMap((item) => item.result ? [item.result] : []);
6510
6467
  }
6511
- function advisorCard(card) {
6512
- const title = msg6.card.advisorGuidance(decisionText(card.advisor.verdict));
6513
- const body = [advisorModelLine(card.advisorModel), "", ...adviceLines(card.advisor.advice)];
6514
- const footer = card.elapsedMs === undefined ? [] : [elapsedLine(card.elapsedMs, undefined, false)];
6515
- return spec("advisor", title, withFooter(body, footer), "neutral");
6468
+ function aggregate(reviewers) {
6469
+ const verdicts = reviewers.filter((item) => item.status === "passed" || item.status === "failed");
6470
+ const details = aggregateDetails(reviewers);
6471
+ if (verdicts.length === 0)
6472
+ return { result: "error", details, feedbackDetails: "", summary: "" };
6473
+ const failed = verdicts.filter((item) => item.status === "failed");
6474
+ if (failed.length > 0)
6475
+ return { result: "failed", details, feedbackDetails: aggregateDetails(failed), summary: "" };
6476
+ const absent = reviewers.filter((item) => item.status === "error");
6477
+ return { result: "passed", details, feedbackDetails: "", summary: aggregatePassSummary(verdicts, absent) };
6516
6478
  }
6517
- function decisionText(verdict) {
6518
- return msg6.card.decision[verdict];
6479
+ function aggregateDetails(reviewers) {
6480
+ return reviewers.map((item) => `${modelLabel(item)}
6481
+ ${item.details.trim()}`).join(`
6482
+
6483
+ `);
6519
6484
  }
6520
- function advisorModelLine(model) {
6521
- return msg6.card.modelLine(shortModel(model));
6485
+ function aggregatePassSummary(passed, absent) {
6486
+ const fallback = msg6.summary.passedFallback;
6487
+ const lines = passed.length === 1 ? [passBody(passed[0]?.summary ?? "") || fallback] : passed.map((item) => msg6.summary.modelBullet(shortModel(item.model), passBody(item.summary) || fallback));
6488
+ if (absent.length > 0)
6489
+ lines.push(msg6.summary.absent(absent.map((item) => ({ model: shortModel(item.model), reason: firstLine(item.details) }))));
6490
+ const suggestions = [...new Set(passed.flatMap((item) => splitSuggestions(item.details)))];
6491
+ if (suggestions.length === 0)
6492
+ return lines.join(`
6493
+ `);
6494
+ return [
6495
+ ...lines,
6496
+ "",
6497
+ `## ${msg6.terms.suggestions}`,
6498
+ ...suggestions.map((item) => `- ${item}`)
6499
+ ].join(`
6500
+ `);
6522
6501
  }
6523
- function adviceLines(advice) {
6524
- const output = [];
6525
- for (const line of advice.split(/\r?\n/u)) {
6526
- if (/^\*\*[^*]+\*\*\s*[::]/u.test(line.trim()) && output.length > 0 && output.at(-1) !== "")
6527
- output.push("");
6528
- output.push(line);
6529
- }
6530
- return output;
6502
+ function firstLine(text) {
6503
+ return text.trim().split(/\r?\n/u, 1)[0]?.split(/[::]/u, 1)[0]?.trim() ?? "";
6531
6504
  }
6532
- function qualityTitle(round, title) {
6533
- return round <= 1 ? title : msg6.card.round(round, title);
6505
+ function splitSuggestions(details) {
6506
+ const lines = details.split(/\r?\n/u);
6507
+ const index = lines.findIndex((line) => SUGGESTIONS_HEADING.test(line.trim()));
6508
+ if (index < 0)
6509
+ return [];
6510
+ return lines.slice(index + 1).map((line) => line.replace(/^[-*]\s*/u, "").trim()).filter(Boolean);
6534
6511
  }
6535
- function withFooter(lines, footer) {
6536
- if (footer.length === 0)
6537
- return lines;
6538
- return [...lines, ...lines.length > 0 ? ["", "---", ""] : [], ...footer];
6512
+ function passBody(summary) {
6513
+ return summary.replace(/^(?:PASS\s*)/iu, "").trim();
6539
6514
  }
6540
- function elapsedLine(ms, totalMs, showTotal) {
6541
- return msg6.card.elapsed(formatDuration(ms), showTotal && totalMs !== undefined ? formatDuration(totalMs) : undefined);
6515
+ function shortModel(model) {
6516
+ return model.split("/").at(-1) || model;
6542
6517
  }
6543
- var MODEL_SECTION = new RegExp(String.raw`^${termPattern((terms) => terms.model)}\s+\d+\s+·\s+`, "iu");
6544
- var EVIDENCE_LABEL = new RegExp(String.raw`^[-*+]?\s*(?:\*\*)?${termPattern((terms) => terms.field.evidence)}(?:\*\*)?\s*[::]`, "u");
6545
- function formatReviewResultLines(review) {
6546
- const lines = review.split(/\r?\n/u);
6547
- const sections = [];
6548
- const preface = [];
6549
- let current;
6550
- for (const line of lines) {
6551
- if (MODEL_SECTION.test(line.trim())) {
6552
- if (current)
6553
- sections.push(current);
6554
- current = { title: line.trim(), body: [] };
6555
- } else if (current)
6556
- current.body.push(line);
6557
- else
6558
- preface.push(line);
6559
- }
6560
- if (current)
6561
- sections.push(current);
6562
- if (sections.length === 0)
6563
- return normalizedReviewLines(review);
6564
- return [
6565
- ...normalizedReviewLines(preface.join(`
6566
- `)),
6567
- ...preface.join("").trim() ? [""] : [],
6568
- ...sections.flatMap((section, index) => [
6569
- ...index > 0 ? ["", "---", ""] : [],
6570
- `**${section.title}**`,
6571
- "",
6572
- ...normalizedReviewLines(section.body.join(`
6573
- `))
6574
- ])
6518
+ function appendClosedFindings(summary, history, beforeRound) {
6519
+ const seen = new Set;
6520
+ const findings = [];
6521
+ for (const round of history) {
6522
+ if (round.round >= beforeRound || round.result !== "failed")
6523
+ continue;
6524
+ for (const line of round.details.split(/\r?\n/u)) {
6525
+ const match = ISSUE_LINE.exec(line.trim());
6526
+ const issue = match?.[1]?.replace(/`([^`]+)`/gu, "$1").trim();
6527
+ if (!issue)
6528
+ continue;
6529
+ const key = issue.replace(/\s+/gu, "");
6530
+ if (seen.has(key))
6531
+ continue;
6532
+ seen.add(key);
6533
+ findings.push({ round: round.round, issue });
6534
+ }
6535
+ }
6536
+ if (findings.length === 0)
6537
+ return summary;
6538
+ const shown = findings.slice(0, 6);
6539
+ const recap = [msg6.summary.closedHeading, ...shown.map((finding) => msg6.summary.closedItem(finding.issue, finding.round))];
6540
+ if (findings.length > shown.length)
6541
+ recap.push(msg6.summary.closedMore(findings.length - shown.length));
6542
+ return `${summary}
6543
+
6544
+ ${recap.join(`
6545
+ `)}`;
6546
+ }
6547
+ function modelLabel(item) {
6548
+ return msg6.summary.modelSection(item.index + 1, shortModel(item.model));
6549
+ }
6550
+ function interruptedRound(state, now, result, reason) {
6551
+ if (state.phase === "reviewing" && state.active)
6552
+ return closeRound(state, now, { round: state.active.round, result, details: "", reviewers: resultsOf(state.active.reviewers), reason });
6553
+ if (state.phase === "needs_fix" && state.pending)
6554
+ return closeRound(state, now, { round: state.pending.round, result, details: "", reviewers: state.pending.reviewers, reason });
6555
+ return;
6556
+ }
6557
+ function closeRound(state, now, record) {
6558
+ return { ...record, elapsedMs: state.roundStartedAt ? Math.max(0, now - state.roundStartedAt) : 0 };
6559
+ }
6560
+
6561
+ // review/card.ts
6562
+ var CARD_TYPE = "firecode-review-card";
6563
+ var VERSION2 = 1;
6564
+ var MARKS = {
6565
+ start: { glyph: "⠿", color: "review" },
6566
+ advisor: { glyph: "⠿", color: "review" },
6567
+ pass: { glyph: "✓", color: "success" },
6568
+ fail: { glyph: "✗", color: "error" },
6569
+ stop: { glyph: "✗", color: "error" },
6570
+ timeout: { glyph: "◌", color: "error" },
6571
+ error: { glyph: "◌", color: "error" }
6572
+ };
6573
+ var CARD_KINDS = new Set(Object.keys(MARKS));
6574
+ var TONES = ["success", "warning", "neutral"];
6575
+ function paintMark(details, theme) {
6576
+ const { color } = MARKS[details.kind];
6577
+ return color === "review" ? paint(HEAT_COLORS.gold, details.icon) : theme.fg(color, details.icon);
6578
+ }
6579
+ function isValidCardDetails(value) {
6580
+ if (typeof value !== "object" || value === null || Array.isArray(value))
6581
+ return false;
6582
+ const record = value;
6583
+ if (Object.keys(record).length !== 6)
6584
+ return false;
6585
+ if (record.version !== VERSION2)
6586
+ return false;
6587
+ if (typeof record.kind !== "string" || !CARD_KINDS.has(record.kind))
6588
+ return false;
6589
+ if (typeof record.title !== "string")
6590
+ return false;
6591
+ if (!Array.isArray(record.lines) || !record.lines.every((line) => typeof line === "string"))
6592
+ return false;
6593
+ if (!TONES.includes(record.tone))
6594
+ return false;
6595
+ return typeof record.icon === "string";
6596
+ }
6597
+ function registerCardRenderer(pi) {
6598
+ pi.registerMessageRenderer(CARD_TYPE, (message, _options, theme) => new ReviewCard(message.details, message.content, theme));
6599
+ }
6600
+
6601
+ class ReviewCard {
6602
+ card;
6603
+ fallback;
6604
+ constructor(details, content, theme) {
6605
+ this.fallback = new Text5(plainContent(content), 0, 0);
6606
+ let card;
6607
+ try {
6608
+ card = isValidCardDetails(details) ? nativeCard(details, theme) : undefined;
6609
+ } catch {
6610
+ card = undefined;
6611
+ }
6612
+ this.card = card;
6613
+ }
6614
+ render(width) {
6615
+ try {
6616
+ return (this.card ?? this.fallback).render(Math.max(1, width));
6617
+ } catch {
6618
+ try {
6619
+ return this.fallback.render(Math.max(1, width));
6620
+ } catch {
6621
+ return [];
6622
+ }
6623
+ }
6624
+ }
6625
+ invalidate() {
6626
+ this.card?.invalidate?.();
6627
+ this.fallback.invalidate?.();
6628
+ }
6629
+ }
6630
+ function nativeCard(details, theme) {
6631
+ const box = new Box(1, 0, (text) => theme.bg(backgroundFor(details.tone), text));
6632
+ box.addChild(new Text5(`${paintMark(details, theme)} ${details.title}`, 0, 0));
6633
+ box.addChild(new Spacer4(1));
6634
+ box.addChild(new Markdown3(details.lines.join(`
6635
+ `), 0, 0, getMarkdownTheme4()));
6636
+ return box;
6637
+ }
6638
+ function backgroundFor(tone) {
6639
+ if (tone === "success")
6640
+ return "toolSuccessBg";
6641
+ return tone === "warning" ? "toolErrorBg" : "customMessageBg";
6642
+ }
6643
+ function plainContent(content) {
6644
+ if (typeof content === "string")
6645
+ return content;
6646
+ if (!Array.isArray(content))
6647
+ return "";
6648
+ return content.map(plainPart).filter(Boolean).join(`
6649
+ `);
6650
+ }
6651
+ function plainPart(part) {
6652
+ return typeof part === "object" && part !== null && "text" in part ? String(part.text) : "";
6653
+ }
6654
+ function buildCard(card) {
6655
+ switch (card.kind) {
6656
+ case "start":
6657
+ return started(card);
6658
+ case "pass":
6659
+ return passed(card);
6660
+ case "fail":
6661
+ return failed2(card);
6662
+ case "stop":
6663
+ return stopped(card);
6664
+ case "timeout":
6665
+ return timedOut();
6666
+ case "error":
6667
+ return errored(card);
6668
+ case "advisor":
6669
+ return advisorCard(card);
6670
+ }
6671
+ }
6672
+ function started(card) {
6673
+ return spec("start", msg6.card.started, [msg6.card.startedModels(card.models.map(shortModel))], "neutral");
6674
+ }
6675
+ function passed(card) {
6676
+ const title = qualityTitle(card.round, msg6.card.passed);
6677
+ const lines = withFooter(formatReviewResultLines(card.summary), [
6678
+ elapsedLine(card.elapsedMs, card.round > 1 ? card.totalElapsedMs : undefined)
6679
+ ]);
6680
+ return spec("pass", title, lines, "success");
6681
+ }
6682
+ function failed2(card) {
6683
+ const title = qualityTitle(card.round, msg6.card.failed);
6684
+ return spec("fail", title, withFooter(formatReviewResultLines(card.details), [elapsedLine(card.elapsedMs)]), "warning");
6685
+ }
6686
+ function stopped(card) {
6687
+ const footer = card.elapsedMs === undefined ? [] : [elapsedLine(card.elapsedMs)];
6688
+ if (card.reason === "advisor") {
6689
+ const title = qualityTitle(card.round, msg6.card.stoppedByAdvisor);
6690
+ const body = [advisorModelLine(card.advisorModel), "", ...adviceLines(card.advisor.advice)];
6691
+ return spec("stop", title, withFooter(body, footer), "warning");
6692
+ }
6693
+ const title = qualityTitle(card.round, msg6.card.failed);
6694
+ const body = formatReviewResultLines(card.details);
6695
+ return spec("stop", title, withFooter(body, footer), "warning");
6696
+ }
6697
+ function timedOut() {
6698
+ return spec("timeout", msg6.card.incomplete, [msg6.card.timeoutBlocker, msg6.card.timeoutReason], "warning");
6699
+ }
6700
+ function errored(card) {
6701
+ const lines = [
6702
+ msg6.card.errorBlocker,
6703
+ msg6.card.errorReason(card.message),
6704
+ ...card.elapsedMs === undefined ? [] : ["", elapsedLine(card.elapsedMs)]
6705
+ ];
6706
+ return spec("error", msg6.card.incomplete, lines, "warning");
6707
+ }
6708
+ function advisorCard(card) {
6709
+ const title = msg6.card.advisorGuidance(msg6.card.decision[card.advisor.verdict]);
6710
+ const body = [advisorModelLine(card.advisorModel), "", ...adviceLines(card.advisor.advice)];
6711
+ return spec("advisor", title, withFooter(body, [elapsedLine(card.elapsedMs)]), "neutral");
6712
+ }
6713
+ function advisorModelLine(model) {
6714
+ return msg6.card.modelLine(shortModel(model));
6715
+ }
6716
+ function adviceLines(advice) {
6717
+ const output = [];
6718
+ for (const line of advice.split(/\r?\n/u)) {
6719
+ if (/^\*\*[^*]+\*\*\s*[::]/u.test(line.trim()) && output.length > 0 && output.at(-1) !== "")
6720
+ output.push("");
6721
+ output.push(line);
6722
+ }
6723
+ return output;
6724
+ }
6725
+ function qualityTitle(round, title) {
6726
+ return round <= 1 ? title : msg6.card.round(round, title);
6727
+ }
6728
+ function withFooter(lines, footer) {
6729
+ if (footer.length === 0)
6730
+ return lines;
6731
+ return [...lines, ...lines.length > 0 ? ["", "---", ""] : [], ...footer];
6732
+ }
6733
+ function elapsedLine(ms, totalMs) {
6734
+ return msg6.card.elapsed(formatDuration(ms), totalMs === undefined ? undefined : formatDuration(totalMs));
6735
+ }
6736
+ var MODEL_SECTION = new RegExp(String.raw`^${termPattern((terms) => terms.model)}\s+\d+\s+·\s+`, "iu");
6737
+ var EVIDENCE_LABEL = new RegExp(String.raw`^[-*+]?\s*(?:\*\*)?${termPattern((terms) => terms.field.evidence)}(?:\*\*)?\s*[::]`, "u");
6738
+ function formatReviewResultLines(review) {
6739
+ const lines = review.split(/\r?\n/u);
6740
+ const sections = [];
6741
+ const preface = [];
6742
+ let current;
6743
+ for (const line of lines) {
6744
+ if (MODEL_SECTION.test(line.trim())) {
6745
+ if (current)
6746
+ sections.push(current);
6747
+ current = { title: line.trim(), body: [] };
6748
+ } else if (current)
6749
+ current.body.push(line);
6750
+ else
6751
+ preface.push(line);
6752
+ }
6753
+ if (current)
6754
+ sections.push(current);
6755
+ if (sections.length === 0)
6756
+ return normalizedReviewLines(review);
6757
+ return [
6758
+ ...normalizedReviewLines(preface.join(`
6759
+ `)),
6760
+ ...preface.join("").trim() ? [""] : [],
6761
+ ...sections.flatMap((section, index) => [
6762
+ ...index > 0 ? ["", "---", ""] : [],
6763
+ `**${section.title}**`,
6764
+ "",
6765
+ ...normalizedReviewLines(section.body.join(`
6766
+ `))
6767
+ ])
6575
6768
  ];
6576
6769
  }
6577
6770
  function normalizedReviewLines(review) {
@@ -6639,13 +6832,7 @@ var ROUND_RESULTS = new Set([
6639
6832
  ]);
6640
6833
  var REVIEWER_STATUSES = new Set(["running", "passed", "failed", "error"]);
6641
6834
  var ADVISOR_VERDICTS = new Set(["continue", "stop", "narrow"]);
6642
- var STOP_REASONS = new Set([
6643
- "advisor",
6644
- "max_rounds",
6645
- "user",
6646
- "shutdown",
6647
- "timeout"
6648
- ]);
6835
+ var STOP_REASONS = new Set(["user", "shutdown", "timeout"]);
6649
6836
  var REPAIR_STATUSES = new Set(["pending", "awaiting_start", "running", "completed"]);
6650
6837
  var SUMMARY_KINDS = new Set(["passed", "max_rounds", "advisor_stop"]);
6651
6838
  var SUMMARY_STATUSES = new Set(["pending", "awaiting_start", "running"]);
@@ -6716,9 +6903,6 @@ var CHECKPOINT_KEYS = keysOf({
6716
6903
  roundStartedAt: true,
6717
6904
  updatedAt: true
6718
6905
  });
6719
- function isRecord7(value) {
6720
- return typeof value === "object" && value !== null && !Array.isArray(value);
6721
- }
6722
6906
  function isString(value) {
6723
6907
  return typeof value === "string";
6724
6908
  }
@@ -6732,21 +6916,21 @@ function hasOnlyKeys(record, keys) {
6732
6916
  return Object.keys(record).every((key) => keys.includes(key));
6733
6917
  }
6734
6918
  function isValidReviewerResult(value) {
6735
- if (!isRecord7(value))
6919
+ if (!isRecord(value))
6736
6920
  return false;
6737
6921
  if (!isNonNegativeInt(value.index) || !hasOnlyKeys(value, REVIEWER_RESULT_KEYS))
6738
6922
  return false;
6739
6923
  return isString(value.model) && isString(value.thinking) && oneOf(value.status, REVIEWER_STATUSES) && value.status !== "running" && isString(value.summary) && isString(value.details);
6740
6924
  }
6741
6925
  function isValidActiveReviewer(value) {
6742
- if (!isRecord7(value) || !isNonNegativeInt(value.index))
6926
+ if (!isRecord(value) || !isNonNegativeInt(value.index))
6743
6927
  return false;
6744
6928
  if (!hasOnlyKeys(value, ACTIVE_REVIEWER_KEYS))
6745
6929
  return false;
6746
6930
  return isString(value.model) && isString(value.thinking) && oneOf(value.status, REVIEWER_STATUSES) && (value.result === null || isValidReviewerResult(value.result));
6747
6931
  }
6748
6932
  function isValidActiveCheck(value) {
6749
- if (!isRecord7(value))
6933
+ if (!isRecord(value))
6750
6934
  return false;
6751
6935
  if (!hasOnlyKeys(value, ACTIVE_CHECK_KEYS))
6752
6936
  return false;
@@ -6757,19 +6941,19 @@ function isValidActiveCheck(value) {
6757
6941
  return round >= 1 && Array.isArray(value.reviewers) && value.reviewers.every(isValidActiveReviewer) && value.reviewers.length > 0 && settledCount <= value.reviewers.length;
6758
6942
  }
6759
6943
  function isValidAdvisor(value) {
6760
- if (!isRecord7(value) || !hasOnlyKeys(value, ADVISOR_KEYS))
6944
+ if (!isRecord(value) || !hasOnlyKeys(value, ADVISOR_KEYS))
6761
6945
  return false;
6762
6946
  return oneOf(value.verdict, ADVISOR_VERDICTS) && isString(value.advice);
6763
6947
  }
6764
6948
  function isValidRound(value) {
6765
- if (!isRecord7(value))
6949
+ if (!isRecord(value))
6766
6950
  return false;
6767
6951
  if (!hasOnlyKeys(value, ROUND_KEYS))
6768
6952
  return false;
6769
6953
  return isNonNegativeInt(value.round) && oneOf(value.result, ROUND_RESULTS) && isString(value.details) && Array.isArray(value.reviewers) && value.reviewers.every(isValidReviewerResult) && (value.advisor === undefined || isValidAdvisor(value.advisor)) && (value.reason === undefined || oneOf(value.reason, STOP_REASONS)) && isNonNegativeInt(value.elapsedMs);
6770
6954
  }
6771
6955
  function isValidPendingRound(value) {
6772
- if (!isRecord7(value))
6956
+ if (!isRecord(value))
6773
6957
  return false;
6774
6958
  if (!hasOnlyKeys(value, PENDING_ROUND_KEYS))
6775
6959
  return false;
@@ -6777,13 +6961,13 @@ function isValidPendingRound(value) {
6777
6961
  return isNonNegativeInt(round) && round >= 1 && Array.isArray(value.reviewers) && value.reviewers.every(isValidReviewerResult) && isString(value.details);
6778
6962
  }
6779
6963
  function isValidRepair(value) {
6780
- return isRecord7(value) && hasOnlyKeys(value, REPAIR_KEYS) && isString(value.details) && (value.advisor === null || isValidAdvisor(value.advisor)) && oneOf(value.status, REPAIR_STATUSES);
6964
+ return isRecord(value) && hasOnlyKeys(value, REPAIR_KEYS) && isString(value.details) && (value.advisor === null || isValidAdvisor(value.advisor)) && oneOf(value.status, REPAIR_STATUSES);
6781
6965
  }
6782
6966
  function isValidSummary(value) {
6783
- return isRecord7(value) && hasOnlyKeys(value, SUMMARY_KEYS) && oneOf(value.kind, SUMMARY_KINDS) && oneOf(value.status, SUMMARY_STATUSES);
6967
+ return isRecord(value) && hasOnlyKeys(value, SUMMARY_KEYS) && oneOf(value.kind, SUMMARY_KINDS) && oneOf(value.status, SUMMARY_STATUSES);
6784
6968
  }
6785
6969
  function isValidCheckpoint(value) {
6786
- if (!isRecord7(value))
6970
+ if (!isRecord(value))
6787
6971
  return false;
6788
6972
  if (!hasOnlyKeys(value, CHECKPOINT_KEYS))
6789
6973
  return false;
@@ -6810,29 +6994,23 @@ function toCheckpoint(state) {
6810
6994
  };
6811
6995
  }
6812
6996
  function readCheckpoint(ctx) {
6813
- const entry = latestEntry(ctx);
6814
- return entry ? entry : undefined;
6997
+ return latestData(ctx);
6998
+ }
6999
+ function isCheckpointEntry(value) {
7000
+ return isRecord(value) && value.type === "custom" && value.customType === CHECKPOINT_TYPE && "data" in value;
6815
7001
  }
6816
- function latestEntry(ctx) {
7002
+ function latestData(ctx) {
6817
7003
  const entries = ctx.sessionManager.getBranch();
6818
7004
  for (let index = entries.length - 1;index >= 0; index -= 1) {
6819
7005
  const entry = entries[index];
6820
- if (!isRecord7(entry) || entry.type !== "custom")
6821
- continue;
6822
- if (entry.customType !== CHECKPOINT_TYPE)
6823
- continue;
6824
- if (!isValidCheckpoint(entry.data))
6825
- return;
6826
- return entry.data;
7006
+ if (isCheckpointEntry(entry))
7007
+ return isValidCheckpoint(entry.data) ? entry.data : undefined;
6827
7008
  }
6828
7009
  return;
6829
7010
  }
6830
7011
  function readStamp(ctx) {
6831
- const entry = latestEntry(ctx);
6832
- if (!entry)
6833
- return null;
6834
- const data = entry;
6835
- return { runId: data.runId, seq: data.seq };
7012
+ const data = latestData(ctx);
7013
+ return data ? { runId: data.runId, seq: data.seq } : null;
6836
7014
  }
6837
7015
  function appendCheckpoint(pi, state, seq) {
6838
7016
  const data = { ...toCheckpoint(state), seq };
@@ -6841,12 +7019,12 @@ function appendCheckpoint(pi, state, seq) {
6841
7019
  pi.appendEntry(CHECKPOINT_TYPE, data);
6842
7020
  return { runId: state.runId, seq };
6843
7021
  }
6844
- function beginCheckpoint(pi, _ctx, state) {
7022
+ function beginCheckpoint(pi, state) {
6845
7023
  return appendCheckpoint(pi, state, 1);
6846
7024
  }
6847
7025
  function writeCheckpoint(pi, ctx, state, expected) {
6848
7026
  const current = readStamp(ctx);
6849
- if (!current || current.runId !== expected.runId || current.seq !== expected.seq)
7027
+ if (current?.runId !== expected.runId || current.seq !== expected.seq)
6850
7028
  throw new CheckpointConflictError;
6851
7029
  return appendCheckpoint(pi, state, expected.seq + 1);
6852
7030
  }
@@ -6888,7 +7066,7 @@ ${omitted > 0 ? `${msg6.evidence.gap(omitted)}
6888
7066
  function collectFailedCalls(entries) {
6889
7067
  const failed = new Set;
6890
7068
  for (const entry of entries) {
6891
- if (!isRecord8(entry) || entry.type !== "message")
7069
+ if (!isRecord(entry) || entry.type !== "message")
6892
7070
  continue;
6893
7071
  const message = asRecord2(entry.message);
6894
7072
  if (message?.role !== "toolResult" || message.isError !== true)
@@ -6899,7 +7077,7 @@ function collectFailedCalls(entries) {
6899
7077
  return failed;
6900
7078
  }
6901
7079
  function renderEntry(entry, render) {
6902
- if (!isRecord8(entry))
7080
+ if (!isRecord(entry))
6903
7081
  return [];
6904
7082
  switch (entry.type) {
6905
7083
  case "message": {
@@ -6926,11 +7104,13 @@ ${assistantBody(message.content, render)}` }];
6926
7104
  ${clip2(textOf(entry.content), render)}` }];
6927
7105
  }
6928
7106
  case "compaction":
6929
- return typeof entry.summary === "string" && entry.summary ? [{ text: `## ${msg6.evidence.compaction}
6930
- ${clip2(entry.summary, render)}` }] : [];
6931
- case "branch_summary":
6932
- return typeof entry.summary === "string" && entry.summary ? [{ text: `## ${msg6.evidence.branchSummary}
6933
- ${clip2(entry.summary, render)}` }] : [];
7107
+ case "branch_summary": {
7108
+ if (typeof entry.summary !== "string" || !entry.summary)
7109
+ return [];
7110
+ const label = entry.type === "compaction" ? msg6.evidence.compaction : msg6.evidence.branchSummary;
7111
+ return [{ text: `## ${label}
7112
+ ${clip2(entry.summary, render)}` }];
7113
+ }
6934
7114
  default:
6935
7115
  return [];
6936
7116
  }
@@ -6985,11 +7165,8 @@ function isCjk(char) {
6985
7165
  const code = char.codePointAt(0) ?? 0;
6986
7166
  return code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255;
6987
7167
  }
6988
- function isRecord8(value) {
6989
- return typeof value === "object" && value !== null && !Array.isArray(value);
6990
- }
6991
7168
  function asRecord2(value) {
6992
- return isRecord8(value) ? value : undefined;
7169
+ return isRecord(value) ? value : undefined;
6993
7170
  }
6994
7171
 
6995
7172
  // review/ui.ts
@@ -6997,7 +7174,6 @@ import { basename as basename2 } from "node:path";
6997
7174
  import {
6998
7175
  CustomEditor as CustomEditor2
6999
7176
  } from "@earendil-works/pi-coding-agent";
7000
- var reviewTitleActive = false;
7001
7177
  function lockEditor(ctx, cancel) {
7002
7178
  if (ctx.hasUI === false || typeof ctx.ui.setEditorComponent !== "function")
7003
7179
  return () => {};
@@ -7032,26 +7208,47 @@ class ReviewEditor extends CustomEditor2 {
7032
7208
  return [lines[0], `\x1B[2m${hint}\x1B[22m`, lines[lines.length - 1]];
7033
7209
  }
7034
7210
  }
7035
- function showReviewTitle(ctx, round) {
7036
- if (!ctx.hasUI || typeof ctx.ui.setTitle !== "function")
7037
- return;
7038
- const manager = ctx.sessionManager;
7039
- const rawName = manager.getSessionName?.();
7040
- const rawCwd = manager.getCwd?.();
7041
- const who = typeof rawName === "string" && rawName ? rawName : typeof rawCwd === "string" ? basename2(rawCwd) : "";
7042
- reviewTitleActive = true;
7043
- ctx.ui.setTitle(`${msg6.ui.reviewing}${round > 0 ? ` R${round}` : ""}${who ? ` · ${who}` : ""}`);
7044
- }
7045
- function hideReviewTitle(ctx) {
7046
- if (!reviewTitleActive || !ctx.hasUI || typeof ctx.ui.setTitle !== "function")
7047
- return;
7048
- reviewTitleActive = false;
7211
+
7212
+ class ReviewUi {
7213
+ unlockEditor;
7214
+ titleShown = false;
7215
+ show(ctx, round, canCancel, cancel) {
7216
+ this.showTitle(ctx, round);
7217
+ if (!canCancel)
7218
+ return this.releaseEditor();
7219
+ this.unlockEditor ??= lockEditor(ctx, cancel);
7220
+ }
7221
+ clear(ctx) {
7222
+ if (this.titleShown && canSetTitle(ctx))
7223
+ ctx.ui.setTitle(restoredTitle(ctx));
7224
+ this.titleShown = false;
7225
+ this.releaseEditor();
7226
+ }
7227
+ showTitle(ctx, round) {
7228
+ if (!canSetTitle(ctx))
7229
+ return;
7230
+ const { name, dir } = identity(ctx);
7231
+ const who = name || dir;
7232
+ this.titleShown = true;
7233
+ ctx.ui.setTitle(`${msg6.ui.reviewing}${round > 0 ? ` R${round}` : ""}${who ? ` · ${who}` : ""}`);
7234
+ }
7235
+ releaseEditor() {
7236
+ this.unlockEditor?.();
7237
+ this.unlockEditor = undefined;
7238
+ }
7239
+ }
7240
+ function canSetTitle(ctx) {
7241
+ return ctx.hasUI && typeof ctx.ui.setTitle === "function";
7242
+ }
7243
+ function identity(ctx) {
7049
7244
  const manager = ctx.sessionManager;
7050
- const rawName = manager.getSessionName?.();
7051
- const rawCwd = manager.getCwd?.();
7052
- const name = typeof rawName === "string" && rawName ? rawName : undefined;
7053
- const dir = typeof rawCwd === "string" ? basename2(rawCwd) : "";
7054
- ctx.ui.setTitle(name ? `π - ${name} - ${dir}` : `π - ${dir}`);
7245
+ const name = manager.getSessionName?.();
7246
+ const cwd = manager.getCwd?.();
7247
+ return { name: typeof name === "string" && name ? name : undefined, dir: typeof cwd === "string" ? basename2(cwd) : "" };
7248
+ }
7249
+ function restoredTitle(ctx) {
7250
+ const { name, dir } = identity(ctx);
7251
+ return name ? `π - ${name} - ${dir}` : `π - ${dir}`;
7055
7252
  }
7056
7253
 
7057
7254
  // review/prompt.ts
@@ -7120,16 +7317,14 @@ function buildFixFeedback(input) {
7120
7317
  `));
7121
7318
  }
7122
7319
  var SUMMARY_MATERIAL_LIMIT = 4000;
7123
- var SUMMARY_KEYS2 = { passed: "passed", max_rounds: "maxRounds", advisor_stop: "advisorStop" };
7124
7320
  function buildSummaryPrompt(input) {
7125
7321
  const omitted = input.material.length - SUMMARY_MATERIAL_LIMIT;
7126
7322
  const material = omitted > 0 ? `${input.material.slice(0, SUMMARY_MATERIAL_LIMIT)}
7127
7323
  ${msg6.prompt.materialTruncated(omitted)}` : input.material;
7128
- const key = SUMMARY_KEYS2[input.kind];
7129
- const body = msg6.prompt.summaryInstruction[key](input.rounds);
7324
+ const body = msg6.prompt.summaryInstruction[input.kind](input.rounds);
7130
7325
  const content = material.trim() ? `${body}
7131
7326
 
7132
- ${msg6.prompt.materialLabel[key]}
7327
+ ${msg6.prompt.materialLabel[input.kind]}
7133
7328
  ${material}` : body;
7134
7329
  return wrapEnvelope("firecode_review", content);
7135
7330
  }
@@ -7138,28 +7333,16 @@ ${material}` : body;
7138
7333
  async function runAdvisor(options) {
7139
7334
  const result = await options.runSession({
7140
7335
  role: "advisor",
7141
- model: options.config.model,
7142
- thinking: options.config.thinking,
7143
- tools: options.config.tools,
7336
+ config: options.config,
7144
7337
  prompt: options.prompt,
7145
7338
  cwd: options.cwd,
7146
- timeoutMs: options.config.timeoutMs,
7147
7339
  signal: options.signal
7148
7340
  });
7149
7341
  if (result.kind !== "output")
7150
- throw new Error(advisorProcessError(result));
7342
+ throw new Error(msg6.advisor.unavailable(result.kind === "error" ? result.message : result.kind));
7151
7343
  return parseAdvisorOutput(result.text);
7152
7344
  }
7153
7345
  var VERDICTS = new Set(["continue", "stop", "narrow"]);
7154
- function advisorProcessError(result) {
7155
- if (result.kind === "aborted")
7156
- return msg6.advisor.unavailable("aborted");
7157
- if (result.kind === "timeout")
7158
- return msg6.advisor.unavailable("timeout");
7159
- if (result.kind === "empty")
7160
- return msg6.advisor.unavailable("empty output");
7161
- return msg6.advisor.unavailable(result.message);
7162
- }
7163
7346
  var VERDICT_SCAN_LINES = 8;
7164
7347
  function parseAdvisorOutput(text) {
7165
7348
  const lines = unwrapCodeFence(text.trim().split(/\r?\n/));
@@ -7205,12 +7388,9 @@ function normalizeVerdict(line) {
7205
7388
  async function runReviewer(options) {
7206
7389
  const result = await options.runSession({
7207
7390
  role: "reviewer",
7208
- model: options.config.model,
7209
- thinking: options.config.thinking,
7210
- tools: options.config.tools,
7391
+ config: options.config,
7211
7392
  prompt: options.prompt,
7212
7393
  cwd: options.cwd,
7213
- timeoutMs: options.config.timeoutMs,
7214
7394
  signal: options.signal
7215
7395
  });
7216
7396
  const parsed = result.kind === "output" ? parseReviewOutput(result.text) : processFailure(result);
@@ -7272,22 +7452,13 @@ var FILE_SEGMENT = new RegExp(`${termPattern((terms) => terms.anchor.files)}\\s*
7272
7452
  var COMMAND_SEGMENT = new RegExp(`${termPattern((terms) => terms.anchor.commands)}\\s*=\\s*([^;;]*)`, "iu");
7273
7453
  var FILE_ANCHOR = /[\w@./-]*\w\.[a-zA-Z]\w{0,5}\b/u;
7274
7454
  var FINDING_HEADING = new RegExp(`^#{1,6}\\s*${termPattern((terms) => terms.finding)}`, "u");
7275
- var SUGGESTIONS_HEADING = new RegExp(`^##\\s+${termPattern((terms) => terms.suggestions)}\\s*$`, "iu");
7276
7455
  var SUGGESTIONS_HEADING_SPLIT = new RegExp(SUGGESTIONS_HEADING.source, "imu");
7277
7456
  var bulletField = (label, value = "") => new RegExp(`^[-*+]\\s*(?:\\*\\*)?${label}(?:\\*\\*)?\\s*[::]\\s*${value}`, "iu");
7278
7457
  var FINDING_ISSUE = bulletField(termPattern((terms) => terms.field.issue), "(.*)$");
7279
- var FINDING_FIELDS = [
7280
- { key: "issue", label: termPattern((terms) => terms.field.issue) },
7281
- { key: "evidence", label: termPattern((terms) => terms.field.evidence) },
7282
- {
7283
- key: "contract",
7284
- label: termPattern((terms) => terms.field.contract, "违反的约定与期望", "违反的契约或期望行为", "违反的契约", "Violated agreement", "Contract or expected behavior violated", "Contract violated")
7285
- },
7286
- {
7287
- key: "commands",
7288
- label: termPattern((terms) => terms.field.commands, "需要运行的验证命令", "Verification commands", "Verification command to run", "Verification commands to run")
7289
- }
7290
- ];
7458
+ var FINDING_FIELDS = ["issue", "evidence", "contract", "commands"].map((key) => ({
7459
+ key,
7460
+ label: termPattern((terms) => terms.field[key])
7461
+ }));
7291
7462
  var SEVERITY_LABEL = termPattern((terms) => terms.field.severity);
7292
7463
  var SEVERITY_LINE = bulletField(SEVERITY_LABEL, `(?:${termPattern((terms) => terms.high)}|${termPattern((terms) => terms.medium)})\\s*$`);
7293
7464
  var FIELD_START = new RegExp(`^[-*+]\\s*(?:\\*\\*)?(?:${[SEVERITY_LABEL, ...FINDING_FIELDS.map((field) => field.label)].join("|")})`, "iu");
@@ -7378,28 +7549,28 @@ function clean(text) {
7378
7549
 
7379
7550
  // review/session.ts
7380
7551
  function createReviewSessionRunner(pool) {
7381
- return (options) => runReviewSession({ ...options, pool });
7552
+ return (request) => runReviewSession(pool, request);
7382
7553
  }
7383
- async function runReviewSession(options) {
7384
- if (options.signal?.aborted)
7554
+ async function runReviewSession(pool, { role, config, prompt, cwd, signal }) {
7555
+ if (signal?.aborted)
7385
7556
  return { kind: "aborted" };
7386
7557
  let spawned;
7387
7558
  try {
7388
- spawned = await options.pool.spawn({
7389
- cwd: options.cwd,
7390
- role: options.role,
7391
- model: await options.pool.resolveModel(options.model),
7392
- thinking: options.thinking,
7393
- tools: [...new Set(options.tools)].filter((tool) => tool !== "write" && tool !== "edit"),
7394
- systemPrompt: { mode: "replace", text: clean2(options.prompt.system) },
7559
+ spawned = await pool.spawn({
7560
+ cwd,
7561
+ role,
7562
+ model: await pool.resolveModel(config.model),
7563
+ thinking: config.thinking,
7564
+ tools: [...new Set(config.tools)].filter((tool) => tool !== "write" && tool !== "edit"),
7565
+ systemPrompt: { mode: "replace", text: clean2(prompt.system) },
7395
7566
  contextFiles: false,
7396
7567
  persistence: { type: "memory" },
7397
7568
  isolated: true
7398
7569
  });
7399
7570
  } catch (error) {
7400
- return options.signal?.aborted ? { kind: "aborted" } : { kind: "error", message: errorText(error) };
7571
+ return signal?.aborted ? { kind: "aborted" } : { kind: "error", message: errorText(error) };
7401
7572
  }
7402
- if (options.signal?.aborted) {
7573
+ if (signal?.aborted) {
7403
7574
  await spawned.dispose();
7404
7575
  return { kind: "aborted" };
7405
7576
  }
@@ -7421,13 +7592,13 @@ async function runReviewSession(options) {
7421
7592
  interrupted = "aborted";
7422
7593
  wake();
7423
7594
  };
7424
- options.signal?.addEventListener("abort", onAbort, { once: true });
7595
+ signal?.addEventListener("abort", onAbort, { once: true });
7425
7596
  const timeout = setTimeout(() => {
7426
7597
  interrupted = "timeout";
7427
7598
  wake();
7428
- }, options.timeoutMs);
7599
+ }, config.timeoutMs);
7429
7600
  try {
7430
- const run = spawned.prompt(clean2(options.prompt.user)).catch((error) => {
7601
+ const run = spawned.prompt(clean2(prompt.user)).catch((error) => {
7431
7602
  finalError = errorText(error);
7432
7603
  });
7433
7604
  await Promise.race([run, interruption]);
@@ -7438,7 +7609,7 @@ async function runReviewSession(options) {
7438
7609
  return finalText?.trim() ? { kind: "output", text: finalText } : { kind: "empty" };
7439
7610
  } finally {
7440
7611
  clearTimeout(timeout);
7441
- options.signal?.removeEventListener("abort", onAbort);
7612
+ signal?.removeEventListener("abort", onAbort);
7442
7613
  unsubscribe();
7443
7614
  await spawned.dispose();
7444
7615
  }
@@ -7448,572 +7619,25 @@ function assistantMessage(event) {
7448
7619
  return event.message.role === "assistant" ? event.message : undefined;
7449
7620
  if (event.type !== "agent_end")
7450
7621
  return;
7451
- return [...event.messages].reverse().find((message) => message.role === "assistant");
7452
- }
7453
- function clean2(text) {
7454
- return text.replaceAll("\x00", "");
7455
- }
7456
- function errorText(error) {
7457
- return error instanceof Error ? error.message : String(error);
7458
- }
7459
-
7460
- // review/state.ts
7461
- var SUGGESTIONS_HEADING2 = new RegExp(`^##\\s*${termPattern((terms) => terms.suggestions)}`, "iu");
7462
- var ISSUE_LINE = new RegExp(`^[-*+]\\s*(?:\\*\\*)?${termPattern((terms) => terms.field.issue)}(?:\\*\\*)?\\s*[::]\\s*(.+)$`, "iu");
7463
- function initialState(runId) {
7464
- return {
7465
- runId,
7466
- phase: "idle",
7467
- round: 0,
7468
- focus: "",
7469
- history: [],
7470
- active: null,
7471
- pending: null,
7472
- repair: null,
7473
- summary: null,
7474
- consecutiveFailures: 0,
7475
- startedAt: 0,
7476
- roundStartedAt: 0,
7477
- updatedAt: 0
7478
- };
7479
- }
7480
- function reduce(state, event, limits, now) {
7481
- switch (event.type) {
7482
- case "START":
7483
- return onStart(state, event, limits, now);
7484
- case "RECOVER":
7485
- return onRecover(state);
7486
- case "REVIEWER_SETTLED":
7487
- return onReviewerSettled(state, event, limits, now);
7488
- case "ADVISOR_SETTLED":
7489
- return onAdvisorSettled(state, event, limits, now);
7490
- case "ADVISOR_SKIPPED":
7491
- return onAdvisorSkipped(state, now);
7492
- case "INFRASTRUCTURE_ERROR":
7493
- return onInfrastructureError(state, event.details, now);
7494
- case "ADVANCE":
7495
- return onAdvance(state, limits, now);
7496
- case "FEEDBACK_DISPATCHED":
7497
- return updateRepairStatus(state, "pending", "awaiting_start", now);
7498
- case "REPAIR_STARTED":
7499
- return updateRepairStatus(state, "awaiting_start", "running", now);
7500
- case "REPAIR_COMPLETED":
7501
- return updateRepairStatus(state, "running", "completed", now);
7502
- case "SUMMARY_DISPATCHED":
7503
- return updateSummaryStatus(state, "pending", "awaiting_start", now);
7504
- case "SUMMARY_STARTED":
7505
- return updateSummaryStatus(state, "awaiting_start", "running", now);
7506
- case "SUMMARY_SETTLED":
7507
- return onSummarySettled(state, now);
7508
- case "CANCEL":
7509
- return onCancel(state, event.reason, now);
7510
- case "TIMEOUT":
7511
- return onTimeout(state, now);
7512
- }
7513
- }
7514
- function onStart(state, event, limits, now) {
7515
- if (state.phase === "reviewing" || state.phase === "needs_fix" || state.phase === "awaiting_fix")
7516
- return { state, effects: [] };
7517
- const focus = event.focus.trim();
7518
- if (event.busy)
7519
- return {
7520
- state: {
7521
- ...initialState(state.runId),
7522
- phase: "queued",
7523
- focus,
7524
- startedAt: now,
7525
- updatedAt: now
7526
- },
7527
- effects: [{ kind: "advance" }]
7528
- };
7529
- return {
7530
- state: beginRound({ ...initialState(state.runId), startedAt: now }, focus, 1, limits, now),
7531
- effects: [
7532
- {
7533
- kind: "send_card",
7534
- card: {
7535
- kind: "start",
7536
- round: 1,
7537
- focus,
7538
- models: limits.reviewers.map((item) => item.model)
7539
- }
7540
- },
7541
- { kind: "advance" }
7542
- ]
7543
- };
7544
- }
7545
- function onRecover(state) {
7546
- if (state.phase === "idle" || state.phase === "settled")
7547
- return { state, effects: [] };
7548
- const repair = state.phase === "awaiting_fix" && state.repair && state.repair.status !== "completed" ? { ...state.repair, status: "pending" } : state.repair;
7549
- const summary = state.phase === "summarizing" && state.summary ? { ...state.summary, status: "pending" } : state.summary;
7550
- return { state: { ...state, repair, summary }, effects: [] };
7551
- }
7552
- function updateRepairStatus(state, expected, status, now) {
7553
- if (state.phase !== "awaiting_fix" || state.repair?.status !== expected)
7554
- return { state, effects: [] };
7555
- return {
7556
- state: { ...state, repair: { ...state.repair, status }, updatedAt: now },
7557
- effects: []
7558
- };
7559
- }
7560
- function updateSummaryStatus(state, expected, status, now) {
7561
- if (state.phase !== "summarizing" || state.summary?.status !== expected)
7562
- return { state, effects: [] };
7563
- return {
7564
- state: { ...state, summary: { ...state.summary, status }, updatedAt: now },
7565
- effects: []
7566
- };
7567
- }
7568
- function onSummarySettled(state, now) {
7569
- if (state.phase !== "summarizing")
7570
- return { state, effects: [] };
7571
- return {
7572
- state: { ...state, phase: "settled", summary: null, updatedAt: now },
7573
- effects: []
7574
- };
7575
- }
7576
- function summarizing(kind) {
7577
- return { phase: "summarizing", summary: { kind, status: "pending" } };
7578
- }
7579
- function onAdvance(state, limits, now) {
7580
- if (state.phase === "queued")
7581
- return {
7582
- state: beginRound(state, state.focus, 1, limits, now),
7583
- effects: [
7584
- {
7585
- kind: "send_card",
7586
- card: {
7587
- kind: "start",
7588
- round: 1,
7589
- focus: state.focus,
7590
- models: limits.reviewers.map((item) => item.model)
7591
- }
7592
- },
7593
- { kind: "advance" }
7594
- ]
7595
- };
7596
- if (state.phase !== "awaiting_fix" || state.repair?.status !== "completed")
7597
- return { state, effects: [] };
7598
- if (state.round >= limits.maxRounds)
7599
- return {
7600
- state: { ...state, ...summarizing("max_rounds"), repair: null, updatedAt: now },
7601
- effects: [
7602
- {
7603
- kind: "send_card",
7604
- card: { kind: "stop", reason: "max_rounds", round: state.round, details: "" }
7605
- },
7606
- { kind: "advance" }
7607
- ]
7608
- };
7609
- return {
7610
- state: beginRound(state, state.focus, state.round + 1, limits, now),
7611
- effects: [{ kind: "advance" }]
7612
- };
7613
- }
7614
- function onReviewerSettled(state, event, limits, now) {
7615
- if (state.phase !== "reviewing" || !state.active)
7616
- return { state, effects: [] };
7617
- const active = state.active;
7618
- const settled = [];
7619
- const reviewers = active.reviewers.map((item) => {
7620
- if (item.index !== event.index)
7621
- return item;
7622
- settled.push(event.result);
7623
- return { ...item, status: event.result.status, result: event.result };
7624
- });
7625
- const settledCount = active.settledCount + 1;
7626
- const nextActive = { ...active, reviewers, settledCount };
7627
- if (settledCount < reviewers.length)
7628
- return {
7629
- state: { ...state, active: nextActive, updatedAt: now },
7630
- effects: []
7631
- };
7632
- const allSettled = nextActive.reviewers.flatMap((item) => item.result ? [item.result] : []);
7633
- return settleRound(state, nextActive, allSettled, limits, now);
7634
- }
7635
- function settleRound(state, active, settled, limits, now) {
7636
- const { result, displayDetails, feedbackDetails, archiveDetails, summary } = aggregate(settled);
7637
- const base = { ...state, active: null, updatedAt: now };
7638
- const totalElapsedMs = Math.max(0, now - state.startedAt);
7639
- const passSummary = result === "passed" ? appendClosedFindings(summary, state.history, active.round) : summary;
7640
- if (result === "passed" || result === "error") {
7641
- const round = roundRecord(active.round, result, archiveDetails, settled, undefined, state.roundStartedAt, now);
7642
- const history = [...state.history, round];
7643
- if (result === "passed")
7644
- return {
7645
- state: { ...base, ...summarizing("passed"), history },
7646
- effects: [
7647
- {
7648
- kind: "send_card",
7649
- card: {
7650
- kind: "pass",
7651
- round: active.round,
7652
- summary: passSummary,
7653
- details: archiveDetails,
7654
- elapsedMs: round.elapsedMs,
7655
- totalElapsedMs
7656
- }
7657
- },
7658
- { kind: "advance" }
7659
- ]
7660
- };
7661
- return {
7662
- state: { ...base, phase: "settled", history },
7663
- effects: [
7664
- {
7665
- kind: "send_card",
7666
- card: { kind: "error", message: displayDetails, elapsedMs: round.elapsedMs, totalElapsedMs }
7667
- }
7668
- ]
7669
- };
7670
- }
7671
- const consecutiveFailures = state.consecutiveFailures + 1;
7672
- if (active.round >= limits.maxRounds) {
7673
- const round = roundRecord(active.round, "failed", archiveDetails, settled, undefined, state.roundStartedAt, now);
7674
- return {
7675
- state: {
7676
- ...base,
7677
- ...summarizing("max_rounds"),
7678
- consecutiveFailures,
7679
- history: [...state.history, round]
7680
- },
7681
- effects: [
7682
- {
7683
- kind: "send_card",
7684
- card: {
7685
- kind: "stop",
7686
- reason: "max_rounds",
7687
- round: active.round,
7688
- details: displayDetails,
7689
- elapsedMs: round.elapsedMs
7690
- }
7691
- },
7692
- { kind: "advance" }
7693
- ]
7694
- };
7695
- }
7696
- const failCard = {
7697
- kind: "send_card",
7698
- card: {
7699
- kind: "fail",
7700
- round: active.round,
7701
- details: displayDetails,
7702
- advisor: null,
7703
- elapsedMs: Math.max(0, now - state.roundStartedAt),
7704
- totalElapsedMs
7705
- }
7706
- };
7707
- const pending = { round: active.round, reviewers: settled, details: feedbackDetails };
7708
- if (consecutiveFailures >= limits.advisorAfterFailures)
7709
- return {
7710
- state: { ...base, phase: "needs_fix", pending, consecutiveFailures },
7711
- effects: [failCard, { kind: "advance" }]
7712
- };
7713
- return {
7714
- state: {
7715
- ...base,
7716
- phase: "awaiting_fix",
7717
- pending: null,
7718
- repair: { details: feedbackDetails, advisor: null, status: "pending" },
7719
- consecutiveFailures,
7720
- history: [
7721
- ...state.history,
7722
- roundRecord(active.round, "failed", archiveDetails, settled, undefined, state.roundStartedAt, now)
7723
- ]
7724
- },
7725
- effects: [failCard, { kind: "advance" }]
7726
- };
7727
- }
7728
- function onAdvisorSettled(state, event, limits, now) {
7729
- if (state.phase !== "needs_fix" || !state.pending)
7730
- return { state, effects: [] };
7731
- const pending = state.pending;
7732
- const advisor = event.result;
7733
- if (advisor.verdict === "stop") {
7734
- const round = roundRecord(pending.round, "stopped", advisor.advice, pending.reviewers, advisor, state.roundStartedAt, now);
7735
- return {
7736
- state: { ...state, ...summarizing("advisor_stop"), pending: null, history: [...state.history, round], updatedAt: now },
7737
- effects: [
7738
- {
7739
- kind: "send_card",
7740
- card: {
7741
- kind: "stop",
7742
- reason: "advisor",
7743
- round: pending.round,
7744
- details: advisor.advice,
7745
- advisor,
7746
- advisorModel: limits.advisorModel,
7747
- elapsedMs: Math.max(0, now - state.updatedAt),
7748
- totalElapsedMs: Math.max(0, now - state.startedAt)
7749
- }
7750
- },
7751
- { kind: "advance" }
7752
- ]
7753
- };
7754
- }
7755
- const round = roundRecord(pending.round, "failed", pending.details, pending.reviewers, advisor, state.roundStartedAt, now);
7756
- return {
7757
- state: {
7758
- ...state,
7759
- phase: "awaiting_fix",
7760
- pending: null,
7761
- repair: { details: pending.details, advisor, status: "pending" },
7762
- history: [...state.history, round],
7763
- updatedAt: now
7764
- },
7765
- effects: [
7766
- {
7767
- kind: "send_card",
7768
- card: {
7769
- kind: "advisor",
7770
- advisor,
7771
- advisorModel: limits.advisorModel,
7772
- elapsedMs: Math.max(0, now - state.updatedAt)
7773
- }
7774
- },
7775
- { kind: "advance" }
7776
- ]
7777
- };
7778
- }
7779
- function onAdvisorSkipped(state, now) {
7780
- if (state.phase !== "needs_fix" || !state.pending)
7781
- return { state, effects: [] };
7782
- const pending = state.pending;
7783
- const round = roundRecord(pending.round, "failed", pending.details, pending.reviewers, undefined, state.roundStartedAt, now);
7784
- return {
7785
- state: {
7786
- ...state,
7787
- phase: "awaiting_fix",
7788
- pending: null,
7789
- repair: { details: pending.details, advisor: null, status: "pending" },
7790
- history: [...state.history, round],
7791
- updatedAt: now
7792
- },
7793
- effects: [{ kind: "advance" }]
7794
- };
7795
- }
7796
- function onCancel(state, reason, now) {
7797
- if (state.phase === "idle" || state.phase === "settled")
7798
- return { state, effects: [] };
7799
- if (state.phase === "summarizing")
7800
- return onSummarySettled(state, now);
7801
- if (state.phase === "queued")
7802
- return {
7803
- state: { ...state, phase: "settled", updatedAt: now },
7804
- effects: [{ kind: "send_card", card: { kind: "cancel", round: 0, reason } }]
7805
- };
7806
- const round = resolveRoundRecord(state, "cancelled", reason, now);
7807
- return {
7808
- state: {
7809
- ...state,
7810
- phase: "settled",
7811
- active: null,
7812
- pending: null,
7813
- repair: null,
7814
- history: round ? [...state.history, round] : state.history,
7815
- updatedAt: now
7816
- },
7817
- effects: [{ kind: "send_card", card: { kind: "cancel", round: state.round, reason } }]
7818
- };
7819
- }
7820
- function onInfrastructureError(state, details, now) {
7821
- if (state.phase === "idle" || state.phase === "settled")
7822
- return { state, effects: [] };
7823
- if (state.phase === "summarizing")
7824
- return onSummarySettled(state, now);
7825
- const message = details.trim() || "review infrastructure unavailable";
7826
- const reviewers = state.active?.reviewers.flatMap((item) => item.result ? [item.result] : []) ?? state.pending?.reviewers ?? [];
7827
- const round = state.round > 0 ? roundRecord(state.round, "error", message, reviewers, undefined, state.roundStartedAt, now) : undefined;
7828
- return {
7829
- state: {
7830
- ...state,
7831
- phase: "settled",
7832
- active: null,
7833
- pending: null,
7834
- repair: null,
7835
- history: round ? [...state.history, round] : state.history,
7836
- updatedAt: now
7837
- },
7838
- effects: [{ kind: "send_card", card: { kind: "error", message } }]
7839
- };
7840
- }
7841
- function onTimeout(state, now) {
7842
- if (state.phase === "idle" || state.phase === "settled")
7843
- return { state, effects: [] };
7844
- if (state.phase === "summarizing")
7845
- return onSummarySettled(state, now);
7846
- if (state.phase === "queued")
7847
- return {
7848
- state: { ...state, phase: "settled", updatedAt: now },
7849
- effects: [{ kind: "send_card", card: { kind: "timeout", round: 0, reason: "timeout" } }]
7850
- };
7851
- const round = resolveRoundRecord(state, "timed_out", "timeout", now);
7852
- return {
7853
- state: {
7854
- ...state,
7855
- phase: "settled",
7856
- active: null,
7857
- pending: null,
7858
- repair: null,
7859
- history: round ? [...state.history, round] : state.history,
7860
- updatedAt: now
7861
- },
7862
- effects: [
7863
- { kind: "send_card", card: { kind: "timeout", round: state.round, reason: "timeout" } }
7864
- ]
7865
- };
7866
- }
7867
- function beginRound(state, focus, round, limits, now) {
7868
- const reviewers = limits.reviewers.map((item, index) => ({
7869
- index,
7870
- model: item.model,
7871
- thinking: item.thinking,
7872
- status: "running",
7873
- result: null
7874
- }));
7875
- return {
7876
- ...state,
7877
- phase: "reviewing",
7878
- round,
7879
- focus,
7880
- active: { round, reviewers, settledCount: 0 },
7881
- pending: null,
7882
- repair: null,
7883
- summary: null,
7884
- roundStartedAt: now,
7885
- updatedAt: now
7886
- };
7887
- }
7888
- function aggregate(reviewers) {
7889
- const verdicts = reviewers.filter((item) => item.status === "passed" || item.status === "failed");
7890
- const absent = reviewers.filter((item) => item.status === "error");
7891
- const archiveDetails = aggregateDetails(reviewers);
7892
- if (verdicts.length === 0)
7893
- return { result: "error", displayDetails: archiveDetails, feedbackDetails: "", archiveDetails, summary: "" };
7894
- const failed = verdicts.filter((item) => item.status === "failed");
7895
- if (failed.length > 0)
7896
- return {
7897
- result: "failed",
7898
- displayDetails: archiveDetails,
7899
- feedbackDetails: aggregateDetails(failed),
7900
- archiveDetails,
7901
- summary: ""
7902
- };
7903
- return {
7904
- result: "passed",
7905
- displayDetails: archiveDetails,
7906
- feedbackDetails: "",
7907
- archiveDetails,
7908
- summary: aggregatePassSummary(verdicts, absent)
7909
- };
7910
- }
7911
- function aggregateDetails(reviewers) {
7912
- return reviewers.map((item) => `${modelLabel(item)}
7913
- ${item.details.trim()}`).join(`
7914
-
7915
- `);
7916
- }
7917
- function aggregatePassSummary(passed, absent) {
7918
- const fallback = msg6.summary.passedFallback;
7919
- const lines = passed.length === 1 ? [passBody(passed[0]?.summary ?? "") || fallback] : passed.map((item) => msg6.summary.modelBullet(shortModel2(item.model), passBody(item.summary) || fallback));
7920
- if (absent.length > 0)
7921
- lines.push(msg6.summary.absent(absent.map((item) => ({ model: shortModel2(item.model), reason: firstLine(item.details) }))));
7922
- const suggestions = [...new Set(passed.flatMap((item) => splitSuggestions(item.details).suggestions))];
7923
- if (suggestions.length === 0)
7924
- return lines.join(`
7925
- `);
7926
- return [
7927
- ...lines,
7928
- "",
7929
- `## ${msg6.terms.suggestions}`,
7930
- ...suggestions.map((item) => `- ${item}`)
7931
- ].join(`
7932
- `);
7933
- }
7934
- function firstLine(text) {
7935
- return text.trim().split(/\r?\n/u, 1)[0]?.split(/[::]/u, 1)[0]?.trim() ?? "";
7936
- }
7937
- function splitSuggestions(summary) {
7938
- const lines = summary.split(/\r?\n/u);
7939
- const index = lines.findIndex((line) => SUGGESTIONS_HEADING2.test(line.trim()));
7940
- if (index < 0)
7941
- return { body: summary, suggestions: [] };
7942
- return {
7943
- body: lines.slice(0, index).join(`
7944
- `),
7945
- suggestions: lines.slice(index + 1).map((line) => line.replace(/^[-*]\s*/u, "").trim()).filter(Boolean)
7946
- };
7947
- }
7948
- function passBody(summary) {
7949
- return summary.replace(/^(?:PASS\s*)/iu, "").trim();
7950
- }
7951
- function shortModel2(model) {
7952
- return model.split("/").at(-1) || model;
7953
- }
7954
- function appendClosedFindings(summary, history, beforeRound) {
7955
- const seen = new Set;
7956
- const findings = [];
7957
- for (const round of history) {
7958
- if (round.round >= beforeRound || round.result !== "failed")
7959
- continue;
7960
- for (const line of round.details.split(/\r?\n/u)) {
7961
- const match = ISSUE_LINE.exec(line.trim());
7962
- const issue = match?.[1]?.replace(/`([^`]+)`/gu, "$1").trim();
7963
- if (!issue)
7964
- continue;
7965
- const key = issue.replace(/\s+/gu, "");
7966
- if (seen.has(key))
7967
- continue;
7968
- seen.add(key);
7969
- findings.push({ round: round.round, issue });
7970
- }
7971
- }
7972
- if (findings.length === 0)
7973
- return summary;
7974
- const shown = findings.slice(0, 6);
7975
- const recap = [msg6.summary.closedHeading, ...shown.map((finding) => msg6.summary.closedItem(finding.issue, finding.round))];
7976
- if (findings.length > shown.length)
7977
- recap.push(msg6.summary.closedMore(findings.length - shown.length));
7978
- return `${summary}
7979
-
7980
- ${recap.join(`
7981
- `)}`;
7982
- }
7983
- function modelLabel(item) {
7984
- return msg6.summary.modelSection(item.index + 1, shortModel2(item.model));
7622
+ return [...event.messages].reverse().find((message) => message.role === "assistant");
7985
7623
  }
7986
- function resolveRoundRecord(state, result, reason, now) {
7987
- if (state.phase === "reviewing" && state.active) {
7988
- const settled = state.active.reviewers.flatMap((item) => item.result ? [item.result] : []);
7989
- return roundRecord(state.active.round, result, "", settled, undefined, state.roundStartedAt, now, reason);
7990
- }
7991
- if (state.phase === "needs_fix" && state.pending)
7992
- return roundRecord(state.pending.round, result, "", state.pending.reviewers, undefined, state.roundStartedAt, now, reason);
7993
- return;
7624
+ function clean2(text) {
7625
+ return text.replaceAll("\x00", "");
7994
7626
  }
7995
- function roundRecord(round, result, details, reviewers, advisor, roundStartedAt, now, reason) {
7996
- return {
7997
- round,
7998
- result,
7999
- details,
8000
- reviewers,
8001
- ...advisor ? { advisor } : {},
8002
- ...reason ? { reason } : {},
8003
- elapsedMs: roundStartedAt ? Math.max(0, now - roundStartedAt) : 0
8004
- };
7627
+ function errorText(error) {
7628
+ return error instanceof Error ? error.message : String(error);
8005
7629
  }
8006
7630
 
8007
7631
  // review/index.ts
8008
7632
  var FEEDBACK_TYPE = "firecode-review-feedback";
8009
7633
  var SUMMARY_REQUEST_TYPE = "firecode-review-summary";
8010
- var FEEDBACK_START_TIMEOUT_MS = 2000;
7634
+ var FOLLOW_UP_START_TIMEOUT_MS = 2000;
8011
7635
  var PROMPTS = new URL("./prompts/", new URL("./review/index.js", import.meta.url).href);
8012
7636
  function overallTimeoutMs(config) {
8013
7637
  return Math.max(30 * 60000, config.maxRounds * config.timeoutMinutes * 2 * 60000);
8014
7638
  }
8015
7639
  function isActive(state) {
8016
- return state.phase === "queued" || state.phase === "reviewing" || state.phase === "needs_fix" || state.phase === "awaiting_fix" || state.phase === "summarizing";
7640
+ return state.phase !== "idle" && state.phase !== "settled";
8017
7641
  }
8018
7642
  function registerReview(pi, enabled = true, configBroken = false, dependencies = {}) {
8019
7643
  registerCardRenderer(pi);
@@ -8048,23 +7672,23 @@ function registerReview(pi, enabled = true, configBroken = false, dependencies =
8048
7672
  pi.on("session_shutdown", (event, ctx) => handleShutdown(rt, event.reason, ctx));
8049
7673
  return handle;
8050
7674
  }
7675
+ function sealCheckpoint(pi, state) {
7676
+ beginCheckpoint(pi, {
7677
+ ...state,
7678
+ phase: "settled",
7679
+ active: null,
7680
+ pending: null,
7681
+ repair: null,
7682
+ summary: null,
7683
+ updatedAt: Date.now()
7684
+ });
7685
+ }
8051
7686
  function settleDisabledCheckpoint(pi, ctx) {
8052
7687
  const checkpoint = readCheckpoint(ctx);
8053
7688
  if (!checkpoint || !isActive(checkpoint))
8054
7689
  return;
8055
- settleUnavailableCheckpoint(pi, ctx, checkpoint);
8056
- }
8057
- function settleUnavailableCheckpoint(pi, ctx, checkpoint) {
8058
7690
  try {
8059
- beginCheckpoint(pi, ctx, {
8060
- ...checkpoint,
8061
- phase: "settled",
8062
- active: null,
8063
- pending: null,
8064
- repair: null,
8065
- summary: null,
8066
- updatedAt: Date.now()
8067
- });
7691
+ sealCheckpoint(pi, checkpoint);
8068
7692
  } catch (error) {
8069
7693
  if (ctx.hasUI)
8070
7694
  ctx.ui.notify(msg6.notify.cannotSealOld(errorText2(error)), "error");
@@ -8081,23 +7705,22 @@ function dispatch(rt, event) {
8081
7705
  if (!persist(rt, state))
8082
7706
  return;
8083
7707
  if (!isActive(state))
8084
- clearWatchdog(rt);
7708
+ clearWatchdog(active);
8085
7709
  syncOccupancy(rt, active);
8086
- syncUi(rt);
7710
+ syncUi(rt, active);
8087
7711
  }
8088
7712
  await runEffects(rt, effects);
8089
7713
  });
8090
- rt.queue = run.catch((error) => {
8091
- notifyEffectFailure(rt, error);
8092
- });
7714
+ rt.queue = run.catch((error) => notifyEffectFailure(rt, error));
8093
7715
  return rt.queue;
8094
7716
  }
7717
+ function notify(active, message, level) {
7718
+ if (active.ctx.hasUI)
7719
+ active.ctx.ui.notify(message, level);
7720
+ }
8095
7721
  function notifyEffectFailure(rt, error) {
8096
- const active = rt.controller;
8097
- if (!active?.ctx.hasUI)
8098
- return;
8099
- const message = error instanceof Error ? error.message : String(error);
8100
- active.ctx.ui.notify(msg6.notify.stepFailed(message), "warning");
7722
+ if (rt.controller)
7723
+ notify(rt.controller, msg6.notify.stepFailed(errorText2(error)), "warning");
8101
7724
  }
8102
7725
  function loadReviewConfig() {
8103
7726
  return loadConfig().review;
@@ -8113,31 +7736,30 @@ function limitsOf(config) {
8113
7736
  }))
8114
7737
  };
8115
7738
  }
7739
+ function newController(ctx, config, state, stamp) {
7740
+ return {
7741
+ ctx,
7742
+ config,
7743
+ state,
7744
+ signal: new AbortController,
7745
+ watchdog: undefined,
7746
+ persistedStamp: stamp,
7747
+ pendingCards: [],
7748
+ ui: new ReviewUi
7749
+ };
7750
+ }
8116
7751
  async function handleCommand(rt, args, ctx) {
8117
7752
  const loaded = loadReviewConfig();
8118
7753
  if ("error" in loaded)
8119
7754
  return refuse(rt, ctx, loaded.error, "error");
8120
- const config = loaded.config;
8121
7755
  if (rt.controller && isActive(rt.controller.state))
8122
7756
  return refuse(rt, ctx, msg6.command.alreadyRunning, "info");
8123
- const command = parseCommand(args);
8124
- if ("error" in command)
8125
- return refuse(rt, ctx, command.error, "error");
8126
- rt.controller = {
8127
- ctx,
8128
- config,
8129
- state: initialState(randomUUID()),
8130
- signal: new AbortController,
8131
- watchdog: undefined,
8132
- persistedStamp: null,
8133
- pendingCards: []
8134
- };
7757
+ const input = args.trim();
7758
+ if (input.startsWith("--"))
7759
+ return refuse(rt, ctx, msg6.command.invalidArgs, "error");
7760
+ rt.controller = newController(ctx, loaded.config, initialState(randomUUID()), null);
8135
7761
  armWatchdog(rt);
8136
- dispatch(rt, {
8137
- type: "START",
8138
- focus: command.focus,
8139
- busy: true
8140
- });
7762
+ dispatch(rt, { type: "START", focus: input });
8141
7763
  }
8142
7764
  function refuse(rt, ctx, message, level) {
8143
7765
  recordRefusal(rt.pi, message);
@@ -8149,35 +7771,27 @@ function handleSessionStart(rt, ctx) {
8149
7771
  if (!checkpoint || !isActive(checkpoint))
8150
7772
  return;
8151
7773
  const loaded = loadReviewConfig();
8152
- if ("error" in loaded) {
7774
+ if ("error" in loaded)
8153
7775
  return;
8154
- }
8155
- const config = loaded.config;
8156
- rt.controller = {
8157
- ctx,
8158
- config,
8159
- state: checkpoint,
8160
- signal: new AbortController,
8161
- watchdog: undefined,
8162
- persistedStamp: readStamp(ctx),
8163
- pendingCards: []
8164
- };
7776
+ const active = newController(ctx, loaded.config, checkpoint, readStamp(ctx));
7777
+ rt.controller = active;
8165
7778
  armWatchdog(rt);
8166
- syncOccupancy(rt, rt.controller);
8167
- syncUi(rt);
7779
+ syncOccupancy(rt, active);
7780
+ syncUi(rt, active);
8168
7781
  return dispatch(rt, { type: "RECOVER" });
8169
7782
  }
8170
7783
  async function handleAgentStart(rt) {
8171
7784
  const active = rt.controller;
8172
7785
  if (!active)
8173
7786
  return;
8174
- if (active.state.phase === "awaiting_fix" && active.state.repair?.status === "awaiting_start") {
8175
- clearFeedbackStartTimer(active);
7787
+ const { state } = active;
7788
+ if (state.phase === "awaiting_fix" && state.repair?.status === "awaiting_start") {
7789
+ clearStartTimer(active);
8176
7790
  await dispatch(rt, { type: "REPAIR_STARTED" });
8177
7791
  return;
8178
7792
  }
8179
- if (active.state.phase === "summarizing" && active.state.summary?.status === "awaiting_start") {
8180
- clearFeedbackStartTimer(active);
7793
+ if (state.phase === "summarizing" && state.summary?.status === "awaiting_start") {
7794
+ clearStartTimer(active);
8181
7795
  await dispatch(rt, { type: "SUMMARY_STARTED" });
8182
7796
  return;
8183
7797
  }
@@ -8198,8 +7812,8 @@ function handleAgentEnd(rt, event) {
8198
7812
  return dispatch(rt, { type: "SUMMARY_SETTLED" });
8199
7813
  if (active.state.phase !== "awaiting_fix" || active.state.repair?.status !== "running")
8200
7814
  return;
8201
- const assistant = [...event.messages].reverse().find((message) => isRecord9(message) && message.role === "assistant");
8202
- if (isRecord9(assistant) && assistant.stopReason !== "error" && assistant.stopReason !== "aborted")
7815
+ const assistant = [...event.messages].reverse().find((message) => message.role === "assistant");
7816
+ if (assistant && assistant.stopReason !== "error" && assistant.stopReason !== "aborted")
8203
7817
  return dispatch(rt, { type: "REPAIR_COMPLETED" });
8204
7818
  active.signal.abort();
8205
7819
  return dispatch(rt, { type: "CANCEL", reason: "user" });
@@ -8208,8 +7822,7 @@ function requestAdvance(rt, ctx) {
8208
7822
  const active = rt.controller;
8209
7823
  if (!active)
8210
7824
  return;
8211
- const runId = active.state.runId;
8212
- advanceWhenIdle(rt, ctx, runId).catch((error) => {
7825
+ advanceWhenIdle(rt, ctx, active.state.runId).catch((error) => {
8213
7826
  notifyEffectFailure(rt, error);
8214
7827
  if (rt.controller !== active)
8215
7828
  return;
@@ -8224,51 +7837,41 @@ async function advanceWhenIdle(rt, ctx, runId) {
8224
7837
  active.ctx = ctx;
8225
7838
  flushPendingCards(rt);
8226
7839
  const { state } = active;
8227
- if (state.phase === "queued") {
8228
- await dispatch(rt, { type: "ADVANCE" });
8229
- return;
8230
- }
8231
- if (state.phase === "reviewing") {
8232
- startAction(rt, active, `review:${state.round}`, "reviewer", () => startReviewers(rt));
8233
- return;
8234
- }
8235
- if (state.phase === "needs_fix") {
8236
- startAction(rt, active, `advisor:${state.round}`, "advisor", () => consultAdvisor(rt));
8237
- return;
8238
- }
8239
- if (state.phase === "summarizing") {
8240
- if (state.summary?.status !== "pending")
7840
+ switch (state.phase) {
7841
+ case "queued":
7842
+ return dispatch(rt, { type: "ADVANCE" });
7843
+ case "reviewing":
7844
+ return startAction(rt, active, `review:${state.round}`, "reviewer", (signal) => startReviewers(rt, active, signal));
7845
+ case "needs_fix":
7846
+ return startAction(rt, active, `advisor:${state.round}`, "advisor", (signal) => consultAdvisor(rt, active, signal));
7847
+ case "summarizing":
7848
+ if (state.summary?.status !== "pending")
7849
+ return;
7850
+ await dispatch(rt, { type: "SUMMARY_DISPATCHED" });
7851
+ if (rt.controller === active && active.state.phase === "summarizing" && active.state.summary?.status === "awaiting_start")
7852
+ deliverSummary(rt, active, active.state);
7853
+ return;
7854
+ case "awaiting_fix":
7855
+ if (state.repair?.status === "completed")
7856
+ return dispatch(rt, { type: "ADVANCE" });
7857
+ if (state.repair?.status !== "pending")
7858
+ return;
7859
+ await dispatch(rt, { type: "FEEDBACK_DISPATCHED" });
7860
+ if (rt.controller === active && active.state.repair?.status === "awaiting_start")
7861
+ deliverFeedback(rt, active, active.state.repair);
8241
7862
  return;
8242
- await dispatch(rt, { type: "SUMMARY_DISPATCHED" });
8243
- const current = rt.controller?.state;
8244
- if (rt.controller === active && current?.phase === "summarizing" && current.summary?.status === "awaiting_start")
8245
- deliverSummaryNow(rt, current);
8246
- return;
8247
- }
8248
- if (state.phase !== "awaiting_fix" || !state.repair)
8249
- return;
8250
- if (state.repair.status === "pending") {
8251
- await dispatch(rt, { type: "FEEDBACK_DISPATCHED" });
8252
- const repair = rt.controller?.state.repair;
8253
- if (rt.controller === active && repair?.status === "awaiting_start")
8254
- deliverFeedbackNow(rt, repair.details, repair.advisor);
8255
- return;
8256
7863
  }
8257
- if (state.repair.status === "completed")
8258
- await dispatch(rt, { type: "ADVANCE" });
8259
7864
  }
8260
7865
  function startAction(rt, active, key, kind, run) {
8261
7866
  if (active.runningAction === key)
8262
7867
  return;
8263
7868
  active.runningAction = key;
8264
- active.actionController = new AbortController;
8265
- const action = run().catch(async (error) => {
8266
- if (rt.controller !== active || active.actionController?.signal.aborted)
7869
+ const controller = new AbortController;
7870
+ active.actionController = controller;
7871
+ const action = run(controller.signal).catch(async (error) => {
7872
+ if (rt.controller !== active || controller.signal.aborted)
8267
7873
  return;
8268
- await dispatch(rt, {
8269
- type: "INFRASTRUCTURE_ERROR",
8270
- details: sessionErrorText(kind, error)
8271
- });
7874
+ await dispatch(rt, { type: "INFRASTRUCTURE_ERROR", details: sessionErrorText(kind, error) });
8272
7875
  }).finally(() => {
8273
7876
  if (active.actionPromise !== action)
8274
7877
  return;
@@ -8285,15 +7888,14 @@ async function handleShutdown(rt, reason, ctx) {
8285
7888
  setOccupancy(rt, active, false);
8286
7889
  active.signal.abort();
8287
7890
  active.actionController?.abort();
8288
- clearWatchdog(rt);
8289
- clearFeedbackStartTimer(active);
7891
+ clearWatchdog(active);
7892
+ clearStartTimer(active);
8290
7893
  await active.actionPromise;
8291
- if (active.ctx !== ctx)
8292
- active.ctx = ctx;
7894
+ active.ctx = ctx;
8293
7895
  if (reason === "quit")
8294
7896
  await dispatch(rt, { type: "CANCEL", reason: "shutdown" });
8295
7897
  else {
8296
- clearUi(active);
7898
+ active.ui.clear(active.ctx);
8297
7899
  await rt.queue;
8298
7900
  }
8299
7901
  if (rt.controller === active)
@@ -8303,7 +7905,7 @@ function armWatchdog(rt) {
8303
7905
  const active = rt.controller;
8304
7906
  if (!active)
8305
7907
  return;
8306
- clearWatchdog(rt);
7908
+ clearWatchdog(active);
8307
7909
  const elapsed = active.state.startedAt ? Math.max(0, Date.now() - active.state.startedAt) : 0;
8308
7910
  const remaining = Math.max(0, overallTimeoutMs(active.config) - elapsed);
8309
7911
  if (remaining === 0) {
@@ -8320,14 +7922,14 @@ function armWatchdog(rt) {
8320
7922
  }, remaining);
8321
7923
  active.watchdog.unref?.();
8322
7924
  }
8323
- function clearWatchdog(rt) {
8324
- if (rt.controller?.watchdog)
8325
- clearTimeout(rt.controller.watchdog);
7925
+ function clearWatchdog(active) {
7926
+ if (active.watchdog)
7927
+ clearTimeout(active.watchdog);
8326
7928
  }
8327
- function clearFeedbackStartTimer(active) {
8328
- if (active.feedbackStartTimer)
8329
- clearTimeout(active.feedbackStartTimer);
8330
- active.feedbackStartTimer = undefined;
7929
+ function clearStartTimer(active) {
7930
+ if (active.startTimer)
7931
+ clearTimeout(active.startTimer);
7932
+ active.startTimer = undefined;
8331
7933
  }
8332
7934
  function persist(rt, state) {
8333
7935
  const active = rt.controller;
@@ -8337,45 +7939,37 @@ function persist(rt, state) {
8337
7939
  if (persisted === undefined)
8338
7940
  return false;
8339
7941
  try {
8340
- active.persistedStamp = persisted === null ? beginCheckpoint(rt.pi, active.ctx, state) : writeCheckpoint(rt.pi, active.ctx, state, persisted);
7942
+ active.persistedStamp = persisted === null ? beginCheckpoint(rt.pi, state) : writeCheckpoint(rt.pi, active.ctx, state, persisted);
8341
7943
  return true;
8342
7944
  } catch (error) {
8343
- const halt = (message, level) => {
8344
- setOccupancy(rt, active, false);
8345
- active.persistedStamp = undefined;
8346
- active.signal.abort();
8347
- active.actionController?.abort();
8348
- if (active.ctx.hasUI)
8349
- active.ctx.ui.notify(message, level);
8350
- };
8351
- if (error instanceof CheckpointConflictError) {
8352
- halt(msg6.notify.checkpointConflict, "warning");
8353
- dispatch(rt, { type: "CANCEL", reason: "shutdown" });
8354
- return false;
8355
- }
8356
- halt(msg6.notify.checkpointWriteFailed(errorText2(error)), "error");
8357
- clearUi(active);
8358
- let sealed = true;
8359
- try {
8360
- beginCheckpoint(rt.pi, active.ctx, {
8361
- ...state,
8362
- phase: "settled",
8363
- active: null,
8364
- pending: null,
8365
- repair: null,
8366
- summary: null
8367
- });
8368
- } catch {
8369
- sealed = false;
8370
- }
8371
- clearWatchdog(rt);
8372
- clearFeedbackStartTimer(active);
8373
- rt.controller = undefined;
8374
- if (!sealed && active.ctx.hasUI)
8375
- active.ctx.ui.notify(msg6.notify.cannotSeal, "warning");
7945
+ haltOnPersistFailure(rt, active, state, error);
8376
7946
  return false;
8377
7947
  }
8378
7948
  }
7949
+ function haltOnPersistFailure(rt, active, state, error) {
7950
+ setOccupancy(rt, active, false);
7951
+ active.persistedStamp = undefined;
7952
+ active.signal.abort();
7953
+ active.actionController?.abort();
7954
+ if (error instanceof CheckpointConflictError) {
7955
+ notify(active, msg6.notify.checkpointConflict, "warning");
7956
+ dispatch(rt, { type: "CANCEL", reason: "shutdown" });
7957
+ return;
7958
+ }
7959
+ notify(active, msg6.notify.checkpointWriteFailed(errorText2(error)), "error");
7960
+ active.ui.clear(active.ctx);
7961
+ let sealed = true;
7962
+ try {
7963
+ sealCheckpoint(rt.pi, state);
7964
+ } catch {
7965
+ sealed = false;
7966
+ }
7967
+ clearWatchdog(active);
7968
+ clearStartTimer(active);
7969
+ rt.controller = undefined;
7970
+ if (!sealed)
7971
+ notify(active, msg6.notify.cannotSeal, "warning");
7972
+ }
8379
7973
  function errorText2(error) {
8380
7974
  return error instanceof Error ? error.message : String(error);
8381
7975
  }
@@ -8391,29 +7985,16 @@ function setOccupancy(rt, active, held) {
8391
7985
  rt.pi.events.emit(OCCUPANCY_CHANNEL, payload);
8392
7986
  } catch (error) {
8393
7987
  try {
8394
- if (active.ctx.hasUI)
8395
- active.ctx.ui.notify(msg6.notify.occupancyFailed(errorText2(error)), "warning");
7988
+ notify(active, msg6.notify.occupancyFailed(errorText2(error)), "warning");
8396
7989
  } catch {}
8397
7990
  }
8398
7991
  }
8399
- function syncUi(rt) {
8400
- const active = rt.controller;
8401
- if (!active)
8402
- return;
7992
+ function syncUi(rt, active) {
8403
7993
  if (!isActive(active.state) || !active.ctx.hasUI)
8404
- return clearUi(active);
8405
- showReviewTitle(active.ctx, active.state.round);
8406
- if (!canCancelWithKey(rt))
8407
- return releaseEditor(active);
8408
- active.unlockEditor ??= lockEditor(active.ctx, () => cancelByUser(rt));
8409
- }
8410
- function clearUi(active) {
8411
- hideReviewTitle(active.ctx);
8412
- releaseEditor(active);
8413
- }
8414
- function releaseEditor(active) {
8415
- active.unlockEditor?.();
8416
- active.unlockEditor = undefined;
7994
+ return active.ui.clear(active.ctx);
7995
+ const { phase, round } = active.state;
7996
+ const canCancel = phase === "queued" || phase === "reviewing" || phase === "needs_fix";
7997
+ active.ui.show(active.ctx, round, canCancel, () => cancelByUser(rt));
8417
7998
  }
8418
7999
  var STAGE = {
8419
8000
  queued: "queued",
@@ -8424,17 +8005,13 @@ var STAGE = {
8424
8005
  };
8425
8006
  function reviewProgress(rt) {
8426
8007
  const state = rt.controller?.state;
8427
- const stage = state && isActive(state) ? STAGE[state.phase] : undefined;
8008
+ const stage = state && STAGE[state.phase];
8428
8009
  if (!state || !stage)
8429
8010
  return;
8430
8011
  const reviewers = stage === "reviewing" ? state.active?.reviewers ?? [] : [];
8431
8012
  const count = (status) => reviewers.filter((reviewer) => reviewer.status === status).length;
8432
8013
  return { stage, round: state.round, passed: count("passed"), blocked: count("failed"), total: reviewers.length };
8433
8014
  }
8434
- function canCancelWithKey(rt) {
8435
- const phase = rt.controller?.state.phase;
8436
- return phase === "queued" || phase === "reviewing" || phase === "needs_fix";
8437
- }
8438
8015
  function cancelByUser(rt) {
8439
8016
  const active = rt.controller;
8440
8017
  if (!active)
@@ -8453,35 +8030,31 @@ async function runEffects(rt, effects) {
8453
8030
  const active = rt.controller;
8454
8031
  if (!active)
8455
8032
  return;
8456
- if (effect.kind === "advance") {
8033
+ if (effect.kind === "advance")
8457
8034
  requestAdvance(rt, active.ctx);
8458
- continue;
8459
- }
8460
- try {
8461
- sendCard(rt, effect.card);
8462
- } catch (error) {
8463
- notifyEffectFailure(rt, error);
8464
- }
8035
+ else if (effect.kind === "notify_cancelled")
8036
+ notify(active, msg6.notify.cancelled, "info");
8037
+ else
8038
+ try {
8039
+ sendCard(rt, active, effect.card);
8040
+ } catch (error) {
8041
+ notifyEffectFailure(rt, error);
8042
+ }
8465
8043
  }
8466
8044
  }
8467
- function reviewerModelConfig(model, config) {
8045
+ function modelConfig(atom, config) {
8468
8046
  return {
8469
- model: model.model,
8470
- thinking: model.thinking,
8047
+ model: atom.model,
8048
+ thinking: atom.thinking,
8471
8049
  tools: config.tools,
8472
8050
  timeoutMs: config.timeoutMinutes * 60000
8473
8051
  };
8474
8052
  }
8475
- async function startReviewers(rt) {
8476
- const active = rt.controller;
8477
- if (!active)
8478
- return;
8053
+ async function startReviewers(rt, active, signal) {
8479
8054
  const { state, config } = active;
8480
8055
  if (!state.active)
8481
8056
  return;
8482
- const currentActive = state.active;
8483
- const actionSignal = active.actionController?.signal ?? active.signal.signal;
8484
- const evidence = buildEvidence(sessionEntries(rt), { sessionFile: active.ctx.sessionManager.getSessionFile() });
8057
+ const evidence = buildEvidence(sessionEntries(active), { sessionFile: active.ctx.sessionManager.getSessionFile() });
8485
8058
  const prompt = buildReviewPrompt(readPrompt(PROMPTS, "review"), {
8486
8059
  scope: msg6.command.scope,
8487
8060
  focus: state.focus,
@@ -8489,20 +8062,20 @@ async function startReviewers(rt) {
8489
8062
  history: state.history,
8490
8063
  round: state.round
8491
8064
  });
8492
- const tasks = currentActive.reviewers.filter((reviewer) => reviewer.status === "running").map(async (reviewer) => {
8065
+ const tasks = state.active.reviewers.filter((reviewer) => reviewer.status === "running").map(async (reviewer) => {
8493
8066
  try {
8494
8067
  const result = await runReviewer({
8495
8068
  index: reviewer.index,
8496
- config: reviewerModelConfig(reviewer, config),
8069
+ config: modelConfig(reviewer, config),
8497
8070
  prompt,
8498
8071
  cwd: active.ctx.cwd,
8499
- signal: actionSignal,
8072
+ signal,
8500
8073
  runSession: rt.runSession
8501
8074
  });
8502
- if (!actionSignal.aborted)
8075
+ if (!signal.aborted)
8503
8076
  await dispatch(rt, { type: "REVIEWER_SETTLED", index: result.index, result });
8504
8077
  } catch (error) {
8505
- if (actionSignal.aborted)
8078
+ if (signal.aborted)
8506
8079
  return;
8507
8080
  await dispatch(rt, {
8508
8081
  type: "REVIEWER_SETTLED",
@@ -8520,128 +8093,103 @@ async function startReviewers(rt) {
8520
8093
  });
8521
8094
  await Promise.all(tasks);
8522
8095
  }
8523
- async function consultAdvisor(rt) {
8524
- const active = rt.controller;
8525
- if (!active)
8526
- return;
8096
+ async function consultAdvisor(rt, active, signal) {
8527
8097
  const { state, config } = active;
8528
8098
  if (!state.pending)
8529
8099
  return;
8530
- const pending = state.pending;
8531
- const actionSignal = active.actionController?.signal ?? active.signal.signal;
8532
8100
  const prompt = buildAdvisorPrompt(readPrompt(PROMPTS, "advisor"), {
8533
8101
  focus: state.focus,
8534
- details: pending.details,
8102
+ details: state.pending.details,
8535
8103
  history: state.history,
8536
- round: pending.round
8104
+ round: state.pending.round
8537
8105
  });
8538
8106
  try {
8539
8107
  const result = await runAdvisor({
8540
- config: reviewerModelConfig(config.advisor, config),
8108
+ config: modelConfig(config.advisor, config),
8541
8109
  prompt,
8542
8110
  cwd: active.ctx.cwd,
8543
- signal: actionSignal,
8111
+ signal,
8544
8112
  runSession: rt.runSession
8545
8113
  });
8546
- if (!actionSignal.aborted)
8114
+ if (!signal.aborted)
8547
8115
  await dispatch(rt, { type: "ADVISOR_SETTLED", result });
8548
8116
  } catch (error) {
8549
- if (actionSignal.aborted)
8117
+ if (signal.aborted)
8550
8118
  return;
8551
- await dispatch(rt, {
8552
- type: "INFRASTRUCTURE_ERROR",
8553
- details: sessionErrorText("advisor", error)
8554
- });
8119
+ await dispatch(rt, { type: "INFRASTRUCTURE_ERROR", details: sessionErrorText("advisor", error) });
8555
8120
  }
8556
8121
  }
8557
8122
  function sessionErrorText(kind, error) {
8558
8123
  const reason = errorText2(error);
8559
8124
  return kind === "reviewer" ? msg6.failure.reviewerSession(reason) : msg6.failure.advisorSession(reason);
8560
8125
  }
8561
- function deliverFeedbackNow(rt, details, advisor) {
8562
- const active = rt.controller;
8563
- if (!active)
8564
- return;
8565
- const feedback = buildFixFeedback({
8566
- details,
8567
- advisor
8568
- });
8569
- clearFeedbackStartTimer(active);
8570
- active.feedbackStartTimer = setTimeout(() => {
8571
- if (rt.controller !== active || active.state.phase !== "awaiting_fix" || active.state.repair?.status !== "awaiting_start")
8126
+ function sendFollowUp(rt, active, customType, content, handlers) {
8127
+ clearStartTimer(active);
8128
+ active.startTimer = setTimeout(() => {
8129
+ if (rt.controller !== active || !handlers.stillAwaiting(active.state))
8572
8130
  return;
8573
- active.feedbackStartTimer = undefined;
8574
- if (active.ctx.hasUI)
8575
- active.ctx.ui.notify(msg6.notify.feedbackNotStarted, "error");
8576
- active.signal.abort();
8577
- dispatch(rt, { type: "CANCEL", reason: "user" });
8578
- }, FEEDBACK_START_TIMEOUT_MS);
8579
- active.feedbackStartTimer.unref?.();
8131
+ active.startTimer = undefined;
8132
+ handlers.onNotStarted();
8133
+ }, FOLLOW_UP_START_TIMEOUT_MS);
8134
+ active.startTimer.unref?.();
8580
8135
  try {
8581
- rt.pi.sendMessage({ customType: FEEDBACK_TYPE, content: feedback, display: false }, { deliverAs: "followUp", triggerTurn: true });
8136
+ rt.pi.sendMessage({ customType, content, display: false }, { deliverAs: "followUp", triggerTurn: true });
8582
8137
  } catch (error) {
8583
- clearFeedbackStartTimer(active);
8584
- throw error;
8138
+ clearStartTimer(active);
8139
+ handlers.onSendError(error);
8585
8140
  }
8586
8141
  }
8587
- function deliverSummaryNow(rt, state) {
8588
- const active = rt.controller;
8589
- if (!active || !state.summary)
8142
+ function deliverFeedback(rt, active, repair) {
8143
+ sendFollowUp(rt, active, FEEDBACK_TYPE, buildFixFeedback(repair), {
8144
+ stillAwaiting: (state) => state.phase === "awaiting_fix" && state.repair?.status === "awaiting_start",
8145
+ onNotStarted: () => {
8146
+ notify(active, msg6.notify.feedbackNotStarted, "error");
8147
+ active.signal.abort();
8148
+ dispatch(rt, { type: "CANCEL", reason: "user" });
8149
+ },
8150
+ onSendError: (error) => {
8151
+ throw error;
8152
+ }
8153
+ });
8154
+ }
8155
+ function deliverSummary(rt, active, state) {
8156
+ if (!state.summary)
8590
8157
  return;
8591
8158
  const last = state.history.at(-1);
8592
8159
  const material = state.summary.kind === "advisor_stop" ? last?.advisor?.advice ?? last?.details ?? "" : last?.details ?? "";
8593
- const prompt = buildSummaryPrompt({
8594
- kind: state.summary.kind,
8595
- rounds: state.history.length,
8596
- material
8160
+ const prompt = buildSummaryPrompt({ kind: state.summary.kind, rounds: state.history.length, material });
8161
+ sendFollowUp(rt, active, SUMMARY_REQUEST_TYPE, prompt, {
8162
+ stillAwaiting: (current) => current.phase === "summarizing" && current.summary?.status === "awaiting_start",
8163
+ onNotStarted: () => {
8164
+ notify(active, msg6.notify.summaryNotStarted, "warning");
8165
+ dispatch(rt, { type: "SUMMARY_SETTLED" });
8166
+ },
8167
+ onSendError: (error) => {
8168
+ notifyEffectFailure(rt, error);
8169
+ dispatch(rt, { type: "SUMMARY_SETTLED" });
8170
+ }
8597
8171
  });
8598
- clearFeedbackStartTimer(active);
8599
- active.feedbackStartTimer = setTimeout(() => {
8600
- if (rt.controller !== active || active.state.phase !== "summarizing" || active.state.summary?.status !== "awaiting_start")
8601
- return;
8602
- active.feedbackStartTimer = undefined;
8603
- if (active.ctx.hasUI)
8604
- active.ctx.ui.notify(msg6.notify.summaryNotStarted, "warning");
8605
- dispatch(rt, { type: "SUMMARY_SETTLED" });
8606
- }, FEEDBACK_START_TIMEOUT_MS);
8607
- active.feedbackStartTimer.unref?.();
8608
- try {
8609
- rt.pi.sendMessage({ customType: SUMMARY_REQUEST_TYPE, content: prompt, display: false }, { deliverAs: "followUp", triggerTurn: true });
8610
- } catch (error) {
8611
- clearFeedbackStartTimer(active);
8612
- notifyEffectFailure(rt, error);
8613
- dispatch(rt, { type: "SUMMARY_SETTLED" });
8614
- }
8615
8172
  }
8616
- function sendCard(rt, card) {
8617
- const active = rt.controller;
8618
- if (!active)
8619
- return;
8620
- if (card.kind === "cancel") {
8621
- if (card.reason === "user" && active.ctx.hasUI)
8622
- active.ctx.ui.notify(msg6.notify.cancelled, "info");
8623
- return;
8624
- }
8173
+ function sendCard(rt, active, card) {
8625
8174
  if (!active.ctx.isIdle()) {
8626
8175
  active.pendingCards.push(card);
8627
8176
  return;
8628
8177
  }
8629
- sendCardNow(rt, active, card);
8178
+ sendCardNow(rt, card);
8630
8179
  }
8631
8180
  function flushPendingCards(rt) {
8632
8181
  const active = rt.controller;
8633
8182
  if (!active || !active.ctx.isIdle() || active.pendingCards.length === 0)
8634
8183
  return;
8635
- const cards = active.pendingCards.splice(0);
8636
- for (const card of cards) {
8184
+ for (const card of active.pendingCards.splice(0)) {
8637
8185
  try {
8638
- sendCardNow(rt, active, card);
8186
+ sendCardNow(rt, card);
8639
8187
  } catch (error) {
8640
8188
  notifyEffectFailure(rt, error);
8641
8189
  }
8642
8190
  }
8643
8191
  }
8644
- function sendCardNow(rt, active, card) {
8192
+ function sendCardNow(rt, card) {
8645
8193
  const built = buildCard(card);
8646
8194
  rt.pi.sendMessage({
8647
8195
  customType: CARD_TYPE,
@@ -8650,22 +8198,13 @@ function sendCardNow(rt, active, card) {
8650
8198
  details: built.details
8651
8199
  });
8652
8200
  }
8653
- function parseCommand(args) {
8654
- const input = args.trim();
8655
- return input.startsWith("--") ? { error: msg6.command.invalidArgs } : { focus: input };
8656
- }
8657
- function sessionEntries(rt) {
8658
- const manager = rt.controller?.ctx.sessionManager;
8659
- const entries = manager?.getBranch?.() ?? [];
8660
- return entries.filter((entry) => !isRecord9(entry) || entry.type !== "custom_message" || entry.customType !== CARD_TYPE && entry.customType !== FEEDBACK_TYPE && entry.customType !== SUMMARY_REQUEST_TYPE);
8661
- }
8662
- function isRecord9(value) {
8663
- return typeof value === "object" && value !== null && !Array.isArray(value);
8201
+ function sessionEntries(active) {
8202
+ const own = new Set([CARD_TYPE, FEEDBACK_TYPE, SUMMARY_REQUEST_TYPE]);
8203
+ return active.ctx.sessionManager.getBranch().filter((entry) => entry.type !== "custom_message" || !own.has(entry.customType));
8664
8204
  }
8665
8205
 
8666
8206
  // master/index.ts
8667
8207
  import { StringEnum, Type } from "@earendil-works/pi-ai";
8668
- import { getAgentDir as getAgentDir5 } from "@earendil-works/pi-coding-agent";
8669
8208
 
8670
8209
  // master/actions.ts
8671
8210
  import { existsSync as existsSync3 } from "node:fs";
@@ -8681,7 +8220,7 @@ function readReviewOutcome(sessionPath) {
8681
8220
  } catch (error) {
8682
8221
  if (isMissingFile(error))
8683
8222
  return { status: "none" };
8684
- return { status: "error", message: msg6.failure.cannotReadSession(errorMessage2(error)) };
8223
+ return { status: "error", message: msg6.failure.cannotReadSession(error instanceof Error ? error.message : String(error)) };
8685
8224
  }
8686
8225
  let latest;
8687
8226
  let damage;
@@ -8734,14 +8273,8 @@ function outcomeOf2(latest) {
8734
8273
  }
8735
8274
  return { status: "failed", runId: latest.runId, rounds, reason: last?.details?.trim() || result || "unknown" };
8736
8275
  }
8737
- function isCheckpointEntry(value) {
8738
- return typeof value === "object" && value !== null && value.type === "custom" && value.customType === CHECKPOINT_TYPE && "data" in value;
8739
- }
8740
8276
  function isMissingFile(error) {
8741
- return typeof error === "object" && error !== null && error.code === "ENOENT";
8742
- }
8743
- function errorMessage2(error) {
8744
- return error instanceof Error ? error.message : String(error);
8277
+ return error?.code === "ENOENT";
8745
8278
  }
8746
8279
 
8747
8280
  // master/list-view.ts
@@ -8790,42 +8323,40 @@ function currentWorkerAction(worker, live) {
8790
8323
  const current = [...live?.currentTools.values() ?? []].at(-1);
8791
8324
  return current ? { kind: "tool", ...current } : undefined;
8792
8325
  }
8326
+ function actionParts(action) {
8327
+ if (action?.kind === "tool")
8328
+ return [{ text: ` · ${action.tool} · ${msg7.list.running(formatDuration(Math.max(0, Date.now() - action.startedAt)))}`, color: "accent" }];
8329
+ if (action?.kind === "idle")
8330
+ return [{
8331
+ text: action.since ? ` · ${msg7.list.settledAgo(formatDuration(Date.now() - action.since))}` : ` · ${msg7.list.settled}`,
8332
+ color: "muted"
8333
+ }];
8334
+ if (action?.kind === "review")
8335
+ return [{ text: ` · ${msg7.list.reviewRound(action.round, action.settled, action.total)}`, color: "accent" }];
8336
+ return [];
8337
+ }
8793
8338
  function expandedWorkerList(workers, theme, context) {
8794
8339
  return {
8795
8340
  invalidate() {},
8796
8341
  render(width) {
8797
- return ["", ...workers.flatMap((value) => {
8798
- const worker = value;
8799
- const action = worker.currentAction;
8800
- const actionParts = action?.kind === "tool" && action.tool && action.startedAt ? [{
8801
- text: ` · ${action.tool} · ${msg7.list.running(formatDuration(Math.max(0, Date.now() - action.startedAt)))}`,
8802
- color: "accent"
8803
- }] : action?.kind === "idle" ? [{
8804
- text: action.since ? ` · ${msg7.list.settledAgo(formatDuration(Date.now() - action.since))}` : ` · ${msg7.list.settled}`,
8805
- color: "muted"
8806
- }] : action?.kind === "review" ? [{ text: ` · ${msg7.list.reviewRound(action.round, action.settled, action.total)}`, color: "accent" }] : [];
8807
- return new ToolLine({
8808
- label: String(worker.name),
8809
- value: [
8810
- { text: roleStatusText(worker), color: "accent" },
8811
- ...actionParts,
8812
- { text: ` · ${String(worker.model).split("/").pop()}/${String(worker.thinking)}`, color: "muted" }
8813
- ],
8814
- clip: "end",
8815
- theme,
8816
- ctx: { ...context, state: {}, expanded: false }
8817
- }).render(width);
8818
- })];
8342
+ return ["", ...workers.flatMap((worker) => new ToolLine({
8343
+ label: worker.name,
8344
+ value: [
8345
+ { text: roleStatusText(worker), color: "accent" },
8346
+ ...actionParts(worker.currentAction),
8347
+ { text: ` · ${worker.model.split("/").pop()}/${worker.thinking}`, color: "muted" }
8348
+ ],
8349
+ clip: "end",
8350
+ theme,
8351
+ ctx: { ...context, state: {}, expanded: false }
8352
+ }).render(width))];
8819
8353
  }
8820
8354
  };
8821
8355
  }
8822
8356
  function listMeta(workers) {
8823
8357
  if (!workers.length)
8824
8358
  return [{ text: ` — ${msg7.list.poolEmpty}`, color: "muted" }];
8825
- const summary = workers.map((value) => {
8826
- const worker = value;
8827
- return `${String(worker.name)} ${roleStatusText(worker)}`;
8828
- }).join(" · ");
8359
+ const summary = workers.map((worker) => `${worker.name} ${roleStatusText(worker)}`).join(" · ");
8829
8360
  return [{ text: ` — ${msg7.list.pool(workers.length, summary)}`, color: "muted" }];
8830
8361
  }
8831
8362
  function statusText(workers) {
@@ -8833,8 +8364,7 @@ function statusText(workers) {
8833
8364
  `) : msg7.list.noWorkers;
8834
8365
  }
8835
8366
  function roleStatusText(worker) {
8836
- const status = STATUS_WORD[worker.status] ?? String(worker.status);
8837
- return worker.role ? `${String(worker.role)}·${status}` : status;
8367
+ return `${worker.role}·${STATUS_WORD[worker.status]}`;
8838
8368
  }
8839
8369
  function optionalText(value) {
8840
8370
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
@@ -8923,7 +8453,7 @@ async function spawnWorker(active, worker, resume) {
8923
8453
  thinking: worker.thinking,
8924
8454
  tools: active.setup.pi.getActiveTools().includes(CODEMODE_TOOL) ? [...WORKER_TOOLS, CODEMODE_TOOL] : WORKER_TOOLS,
8925
8455
  excludeExtensions: exclusions,
8926
- systemPrompt: { mode: "append", text: assembleWorkerPrompt(active.setup.workerPrompt(), worker.name) },
8456
+ systemPrompt: { mode: "append", text: assembleWorkerPrompt(active.setup.workerPrompt, worker.name) },
8927
8457
  contextFiles: true,
8928
8458
  persistence: { type: "file", sessionPath: worker.sessionPath, ...resume ? { resume: true } : {} }
8929
8459
  });
@@ -8989,7 +8519,7 @@ function settleInterrupted(active, identity) {
8989
8519
  if (!current)
8990
8520
  return;
8991
8521
  const interrupted = { ...current, status: "idle", interruptedAt: Date.now() };
8992
- active.store.dispatch({ type: "UPSERT_WORKER", worker: interrupted });
8522
+ active.store.upsert(interrupted);
8993
8523
  active.markIdle(interrupted, { kind: "interrupted" }, interrupted.interruptedAt);
8994
8524
  active.outbox.enqueue(masterEvent.interrupted(identity.name, current.reviewNeeded === true, takeViewPrompts(active, identity)), identity.name);
8995
8525
  }
@@ -9006,7 +8536,7 @@ function armInterruptReminder(active, worker) {
9006
8536
  const current = active.find(worker);
9007
8537
  if (!current?.interruptedAt || current.interruptedAt !== worker.interruptedAt)
9008
8538
  return;
9009
- active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...current, disposition: "reminded" } });
8539
+ active.store.upsert({ ...current, disposition: "reminded" });
9010
8540
  active.outbox.enqueue(masterEvent.resumeReminder(worker.name), worker.name);
9011
8541
  }, delay);
9012
8542
  live.interruptTimer.unref?.();
@@ -9044,7 +8574,7 @@ function settleWorker(active, identity, terminal, error) {
9044
8574
  return;
9045
8575
  const failure = error instanceof Error ? error.message : error === undefined ? terminalFailure(terminal) : String(error);
9046
8576
  const idle = { ...current, status: "idle" };
9047
- active.store.dispatch({ type: "UPSERT_WORKER", worker: idle });
8577
+ active.store.upsert(idle);
9048
8578
  active.markIdle(idle, failure ? { kind: "failed", note: firstSentence(failure) } : { kind: "done", note: firstSentence(terminal.text) });
9049
8579
  const obligation = current.reviewNeeded === true;
9050
8580
  const event = failure ? masterEvent.failed(identity.name, failure, obligation, takeViewPrompts(active, identity)) : masterEvent.returned(identity.name, terminal.text, obligation, takeViewPrompts(active, identity));
@@ -9062,15 +8592,15 @@ function monitorAndSettleReview(active, target, session, previousRunId) {
9062
8592
  return;
9063
8593
  const { reviewNeeded: _needed, ...fulfilled } = current;
9064
8594
  const idle = { ...outcome.status === "passed" || outcome.status === "stopped" ? fulfilled : current, status: "idle" };
9065
- active.store.dispatch({ type: "UPSERT_WORKER", worker: idle });
8595
+ active.store.upsert(idle);
9066
8596
  active.markIdle(idle, reviewOutcomeRow(outcome));
9067
- active.outbox.enqueue(masterEvent.review(target.name, outcome, latestAssistantText(session.messages)), target.name);
8597
+ active.outbox.enqueue(masterEvent.review(target.name, outcome, textOf(lastAssistant(session.messages)?.content)), target.name);
9068
8598
  }, (error) => {
9069
8599
  const current = reviewing();
9070
8600
  if (!current)
9071
8601
  return;
9072
8602
  const idle = { ...current, status: "idle" };
9073
- active.store.dispatch({ type: "UPSERT_WORKER", worker: idle });
8603
+ active.store.upsert(idle);
9074
8604
  active.markIdle(idle, { kind: "failed", note: msg7.run.reviewIncomplete });
9075
8605
  active.outbox.enqueue(masterEvent.reviewIncomplete(target.name, String(error)), target.name);
9076
8606
  });
@@ -9127,7 +8657,7 @@ function reviewOutcomeRow(outcome) {
9127
8657
  return { kind: "failed", note: outcome.status === "stopped" ? msg7.run.reviewStopped : msg7.run.reviewIncomplete };
9128
8658
  }
9129
8659
  function captureWorkerTerminal(messages) {
9130
- const message = messages.findLast((candidate) => candidate.role === "assistant");
8660
+ const message = lastAssistant(messages);
9131
8661
  if (!message)
9132
8662
  return;
9133
8663
  return {
@@ -9154,8 +8684,8 @@ function faultSummary(terminal) {
9154
8684
  const [first = message] = message.split(/(?<=[.。!!??])\s|\n/u);
9155
8685
  return clip(first.trim(), FAULT_SUMMARY_WIDTH);
9156
8686
  }
9157
- function latestAssistantText(messages) {
9158
- return textOf(messages.findLast((candidate) => candidate.role === "assistant")?.content);
8687
+ function lastAssistant(messages) {
8688
+ return messages.findLast((candidate) => candidate.role === "assistant");
9159
8689
  }
9160
8690
  function nextFallback(role, worker) {
9161
8691
  const chain = [role, ...role.fallback];
@@ -9175,174 +8705,106 @@ function fallbackResumePrompt(from, to, reason) {
9175
8705
  }
9176
8706
 
9177
8707
  // master/state.ts
9178
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
9179
- import { dirname as dirname4, join as join3 } from "node:path";
8708
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
8709
+ import { dirname as dirname3, join as join3 } from "node:path";
9180
8710
  var STATE_VERSION = 9;
8711
+ var WORKER_NAME = /^[a-z][a-z0-9_-]{0,31}$/u;
9181
8712
  var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
9182
- function initialMasterState() {
9183
- return { version: STATE_VERSION, workers: [] };
9184
- }
9185
- function reduceMaster(state, event) {
9186
- switch (event.type) {
9187
- case "UPSERT_WORKER":
9188
- return { ...state, workers: upsertWorker(state.workers, event.worker) };
9189
- case "REMOVE_WORKER": {
9190
- const workers = state.workers.filter((worker) => worker.name !== event.name);
9191
- return workers.length === state.workers.length ? state : { ...state, workers };
9192
- }
9193
- case "CLEAR":
9194
- return initialMasterState();
9195
- }
9196
- }
9197
- function recoverMasterState(state, interruptedAt = Date.now()) {
9198
- let changed = false;
9199
- const workers = state.workers.map((worker) => {
9200
- if (worker.status === "idle")
9201
- return worker;
9202
- changed = true;
9203
- return { ...worker, status: "idle", interruptedAt };
9204
- });
9205
- return changed ? { ...state, workers } : state;
9206
- }
9207
- function restoreMasterState(data) {
9208
- if (!data || typeof data !== "object" || Array.isArray(data))
9209
- return;
9210
- const record = data;
9211
- if (record.version !== STATE_VERSION || !Array.isArray(record.workers) || !record.workers.every(isWorker))
9212
- return;
9213
- const workers = record.workers;
9214
- if (new Set(workers.map((worker) => worker.name)).size !== workers.length)
9215
- return;
9216
- if (new Set(workers.map((worker) => worker.sessionPath)).size !== workers.length)
9217
- return;
9218
- return { version: STATE_VERSION, workers };
9219
- }
9220
-
9221
- class LegacyMasterStateError extends Error {
9222
- version;
9223
- constructor(version) {
9224
- super(msg7.state.legacy(version, STATE_VERSION));
9225
- this.version = version;
9226
- }
9227
- }
9228
8713
  function masterStatePath(agentDir, sessionId) {
9229
8714
  const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/gu, "-");
9230
8715
  return join3(agentDir, "tmp", `firecode-master-${safeId}.json`);
9231
8716
  }
9232
- function loadMasterState(path) {
9233
- let raw;
9234
- try {
9235
- raw = readFileSync6(path, "utf8");
9236
- } catch (error) {
9237
- if (error.code === "ENOENT")
9238
- return;
9239
- throw error;
9240
- }
9241
- let data;
9242
- try {
9243
- data = JSON.parse(raw);
9244
- } catch {
9245
- throw new Error(msg7.state.invalidJson(path));
9246
- }
9247
- const version = data?.version;
9248
- if (typeof version === "number" && version !== STATE_VERSION && version !== 8)
9249
- throw new LegacyMasterStateError(version);
9250
- const state = restoreMasterState(version === 8 ? migrateFromV8(data) : data);
9251
- if (!state)
9252
- throw new Error(msg7.state.invalidShape(path));
9253
- return state;
9254
- }
9255
8717
 
9256
8718
  class MasterStore {
9257
- stateValue;
9258
8719
  path;
9259
8720
  onChange;
8721
+ list;
9260
8722
  discardedLegacyVersion;
9261
- constructor(path, restored, onChange) {
8723
+ constructor(path, onChange) {
9262
8724
  this.path = path;
9263
8725
  this.onChange = onChange;
9264
- if (restored)
9265
- this.stateValue = restored;
9266
- else {
9267
- const loaded = this.loadOwnedState();
9268
- this.stateValue = loaded.state;
9269
- this.discardedLegacyVersion = loaded.discardedLegacyVersion;
9270
- }
9271
- }
9272
- get state() {
9273
- return this.stateValue;
9274
- }
9275
- dispatch(event) {
9276
- const next = reduceMaster(this.stateValue, event);
9277
- if (next === this.stateValue)
9278
- return next;
9279
- if (event.type === "CLEAR")
9280
- rmSync2(this.path, { force: true });
9281
- else
9282
- writeState(this.path, next);
9283
- this.stateValue = next;
8726
+ const loaded = load(path);
8727
+ this.discardedLegacyVersion = loaded.legacyVersion;
8728
+ const now = Date.now();
8729
+ this.list = loaded.workers.map((worker) => worker.status === "idle" ? worker : { ...worker, status: "idle", interruptedAt: now });
8730
+ if (this.list.some((worker, index) => worker !== loaded.workers[index]))
8731
+ this.persist();
8732
+ }
8733
+ get workers() {
8734
+ return this.list;
8735
+ }
8736
+ find(name) {
8737
+ return this.list.find((worker) => worker.name === name);
8738
+ }
8739
+ require(name) {
8740
+ const worker = this.find(name);
8741
+ if (!worker)
8742
+ throw new Error(msg7.state.missing(name));
8743
+ return worker;
8744
+ }
8745
+ upsert(worker) {
8746
+ const owner = this.list.find((candidate) => candidate.sessionPath === worker.sessionPath);
8747
+ if (owner && owner.name !== worker.name)
8748
+ throw new Error(msg7.state.pathTaken(worker.sessionPath));
8749
+ const existing = this.find(worker.name);
8750
+ if (existing && existing.sessionPath !== worker.sessionPath)
8751
+ throw new Error(msg7.state.pathChange(worker.name));
8752
+ this.list = existing ? this.list.map((candidate) => candidate === existing ? worker : candidate) : [...this.list, worker];
8753
+ this.commit();
8754
+ }
8755
+ remove(name) {
8756
+ if (!this.find(name))
8757
+ return false;
8758
+ this.list = this.list.filter((worker) => worker.name !== name);
8759
+ this.commit();
8760
+ return true;
8761
+ }
8762
+ commit() {
8763
+ this.persist();
9284
8764
  this.onChange?.();
9285
- return next;
9286
8765
  }
9287
- loadOwnedState() {
8766
+ persist() {
8767
+ mkdirSync2(dirname3(this.path), { recursive: true, mode: 448 });
8768
+ const temporary = `${this.path}.${process.pid}.${crypto.randomUUID()}.tmp`;
9288
8769
  try {
9289
- return { state: loadMasterState(this.path) ?? initialMasterState() };
8770
+ writeFileSync3(temporary, `${JSON.stringify({ version: STATE_VERSION, workers: this.list })}
8771
+ `, { encoding: "utf8", mode: 384 });
8772
+ renameSync2(temporary, this.path);
9290
8773
  } catch (error) {
9291
- if (!(error instanceof LegacyMasterStateError))
9292
- throw error;
9293
- rmSync2(this.path, { force: true });
9294
- return { state: initialMasterState(), discardedLegacyVersion: error.version };
8774
+ rmSync2(temporary, { force: true });
8775
+ throw error;
9295
8776
  }
9296
8777
  }
9297
8778
  }
9298
- function migrateFromV8(data) {
9299
- if (!Array.isArray(data.workers))
9300
- return data;
9301
- const workers = data.workers;
9302
- const created = (index) => typeof workers[index]?.createdAt === "number" ? workers[index].createdAt : -Infinity;
9303
- const order = workers.map((_, index) => index).sort((a, b) => created(a) - created(b) || a - b);
9304
- return {
9305
- version: STATE_VERSION,
9306
- workers: workers.map((worker, index) => {
9307
- const { createdAt: _createdAt, ...rest } = worker ?? {};
9308
- return { ...rest, launch: order.indexOf(index) + 1 };
9309
- })
9310
- };
9311
- }
9312
- function requireWorker(state, name) {
9313
- const worker = state.workers.find((candidate) => candidate.name === name);
9314
- if (!worker)
9315
- throw new Error(msg7.state.missing(name));
9316
- return worker;
9317
- }
9318
- function writeState(path, state) {
9319
- mkdirSync3(dirname4(path), { recursive: true, mode: 448 });
9320
- const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
8779
+ function load(path) {
8780
+ let raw;
9321
8781
  try {
9322
- writeFileSync3(temporary, `${JSON.stringify(state)}
9323
- `, { encoding: "utf8", mode: 384 });
9324
- renameSync2(temporary, path);
8782
+ raw = readFileSync6(path, "utf8");
9325
8783
  } catch (error) {
9326
- rmSync2(temporary, { force: true });
8784
+ if (error.code === "ENOENT")
8785
+ return { workers: [] };
9327
8786
  throw error;
9328
8787
  }
9329
- }
9330
- function upsertWorker(workers, worker) {
9331
- const index = workers.findIndex((candidate) => candidate.name === worker.name);
9332
- const sessionOwner = workers.find((candidate) => candidate.sessionPath === worker.sessionPath);
9333
- if (sessionOwner && sessionOwner.name !== worker.name)
9334
- throw new Error(msg7.state.pathTaken(worker.sessionPath));
9335
- if (index < 0)
9336
- return [...workers, worker];
9337
- if (workers[index].sessionPath !== worker.sessionPath)
9338
- throw new Error(msg7.state.pathChange(worker.name));
9339
- return workers.map((candidate, position) => position === index ? worker : candidate);
8788
+ let data;
8789
+ try {
8790
+ data = JSON.parse(raw);
8791
+ } catch {
8792
+ throw new Error(msg7.state.invalidJson(path));
8793
+ }
8794
+ if (typeof data?.version === "number" && data.version !== STATE_VERSION) {
8795
+ rmSync2(path, { force: true });
8796
+ return { workers: [], legacyVersion: data.version };
8797
+ }
8798
+ const workers = data?.workers;
8799
+ if (data?.version !== STATE_VERSION || !Array.isArray(workers) || !workers.every(isWorker) || new Set(workers.map((worker) => worker.name)).size !== workers.length || new Set(workers.map((worker) => worker.sessionPath)).size !== workers.length)
8800
+ throw new Error(msg7.state.invalidShape(path));
8801
+ return { workers };
9340
8802
  }
9341
8803
  function isWorker(value) {
9342
8804
  if (!value || typeof value !== "object" || Array.isArray(value))
9343
8805
  return false;
9344
8806
  const record = value;
9345
- if (typeof record.name !== "string" || !/^[a-z][a-z0-9_-]{0,31}$/u.test(record.name) || typeof record.role !== "string" || !record.role || typeof record.model !== "string" || !record.model || typeof record.thinking !== "string" || !THINKING_LEVELS2.includes(record.thinking) || typeof record.status !== "string" || !isStatus(record.status) || typeof record.sessionPath !== "string" || !record.sessionPath)
8807
+ if (typeof record.name !== "string" || !WORKER_NAME.test(record.name) || typeof record.role !== "string" || !record.role || typeof record.model !== "string" || !record.model || typeof record.thinking !== "string" || !THINKING_LEVELS2.includes(record.thinking) || record.status !== "working" && record.status !== "idle" && record.status !== "reviewing" || typeof record.sessionPath !== "string" || !record.sessionPath)
9346
8808
  return false;
9347
8809
  if (record.cwd !== undefined && (typeof record.cwd !== "string" || !record.cwd))
9348
8810
  return false;
@@ -9352,12 +8814,7 @@ function isWorker(value) {
9352
8814
  return false;
9353
8815
  if (typeof record.launch !== "number" || !Number.isInteger(record.launch) || record.launch <= 0)
9354
8816
  return false;
9355
- if (record.disposition !== undefined && record.disposition !== "pending" && record.disposition !== "reminded")
9356
- return false;
9357
- return true;
9358
- }
9359
- function isStatus(value) {
9360
- return value === "working" || value === "idle" || value === "reviewing";
8817
+ return record.disposition === undefined || record.disposition === "pending" || record.disposition === "reminded";
9361
8818
  }
9362
8819
 
9363
8820
  // master/actions.ts
@@ -9365,24 +8822,24 @@ var ACTIONS = ["start", "send", "interrupt", "review", "tail", "ack", "kill"];
9365
8822
  var MAX_IN_FLIGHT = 15;
9366
8823
  var ACTION_HANDLERS = { start, send, interrupt, review, tail: tail2, ack, kill };
9367
8824
  async function kill(active, params) {
9368
- const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
8825
+ const target = targetOf(active, params);
9369
8826
  active.remove(target.name);
9370
8827
  await active.setup.pool.dispose(target.sessionPath);
9371
8828
  return toolResult({ killed: true });
9372
8829
  }
9373
8830
  async function tail2(active, params) {
9374
- const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
8831
+ const target = targetOf(active, params);
9375
8832
  return { content: [{ type: "text", text: await readWorkerTrace(target) }], details: undefined };
9376
8833
  }
9377
8834
  async function ack(active, params) {
9378
- const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
8835
+ const target = targetOf(active, params);
9379
8836
  if (target.reviewNeeded)
9380
8837
  throw new Error(msg7.action.ackObligation(target.name));
9381
8838
  if (target.status !== "idle")
9382
8839
  throw new Error(msg7.action.ackStatus(target.name, target.status));
9383
8840
  if (target.disposition) {
9384
8841
  const { disposition: _disposition, ...rest } = target;
9385
- active.store.dispatch({ type: "UPSERT_WORKER", worker: rest });
8842
+ active.store.upsert(rest);
9386
8843
  }
9387
8844
  const live = active.live.get(target.name);
9388
8845
  if (live?.outcome && live.outcome.kind !== "done")
@@ -9393,7 +8850,7 @@ async function ack(active, params) {
9393
8850
  async function review(active, params) {
9394
8851
  if (active.setup.reviewGate)
9395
8852
  throw new Error(active.setup.reviewGate);
9396
- const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
8853
+ const target = targetOf(active, params);
9397
8854
  const live = active.liveOf(target.name);
9398
8855
  if (target.status !== "idle" || live.transitioning)
9399
8856
  throw new Error(msg7.action.reviewBusy(target.name));
@@ -9413,7 +8870,7 @@ async function review(active, params) {
9413
8870
  }
9414
8871
  }
9415
8872
  async function interrupt(active, params) {
9416
- const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
8873
+ const target = targetOf(active, params);
9417
8874
  if (target.status !== "working")
9418
8875
  throw new Error(msg7.action.interruptStatus(target.name, target.status));
9419
8876
  const session = active.setup.pool.getSession(target.sessionPath);
@@ -9438,7 +8895,7 @@ async function send(active, params) {
9438
8895
  const { reviewGate, pool, roster } = active.setup;
9439
8896
  if (params.review === true && reviewGate)
9440
8897
  throw new Error(reviewGate);
9441
- const target = requireWorker(active.store.state, requiredString(params.worker, "worker"));
8898
+ const target = targetOf(active, params);
9442
8899
  const live = active.liveOf(target.name);
9443
8900
  if (live.transitioning)
9444
8901
  throw new Error(msg7.action.switching(target.name));
@@ -9527,9 +8984,9 @@ async function start(active, params, ctx) {
9527
8984
  const name = params.worker.trim();
9528
8985
  validateWorkerName(name);
9529
8986
  const starting = [...active.live].flatMap(([candidate, live]) => live.starting ? [candidate] : []);
9530
- if (active.store.state.workers.some((worker) => worker.name === name) || starting.includes(name))
8987
+ if (active.store.find(name) || starting.includes(name))
9531
8988
  throw new Error(msg7.action.exists(name));
9532
- const inFlight = active.store.state.workers.filter((worker) => worker.status === "working" || worker.status === "reviewing");
8989
+ const inFlight = active.store.workers.filter((worker) => worker.status === "working" || worker.status === "reviewing");
9533
8990
  if (inFlight.length + starting.length >= MAX_IN_FLIGHT)
9534
8991
  throw new Error(msg7.action.limit(MAX_IN_FLIGHT, [...inFlight.map((worker) => worker.name), ...starting]));
9535
8992
  const prompt = requiredString(params.prompt, "prompt");
@@ -9557,7 +9014,7 @@ async function start(active, params, ctx) {
9557
9014
  launch,
9558
9015
  ...params.review === true ? { reviewNeeded: true } : {}
9559
9016
  };
9560
- active.store.dispatch({ type: "UPSERT_WORKER", worker });
9017
+ active.store.upsert(worker);
9561
9018
  live.starting = undefined;
9562
9019
  active.beginRun(name);
9563
9020
  const session = await spawnWorker(active, worker, false);
@@ -9596,6 +9053,9 @@ ${lines.join(`
9596
9053
  function toolResult(value) {
9597
9054
  return { content: [{ type: "text", text: JSON.stringify(value) }], details: value };
9598
9055
  }
9056
+ function targetOf(active, params) {
9057
+ return active.store.require(requiredString(params.worker, "worker"));
9058
+ }
9599
9059
  function requiredString(value, field) {
9600
9060
  if (typeof value !== "string" || !value.trim())
9601
9061
  throw new Error(msg7.action.empty(field));
@@ -9613,7 +9073,7 @@ function resolveRole(roles, role) {
9613
9073
  return roles.find((candidate) => candidate.role === role);
9614
9074
  }
9615
9075
  function validateWorkerName(name) {
9616
- if (!/^[a-z][a-z0-9_-]{0,31}$/u.test(name))
9076
+ if (!WORKER_NAME.test(name))
9617
9077
  throw new Error(msg7.action.badName);
9618
9078
  }
9619
9079
  function validateDelegationText(prompt) {
@@ -9640,40 +9100,12 @@ async function resolveSendCwd(worker, requested) {
9640
9100
 
9641
9101
  // master/event-card.ts
9642
9102
  import { getMarkdownTheme as getMarkdownTheme5 } from "@earendil-works/pi-coding-agent";
9643
- import { Box as Box2, Markdown as Markdown4, Text as Text6 } from "@earendil-works/pi-tui";
9103
+ import { Box as Box2, Markdown as Markdown4 } from "@earendil-works/pi-tui";
9644
9104
  function registerMasterEventRenderer(pi) {
9645
- pi.registerMessageRenderer(MASTER_EVENT_TYPE, (message, options, theme) => new MasterEventCard(message.content, options.expanded, theme));
9646
- }
9647
-
9648
- class MasterEventCard {
9649
- card;
9650
- fallback;
9651
- constructor(content, expanded, theme) {
9652
- const text = textOf(content);
9653
- this.fallback = new Text6(text, 0, 0);
9654
- let card;
9655
- try {
9656
- card = !expanded ? compactCard(text, theme) : fullCard(text, theme);
9657
- } catch {
9658
- card = undefined;
9659
- }
9660
- this.card = card ?? this.fallback;
9661
- }
9662
- render(width) {
9663
- try {
9664
- return this.card.render(Math.max(1, width));
9665
- } catch {
9666
- try {
9667
- return this.fallback.render(Math.max(1, width));
9668
- } catch {
9669
- return [];
9670
- }
9671
- }
9672
- }
9673
- invalidate() {
9674
- this.card.invalidate?.();
9675
- this.fallback.invalidate?.();
9676
- }
9105
+ pi.registerMessageRenderer(MASTER_EVENT_TYPE, (message, options, theme) => {
9106
+ const text = textOf(message.content);
9107
+ return options.expanded ? fullCard(text, theme) : compactCard(text, theme);
9108
+ });
9677
9109
  }
9678
9110
  function compactCard(text, theme) {
9679
9111
  const entries = machineEntries(text);
@@ -9708,7 +9140,7 @@ function card(theme, children) {
9708
9140
  // master/guard.ts
9709
9141
  import { realpath as realpath2 } from "node:fs/promises";
9710
9142
  import { tmpdir } from "node:os";
9711
- import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
9143
+ import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute2, relative, resolve, sep } from "node:path";
9712
9144
  import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
9713
9145
  function registerWorkerGuard(pi) {
9714
9146
  pi.on("tool_call", async (event, ctx) => {
@@ -9721,7 +9153,7 @@ function registerWorkerGuard(pi) {
9721
9153
  }
9722
9154
  var TEMP_ROOTS = [tmpdir(), "/tmp"];
9723
9155
  async function outsideCheckoutReason(path, cwd) {
9724
- const target = await canonicalWritePath(resolve2(cwd, path));
9156
+ const target = await canonicalWritePath(resolve(cwd, path));
9725
9157
  const roots = await Promise.all([cwd, ...TEMP_ROOTS].map(canonicalWritePath));
9726
9158
  return roots.some((root) => within(root, target)) ? undefined : msg7.guard(path);
9727
9159
  }
@@ -9734,9 +9166,9 @@ async function canonicalWritePath(path) {
9734
9166
  const missing = [];
9735
9167
  while (true) {
9736
9168
  try {
9737
- return resolve2(await realpath2(ancestor), ...missing.reverse());
9169
+ return resolve(await realpath2(ancestor), ...missing.reverse());
9738
9170
  } catch {
9739
- const parent = dirname5(ancestor);
9171
+ const parent = dirname4(ancestor);
9740
9172
  if (parent === ancestor)
9741
9173
  return path;
9742
9174
  missing.push(basename3(ancestor));
@@ -10038,7 +9470,7 @@ class Outbox {
10038
9470
  if (this.active.closed)
10039
9471
  return;
10040
9472
  const names = new Set;
10041
- for (const worker of this.active.store.state.workers)
9473
+ for (const worker of this.active.store.workers)
10042
9474
  if ((worker.status === "working" || worker.status === "reviewing") && this.active.live.get(worker.name)?.origin !== "view")
10043
9475
  names.add(worker.name);
10044
9476
  for (const event of [...this.queued, ...this.delivering])
@@ -10105,9 +9537,9 @@ class Outbox {
10105
9537
  for (const event of batch) {
10106
9538
  if (!event.worker)
10107
9539
  continue;
10108
- const worker = active.store.state.workers.find((candidate) => candidate.name === event.worker);
9540
+ const worker = active.store.find(event.worker);
10109
9541
  if (worker?.status === "idle" && worker.disposition !== "reminded")
10110
- active.store.dispatch({ type: "UPSERT_WORKER", worker: { ...worker, disposition: "pending" } });
9542
+ active.store.upsert({ ...worker, disposition: "pending" });
10111
9543
  }
10112
9544
  this.scheduleInFlight();
10113
9545
  }, (error) => {
@@ -10191,7 +9623,7 @@ function runtimeSource(active) {
10191
9623
  runtimeDrafts.set(active, drafts = new Map);
10192
9624
  return {
10193
9625
  facts: () => active.activityFacts(),
10194
- worker: (name) => active.store.state.workers.find((worker) => worker.name === name),
9626
+ worker: (name) => active.store.find(name),
10195
9627
  session: (worker) => active.setup.pool.getSession(worker.sessionPath),
10196
9628
  onSession: (listener) => active.onWorkerSession(listener),
10197
9629
  onWorkerRemoved: (listener) => active.onWorkerRemoved(listener),
@@ -10622,6 +10054,7 @@ class WorkerView {
10622
10054
  // master/runtime.ts
10623
10055
  var LIST_WIDGET_KEY = "firecode-master-list";
10624
10056
  var MASTER_IDENTITY = paint(HEAT_COLORS.orange, msg7.command.identity);
10057
+ var newLive = () => ({ currentTools: new Map, viewPrompts: [], origin: "master" });
10625
10058
 
10626
10059
  class MasterRuntime {
10627
10060
  setup;
@@ -10635,12 +10068,12 @@ class MasterRuntime {
10635
10068
  stopReleaseWatch;
10636
10069
  sessionListeners = new Set;
10637
10070
  removedListeners = new Set;
10638
- constructor(setup, ctx, restored) {
10071
+ constructor(setup, ctx) {
10639
10072
  this.setup = setup;
10640
10073
  this.ctx = ctx;
10641
10074
  this.outbox = new Outbox(this);
10642
- this.store = new MasterStore(masterStatePath(getAgentDir4(), ctx.sessionManager.getSessionId()), restored, () => this.render());
10643
- this.launchSeq = Math.max(0, ...this.store.state.workers.map((worker) => worker.launch));
10075
+ this.store = new MasterStore(masterStatePath(getAgentDir4(), ctx.sessionManager.getSessionId()), () => this.render());
10076
+ this.launchSeq = Math.max(0, ...this.store.workers.map((worker) => worker.launch));
10644
10077
  this.stopReleaseWatch = setup.pool.onRelease((sessionPath) => {
10645
10078
  for (const live of this.live.values())
10646
10079
  if (live.observed?.sessionPath === sessionPath)
@@ -10671,11 +10104,11 @@ class MasterRuntime {
10671
10104
  liveOf(name) {
10672
10105
  let live = this.live.get(name);
10673
10106
  if (!live)
10674
- this.live.set(name, live = { currentTools: new Map, viewPrompts: [], origin: "master" });
10107
+ this.live.set(name, live = newLive());
10675
10108
  return live;
10676
10109
  }
10677
10110
  reserve(name) {
10678
- const live = { currentTools: new Map, viewPrompts: [], origin: "master", starting: true };
10111
+ const live = { ...newLive(), starting: true };
10679
10112
  this.live.set(name, live);
10680
10113
  return { live, launch: ++this.launchSeq };
10681
10114
  }
@@ -10687,27 +10120,26 @@ class MasterRuntime {
10687
10120
  this.unobserve(live);
10688
10121
  this.live.delete(name);
10689
10122
  }
10690
- const before = this.store.state;
10691
- if (this.store.dispatch({ type: "REMOVE_WORKER", name }) === before)
10123
+ if (!this.store.remove(name))
10692
10124
  return;
10693
10125
  for (const notify of this.removedListeners)
10694
10126
  notify(name);
10695
10127
  }
10696
10128
  current(identity) {
10697
10129
  this.assertOpen();
10698
- const current = this.store.state.workers.find((worker) => worker.name === identity.name);
10699
- if (current?.sessionPath === identity.sessionPath)
10130
+ const current = this.find(identity);
10131
+ if (current)
10700
10132
  return current;
10701
10133
  this.setup.pool.dispose(identity.sessionPath);
10702
10134
  throw new Error(msg7.runtime.killed(identity.name));
10703
10135
  }
10704
10136
  commit(identity, update) {
10705
10137
  const next = update(this.current(identity));
10706
- this.store.dispatch({ type: "UPSERT_WORKER", worker: next });
10138
+ this.store.upsert(next);
10707
10139
  return next;
10708
10140
  }
10709
10141
  find(identity) {
10710
- const current = this.store.state.workers.find((worker) => worker.name === identity.name);
10142
+ const current = this.store.find(identity.name);
10711
10143
  return current?.sessionPath === identity.sessionPath ? current : undefined;
10712
10144
  }
10713
10145
  markIdle(worker, outcome, at = Date.now()) {
@@ -10768,7 +10200,7 @@ class MasterRuntime {
10768
10200
  live.observed = undefined;
10769
10201
  }
10770
10202
  activityFacts() {
10771
- const workers = this.store.state.workers;
10203
+ const workers = this.store.workers;
10772
10204
  const byPath = (pick) => new Map(workers.flatMap((worker) => {
10773
10205
  const live = this.live.get(worker.name);
10774
10206
  const value = live && pick(live);
@@ -10794,16 +10226,12 @@ var WAKE_QUIET_MS = 1500;
10794
10226
  function registerMaster(pi, dependencies = {}, worker = false) {
10795
10227
  if (worker)
10796
10228
  return registerWorkerGuard(pi);
10797
- const loaded = loadConfig().master;
10229
+ const all = loadConfig();
10230
+ const loaded = all.master;
10798
10231
  const prompts = loadMasterPrompts();
10799
- const startupError = "error" in loaded ? loaded.error : ("error" in prompts) ? prompts.error : undefined;
10232
+ const startupError = "error" in loaded ? loaded.error : prompts.error;
10800
10233
  const roster = "error" in loaded ? [] : loaded.config.roles;
10801
10234
  const autoActivate = "error" in loaded ? false : loaded.config.autoActivate;
10802
- const requirePrompts = () => {
10803
- if ("error" in prompts)
10804
- throw new Error(prompts.error);
10805
- return prompts;
10806
- };
10807
10235
  let publishedInFlight = 0;
10808
10236
  const publishInFlight = (count, teardown = false) => {
10809
10237
  if (count === publishedInFlight)
@@ -10820,8 +10248,8 @@ function registerMaster(pi, dependencies = {}, worker = false) {
10820
10248
  pool: dependencies.pool ?? new InProcessSessionPool,
10821
10249
  roster,
10822
10250
  exclusions: "error" in loaded ? [] : loaded.config.workerExcludeExtensions,
10823
- workerPrompt: () => requirePrompts().worker,
10824
- reviewGate: reviewGateError(),
10251
+ workerPrompt: prompts.worker,
10252
+ reviewGate: all.config.features.review === false ? msg7.command.reviewOff : ("error" in all.review) ? all.review.error : undefined,
10825
10253
  interruptResumeMs: dependencies.interruptResumeMs ?? INTERRUPT_RESUME_MS,
10826
10254
  wakeQuietMs: dependencies.wakeQuietMs ?? WAKE_QUIET_MS,
10827
10255
  publishInFlight,
@@ -10833,19 +10261,18 @@ function registerMaster(pi, dependencies = {}, worker = false) {
10833
10261
  const tools = pi.getActiveTools().filter((name) => !MASTER_TOOLS.includes(name));
10834
10262
  pi.setActiveTools(active ? [...tools, ...MASTER_TOOLS] : tools);
10835
10263
  };
10836
- const activate = (ctx, restored) => {
10264
+ const activate = (ctx) => {
10837
10265
  if (startupError)
10838
10266
  throw new Error(startupError);
10839
- if (runtime) {
10840
- runtime.ctx = ctx;
10841
- return runtime;
10842
- }
10843
- const active = runtime = new MasterRuntime(setup, ctx, restored);
10267
+ const active = runtime = new MasterRuntime(setup, ctx);
10844
10268
  setTools(true);
10845
10269
  if (active.store.discardedLegacyVersion !== undefined)
10846
10270
  ctx.ui.notify(msg7.command.legacyPool(active.store.discardedLegacyVersion), "warning");
10847
10271
  active.render();
10848
- return active;
10272
+ for (const worker of active.store.workers)
10273
+ if (worker.interruptedAt)
10274
+ armInterruptReminder(active, worker);
10275
+ active.outbox.replayUnacked(ctx);
10849
10276
  };
10850
10277
  const deactivate = async () => {
10851
10278
  const active = runtime;
@@ -10855,34 +10282,12 @@ function registerMaster(pi, dependencies = {}, worker = false) {
10855
10282
  await setup.pool.disposeAll();
10856
10283
  setTools(false);
10857
10284
  };
10858
- const activateSession = (ctx) => {
10859
- if (runtime)
10860
- return activate(ctx);
10861
- let restored;
10862
- try {
10863
- restored = loadMasterState(masterStatePath(getAgentDir5(), ctx.sessionManager.getSessionId()));
10864
- } catch {
10865
- return activate(ctx);
10866
- }
10867
- const active = activate(ctx, restored);
10868
- if (restored) {
10869
- const recovered = recoverMasterState(restored);
10870
- for (const worker of recovered.workers) {
10871
- if (worker !== restored.workers.find((candidate) => candidate.name === worker.name))
10872
- active.store.dispatch({ type: "UPSERT_WORKER", worker });
10873
- if (worker.interruptedAt)
10874
- armInterruptReminder(active, worker);
10875
- }
10876
- }
10877
- active.outbox.replayUnacked(ctx);
10878
- return active;
10879
- };
10880
10285
  pi.registerCommand("fire-master", {
10881
10286
  description: msg7.command.description,
10882
10287
  handler: async (args, ctx) => {
10883
10288
  const input = args.trim();
10884
10289
  if (input === "status") {
10885
- ctx.ui.notify(runtime ? statusText(runtime.store.state.workers) : msg7.command.notStarted, "info");
10290
+ ctx.ui.notify(runtime ? statusText(runtime.store.workers) : msg7.command.notStarted, "info");
10886
10291
  return;
10887
10292
  }
10888
10293
  if (input) {
@@ -10895,7 +10300,7 @@ function registerMaster(pi, dependencies = {}, worker = false) {
10895
10300
  return;
10896
10301
  }
10897
10302
  try {
10898
- activateSession(ctx);
10303
+ activate(ctx);
10899
10304
  ctx.ui.notify(msg7.command.on, "info");
10900
10305
  } catch (error) {
10901
10306
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
@@ -10912,7 +10317,7 @@ function registerMaster(pi, dependencies = {}, worker = false) {
10912
10317
  return {
10913
10318
  systemPrompt: `${event.systemPrompt}
10914
10319
 
10915
- ${assembleMasterPrompt(requirePrompts().master, rosterText(roster))}`
10320
+ ${assembleMasterPrompt(prompts.master, rosterText(roster))}`
10916
10321
  };
10917
10322
  });
10918
10323
  pi.registerTool({
@@ -10922,10 +10327,10 @@ ${assembleMasterPrompt(requirePrompts().master, rosterText(roster))}`
10922
10327
  renderShell: "self",
10923
10328
  renderCall: (_args, theme, ctx) => new ToolLine({ label: msg7.tool.label, value: subagentsCallParts({ action: "list" }), clip: "end", theme, ctx }),
10924
10329
  renderResult: (result, options, theme, context) => {
10925
- const details = result.details;
10926
- context.state.meta = !context.isError && Array.isArray(details?.workers) ? listMeta(details.workers) : undefined;
10927
- if (options.expanded && Array.isArray(details?.workers))
10928
- return expandedWorkerList(details.workers, theme, context);
10330
+ const workers = result.details?.workers;
10331
+ context.state.meta = !context.isError && workers ? listMeta(workers) : undefined;
10332
+ if (options.expanded && workers)
10333
+ return expandedWorkerList(workers, theme, context);
10929
10334
  return renderSubagentsResult(result, options, theme, context);
10930
10335
  },
10931
10336
  parameters: Type.Object({}),
@@ -10933,7 +10338,7 @@ ${assembleMasterPrompt(requirePrompts().master, rosterText(roster))}`
10933
10338
  const active = runtime;
10934
10339
  if (!active)
10935
10340
  throw new Error(msg7.tool.onlyInMaster(MASTER_LIST_TOOL));
10936
- const workers = active.store.state.workers.map(compactWorker);
10341
+ const workers = active.store.workers.map(compactWorker);
10937
10342
  return {
10938
10343
  content: [{ type: "text", text: JSON.stringify({ workers }) }],
10939
10344
  details: {
@@ -10962,10 +10367,7 @@ ${assembleMasterPrompt(requirePrompts().master, rosterText(roster))}`
10962
10367
  const active = runtime;
10963
10368
  if (!active)
10964
10369
  throw new Error(msg7.tool.onlyInMaster(MASTER_TOOL));
10965
- const handler = ACTION_HANDLERS[params.action];
10966
- if (!handler)
10967
- throw new Error(msg7.tool.unknownAction(String(params.action)));
10968
- return handler(active, params, ctx);
10370
+ return ACTION_HANDLERS[params.action](active, params, ctx);
10969
10371
  }
10970
10372
  });
10971
10373
  pi.on("session_start", async (_event, ctx) => {
@@ -10973,24 +10375,18 @@ ${assembleMasterPrompt(requirePrompts().master, rosterText(roster))}`
10973
10375
  if (!autoActivate)
10974
10376
  return;
10975
10377
  try {
10976
- activateSession(ctx);
10378
+ activate(ctx);
10977
10379
  } catch (error) {
10978
10380
  ctx.ui.notify(msg7.command.restoreFailed(error instanceof Error ? error.message : String(error)), "error");
10979
10381
  }
10980
10382
  });
10981
10383
  pi.on("session_shutdown", () => deactivate());
10982
10384
  }
10983
- function reviewGateError() {
10984
- const loaded = loadConfig();
10985
- if (loaded.config.features.review === false)
10986
- return msg7.command.reviewOff;
10987
- return "error" in loaded.review ? loaded.review.error : undefined;
10988
- }
10989
10385
  function loadMasterPrompts() {
10990
10386
  try {
10991
10387
  return { master: readMasterPrompt("master"), worker: readMasterPrompt("worker") };
10992
10388
  } catch (error) {
10993
- return { error: error instanceof Error ? error.message : String(error) };
10389
+ return { master: "", worker: "", error: error instanceof Error ? error.message : String(error) };
10994
10390
  }
10995
10391
  }
10996
10392
  function rosterText(models) {
@@ -11001,7 +10397,7 @@ function rosterText(models) {
11001
10397
  }
11002
10398
 
11003
10399
  // watcher/card.ts
11004
- import { Text as Text7 } from "@earendil-works/pi-tui";
10400
+ import { Text as Text6 } from "@earendil-works/pi-tui";
11005
10401
 
11006
10402
  // watcher/messages.ts
11007
10403
  var msg8 = defineMessages({
@@ -11070,17 +10466,14 @@ var msg8 = defineMessages({
11070
10466
  // watcher/card.ts
11071
10467
  var WATCHER_MESSAGE_TYPE = "firecode-watcher-note";
11072
10468
  function adviceMessage(card) {
11073
- return wrapEnvelope("firecode_watcher", `${adviceHeadline(card)}
10469
+ return wrapEnvelope("firecode_watcher", `${msg8.card.headline(card.turnIndex)}
11074
10470
  ${card.note}
11075
10471
  ${msg8.card.weighNotice}`);
11076
10472
  }
11077
- function adviceHeadline(card) {
11078
- return msg8.card.headline(card.turnIndex);
11079
- }
11080
10473
  function registerWatcherCardRenderer(pi) {
11081
10474
  pi.registerMessageRenderer(WATCHER_MESSAGE_TYPE, (message, options, theme) => {
11082
10475
  const advice = parseAdvice(message.content);
11083
- return advice ? new AdviceLine(advice, options.expanded, theme) : new Text7(textOf(message.content), 0, 0);
10476
+ return advice ? new AdviceLine(advice, options.expanded, theme) : new Text6(textOf(message.content), 0, 0);
11084
10477
  });
11085
10478
  }
11086
10479
  function parseAdvice(content) {
@@ -11097,32 +10490,24 @@ class AdviceLine {
11097
10490
  card;
11098
10491
  expanded;
11099
10492
  theme;
11100
- fallback;
11101
10493
  constructor(card, expanded, theme) {
11102
10494
  this.card = card;
11103
10495
  this.expanded = expanded;
11104
10496
  this.theme = theme;
11105
- this.fallback = new Text7(`${card.headline} ${card.note}`, 0, 0);
11106
10497
  }
10498
+ invalidate() {}
11107
10499
  render(width) {
11108
10500
  const columns = Math.max(1, width);
11109
- try {
11110
- const bgFn = (text) => this.theme.bg("toolPendingBg", text);
11111
- if (this.expanded) {
11112
- const headline = this.theme.fg("warning", clip(oneLine(this.card.headline), columns));
11113
- const body = new Text7(this.theme.fg("dim", ` ${this.card.note}
10501
+ const bgFn = (text) => this.theme.bg("toolPendingBg", text);
10502
+ if (this.expanded) {
10503
+ const headline = this.theme.fg("warning", clip(oneLine(this.card.headline), columns));
10504
+ const body = new Text6(this.theme.fg("dim", ` ${this.card.note}
11114
10505
  ${msg8.card.expandedFooter}`), 0, 0);
11115
- return [paintBgLine(headline, columns, bgFn), ...body.render(columns)];
11116
- }
11117
- const firstLine = oneLine(this.card.note.split(/\r?\n/u, 1)[0] ?? "");
11118
- const line = clip(`${this.theme.fg("dim", RAIL)}${this.theme.fg("warning", msg8.card.label)}${this.theme.fg("dim", ` — ${firstLine}`)}`, columns);
11119
- return [paintBgLine(line, columns, bgFn)];
11120
- } catch {
11121
- return this.fallback.render(columns);
10506
+ return [paintBgLine(headline, columns, bgFn), ...body.render(columns)];
11122
10507
  }
11123
- }
11124
- invalidate() {
11125
- this.fallback.invalidate?.();
10508
+ const firstLine = oneLine(this.card.note.split(/\r?\n/u, 1)[0] ?? "");
10509
+ const line = clip(`${this.theme.fg("dim", RAIL)}${this.theme.fg("warning", msg8.card.label)}${this.theme.fg("dim", ` — ${firstLine}`)}`, columns);
10510
+ return [paintBgLine(line, columns, bgFn)];
11126
10511
  }
11127
10512
  }
11128
10513
 
@@ -11138,8 +10523,8 @@ async function createObserver(options) {
11138
10523
  model: options.model,
11139
10524
  thinking: options.thinking,
11140
10525
  tools: OBSERVER_TOOLS,
11141
- customTools: [adviseTool(() => advice, (next) => {
11142
- advice = next;
10526
+ customTools: [adviseTool(() => advice, (note) => {
10527
+ advice = note;
11143
10528
  })],
11144
10529
  systemPrompt: { mode: "replace", text: readPrompt(PROMPTS2, "watch") },
11145
10530
  contextFiles: true,
@@ -11169,7 +10554,7 @@ function adviseTool(current, capture) {
11169
10554
  const note = typeof params.note === "string" ? params.note.trim() : "";
11170
10555
  if (!note)
11171
10556
  throw new Error(msg8.advise.emptyNote);
11172
- capture({ note });
10557
+ capture(note);
11173
10558
  return { content: [{ type: "text", text: msg8.advise.recorded }], details: undefined };
11174
10559
  }
11175
10560
  };
@@ -11205,18 +10590,15 @@ function toolCallLine2(part, context) {
11205
10590
  function argumentText(key, value, context) {
11206
10591
  const text = typeof value === "string" ? value : JSON.stringify(value) ?? "";
11207
10592
  if (context === "full")
11208
- return oneLine2(text);
11209
- return BODY_KEYS.has(key) ? msg8.transcript.characters(text.length) : oneLine2(text);
10593
+ return oneLine(text);
10594
+ return BODY_KEYS.has(key) ? msg8.transcript.characters(text.length) : oneLine(text);
11210
10595
  }
11211
10596
  var BODY_KEYS = new Set(["content", "oldText", "newText", "old_text", "new_text", "edits"]);
11212
10597
  function blocks(content) {
11213
10598
  return Array.isArray(content) ? content.filter((part) => !!part && typeof part === "object") : [];
11214
10599
  }
11215
- function oneLine2(text) {
11216
- return text.replace(/\s+/gu, " ").trim();
11217
- }
11218
10600
  function clipText(text, budget) {
11219
- const single = oneLine2(text);
10601
+ const single = oneLine(text);
11220
10602
  return single.length > budget ? `${single.slice(0, budget)}…` : single;
11221
10603
  }
11222
10604
 
@@ -11265,8 +10647,8 @@ function registerWatcher(pi, dependencies = {}, subsession = false) {
11265
10647
  ctx.ui.setStatus("watcher", ctx.ui.theme.fg("dim", msg8.statusLabel));
11266
10648
  return owner;
11267
10649
  };
11268
- const speak = async (owner, advice, turnIndex) => {
11269
- const card = { note: advice.note, turnIndex };
10650
+ const speak = async (owner, note, turnIndex) => {
10651
+ const card = { note, turnIndex };
11270
10652
  try {
11271
10653
  await deliver(pi, owner.ctx, { customType: WATCHER_MESSAGE_TYPE, content: adviceMessage(card) });
11272
10654
  } catch (error) {
@@ -11387,9 +10769,10 @@ var REGISTRARS = {
11387
10769
  presets: registerPresets,
11388
10770
  stats: registerStats,
11389
10771
  claudeSub: registerClaudeSub,
11390
- openaiNative: registerOpenAINative
10772
+ openaiNative: registerOpenAINative,
10773
+ statusbar: registerStatusBar
11391
10774
  };
11392
- var MAIN_ONLY = new Set(["header", "tools", "presets", "stats"]);
10775
+ var MAIN_ONLY = new Set(["header", "tools", "presets", "stats", "statusbar"]);
11393
10776
  function registerFirecode(pi, role = "main") {
11394
10777
  let seeding = seedForMainSession(role);
11395
10778
  const { config, problems, featuresBroken } = loadConfig();
@@ -11402,13 +10785,12 @@ function registerFirecode(pi, role = "main") {
11402
10785
  continue;
11403
10786
  register(pi);
11404
10787
  }
11405
- if (config.features.statusbar !== false)
11406
- registerStatusBar(pi, subsession);
11407
10788
  if (config.features.watcher !== false)
11408
10789
  registerWatcher(pi, {}, subsession);
11409
10790
  if (config.features.master !== false)
11410
10791
  registerMaster(pi, {}, subsession);
11411
- registerHerdrProjection(pi, subsession);
10792
+ if (!subsession)
10793
+ registerHerdrProjection(pi);
11412
10794
  registerReview(pi, reviewEnabled, featuresBroken);
11413
10795
  if (problems.length === 0 && !seeding)
11414
10796
  return;