@a-t-h-i/bot-lobby 0.6.46 → 0.6.48

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.
@@ -23,11 +23,34 @@ export const reviewerSpec: RoleSpec = {
23
23
 
24
24
  const SEVERITIES: readonly Severity[] = ["critical", "major", "minor", "info"];
25
25
 
26
- function parseVerdict(text: string | undefined): Verdict {
27
- const value = (text ?? "").toUpperCase();
28
- if (value.includes("CHANGES REQUIRED") || value.includes("CHANGES_REQUIRED")) return "changes_required";
29
- if (value.includes("PASS")) return "pass";
30
- return "blocked";
26
+ /** Severity words reviewers use outside the contract's four, mapped onto them. */
27
+ const SEVERITY_ALIASES: ReadonlyArray<[RegExp, Severity]> = [
28
+ [/\b(?:blocker|blocking|severe)\b/, "critical"],
29
+ [/\bhigh\b/, "major"],
30
+ [/\b(?:low|nit|nitpick|trivial|cosmetic|optional|suggestion)\b/, "minor"],
31
+ [/\b(?:note|fyi)\b/, "info"],
32
+ ];
33
+
34
+ /**
35
+ * Verdict words in the order a reviewer writes them. FAIL is the word qa.md
36
+ * used to ask for: it means changes are required, not that QA could not run.
37
+ */
38
+ const VERDICT_TOKENS: ReadonlyArray<[RegExp, Verdict]> = [
39
+ [/\bCHANGES?[\s_-]*(?:REQUIRED|REQUESTED|NEEDED)\b|\b(?:NEEDS|REQUEST(?:ED)?)[\s_-]+CHANGES\b/, "changes_required"],
40
+ [/\bFAIL(?:ED|S|URE)?\b/, "changes_required"],
41
+ [/\bBLOCKED\b/, "blocked"],
42
+ [/\bPASS(?:ED|ES)?\b/, "pass"],
43
+ ];
44
+
45
+ /** The first verdict word in the text, so `PASS — no changes required` reads as the PASS it leads with. */
46
+ function verdictToken(text: string): Verdict | undefined {
47
+ const value = text.toUpperCase();
48
+ let best: { at: number; verdict: Verdict } | undefined;
49
+ for (const [pattern, verdict] of VERDICT_TOKENS) {
50
+ const match = pattern.exec(value);
51
+ if (match && (!best || match.index < best.at)) best = { at: match.index, verdict };
52
+ }
53
+ return best?.verdict;
31
54
  }
32
55
 
33
56
  /** The verdict token may sit in the heading itself (`## Verdict PASS`), leaving an empty body. */
@@ -38,14 +61,35 @@ function verdictHeading(sections: Map<string, string>): string | undefined {
38
61
  return undefined;
39
62
  }
40
63
 
64
+ /**
65
+ * The reviewer's verdict, or undefined when none can be read. The contract's
66
+ * `## Verdict` section comes first; failing that, a line that names the
67
+ * verdict (`**Verdict:** PASS`, `# Verdict: PASS`) and the line after it.
68
+ */
69
+ export function readVerdict(raw: string, sections = parseSections(raw)): Verdict | undefined {
70
+ const heading = verdictHeading(sections);
71
+ if (heading !== undefined) {
72
+ const body = (findSection(sections, "verdict") ?? "").split("\n").filter((line) => line.trim()).slice(0, 2).join("\n");
73
+ const found = verdictToken(`${heading}\n${body}`);
74
+ if (found) return found;
75
+ }
76
+ const lines = raw.split("\n");
77
+ const at = lines.findIndex((line) => /\bverdict\b/i.test(line));
78
+ if (at < 0) return undefined;
79
+ const next = lines.slice(at + 1).find((line) => line.trim()) ?? "";
80
+ return verdictToken(`${lines[at]}\n${next}`);
81
+ }
82
+
41
83
  /** The severity a finding was tagged with, or undefined when it carries none. */
42
84
  function taggedSeverity(text: string): Severity | undefined {
43
- const match = /^\[([^\]]+)\]/.exec(text);
44
- return SEVERITIES.find((candidate) => match?.[1]?.toLowerCase().includes(candidate));
85
+ const tag = /^\*{0,2}\[([^\]]+)\]/.exec(text)?.[1]?.toLowerCase();
86
+ if (!tag) return undefined;
87
+ return SEVERITIES.find((candidate) => tag.includes(candidate))
88
+ ?? SEVERITY_ALIASES.find(([pattern]) => pattern.test(tag))?.[1];
45
89
  }
