@ggui-ai/ui-gen 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/{adapter-BJwn74rx.d.ts → adapter-h4_V69LI.d.ts} +1 -1
  2. package/dist/adapters/base.d.ts +6 -1
  3. package/dist/adapters/base.js.map +1 -1
  4. package/dist/adapters/generation-dispatch.d.ts +2 -2
  5. package/dist/adapters/generation-dispatch.js +233 -17
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.js +36 -14
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/advanced/index.d.ts +4 -4
  10. package/dist/advanced/index.js +268 -28
  11. package/dist/advanced/index.js.map +1 -1
  12. package/dist/boilerplate.js +4 -6
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +1 -7
  15. package/dist/check/index.js +50 -4
  16. package/dist/check/index.js.map +1 -1
  17. package/dist/{create-ui-generator-N4gRXpcL.d.ts → create-ui-generator-BrZHF0cf.d.ts} +2 -2
  18. package/dist/{dispatch-BUoXRj_r.d.ts → dispatch-B07TI30d.d.ts} +1 -1
  19. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  20. package/dist/evaluation/axis-checks/index.js +97 -1
  21. package/dist/evaluation/axis-checks/index.js.map +1 -1
  22. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  23. package/dist/evaluation/axis-checks/registry.js +97 -1
  24. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  25. package/dist/evaluation/index.d.ts +22 -12
  26. package/dist/evaluation/index.js +138 -14
  27. package/dist/evaluation/index.js.map +1 -1
  28. package/dist/evaluation/types.d.ts +1 -1
  29. package/dist/harness/check/runtime-render/index.d.ts +3 -3
  30. package/dist/harness/index.d.ts +1 -1
  31. package/dist/harness/index.js +6 -6
  32. package/dist/harness/index.js.map +1 -1
  33. package/dist/harness/prompts.js +1 -4
  34. package/dist/harness/prompts.js.map +1 -1
  35. package/dist/harness/result-types.d.ts +14 -3
  36. package/dist/harness/result-types.js.map +1 -1
  37. package/dist/harness/runtime.d.ts +1 -1
  38. package/dist/harness/runtime.js +6 -7
  39. package/dist/harness/runtime.js.map +1 -1
  40. package/dist/harness/types-public.d.ts +1 -1
  41. package/dist/harness/types-public.js +4 -6
  42. package/dist/harness/types-public.js.map +1 -1
  43. package/dist/harness/validator-trace-sink.d.ts +1 -1
  44. package/dist/index.d.ts +6 -6
  45. package/dist/index.js +268 -28
  46. package/dist/index.js.map +1 -1
  47. package/dist/patch.d.ts +1 -1
  48. package/dist/{types-public-B7wn4ce5.d.ts → types-public-h8IMnYuh.d.ts} +4 -0
  49. package/dist/{visual-evaluator-C9L-o6ap.d.ts → visual-evaluator-C2strxqD.d.ts} +1 -1
  50. package/dist/workflows.d.ts +1 -1
  51. package/package.json +13 -13
  52. package/src/adapters/base.ts +6 -1
  53. package/src/adapters/claude/message-parser.ts +5 -6
  54. package/src/adapters/claude/sdk-tool-pins.ts +44 -0
  55. package/src/adapters/claude/sdk.ts +3 -3
  56. package/src/adapters/claude/source-capture.ts +20 -0
  57. package/src/adapters/provider-router.ts +1 -0
  58. package/src/boilerplate/system-prompt.ts +3 -2
  59. package/src/check/run-tier0.ts +61 -0
  60. package/src/create-ui-generator.ts +4 -2
  61. package/src/design-system-docs.ts +2 -1
  62. package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
  63. package/src/evaluation/axis-checks/helpers.ts +125 -0
  64. package/src/evaluation/criteria/resolve.ts +19 -1
  65. package/src/evaluation/evaluator.ts +2 -2
  66. package/src/evaluation/llm-evaluator.ts +2 -2
  67. package/src/evaluation/loop.ts +25 -4
  68. package/src/evaluation/message-parsing.ts +11 -12
  69. package/src/evaluation/types-public.ts +4 -0
  70. package/src/evaluation/visual-evaluator.ts +1 -1
  71. package/src/harness/coding/assemble-result.ts +1 -0
  72. package/src/harness/coding/dupe-break.ts +70 -0
  73. package/src/harness/coding/generate-task-runner.ts +5 -1
  74. package/src/harness/coding/run-coding-turn.ts +56 -1
  75. package/src/harness/result-types.ts +12 -0
  76. package/src/index.ts +1 -1
  77. package/src/taught-tokens.ts +5 -6