46
90
 
47
91
  function parseFinding(text: string): ReviewFinding {
48
- const match = /^\[([^\]]+)\]\s*(.*)$/.exec(text);
92
+ const match = /^\*{0,2}\[([^\]]+)\]\*{0,2}\s*(.*)$/.exec(text);
49
93
  return { severity: taggedSeverity(text) ?? "info", text: (match?.[2] ?? text).trim() };
50
94
  }
51
95
 
@@ -55,9 +99,38 @@ export const UNVERIFIED_PASS = "unverified pass: PASS without executed checks un
55
99
  /** Minor and info findings never hold the gate; what the reviewer asked for is kept as follow-ups. */
56
100
  export const MINOR_ONLY = "CHANGES_REQUIRED for minor or info findings only: passed, its asks kept as follow-ups";
57
101
 
58
- /** A verification bullet names a command or check and its result (`- cmd — result`). */
102
+ /** No verdict could be read: the review did not finish, so it is run again rather than stopping the task. */
103
+ export const NO_VERDICT = "no readable verdict: the review did not finish, so nothing is known yet about the work";
104
+
105
+ /** A check names a command and its result: `cmd — result`, `cmd: result`, `✅ cmd`, `cmd (40/40)` or `cmd passed`. */
106
+ const CHECK_RESULT = /\S\s*(?:—|–|-{1,2}|:|=>|→|->)\s*\S|[✅✓✔❌✗✘]|\(\s*\d+\s*\/\s*\d+\s*\)|\b(?:pass(?:ed|es|ing)?|fail(?:ed|s|ing)?|clean|exit(?:ed)?\s*(?:code\s*)?\d+|\d+\s+(?:tests?|passing|passed|failing|failed))\b/i;
107
+
108
+ /** A table row's cells, or undefined for a line that is not a row. */
109
+ function tableCells(line: string): string[] | undefined {
110
+ if (!/^\|.*\|$/.test(line)) return undefined;
111
+ return line.slice(1, -1).split("|").map((cell) => cell.trim());
112
+ }
113
+
114
+ /**
115
+ * The checks under `## Verification`: bullets, numbered items and table rows
116
+ * that name a command and its result. A reviewer's list style never decides
117
+ * whether the gate believes it ran something.
118
+ */
59
119
  function executedChecks(verification: string): string[] {
60
- return bullets(verification).filter((entry) => /\S\s*(?:—|–|-{1,2}|:|=>|→)\s*\S/.test(entry));
120
+ const lines = verification.split("\n").map((line) => line.trim());
121
+ const checks: string[] = [];
122
+ lines.forEach((line, index) => {
123
+ const cells = tableCells(line);
124
+ if (cells) {
125
+ const separator = cells.every((cell) => /^:?-{2,}:?$/.test(cell));
126
+ const header = tableCells(lines[index + 1] ?? "")?.every((cell) => /^:?-{2,}:?$/.test(cell));
127
+ if (!separator && !header && cells.filter(Boolean).length >= 2) checks.push(cells.join(" — "));
128
+ return;
129
+ }
130
+ const item = /^(?:[-*+]|\d+[.)])\s+(\S.*)$/.exec(line)?.[1];
131
+ if (item && CHECK_RESULT.test(item)) checks.push(item);
132
+ });
133
+ return checks;
61
134
  }
62
135
 
63
136
  /** Parse a reviewer's markdown into a structured verdict (never throws). */
@@ -66,11 +139,15 @@ export function parseReviewResult(domain: Domain, raw: string): ReviewResult {
66
139
  const verification = findSection(sections, "verification") ?? "";
67
140
  const requiredChanges = bullets(findSection(sections, "required changes"));
68
141
  const entries = bullets(findSection(sections, "findings"));
69
- let verdict = parseVerdict(`${verdictHeading(sections) ?? ""}\n${findSection(sections, "verdict") ?? ""}`);
142
+ const read = readVerdict(raw, sections);
143
+ let verdict: Verdict = read ?? "changes_required";
70
144
  let downgraded: string | undefined;
71
145
  let relaxed: string | undefined;
72
146
  const checked = executedChecks(verification).length > 0;
73
- if (verdict === "pass" && !checked) {
147
+ if (!read) {
148
+ // Never a pass, and never a stop either: an unreadable review is run again.
149
+ requiredChanges.push("Re-run the QA gate: the last review returned no readable verdict.");
150
+ } else if (verdict === "pass" && !checked) {
74
151
  verdict = "changes_required";
75
152
  downgraded = UNVERIFIED_PASS;
76
153
  requiredChanges.push("Re-run the QA gate and record the checks actually executed, each as `- command — result`.");
@@ -93,6 +170,7 @@ export function parseReviewResult(domain: Domain, raw: string): ReviewResult {
93
170
  pushback: parsePushback(sections),
94
171
  ...(downgraded ? { downgraded } : {}),
95
172
  ...(relaxed ? { relaxed } : {}),
173
+ ...(read ? {} : { unread: NO_VERDICT }),
96
174
  raw,
97
175
  };
98
176
  }
@@ -100,7 +178,7 @@ export function parseReviewResult(domain: Domain, raw: string): ReviewResult {
100
178
  /** An unknown verdict must never be treated as acceptance (§42). */
101
179
  export function validateReviewResult(result: ReviewResult): string[] {
102
180
  const issues: string[] = [];
103
- if (!/##\s*verdict/i.test(result.raw)) issues.push("missing Verdict section");
181
+ if (result.unread) issues.push("missing Verdict section");
104
182
  if (result.downgraded) issues.push(result.downgraded);
105
183
  if (result.verdict !== "pass" && result.findings.length === 0 && result.requiredChanges.length === 0) {
106
184
  issues.push("non-pass verdict without findings or required changes");
@@ -31,6 +31,8 @@ export interface AgentModelConfig {
31
31
  mcpTools?: string[];
32
32
  /** Time limit per run; falls back to `workflow.agentTimeoutMs`. */
33
33
  timeoutMs?: number;
34
+ /** Compact this agent's context on its own as it fills (on unless false); the oracle's also updates the knowledge base. */
35
+ autoCompact?: boolean;
34
36
  }
35
37
 
36
38
  /** Scouts pick a model and a time limit only; their thinking is fixed at `SCOUT_THINKING`. */
@@ -40,6 +42,8 @@ export interface ScoutConfig {
40
42
  timeoutMs: number;
41
43
  /** Tool calls a scout may make: it is told to wrap up at 60% and refused more past this, so it reports with what it has; 0 = no limit. */
42
44
  maxToolCalls: number;
45
+ /** Compact a scout's context on its own as it fills (on unless false). */
46
+ autoCompact?: boolean;
43
47
  }
44
48
 
45
49
  /** Subagent kinds with their own settings entry. */
@@ -351,7 +355,7 @@ function normalizeAgent(base: AgentModelConfig, override: Partial<AgentModelConf
351
355
  const thinking = isThinkingLevel(merged.thinking) ? merged.thinking : base.thinking;
352
356
  const timeoutMs = positive(merged.timeoutMs) ?? base.timeoutMs;
353
357
  const mcpTools = normalizeMcpTools(merged.mcpTools);
354
- return { model: merged.model, thinking, ...(mcpTools.length ? { mcpTools } : {}), ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(timeoutMs ? { timeoutMs } : {}) };
358
+ return { model: merged.model, thinking, ...(mcpTools.length ? { mcpTools } : {}), ...(typeof merged.instructions === "string" ? { instructions: merged.instructions } : {}), ...(timeoutMs ? { timeoutMs } : {}), ...(merged.autoCompact === false ? { autoCompact: false } : {}) };
355
359
  }
356
360
 
357
361
  /** Scouts keep a model and a time limit; any thinking value in the file is dropped. */
@@ -363,6 +367,7 @@ function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig
363
367
  timeoutMs: positive(override?.timeoutMs) ?? base.timeoutMs,
364
368
  maxToolCalls: typeof override?.maxToolCalls === "number" && Number.isFinite(override.maxToolCalls) && override.maxToolCalls >= 0 ? Math.round(override.maxToolCalls) : base.maxToolCalls,
365
369
  ...(mcpTools.length ? { mcpTools } : {}),
370
+ ...(override?.autoCompact === false ? { autoCompact: false } : {}),
366
371
  };
367
372
  }
368
373
 
@@ -513,6 +518,13 @@ export interface AgentProfile {
513
518
  instructions?: string;
514
519
  /** Exact native MCP tool names this profile may call; empty grants none. */
515
520
  mcpTools?: string[];
521
+ /** False when its settings turn auto-compaction off. */
522
+ autoCompact?: boolean;
523
+ }
524
+
525
+ /** `{ autoCompact: false }` when an entry turns auto-compaction off, else nothing. */
526
+ function compactField(entry: { autoCompact?: boolean }): { autoCompact?: false } {
527
+ return entry.autoCompact === false ? { autoCompact: false } : {};
516
528
  }
517
529
 
518
530
  /** The settings entry a domain/role run draws from. */
@@ -537,10 +549,10 @@ export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role)
537
549
  const instructions = config.agents[domain].instructions;
538
550
  const fallback = config.workflow.agentTimeoutMs;
539
551
  if (kind === "scout") {
540
- return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(config.scout.mcpTools?.length ? { mcpTools: config.scout.mcpTools } : {}) };
552
+ return { kind, model: modelOf(config.scout.model), thinking: SCOUT_THINKING, timeoutMs: config.scout.timeoutMs || fallback, instructions, ...(config.scout.mcpTools?.length ? { mcpTools: config.scout.mcpTools } : {}), ...compactField(config.scout) };
541
553
  }
542
554
  const entry = kind === "researcher" ? config.researcher : config.agents[kind];
543
- return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
555
+ return { kind, model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? fallback, instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}), ...compactField(entry) };
544
556
  }