@@ -1,6 +1,7 @@
1
1
  // packages/ui-gen/src/evaluation/loop.ts
2
2
 
3
3
  import { query, type McpServerConfig } from '@anthropic-ai/claude-agent-sdk';
4
+ import { sdkToolPins } from '../adapters/claude/sdk-tool-pins';
4
5
  import { runEvaluation } from './evaluator';
5
6
  import {
6
7
  extractCompiledCodeFromMessage,
@@ -39,8 +40,13 @@ export interface EvaluationLoopOptions {
39
40
  cwd?: string;
40
41
  /** MCP servers available to the fix agent */
41
42
  mcpServers?: Record<string, McpServerConfig>;
42
- /** Tools the fix agent is allowed to call */
43
+ /** Tool names to approve. Absent → every tool of every server in `mcpServers` (ggui#1714). */
43
44
  allowedTools?: string[];
45
+ /**
46
+ * CLI built-in tools to offer the fix agent (e.g. `['Write']`). Default: none.
47
+ * An explicit opt-in: a built-in that writes or executes acts on the host (ggui#1714).
48
+ */
49
+ builtinTools?: readonly string[];
44
50
  /** LLM model for fix rounds */
45
51
  model?: string;
46
52
  /** Environment variables (includes BYOK credentials) */
@@ -90,6 +96,18 @@ export async function runEvaluationLoop(
90
96
  let round = 0;
91
97
  const maxRounds = Math.min(config.maxRounds ?? 3, MAX_EVAL_ROUNDS_HARD_LIMIT);
92
98
 
99
+ // ggui#1714 — a fix round runs with no CLI built-ins unless asked for. With no
100
+ // MCP server and no built-in either, the fix agent has nothing that can return
101
+ // code, and every fix round would silently hand back the original: refuse.
102
+ const offersATool =
103
+ Object.keys(generatorOptions?.mcpServers ?? {}).length > 0 || (generatorOptions?.builtinTools?.length ?? 0) > 0;
104
+ if (maxRounds > 1 && !offersATool) {
105
+ throw new Error(
106
+ 'runEvaluationLoop: a fix round needs a tool that can return code — pass generatorOptions.mcpServers ' +
107
+ "(e.g. a server with compile_component) or opt into a built-in with generatorOptions.builtinTools (e.g. ['Write']).",
108
+ );
109
+ }
110
+
93
111
  while (round < maxRounds) {
94
112
  round++;
95
113
 
@@ -145,12 +163,15 @@ export async function runEvaluationLoop(
145
163
  maxTurns: 15,
146
164
  maxBudgetUsd: config.maxBudgetPerFix,
147
165
  env,
148
- permissionMode: 'bypassPermissions',
149
- allowDangerouslySkipPermissions: true,
150
166
  ...(cliPath && { pathToClaudeCodeExecutable: cliPath }),
151
167
  ...(generatorOptions?.cwd && { cwd: generatorOptions.cwd }),
152
168
  ...(generatorOptions?.mcpServers && { mcpServers: generatorOptions.mcpServers }),
153
- ...(generatorOptions?.allowedTools && { allowedTools: generatorOptions.allowedTools }),
169
+ // ggui#1714 — no CLI built-ins unless asked for, no host settings, no permission bypass.
170
+ ...sdkToolPins({
171
+ mcpServers: generatorOptions?.mcpServers,
172
+ allowedTools: generatorOptions?.allowedTools,
173
+ builtinTools: generatorOptions?.builtinTools,
174
+ }),
154
175
  ...(generatorOptions?.model && { model: generatorOptions.model }),
155
176
  ...(generatorOptions?.stderr && { stderr: generatorOptions.stderr }),
156
177
  },
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { EvaluationResult } from './types';
13
+ import { sourceCodeFromToolUse } from '../adapters/claude/source-capture';
13
14
 
14
15
  // ── SDK message type aliases ────────────────────────────────────────
15
16
 
@@ -137,13 +138,14 @@ export function extractCompiledCodeFromMessage(message: SdkMessage): string | un
137
138
  // ── sourceCode extraction ───────────────────────────────────────────
138
139
 
139
140
  /**
140
- * Extract source code from an assistant's Write tool_use message.
141
+ * Extract source code from an assistant message's tool_use blocks.
141
142
  *
142
- * Looks for `tool_use` blocks with `name === 'Write'` and returns
143
- * the `input.content` string. If multiple Write calls exist, returns
144
- * the last one (the final version).
143
+ * Reads the bridged compile tool (`*__compile_component`, `input.code`)
144
+ * and the CLI's `Write` (`input.content`, offered only by opt-in), by the
145
+ * one rule in `adapters/claude/source-capture.ts` (ggui#1714). If several
146
+ * calls carry source, returns the last one (the final version).
145
147
  *
146
- * Used by: loop.ts, generator.ts
148
+ * Used by: loop.ts
147
149
  */
148
150
  export function extractSourceCodeFromMessage(message: SdkMessage): string | undefined {
149
151
  if ((message.type as string) !== 'assistant') return undefined;
@@ -155,12 +157,9 @@ export function extractSourceCodeFromMessage(message: SdkMessage): string | unde
155
157
  if (!Array.isArray(content)) return undefined;
156
158
 
157
159
  for (const block of content) {
158
- if (block.type === 'tool_use' && block.name === 'Write') {
159
- const input = block.input as { content?: string } | undefined;
160
- if (input?.content) {
161
- sourceCode = input.content;
162
- }
163
- }
160
+ if (block.type !== 'tool_use') continue;
161
+ const value = sourceCodeFromToolUse(block.name, block.input);
162
+ if (value !== undefined) sourceCode = value;
164
163
  }
165
164
 
166
165
  return sourceCode;
@@ -183,7 +182,7 @@ export function extractCompiledCode(messages: SdkMessage[]): string | undefined
183
182
  }
184
183
 
185
184
  /**
186
- * Scan all messages for the last sourceCode from Write tool_use.
185
+ * Scan all messages for the last sourceCode a tool_use carried.
187
186
  *
188
187
  * Convenience wrapper for tests and one-shot extraction.
189
188
  */
@@ -376,6 +376,10 @@ export interface CanvasJudgeRecord {
376
376
  /**
377
377
  * ggui#1687 — the model asked for, and every model the provider said answered this canvas's calls (scoring and
378
378
  * criteria), deduplicated. `served` is empty when no provider named one: unknown, never "the requested model".
379
+ * When the request names an alias and the provider echoes the alias (measured on a hosted route, 125 of 125
380
+ * canvases), `served` confirms the alias was honoured but cannot tell two snapshots behind it apart. A judge that
381
+ * must is pinned to a dated snapshot id instead. With the alias, a snapshot change shows only as a step on a date in
382
+ * the judge's pooled within-call spread, read by week. That is a read someone runs, not a monitor.
379
383
  */
380
384
  readonly models?: { readonly requested: string; readonly served: readonly string[] };
381
385
  }
@@ -2004,7 +2004,7 @@ function parseCriteriaAnswers(raw: unknown): { answers?: CriteriaAnswer[]; count
2004
2004
 
2005
2005
  function getDefaultVisualModel(provider: 'claude' | 'google'): string {
2006
2006
  switch (provider) {
2007
- case 'claude': return 'claude-haiku-4-5-20251001';
2007
+ case 'claude': return 'claude-haiku-5-5';
2008
2008
  case 'google': return 'gemini-3-flash-preview';
2009
2009
  }
2010
2010
  }
@@ -132,6 +132,7 @@ export async function assembleGenerationResult(
132
132
  : telemetry.evalResult,
133
133
  // ggui#404 — present only when the same-exchange guard ended the run.
134
134
  ...(telemetry.sameExchangeBreak !== undefined ? { sameExchangeBreak: telemetry.sameExchangeBreak } : {}),
135
+ ...(telemetry.sameFailureBreak !== undefined ? { sameFailureBreak: telemetry.sameFailureBreak } : {}),
135
136
  needsBackgroundImprovement,
136
137
  selfCheckPassed: telemetry.selfCheckPassed,
137
138
  timing,
@@ -102,6 +102,11 @@ export interface DupeBreakState {
102
102
  * information-free — the model learned nothing it didn't already
103
103
  * have. 1 = first occurrence; 2 = one repeat (nudge); 3 = break. */
104
104
  consecutiveIdenticalExchanges: number;
105
+ /** ggui#1791 — for each failure signature ({@link failureSignatures}), how
106
+ * many failed writes (`apply_changes` / `rewrite`) have reported it since
107
+ * the last write that passed, whatever the patch and whatever failed in
108
+ * between. Cleared when a write passes. */
109
+ failureCounts: Map<string, number>;
105
110
  }
106
111
 
107
112
  /**
@@ -147,6 +152,70 @@ export const SAME_EXCHANGE_NUDGE =
147
152
  "you already have and emit an apply_changes (or scoped) call now; one " +
148
153
  "more identical repeat terminates the run.";
149
154
 
155
+ /**
156
+ * ggui#1791 — the failures a failed write reported, one signature per failing
157
+ * check, with what changes between two identical failures removed: every
158
+ * digit (line and column numbers, counts) and runs of whitespace.
159
+ *
160
+ * A self-check result lists its violations as `[category] description` lines;
161
+ * each one is its own signature, so a check that fails on every turn reads as
162
+ * the same failure whether or not another check fails beside it. A result with
163
+ * no violation line (a build error, a rejected patch) is one signature: its
164
+ * text without the `Committed <oid>: "<message>"` line, which carries a fresh
165
+ * oid and the model's own message every turn.
166
+ *
167
+ * The patch fingerprint above keys on the PATCH, so the same failure under a
168
+ * different patch each turn (a reworded comment, a moved line) never reads as
169
+ * stuck, and a run goes to the turn cap. These key on the FAILURE.
170
+ */
171
+ export function failureSignatures(resultText: string): string[] {
172
+ const norm = (text: string): string => text.replace(/[0-9]+/g, "#").replace(/\s+/g, " ").trim();
173
+ const violations = [...resultText.matchAll(/^\[[^\]\n]+\] [^\n]+/gm)].map((m) => norm(m[0]));
174
+ if (violations.length > 0) return [...new Set(violations)];
175
+ const rest = norm(resultText.replace(/^Committed [^\n]*\n/, ""));
176
+ return rest.length > 0 ? [rest] : [];
177
+ }
178
+
179
+ /** The failure a write has reported most often since the last pass, and how often. */
180
+ export interface RepeatedFailure {
181
+ readonly signature: string;
182
+ readonly repeats: number;
183
+ }
184
+
185
+ /**
186
+ * ggui#1791 — record one failed write: count each of its signatures once, and
187
+ * return the one now reported most often since the last passing write
188
+ * (undefined when the result carried no recognisable failure).
189
+ */
190
+ export function recordWriteFailure(state: DupeBreakState, resultText: string): RepeatedFailure | undefined {
191
+ let top: RepeatedFailure | undefined;
192
+ for (const signature of failureSignatures(resultText)) {
193
+ const repeats = (state.failureCounts.get(signature) ?? 0) + 1;
194
+ state.failureCounts.set(signature, repeats);
195
+ if (top === undefined || repeats > top.repeats) top = { signature, repeats };
196
+ }
197
+ return top;
198
+ }
199
+
200
+ /** ggui#1791 — a write passed: every count starts over. */
201
+ export function clearWriteFailures(state: DupeBreakState): void {
202
+ state.failureCounts.clear();
203
+ }
204
+
205
+ /** A failure reported for the third time since the last pass ⇒ one explicit nudge. */
206
+ export const SAME_FAILURE_NUDGE_AT = 3;
207
+
208
+ /** A failure reported for the fifth time since the last pass ⇒ the loop is not
209
+ * converging; the caller ends the run, which ships what the turn cap would
210
+ * have shipped. */
211
+ export const SAME_FAILURE_BREAK_AT = 5;
212
+
213
+ /** Appended to the tool result of the {@link SAME_FAILURE_NUDGE_AT}th identical failure. */
214
+ export const SAME_FAILURE_NUDGE =
215
+ "\n\n[harness] This same failure has now been reported for three of your " +
216
+ "patches. Changing other code does not clear it: re-read that check above and " +
217
+ "change exactly what it names. Two more reports of it end the run.";
218
+
150
219
  export function createDupeBreakState(): DupeBreakState {
151
220
  return {
152
221
  lastFailedPatchFingerprint: null,
@@ -164,6 +233,7 @@ export function createDupeBreakState(): DupeBreakState {
164
233
  consecutiveNoToolCalls: 0,
165
234
  lastExchangeKey: null,
166
235
  consecutiveIdenticalExchanges: 0,
236
+ failureCounts: new Map(),
167
237
  };
168
238
  }
169
239
 
@@ -25,7 +25,7 @@ import {
25
25
  import { runEvalRound } from "./run-eval-round.js";
26
26
  import type { TokenUsage } from "./run-eval-round.js";
27
27
  import type { VisualLegTokens } from "../../evaluation/visual-evaluator.js";
28
- import type { SameExchangeBreak } from "../result-types.js";
28
+ import type { SameExchangeBreak, SameFailureBreak } from "../result-types.js";
29
29
 
30
30
  type PreWarmedEvalContext =
31
31
  import("../../evaluation/llm-evaluator.js").PreWarmedEvalContext;
@@ -105,6 +105,8 @@ export interface GenerateTelemetry {
105
105
  * undefined unless that guard is what stopped the loop.
106
106
  */
107
107
  sameExchangeBreak: SameExchangeBreak | undefined;
108
+ /** ggui#1791 — set when the same-failure guard ended the run. */
109
+ sameFailureBreak: SameFailureBreak | undefined;
108
110
  /** Latest compiled code — empty string until the first self-check pass. */
109
111
  compiledCode: string;
110
112
  /**
@@ -147,6 +149,7 @@ export function createTelemetry(): GenerateTelemetry {
147
149
  probeRepairTrigger: undefined,
148
150
  contractFeedback: undefined,
149
151
  sameExchangeBreak: undefined,
152
+ sameFailureBreak: undefined,
150
153
  compiledCode: "",
151
154
  pairedSource: "",
152
155
  selfCheckPassed: false,
@@ -223,6 +226,7 @@ export function absorbTurn(telemetry: GenerateTelemetry, turn: CodingTurnResult)
223
226
  else telemetry.counters.outcomes.selfCheckFail++;
224
227
  }
225
228
  if (turn.sameExchangeBreak !== undefined) telemetry.sameExchangeBreak = turn.sameExchangeBreak;
229
+ if (turn.sameFailureBreak !== undefined) telemetry.sameFailureBreak = turn.sameFailureBreak;
226
230
  }
227
231
 
228
232
  export interface CreateGenerateRunnerInput {
@@ -21,7 +21,7 @@ import { executeTool } from "../../coding-agent/tools.js";
21
21
  import type { CostTracker } from "../../evaluation/cost-tracker.js";
22
22
  import type { LLMAgent } from "../llm-router.js";
23
23
  import type { Harness } from "../types-public.js";
24
- import type { SameExchangeBreak } from "../result-types.js";
24
+ import type { SameExchangeBreak, SameFailureBreak } from "../result-types.js";
25
25
  import {
26
26
  APPLY_CHANGES_HASHLINE_TOOL,
27
27
  APPLY_CHANGES_HASHLINE_TOOL_FLAT,
@@ -38,10 +38,16 @@ import {
38
38
  import { LUCIDE_ICON_NAMES } from "@ggui-ai/design";
39
39
  import { formatWithHashlines } from "../hashline.js";
40
40
  import {
41
+ clearWriteFailures,
41
42
  recordToolExchange,
43
+ recordWriteFailure,
42
44
  SAME_EXCHANGE_BREAK_AT,
43
45
  SAME_EXCHANGE_NUDGE,
46
+ SAME_FAILURE_BREAK_AT,
47
+ SAME_FAILURE_NUDGE,
48
+ SAME_FAILURE_NUDGE_AT,
44
49
  } from "./dupe-break.js";
50
+ import type { RepeatedFailure } from "./dupe-break.js";
45
51
 
46
52
  /** Staged process-mode phase machine — also defined in runtime.ts. */
47
53
  export type A1Phase = "scaffold" | "fill" | "post";
@@ -254,6 +260,11 @@ export interface CodingTurnResult {
254
260
  * control value many exits share. No key on any other turn.
255
261
  */
256
262
  readonly sameExchangeBreak?: SameExchangeBreak;
263
+ /**
264
+ * Set only on the `break` the same-failure guard returns (ggui#1791). No
265
+ * key on any other turn.
266
+ */
267
+ readonly sameFailureBreak?: SameFailureBreak;
257
268
  }
258
269
 
259
270
  /**
@@ -952,6 +963,26 @@ ${closingInstruction}`;
952
963
  }
953
964
  }
954
965
 
966
+ // ggui#1791 same-failure guard — the patch fingerprint above keys on the
967
+ // PATCH, so the same failure under a different patch each turn (a reworded
968
+ // comment, a moved line) never counts as stuck. This keys on the FAILURE:
969
+ // each failing check a write reports is counted since the last passing
970
+ // write, whatever failed in between. The third report of one gets a nudge,
971
+ // the fifth ends the run. A passing write clears the counts; a non-write
972
+ // tool (cat, icons) changes nothing and leaves them as they were.
973
+ const isWrite = call.name === "apply_changes" || call.name === "rewrite";
974
+ let repeatedFailure: RepeatedFailure | undefined;
975
+ if (ctx.dupeBreak && isWrite) {
976
+ if (result.done) {
977
+ clearWriteFailures(ctx.dupeBreak);
978
+ } else {
979
+ repeatedFailure = recordWriteFailure(ctx.dupeBreak, result.result);
980
+ if (repeatedFailure?.repeats === SAME_FAILURE_NUDGE_AT) {
981
+ augmentedResultText += SAME_FAILURE_NUDGE;
982
+ }
983
+ }
984
+ }
985
+
955
986
  // Send tool result back to close the API contract (Google/OpenAI/OpenRouter)
956
987
  // This enables proper session chaining on the next callTools() invocation.
957
988
  await codingAgent.sendToolResult([{
@@ -984,6 +1015,30 @@ ${closingInstruction}`;
984
1015
  };
985
1016
  }
986
1017
 
1018
+ if (repeatedFailure !== undefined && repeatedFailure.repeats >= SAME_FAILURE_BREAK_AT) {
1019
+ const signature = repeatedFailure.signature.slice(0, 160);
1020
+ console.log(
1021
+ `[coding-agent] SAME_FAILURE_BREAK | tool=${call.name} | repeats=${repeatedFailure.repeats} | failure=${JSON.stringify(signature.slice(0, 80))} — terminating run (ggui#1791)`,
1022
+ );
1023
+ return {
1024
+ control: "break",
1025
+ compiledCode: "",
1026
+ selfCheckPassed: false,
1027
+ outcome: undefined,
1028
+ phase: undefined,
1029
+ tokens,
1030
+ ...cacheTokens,
1031
+ llmMs,
1032
+ toolMs,
1033
+ lastResultText: augmentedResultText,
1034
+ lastDiffFailed: false,
1035
+ isEvalFeedback: false,
1036
+ iconNamesCache,
1037
+ preWarmedContext,
1038
+ sameFailureBreak: { signature, repeats: repeatedFailure.repeats },
1039
+ };
1040
+ }
1041
+
987
1042
  // Track whether the diff itself failed to apply (vs applied but had violations)
988
1043
  const turnDiffFailed = !!result.error && result.result.includes("DIFF");
989
1044
 
@@ -62,6 +62,16 @@ export interface SameExchangeBreak {
62
62
  readonly repeats: number;
63
63
  }
64
64
 
65
+ /**
66
+ * ggui#1791 — the same-failure guard ended the run: one failing check
67
+ * (`signature`, its first 160 characters) was reported by this many failed
68
+ * writes since the last passing one, whatever the patch.
69
+ */
70
+ export interface SameFailureBreak {
71
+ readonly signature: string;
72
+ readonly repeats: number;
73
+ }
74
+
65
75
  export interface GenerationResult extends AdapterResult {
66
76
  /** Number of generation passes the harness performed */
67
77
  passesUsed: number;
@@ -77,6 +87,8 @@ export interface GenerationResult extends AdapterResult {
77
87
  inLoopVisualTokens?: VisualLegTokens;
78
88
  /** Set only when the same-exchange guard ended the run (ggui#404); no key otherwise. */
79
89
  sameExchangeBreak?: SameExchangeBreak;
90
+ /** ggui#1791 — set only when the same-failure guard ended the run. */
91
+ sameFailureBreak?: SameFailureBreak;
80
92
  /** Whether background improvement should be spawned (auto-improve mode) */
81
93
  needsBackgroundImprovement?: boolean;
82
94
  /** Whether at least one commit passed the self-check (compile + type check + lint) */
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ export {
22
22
  export type { CreateUiGeneratorOptions } from './create-ui-generator.js';
23
23
  export type { RuntimeRenderProbeConfig } from './harness/check/runtime-render/adapter.js';
24
24
  export type { ProviderRetryInfo } from './harness/llm-router.js';
25
- export type { GenerationResult, SameExchangeBreak } from './harness/result-types.js';
25
+ export type { GenerationResult, SameExchangeBreak, SameFailureBreak } from './harness/result-types.js';
26
26
  // The build identity a stamp outside this package can name: the same
27
27
  // digests `createUiGenerator` puts on every result's `metadata.build`, for
28
28
  // either design mode — so a harness definition can carry both modes' digests
@@ -18,13 +18,12 @@ import { consumedTokenManifest } from "@ggui-ai/design/themes";
18
18
  * closed-set check still admits them: a card that references one is valid wherever this release
19
19
  * renders it.
20
20
  *
21
- * - ggui#1494 — the control edges (`controlOutline`, `controlAccentOutline`): derived and completed
22
- * from this release; taught from the next.
21
+ * Empty today. The control edges (`controlOutline`, `controlAccentOutline`, ggui#1494) were held here
22
+ * until every serving and rollback runtime declared them; the derivation (`10a0c00f2`) is in every
23
+ * release from tag 17 on, so they are taught from ggui#1697, in the same change as the prompt rule that
24
+ * teaches a control's edge.
23
25
  */
24
- export const NOT_YET_TAUGHT_TOKENS: ReadonlySet<string> = new Set([
25
- "--ggui-color-controlAccentOutline",
26
- "--ggui-color-controlOutline",
27
- ]);
26
+ export const NOT_YET_TAUGHT_TOKENS: ReadonlySet<string> = new Set<string>([]);
28
27
 
29
28
  /** The manifest as the model is taught it. */
30
29
  export const taughtTokenManifest: readonly string[] = consumedTokenManifest.filter(