545
557
 
546
558
  /** The settings entry of a lobby agent (quick fix or planner). */
@@ -549,19 +561,19 @@ export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind):
549
561
  }
550
562
 
551
563
  /** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
552
- export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[] } {
564
+ export function lobbyAgentProfile(config: BotLobbyConfig, kind: LobbyAgentKind): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[]; autoCompact?: boolean } {
553
565
  const entry = lobbyAgentConfig(config, kind);
554
- return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
566
+ return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs: entry.timeoutMs ?? config.workflow.agentTimeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}), ...compactField(entry) };
555
567
  }
556
568
 
557
569
  /**
558
570
  * A planning panel seat's profile: the domain's (or the researcher's) model,
559
571
  * thinking and custom instructions, bounded by the planner's per-turn limit.
560
572
  */
561
- export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[] } {
573
+ export function panelMemberProfile(config: BotLobbyConfig, member: PanelMember): { model?: string; thinking: string; timeoutMs: number; instructions?: string; mcpTools?: string[]; autoCompact?: boolean } {
562
574
  const entry = member === "researcher" ? config.researcher : config.agents[member];
563
575
  const timeoutMs = config.planner.timeoutMs ?? config.workflow.agentTimeoutMs;
564
- return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}) };
576
+ return { model: modelOf(entry.model), thinking: entry.thinking, timeoutMs, instructions: entry.instructions, ...(entry.mcpTools?.length ? { mcpTools: entry.mcpTools } : {}), ...compactField(entry) };
565
577
  }
566
578
 
567
579
  /** Resolves the model, thinking and time limit one subagent run uses. */
@@ -89,6 +89,8 @@ export interface ReviewResult {
89
89
  downgraded?: string;
90
90
  /** Set when the engine passed a CHANGES_REQUIRED that named only minor or info findings. */
91
91
  relaxed?: string;
92
+ /** Set when no verdict could be read from the review (a failed or off-contract run). */
93
+ unread?: string;
92
94
  raw: string;
93
95
  }
94
96
 
@@ -178,7 +178,8 @@ export const DEFAULT_ID = "mist"
178
178
  * T3 Tools Inc., packages/shared/src/themePalettes.ts), their roles mapped onto
179
179
  * the shadcn variables: canvas is the background, surface the card, accent the
180
180
  * primary, accent surface the accent, error the destructive colour. Slate,
181
- * Sand, Rosewood, Aurora and Midnight are original complementary palettes.
181
+ * Sand, Rosewood, Aurora and Midnight, and Graphite, Lagoon, Dune, Plum, Moss and Crimson, are original
182
+ * complementary palettes.
182
183
  */
183
184
  export const PRESETS: readonly Palette[] = [
184
185
  { id: DEFAULT_ID, name: "Default" },
@@ -642,6 +643,282 @@ export const PRESETS: readonly Palette[] = [
642
643
  "chart-1": "#acbbed",
643
644
  },
644
645
  },
646
+ {
647
+ id: "graphite",
648
+ name: "Graphite",
649
+ light: {
650
+ "background": "oklch(0.985 0 260)",
651
+ "foreground": "oklch(0.25 0 260)",
652
+ "card": "oklch(0.997 0 260)",
653
+ "card-foreground": "oklch(0.25 0 260)",
654
+ "popover": "oklch(0.997 0 260)",
655
+ "popover-foreground": "oklch(0.25 0 260)",
656
+ "primary": "oklch(0.32 0 0)",
657
+ "primary-foreground": "oklch(0.99 0 0)",
658
+ "secondary": "oklch(0.94 0 260)",
659
+ "secondary-foreground": "oklch(0.25 0 260)",
660
+ "muted": "oklch(0.955 0 260)",
661
+ "muted-foreground": "oklch(0.48 0 260)",
662
+ "accent": "oklch(0.93 0 260)",
663
+ "accent-foreground": "oklch(0.25 0 260)",
664
+ "destructive": "oklch(0.56 0.2 25)",
665
+ "border": "oklch(0.9 0 260)",
666
+ "input": "oklch(0.85 0 260)",
667
+ "ring": "oklch(0.32 0 0)",
668
+ "chart-1": "oklch(0.32 0 0)",
669
+ },
670
+ dark: {
671
+ "background": "oklch(0.18 0 260)",
672
+ "foreground": "oklch(0.95 0 260)",
673
+ "card": "oklch(0.215 0 260)",
674
+ "card-foreground": "oklch(0.95 0 260)",
675
+ "popover": "oklch(0.245 0 260)",
676
+ "popover-foreground": "oklch(0.95 0 260)",
677
+ "primary": "oklch(0.92 0 0)",
678
+ "primary-foreground": "oklch(0.18 0 260)",
679
+ "secondary": "oklch(0.28 0 260)",
680
+ "secondary-foreground": "oklch(0.95 0 260)",
681
+ "muted": "oklch(0.26 0 260)",
682
+ "muted-foreground": "oklch(0.74 0 260)",
683
+ "accent": "oklch(0.32 0 260)",
684
+ "accent-foreground": "oklch(0.95 0 260)",
685
+ "destructive": "oklch(0.7 0.17 22)",
686
+ "border": "oklch(0.32 0 260)",
687
+ "input": "oklch(0.38 0 260)",
688
+ "ring": "oklch(0.92 0 0)",
689
+ "chart-1": "oklch(0.92 0 0)",
690
+ },
691
+ },
692
+ {
693
+ id: "lagoon",
694
+ name: "Lagoon",
695
+ light: {
696
+ "background": "oklch(0.985 0.0096 200)",
697
+ "foreground": "oklch(0.25 0.03 200)",
698
+ "card": "oklch(0.997 0.0048 200)",
699
+ "card-foreground": "oklch(0.25 0.03 200)",
700
+ "popover": "oklch(0.997 0.0048 200)",
701
+ "popover-foreground": "oklch(0.25 0.03 200)",
702
+ "primary": "oklch(0.55 0.12 200)",
703
+ "primary-foreground": "oklch(0.99 0 0)",
704
+ "secondary": "oklch(0.94 0.03 200)",
705
+ "secondary-foreground": "oklch(0.25 0.03 200)",
706
+ "muted": "oklch(0.955 0.018 200)",
707
+ "muted-foreground": "oklch(0.48 0.036 200)",
708
+ "accent": "oklch(0.93 0.048 200)",
709
+ "accent-foreground": "oklch(0.25 0.03 200)",
710
+ "destructive": "oklch(0.56 0.2 25)",
711
+ "border": "oklch(0.9 0.024 200)",
712
+ "input": "oklch(0.85 0.03 200)",
713
+ "ring": "oklch(0.55 0.12 200)",
714
+ "chart-1": "oklch(0.55 0.12 200)",
715
+ },
716
+ dark: {
717
+ "background": "oklch(0.18 0.024 200)",
718
+ "foreground": "oklch(0.95 0.0096 200)",
719
+ "card": "oklch(0.215 0.0264 200)",
720
+ "card-foreground": "oklch(0.95 0.0096 200)",
721
+ "popover": "oklch(0.245 0.0264 200)",
722
+ "popover-foreground": "oklch(0.95 0.0096 200)",
723
+ "primary": "oklch(0.76 0.102 200)",
724
+ "primary-foreground": "oklch(0.18 0.024 200)",
725
+ "secondary": "oklch(0.28 0.03 200)",
726
+ "secondary-foreground": "oklch(0.95 0.0096 200)",
727
+ "muted": "oklch(0.26 0.024 200)",
728
+ "muted-foreground": "oklch(0.74 0.0216 200)",
729
+ "accent": "oklch(0.32 0.048 200)",
730
+ "accent-foreground": "oklch(0.95 0.0096 200)",
731
+ "destructive": "oklch(0.7 0.17 22)",
732
+ "border": "oklch(0.32 0.03 200)",
733
+ "input": "oklch(0.38 0.03 200)",
734
+ "ring": "oklch(0.76 0.102 200)",
735
+ "chart-1": "oklch(0.76 0.102 200)",
736
+ },
737
+ },
738
+ {
739
+ id: "dune",
740
+ name: "Dune",
741
+ light: {
742
+ "background": "oklch(0.985 0.0096 70)",
743
+ "foreground": "oklch(0.25 0.03 70)",
744
+ "card": "oklch(0.997 0.0048 70)",
745
+ "card-foreground": "oklch(0.25 0.03 70)",
746
+ "popover": "oklch(0.997 0.0048 70)",
747
+ "popover-foreground": "oklch(0.25 0.03 70)",
748
+ "primary": "oklch(0.55 0.12 70)",
749
+ "primary-foreground": "oklch(0.99 0 0)",
750
+ "secondary": "oklch(0.94 0.03 70)",
751
+ "secondary-foreground": "oklch(0.25 0.03 70)",
752
+ "muted": "oklch(0.955 0.018 70)",
753
+ "muted-foreground": "oklch(0.48 0.036 70)",
754
+ "accent": "oklch(0.93 0.048 70)",
755
+ "accent-foreground": "oklch(0.25 0.03 70)",
756
+ "destructive": "oklch(0.56 0.2 25)",
757
+ "border": "oklch(0.9 0.024 70)",
758
+ "input": "oklch(0.85 0.03 70)",
759
+ "ring": "oklch(0.55 0.12 70)",
760
+ "chart-1": "oklch(0.55 0.12 70)",
761
+ },
762
+ dark: {
763
+ "background": "oklch(0.18 0.024 70)",
764
+ "foreground": "oklch(0.95 0.0096 70)",
765
+ "card": "oklch(0.215 0.0264 70)",
766
+ "card-foreground": "oklch(0.95 0.0096 70)",
767
+ "popover": "oklch(0.245 0.0264 70)",
768
+ "popover-foreground": "oklch(0.95 0.0096 70)",
769
+ "primary": "oklch(0.76 0.102 70)",
770
+ "primary-foreground": "oklch(0.18 0.024 70)",
771
+ "secondary": "oklch(0.28 0.03 70)",
772
+ "secondary-foreground": "oklch(0.95 0.0096 70)",
773
+ "muted": "oklch(0.26 0.024 70)",
774
+ "muted-foreground": "oklch(0.74 0.0216 70)",
775
+ "accent": "oklch(0.32 0.048 70)",
776
+ "accent-foreground": "oklch(0.95 0.0096 70)",
777
+ "destructive": "oklch(0.7 0.17 22)",
778
+ "border": "oklch(0.32 0.03 70)",
779
+ "input": "oklch(0.38 0.03 70)",
780
+ "ring": "oklch(0.76 0.102 70)",
781
+ "chart-1": "oklch(0.76 0.102 70)",
782
+ },
783
+ },
784
+ {
785
+ id: "plum",
786
+ name: "Plum",
787
+ light: {
788
+ "background": "oklch(0.985 0.0112 320)",
789
+ "foreground": "oklch(0.25 0.035 320)",
790
+ "card": "oklch(0.997 0.0056 320)",
791
+ "card-foreground": "oklch(0.25 0.035 320)",
792
+ "popover": "oklch(0.997 0.0056 320)",
793
+ "popover-foreground": "oklch(0.25 0.035 320)",
794
+ "primary": "oklch(0.55 0.14 320)",
795
+ "primary-foreground": "oklch(0.99 0 0)",
796
+ "secondary": "oklch(0.94 0.035 320)",
797
+ "secondary-foreground": "oklch(0.25 0.035 320)",
798
+ "muted": "oklch(0.955 0.021 320)",
799
+ "muted-foreground": "oklch(0.48 0.042 320)",
800
+ "accent": "oklch(0.93 0.056 320)",
801
+ "accent-foreground": "oklch(0.25 0.035 320)",
802
+ "destructive": "oklch(0.56 0.2 25)",
803
+ "border": "oklch(0.9 0.028 320)",
804
+ "input": "oklch(0.85 0.035 320)",
805
+ "ring": "oklch(0.55 0.14 320)",
806
+ "chart-1": "oklch(0.55 0.14 320)",
807
+ },
808
+ dark: {
809
+ "background": "oklch(0.18 0.028 320)",
810
+ "foreground": "oklch(0.95 0.0112 320)",
811
+ "card": "oklch(0.215 0.0308 320)",
812
+ "card-foreground": "oklch(0.95 0.0112 320)",
813
+ "popover": "oklch(0.245 0.0308 320)",
814
+ "popover-foreground": "oklch(0.95 0.0112 320)",
815
+ "primary": "oklch(0.76 0.119 320)",
816
+ "primary-foreground": "oklch(0.18 0.028 320)",
817
+ "secondary": "oklch(0.28 0.035 320)",
818
+ "secondary-foreground": "oklch(0.95 0.0112 320)",
819
+ "muted": "oklch(0.26 0.028 320)",
820
+ "muted-foreground": "oklch(0.74 0.0252 320)",
821
+ "accent": "oklch(0.32 0.056 320)",
822
+ "accent-foreground": "oklch(0.95 0.0112 320)",
823
+ "destructive": "oklch(0.7 0.17 22)",
824
+ "border": "oklch(0.32 0.035 320)",
825
+ "input": "oklch(0.38 0.035 320)",
826
+ "ring": "oklch(0.76 0.119 320)",
827
+ "chart-1": "oklch(0.76 0.119 320)",
828
+ },
829
+ },
830
+ {
831
+ id: "moss",
832
+ name: "Moss",
833
+ light: {
834
+ "background": "oklch(0.985 0.0088 135)",
835
+ "foreground": "oklch(0.25 0.0275 135)",
836
+ "card": "oklch(0.997 0.0044 135)",
837
+ "card-foreground": "oklch(0.25 0.0275 135)",
838
+ "popover": "oklch(0.997 0.0044 135)",
839
+ "popover-foreground": "oklch(0.25 0.0275 135)",
840
+ "primary": "oklch(0.55 0.11 135)",
841
+ "primary-foreground": "oklch(0.99 0 0)",
842
+ "secondary": "oklch(0.94 0.0275 135)",
843
+ "secondary-foreground": "oklch(0.25 0.0275 135)",
844
+ "muted": "oklch(0.955 0.0165 135)",
845
+ "muted-foreground": "oklch(0.48 0.033 135)",
846
+ "accent": "oklch(0.93 0.044 135)",
847
+ "accent-foreground": "oklch(0.25 0.0275 135)",
848
+ "destructive": "oklch(0.56 0.2 25)",
849
+ "border": "oklch(0.9 0.022 135)",
850
+ "input": "oklch(0.85 0.0275 135)",
851
+ "ring": "oklch(0.55 0.11 135)",
852
+ "chart-1": "oklch(0.55 0.11 135)",
853
+ },
854
+ dark: {
855
+ "background": "oklch(0.18 0.022 135)",
856
+ "foreground": "oklch(0.95 0.0088 135)",
857
+ "card": "oklch(0.215 0.0242 135)",
858
+ "card-foreground": "oklch(0.95 0.0088 135)",
859
+ "popover": "oklch(0.245 0.0242 135)",
860
+ "popover-foreground": "oklch(0.95 0.0088 135)",
861
+ "primary": "oklch(0.76 0.0935 135)",
862
+ "primary-foreground": "oklch(0.18 0.022 135)",
863
+ "secondary": "oklch(0.28 0.0275 135)",
864
+ "secondary-foreground": "oklch(0.95 0.0088 135)",
865
+ "muted": "oklch(0.26 0.022 135)",
866
+ "muted-foreground": "oklch(0.74 0.0198 135)",
867
+ "accent": "oklch(0.32 0.044 135)",
868
+ "accent-foreground": "oklch(0.95 0.0088 135)",
869
+ "destructive": "oklch(0.7 0.17 22)",
870
+ "border": "oklch(0.32 0.0275 135)",
871
+ "input": "oklch(0.38 0.0275 135)",
872
+ "ring": "oklch(0.76 0.0935 135)",
873
+ "chart-1": "oklch(0.76 0.0935 135)",
874
+ },
875
+ },
876
+ {
877
+ id: "crimson",
878
+ name: "Crimson",
879
+ light: {
880
+ "background": "oklch(0.985 0.0136 20)",
881
+ "foreground": "oklch(0.25 0.0425 20)",
882
+ "card": "oklch(0.997 0.0068 20)",
883
+ "card-foreground": "oklch(0.25 0.0425 20)",
884
+ "popover": "oklch(0.997 0.0068 20)",
885
+ "popover-foreground": "oklch(0.25 0.0425 20)",
886
+ "primary": "oklch(0.55 0.17 20)",
887
+ "primary-foreground": "oklch(0.99 0 0)",
888
+ "secondary": "oklch(0.94 0.0425 20)",
889
+ "secondary-foreground": "oklch(0.25 0.0425 20)",
890
+ "muted": "oklch(0.955 0.0255 20)",
891
+ "muted-foreground": "oklch(0.48 0.051 20)",
892
+ "accent": "oklch(0.93 0.068 20)",
893
+ "accent-foreground": "oklch(0.25 0.0425 20)",
894
+ "destructive": "oklch(0.56 0.2 25)",
895
+ "border": "oklch(0.9 0.034 20)",
896
+ "input": "oklch(0.85 0.0425 20)",
897
+ "ring": "oklch(0.55 0.17 20)",
898
+ "chart-1": "oklch(0.55 0.17 20)",
899
+ },
900
+ dark: {
901
+ "background": "oklch(0.18 0.034 20)",
902
+ "foreground": "oklch(0.95 0.0136 20)",
903
+ "card": "oklch(0.215 0.0374 20)",
904
+ "card-foreground": "oklch(0.95 0.0136 20)",
905
+ "popover": "oklch(0.245 0.0374 20)",
906
+ "popover-foreground": "oklch(0.95 0.0136 20)",
907
+ "primary": "oklch(0.76 0.1445 20)",
908
+ "primary-foreground": "oklch(0.18 0.034 20)",
909
+ "secondary": "oklch(0.28 0.0425 20)",
910
+ "secondary-foreground": "oklch(0.95 0.0136 20)",
911
+ "muted": "oklch(0.26 0.034 20)",
912
+ "muted-foreground": "oklch(0.74 0.0306 20)",
913
+ "accent": "oklch(0.32 0.068 20)",
914
+ "accent-foreground": "oklch(0.95 0.0136 20)",
915
+ "destructive": "oklch(0.7 0.17 22)",
916
+ "border": "oklch(0.32 0.0425 20)",
917
+ "input": "oklch(0.38 0.0425 20)",
918
+ "ring": "oklch(0.76 0.1445 20)",
919
+ "chart-1": "oklch(0.76 0.1445 20)",
920
+ },
921
+ },
645
922
  ]
646
923
 
647
924
  /** Colours for a preview swatch, per mode: the default's own values by hand, the rest from the theme. */
@@ -1289,7 +1289,9 @@ function qaReport(header: string, result: ReviewResult, decision: LoopDecision,
1289
1289
  : decision === "waived"
1290
1290
  ? "The user accepted the work as it is, without a QA pass. Call action=complete now with a short summary that names what QA still asked for."
1291
1291
  : decision === "iterate"
1292
- ? "The QA gate did not pass: delegate the required changes to the owning domain, then re-run action=qa."
1292
+ ? result.unread
1293
+ ? "QA returned no readable verdict, so nothing is known about the work yet: re-run action=qa; there is nothing to fix first."
1294
+ : "The QA gate did not pass: delegate the required changes to the owning domain, then re-run action=qa."
1293
1295
  : "The review limit is reached and the user did not accept the work: mark the task blocked and tell the user what QA still asks for. They can accept it with /bot-lobby accept.",
1294
1296
  result.pushback ? pushbackLine(result.pushback, "qa") : "",
1295
1297
  issues.length > 0 ? `Issues: ${issues.join("; ")}` : "",