@gaunt-sloth/core 2.0.0-alpha.26 → 2.0.0-alpha.28

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 (51) hide show
  1. package/dist/config/schema.js +64 -15
  2. package/dist/config/schema.js.map +1 -1
  3. package/dist/config/types.d.ts +17 -8
  4. package/dist/config/types.js.map +1 -1
  5. package/dist/constants.d.ts +7 -4
  6. package/dist/constants.js +7 -4
  7. package/dist/constants.js.map +1 -1
  8. package/dist/core/GthAbstractAgent.d.ts +1 -19
  9. package/dist/core/GthAbstractAgent.js +13 -65
  10. package/dist/core/GthAbstractAgent.js.map +1 -1
  11. package/dist/core/GthAgentRunner.d.ts +10 -4
  12. package/dist/core/GthAgentRunner.js +24 -7
  13. package/dist/core/GthAgentRunner.js.map +1 -1
  14. package/dist/core/GthLangChainAgent.js +16 -11
  15. package/dist/core/GthLangChainAgent.js.map +1 -1
  16. package/dist/core/launchBanner.js +36 -17
  17. package/dist/core/launchBanner.js.map +1 -1
  18. package/dist/core/shell/abstention.d.ts +88 -0
  19. package/dist/core/shell/abstention.js +184 -0
  20. package/dist/core/shell/abstention.js.map +1 -0
  21. package/dist/core/shell/openWorld.d.ts +137 -12
  22. package/dist/core/shell/openWorld.js +677 -12
  23. package/dist/core/shell/openWorld.js.map +1 -1
  24. package/dist/core/shell/rater.d.ts +79 -39
  25. package/dist/core/shell/rater.js +132 -71
  26. package/dist/core/shell/rater.js.map +1 -1
  27. package/dist/core/shell/rejection.d.ts +7 -4
  28. package/dist/core/shell/rejection.js +3 -3
  29. package/dist/core/shell/rejection.js.map +1 -1
  30. package/dist/core/toolDisplay.d.ts +12 -3
  31. package/dist/core/toolDisplay.js +27 -7
  32. package/dist/core/toolDisplay.js.map +1 -1
  33. package/dist/providers/openrouter.d.ts +3 -4
  34. package/dist/providers/openrouter.js +15 -30
  35. package/dist/providers/openrouter.js.map +1 -1
  36. package/dist/runtime/askStructured.js +7 -5
  37. package/dist/runtime/askStructured.js.map +1 -1
  38. package/dist/runtime/structuredOutput.d.ts +104 -0
  39. package/dist/runtime/structuredOutput.js +393 -0
  40. package/dist/runtime/structuredOutput.js.map +1 -0
  41. package/dist/utils/displayWidth.d.ts +30 -0
  42. package/dist/utils/displayWidth.js +140 -0
  43. package/dist/utils/displayWidth.js.map +1 -0
  44. package/dist/utils/systemPromptNotes.d.ts +28 -8
  45. package/dist/utils/systemPromptNotes.js +47 -49
  46. package/dist/utils/systemPromptNotes.js.map +1 -1
  47. package/dist/utils/untrustedText.d.ts +66 -0
  48. package/dist/utils/untrustedText.js +80 -0
  49. package/dist/utils/untrustedText.js.map +1 -0
  50. package/package.json +6 -1
  51. package/schema/gsloth-config.schema.json +3 -1
@@ -0,0 +1,393 @@
1
+ /**
2
+ * @module runtime/structuredOutput
3
+ *
4
+ * EXT-88 — the **`withStructuredOutput` boundary**. Every `withStructuredOutput` call in this
5
+ * project goes through {@link structuredOutputBoundary}: `rateShellCommand` (the approvals rater),
6
+ * {@link askStructured} (arbitrary caller schemas, including `gth workflow` scripts) and
7
+ * `@gaunt-sloth/batch`'s eval judge. Review's rating step is deliberately not one of them — it
8
+ * reaches its schema through a bound TOOL, which is a different path with different rules.
9
+ *
10
+ * ## The problem it exists to solve
11
+ *
12
+ * A single Zod object normally does two jobs at once — it is converted into the JSON Schema the
13
+ * provider is sent, and it validates the answer that comes back. Those two jobs want **opposite**
14
+ * things from an optional field, and Zod has only one knob for both:
15
+ *
16
+ * - **On the wire**, OpenAI's strict `json_schema` rule is *"`required` must be supplied and must
17
+ * include every key in `properties`"*. A `.optional()` field is left out of `required`, and the
18
+ * OpenAI API rejects the whole request with `400 Invalid schema for response_format`. Optionality
19
+ * there is spelled as a **nullable type on a required key**, not as an absent key.
20
+ * - **On the way back**, providers do not agree. Some (ChatGroq) rewrite the schema themselves so
21
+ * every property is required-and-nullable, and their models answer `null`. Others (Anthropic,
22
+ * Google GenAI, Ollama) leave the field genuinely optional and their models may simply omit it.
23
+ * Zod's `.optional()` admits `undefined` and **not** `null`, so the first group's answer is
24
+ * rejected by the very schema that asked for it — and the caller sees a parse failure that looks
25
+ * like a bad model rather than a self-contradicting request.
26
+ *
27
+ * ## The shape that satisfies both
28
+ *
29
+ * For every field the caller declared `.optional()`, the boundary sends
30
+ * `inner.nullable().prefault(null)` instead. That one node is the wire/validation twin:
31
+ *
32
+ * - **Wire** — `prefault` does not change the parsed *output* type, so the key lands in `required`
33
+ * and its type is `["string","null"]` / `anyOf: [T, null]`. Strict mode is satisfied, and the
34
+ * model is given a legal way to say "nothing here". Unlike `.default()`, `prefault` emits **no
35
+ * `default` keyword** into the JSON Schema, so nothing extra is added to what the provider sees.
36
+ * - **Validation** — the same node accepts the value, accepts `null`, and accepts the key being
37
+ * **missing** (which is what `prefault` supplies the `null` for). All three arms are needed: the
38
+ * third is what keeps the providers that do *not* hoist working.
39
+ *
40
+ * The `null` never escapes: {@link StructuredOutputBoundary.safeParse} strips it back to the key
41
+ * being **absent** and then validates with the caller's **original** schema, so the parsed type is
42
+ * exactly what the caller declared — an optional string stays `string | undefined` and never
43
+ * becomes `string | null | undefined`.
44
+ *
45
+ * ## Two properties that must not be lost in a "simplification"
46
+ *
47
+ * - **Every `.describe()` survives into the emitted JSON Schema.** The descriptions are the only
48
+ * place the model is told what the fields mean. Expressing the null-to-absent step with
49
+ * `.transform()` on the field is the obvious-looking alternative and it costs the field its
50
+ * description — worse, the conversion OpenAI's path uses refuses to represent a transform at all.
51
+ * Normalization therefore happens in **code**, after the parse, never in the schema.
52
+ * - **A genuinely malformed answer still fails.** This boundary removes a *false* parse failure; it
53
+ * is not a blanket "accept anything". A wrong type in an optional field is still rejected, because
54
+ * the final validation is the caller's own untouched schema. In particular `.catch()` must not be
55
+ * used to express any of this — it would swallow real failures.
56
+ *
57
+ * ## Coverage, and the deliberate limits
58
+ *
59
+ * The rewrite descends through objects, arrays, tuples, records and the single-child wrappers
60
+ * (`nullable`, `readonly`, `default`, `prefault`, `nonoptional`), so an optional nested inside an
61
+ * object inside an array is handled.
62
+ *
63
+ * It deliberately stops at **unions** (including discriminated unions), **intersections**, `lazy`,
64
+ * `map`, `set`, `pipe`/`transform` and `custom`, leaving those subtrees exactly as the caller wrote
65
+ * them, and it leaves a **recursive** schema alone in its entirety. The reason is one rule: the
66
+ * rewrite and the normalization must cover **precisely the same set**. At a union the incoming value
67
+ * gives no reliable answer to *which branch was taken*, so a `null` could not be stripped back out;
68
+ * rewriting there while being unable to normalize would manufacture the very parse failure this
69
+ * module removes. Recursion is refused for the neighbouring reason — a rewrite that stopped at the
70
+ * cycle would make a key required at one depth and optional at the next, which satisfies no
71
+ * provider's strict rule and is harder to reason about than the caller's own schema.
72
+ *
73
+ * An optional inside one of those constructs keeps today's behaviour — correct everywhere it is
74
+ * correct today, and still rejected by OpenAI's strict rule, which is a visible error rather than a
75
+ * silent one.
76
+ */
77
+ import * as z from 'zod';
78
+ /**
79
+ * A normalizer's answer for "this key must not be present at all" — distinct from `undefined`,
80
+ * because an object key explicitly set to `undefined` is still an own property and `Object.hasOwn`
81
+ * would report it. Callers must not have to learn a second spelling of "absent".
82
+ */
83
+ const ABSENT = Symbol('structured-output-absent');
84
+ function defOf(schema) {
85
+ return schema._zod.def;
86
+ }
87
+ /**
88
+ * Rebuild `schema` with part of its definition replaced, keeping everything else — the checks
89
+ * (`minItems`, `minimum`, …) and the registered `.describe()` metadata. Built from Zod's own clone
90
+ * rather than by calling `z.object(...)` / `z.array(...)` afresh, because re-constructing would
91
+ * silently drop those constraints from the schema the provider is sent.
92
+ *
93
+ * The metadata is copied through the registry rather than by cloning with Zod's `parent` option.
94
+ * A parent link is how Zod represents "the same schema, re-described", and its JSON-Schema emitter
95
+ * honours that by emitting a **`$ref` to the parent** with the differences as sibling keys. For a
96
+ * rewritten object that is actively wrong: the `$ref` would point at a definition still carrying the
97
+ * ORIGINAL `required` list — the one missing exactly the optional key this module exists to hoist —
98
+ * and `$ref`-with-siblings is not accepted by every provider's strict mode. Copying the metadata
99
+ * leaves the clone an independent schema, which is what it actually is.
100
+ */
101
+ function cloneWith(schema, patch) {
102
+ const next = { ...defOf(schema), ...patch };
103
+ const cloned = z.core.clone(schema, next);
104
+ const meta = z.globalRegistry.get(schema);
105
+ if (meta)
106
+ z.globalRegistry.add(cloned, meta);
107
+ return cloned;
108
+ }
109
+ function isRecord(value) {
110
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
111
+ }
112
+ /**
113
+ * `.optional()` → `inner.nullable().prefault(null)`, plus the normalizer that puts it back.
114
+ *
115
+ * The description is re-stated on the nullable wrapper so it lands at **field level** in the emitted
116
+ * JSON Schema whichever way the caller spelled it (`z.string().describe(D).optional()` carries it on
117
+ * the inner type, `z.string().optional().describe(D)` on the wrapper).
118
+ *
119
+ * `null` is only stripped where the caller's own declaration gives it no meaning. A field written
120
+ * `z.string().nullable().optional()` asked for `null` to be a value in its own right, so it keeps
121
+ * it; only a plain `.optional()`, for which `null` was never a legal answer and can only have come
122
+ * from the required-and-nullable rewrite, collapses to the key being absent.
123
+ */
124
+ function walkOptional(schema, def) {
125
+ const inner = def.innerType;
126
+ const innerWalk = walk(inner);
127
+ const description = schema.description ?? inner.description;
128
+ const nullIsMeaningful = inner.safeParse(null).success;
129
+ // A field that already admits `null` needs no second nullable wrapper — one would emit a nested
130
+ // `anyOf` inside an `anyOf` that says nothing the inner one does not.
131
+ let nullable = nullIsMeaningful ? innerWalk.schema : innerWalk.schema.nullable();
132
+ if (description !== undefined)
133
+ nullable = nullable.describe(description);
134
+ const wireSchema = nullable.prefault(null);
135
+ const innerNormalize = innerWalk.normalize;
136
+ const normalize = (value) => {
137
+ if (value === undefined)
138
+ return ABSENT;
139
+ if (value === null)
140
+ return nullIsMeaningful ? null : ABSENT;
141
+ return innerNormalize ? innerNormalize(value) : value;
142
+ };
143
+ return { schema: wireSchema, normalize };
144
+ }
145
+ function walkObject(schema, def) {
146
+ const shape = def.shape ?? {};
147
+ const nextShape = {};
148
+ const normalizers = [];
149
+ let changed = false;
150
+ for (const [key, field] of Object.entries(shape)) {
151
+ const result = walk(field);
152
+ nextShape[key] = result.schema;
153
+ if (result.schema !== field)
154
+ changed = true;
155
+ if (result.normalize)
156
+ normalizers.push([key, result.normalize]);
157
+ }
158
+ if (!changed && normalizers.length === 0)
159
+ return { schema };
160
+ const normalize = (value) => {
161
+ if (!isRecord(value))
162
+ return value;
163
+ const out = { ...value };
164
+ for (const [key, normalizeField] of normalizers) {
165
+ if (!Object.hasOwn(out, key))
166
+ continue;
167
+ const normalized = normalizeField(out[key]);
168
+ if (normalized === ABSENT)
169
+ delete out[key];
170
+ else
171
+ out[key] = normalized;
172
+ }
173
+ return out;
174
+ };
175
+ return { schema: changed ? cloneWith(schema, { shape: nextShape }) : schema, normalize };
176
+ }
177
+ function walkArray(schema, def) {
178
+ const element = def.element;
179
+ const result = walk(element);
180
+ if (result.schema === element && !result.normalize)
181
+ return { schema };
182
+ const normalizeElement = result.normalize;
183
+ const normalize = normalizeElement
184
+ ? (value) => {
185
+ if (!Array.isArray(value))
186
+ return value;
187
+ return value.map((item) => {
188
+ const normalized = normalizeElement(item);
189
+ // An element position has no key to remove, so "absent" degrades to `undefined` — which
190
+ // is what an optional element schema accepts anyway.
191
+ return normalized === ABSENT ? undefined : normalized;
192
+ });
193
+ }
194
+ : undefined;
195
+ return {
196
+ schema: result.schema === element ? schema : cloneWith(schema, { element: result.schema }),
197
+ normalize,
198
+ };
199
+ }
200
+ function walkTuple(schema, def) {
201
+ const items = def.items ?? [];
202
+ const rest = def.rest ?? null;
203
+ const itemResults = items.map((item) => walk(item));
204
+ const restResult = rest ? walk(rest) : undefined;
205
+ const changed = itemResults.some((result, index) => result.schema !== items[index]) ||
206
+ (restResult !== undefined && rest !== null && restResult.schema !== rest);
207
+ const needsNormalize = itemResults.some((result) => result.normalize) || restResult?.normalize !== undefined;
208
+ if (!changed && !needsNormalize)
209
+ return { schema };
210
+ const normalize = needsNormalize
211
+ ? (value) => {
212
+ if (!Array.isArray(value))
213
+ return value;
214
+ return value.map((item, index) => {
215
+ // Gate on POSITION: a prefix item is normalized by its own item schema and never by the
216
+ // rest schema, which describes a different position entirely.
217
+ const normalizeItem = index < items.length ? itemResults[index]?.normalize : restResult?.normalize;
218
+ if (!normalizeItem)
219
+ return item;
220
+ const normalized = normalizeItem(item);
221
+ return normalized === ABSENT ? undefined : normalized;
222
+ });
223
+ }
224
+ : undefined;
225
+ return {
226
+ schema: changed
227
+ ? cloneWith(schema, {
228
+ items: itemResults.map((result) => result.schema),
229
+ rest: restResult ? restResult.schema : rest,
230
+ })
231
+ : schema,
232
+ normalize,
233
+ };
234
+ }
235
+ function walkRecord(schema, def) {
236
+ const valueType = def.valueType;
237
+ const result = walk(valueType);
238
+ if (result.schema === valueType && !result.normalize)
239
+ return { schema };
240
+ const normalizeValue = result.normalize;
241
+ const normalize = normalizeValue
242
+ ? (value) => {
243
+ if (!isRecord(value))
244
+ return value;
245
+ const out = {};
246
+ for (const [key, item] of Object.entries(value)) {
247
+ const normalized = normalizeValue(item);
248
+ if (normalized !== ABSENT)
249
+ out[key] = normalized;
250
+ }
251
+ return out;
252
+ }
253
+ : undefined;
254
+ return {
255
+ schema: result.schema === valueType ? schema : cloneWith(schema, { valueType: result.schema }),
256
+ normalize,
257
+ };
258
+ }
259
+ /**
260
+ * The single-child wrappers: `nullable`, `readonly`, `default`, `prefault`, `nonoptional`.
261
+ *
262
+ * A wrapper is **transparent to optionality**. `z.string().optional().default('foo')` is still sent
263
+ * as a required key typed `anyOf: [T, null]`, so a `null` arriving here is the wire's "nothing here"
264
+ * and must reach the inner normalizer that knows how to remove it. Passing every `null` straight
265
+ * through instead would advertise `null` to the provider and then reject the one it sends — the same
266
+ * self-contradiction this module exists to remove, one wrapper deep. What "absent" then means is the
267
+ * caller's own business, because the final validation is the caller's untouched schema: under
268
+ * `.default('foo')` it resolves to `'foo'`, under `.readonly()` the key simply stays absent.
269
+ *
270
+ * The exception is a wrapper that makes `null` a value in its own right — the caller wrote
271
+ * `.nullable()` **outside** the optional. That is the question {@link walkOptional} asks of its own
272
+ * inner type, asked one level up, and it is what keeps the two spellings of nullable-and-optional
273
+ * from disagreeing about the same field.
274
+ *
275
+ * `undefined` is passed through rather than being given a second meaning here: the wire never
276
+ * produces one (`prefault` supplies `null` for a missing key), and a genuinely missing object key
277
+ * never reaches a field normalizer at all — {@link walkObject} skips it.
278
+ */
279
+ function walkWrapper(schema, def) {
280
+ const inner = def.innerType;
281
+ const result = walk(inner);
282
+ if (result.schema === inner && !result.normalize)
283
+ return { schema };
284
+ const innerNormalize = result.normalize;
285
+ const nullIsMeaningful = innerNormalize !== undefined && schema.safeParse(null).success;
286
+ const normalize = innerNormalize
287
+ ? (value) => {
288
+ if (value === undefined)
289
+ return value;
290
+ if (value === null && nullIsMeaningful)
291
+ return null;
292
+ return innerNormalize(value);
293
+ }
294
+ : undefined;
295
+ return {
296
+ schema: result.schema === inner ? schema : cloneWith(schema, { innerType: result.schema }),
297
+ normalize,
298
+ };
299
+ }
300
+ /**
301
+ * Thrown when the walk re-enters a schema it is already inside, i.e. the caller's schema is
302
+ * recursive. Caught in {@link structuredOutputBoundary}, which then leaves the whole schema alone —
303
+ * see the module doc's limits.
304
+ */
305
+ const CYCLIC = Symbol('structured-output-cyclic');
306
+ /** The schemas the current walk is inside. A node reached twice on one path is a cycle. */
307
+ const inProgress = new WeakSet();
308
+ /**
309
+ * Rewrite one node. Every branch here has a matching arm in the normalizer it returns — the two
310
+ * halves are produced by the **same** walk precisely so their coverage cannot drift apart. Anything
311
+ * not listed is returned untouched, with no normalizer.
312
+ *
313
+ * A **recursive** schema aborts the whole walk rather than being partly rewritten. Zod spells
314
+ * recursion as a getter in an object's shape, which reads as an ordinary `object` here and would
315
+ * otherwise descend for ever. Rewriting only the levels reached before the cycle would put an
316
+ * optional key in `required` at one depth and leave it out at the next — an inconsistency that
317
+ * satisfies no provider's strict rule while making the emitted schema harder to reason about than
318
+ * the caller's own.
319
+ */
320
+ function walk(schema) {
321
+ if (inProgress.has(schema))
322
+ throw CYCLIC;
323
+ inProgress.add(schema);
324
+ try {
325
+ return walkNode(schema);
326
+ }
327
+ finally {
328
+ inProgress.delete(schema);
329
+ }
330
+ }
331
+ function walkNode(schema) {
332
+ const def = defOf(schema);
333
+ switch (def.type) {
334
+ case 'optional':
335
+ return walkOptional(schema, def);
336
+ case 'object':
337
+ return walkObject(schema, def);
338
+ case 'array':
339
+ return walkArray(schema, def);
340
+ case 'tuple':
341
+ return walkTuple(schema, def);
342
+ case 'record':
343
+ return walkRecord(schema, def);
344
+ case 'nullable':
345
+ case 'readonly':
346
+ case 'default':
347
+ case 'prefault':
348
+ case 'nonoptional':
349
+ return walkWrapper(schema, def);
350
+ default:
351
+ return { schema };
352
+ }
353
+ }
354
+ /**
355
+ * Memoized per schema instance. Repeated calls with the same schema — the common case, since call
356
+ * sites hold a module-level schema constant — return the same `wireSchema` reference, which is what
357
+ * LangChain's own JSON-Schema conversion cache is keyed on.
358
+ */
359
+ const boundaries = new WeakMap();
360
+ /**
361
+ * Build the {@link StructuredOutputBoundary} for a schema — the one entry point every
362
+ * `withStructuredOutput` call in this project goes through. See the module doc for what it does and
363
+ * why.
364
+ *
365
+ * @param schema The caller's schema, used unchanged for the final validation.
366
+ */
367
+ export function structuredOutputBoundary(schema) {
368
+ const cached = boundaries.get(schema);
369
+ if (cached)
370
+ return cached;
371
+ let walked;
372
+ try {
373
+ walked = walk(schema);
374
+ }
375
+ catch (error) {
376
+ // A recursive schema is left exactly as the caller wrote it — the same answer this module gives
377
+ // for a union, and for the same reason: it will not rewrite what it cannot also normalize.
378
+ if (error !== CYCLIC)
379
+ throw error;
380
+ walked = { schema: schema };
381
+ }
382
+ const normalize = walked.normalize;
383
+ const boundary = {
384
+ wireSchema: walked.schema,
385
+ safeParse(raw) {
386
+ const normalized = normalize ? normalize(raw) : raw;
387
+ return schema.safeParse(normalized === ABSENT ? undefined : normalized);
388
+ },
389
+ };
390
+ boundaries.set(schema, boundary);
391
+ return boundary;
392
+ }
393
+ //# sourceMappingURL=structuredOutput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structuredOutput.js","sourceRoot":"","sources":["../../src/runtime/structuredOutput.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Terminal columns `text` occupies. Zero-width clusters (combining marks, default-ignorables)
3
+ * count 0, wide clusters (CJK, emoji, fullwidth forms) count 2, everything else 1. ANSI escape
4
+ * sequences are not counted, so a pre-coloured string measures as what the user sees — but the
5
+ * slices below do not share that awareness, so do not read this as a licence to feed them one.
6
+ */
7
+ export declare function displayWidth(text: string): number;
8
+ /**
9
+ * The longest LEADING run of whole clusters whose total width is at most `maxWidth` — i.e. keep
10
+ * the head, lose the tail. Total (never throws): a non-positive `maxWidth` yields `''`.
11
+ *
12
+ * A cluster is kept only if it fits ENTIRELY, so the result can come back one column short of
13
+ * `maxWidth` when the next cluster is two columns wide. That is the point: half a wide glyph
14
+ * cannot be drawn, and spending the column anyway is how text over-runs its budget.
15
+ *
16
+ * The clusters are produced one at a time and the walk stops at the budget, so the cost is the
17
+ * length of the RESULT rather than the length of the input — the input here is a whole tool-output
18
+ * line, which is unbounded.
19
+ */
20
+ export declare function sliceToWidth(text: string, maxWidth: number): string;
21
+ /**
22
+ * The longest TRAILING run of whole clusters whose total width is at most `maxWidth` — i.e. keep
23
+ * the tail, lose the head. The mirror of {@link sliceToWidth}, for values whose end is the
24
+ * informative part (a path's leaf directory).
25
+ *
26
+ * This one materialises the clusters: it walks backwards, and cluster boundaries are only
27
+ * derivable from the front. Its callers pass a path or a model id, so the input is a line rather
28
+ * than a file.
29
+ */
30
+ export declare function sliceEndToWidth(text: string, maxWidth: number): string;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @module displayWidth
3
+ * The one place this repo answers "how many terminal COLUMNS does this string occupy, and where
4
+ * may I cut it". Everything that fits text to a terminal — the launch banner's field budgets, the
5
+ * tool-display caps — measures and slices through here.
6
+ *
7
+ * ## Why a code-point count is not a width
8
+ *
9
+ * `[...text].length` counts code points, which is right for UTF-16 safety and wrong for layout: a
10
+ * CJK ideograph or an emoji is ONE code point occupying TWO columns. Text measured that way reads
11
+ * as shorter than it renders, escapes whatever budget it was given, and wraps — and a wrapped
12
+ * continuation line starts back at column 0, which is precisely the failure the budgets exist to
13
+ * prevent. So the rule is: a value destined for a fixed column budget is measured with
14
+ * {@link displayWidth} and cut with {@link sliceToWidth} / {@link sliceEndToWidth}, never with
15
+ * `.length`, a spread, or `String.prototype.slice`.
16
+ *
17
+ * ## Why grapheme clusters are the cutting unit
18
+ *
19
+ * Slicing by code point is safe against halving a surrogate pair but not against halving a
20
+ * CLUSTER: a flag, a skin-toned or ZWJ-joined emoji, or a base letter plus its combining mark are
21
+ * several code points that a terminal draws as one glyph, and cutting between them produces
22
+ * mojibake or an orphaned mark. `Intl.Segmenter` gives the same cluster boundaries the width
23
+ * rules below are defined over, so the two agree by construction and a sliced string's width is
24
+ * exactly the sum of the widths of the clusters kept.
25
+ *
26
+ * ## What the slices expect: PLAIN text
27
+ *
28
+ * {@link displayWidth} is ANSI-aware and the slices are NOT, and that asymmetry is a contract
29
+ * rather than an oversight. An escape sequence is not one cluster — the terminal swallows it
30
+ * whole, but `ESC`, `[`, `3`, `5`, `m` segment as five, four of them printable — so the two
31
+ * disagree on an escape-bearing string and a slice can both under-fill its budget and cut inside
32
+ * a sequence. Feed the slices the text a user will read, and colour it afterwards; that is what
33
+ * every caller here does, since both render surfaces map a style tag to their own escapes at the
34
+ * very end. Making the slices ANSI-aware would change what a coloured preview line renders as,
35
+ * which is a decision for whoever needs it and not a silent detail of this module.
36
+ *
37
+ * That asymmetry is also why a slice decides "does the whole string fit?" two different ways. For
38
+ * plain text the cluster walk decides it: the widths of the clusters sum to the width of the
39
+ * string, so walking until the budget is blown answers the question and answers it WITHOUT
40
+ * touching the rest of the input — which is what keeps a megabyte-long preview line from being
41
+ * measured end to end before it is cut at column 200. Only escape-bearing text is measured whole
42
+ * first, because there the two rulers disagree and a coloured string that MEASURES as fitting must
43
+ * be handed back whole rather than cut short by the bytes of its own escapes.
44
+ *
45
+ * ## Why ambiguous-width characters stay NARROW
46
+ *
47
+ * `string-width` defaults to treating East-Asian "Ambiguous" characters as one column, and that
48
+ * default is load-bearing here rather than incidental: the sloth face is block elements, the
49
+ * wordmark is box-drawing, and `…` is U+2026 — all Ambiguous. Counting them as two columns would
50
+ * measure the 16-column face at 32 and shatter the layout it anchors. Do not pass
51
+ * `ambiguousIsNarrow: false`; the specs pin the face at 16 and the wordmark at 19 so a change of
52
+ * that policy fails loudly instead of quietly doubling the art.
53
+ */
54
+ import stringWidth from 'string-width';
55
+ /**
56
+ * Grapheme-cluster boundaries, i.e. what a terminal draws as one glyph. Built once: constructing
57
+ * an `Intl.Segmenter` is expensive relative to the short strings this module handles.
58
+ */
59
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
60
+ /**
61
+ * Terminal columns `text` occupies. Zero-width clusters (combining marks, default-ignorables)
62
+ * count 0, wide clusters (CJK, emoji, fullwidth forms) count 2, everything else 1. ANSI escape
63
+ * sequences are not counted, so a pre-coloured string measures as what the user sees — but the
64
+ * slices below do not share that awareness, so do not read this as a licence to feed them one.
65
+ */
66
+ export function displayWidth(text) {
67
+ return stringWidth(text);
68
+ }
69
+ /** The clusters of `text`, in order — the only unit either slice is allowed to cut between. */
70
+ function clusters(text) {
71
+ const out = [];
72
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(text))
73
+ out.push(segment);
74
+ return out;
75
+ }
76
+ /**
77
+ * Whether the whole-string measurement is the only ruler that can answer "does this fit" for
78
+ * `text` — true exactly when it carries an escape introducer (`ESC`, or the one-byte `CSI`), the
79
+ * two characters `string-width` itself strips on. On anything else the cluster widths sum to the
80
+ * whole-string width, so the walk decides the same question incrementally and a pre-measurement
81
+ * would be a second full pass over the input for nothing.
82
+ */
83
+ function needsWholeStringMeasure(text) {
84
+ return text.includes('\u001B') || text.includes('\u009B');
85
+ }
86
+ /**
87
+ * The longest LEADING run of whole clusters whose total width is at most `maxWidth` — i.e. keep
88
+ * the head, lose the tail. Total (never throws): a non-positive `maxWidth` yields `''`.
89
+ *
90
+ * A cluster is kept only if it fits ENTIRELY, so the result can come back one column short of
91
+ * `maxWidth` when the next cluster is two columns wide. That is the point: half a wide glyph
92
+ * cannot be drawn, and spending the column anyway is how text over-runs its budget.
93
+ *
94
+ * The clusters are produced one at a time and the walk stops at the budget, so the cost is the
95
+ * length of the RESULT rather than the length of the input — the input here is a whole tool-output
96
+ * line, which is unbounded.
97
+ */
98
+ export function sliceToWidth(text, maxWidth) {
99
+ if (maxWidth <= 0)
100
+ return '';
101
+ if (needsWholeStringMeasure(text) && displayWidth(text) <= maxWidth)
102
+ return text;
103
+ let width = 0;
104
+ let kept = '';
105
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(text)) {
106
+ const clusterWidth = displayWidth(segment);
107
+ if (width + clusterWidth > maxWidth)
108
+ break;
109
+ width += clusterWidth;
110
+ kept += segment;
111
+ }
112
+ return kept;
113
+ }
114
+ /**
115
+ * The longest TRAILING run of whole clusters whose total width is at most `maxWidth` — i.e. keep
116
+ * the tail, lose the head. The mirror of {@link sliceToWidth}, for values whose end is the
117
+ * informative part (a path's leaf directory).
118
+ *
119
+ * This one materialises the clusters: it walks backwards, and cluster boundaries are only
120
+ * derivable from the front. Its callers pass a path or a model id, so the input is a line rather
121
+ * than a file.
122
+ */
123
+ export function sliceEndToWidth(text, maxWidth) {
124
+ if (maxWidth <= 0)
125
+ return '';
126
+ if (needsWholeStringMeasure(text) && displayWidth(text) <= maxWidth)
127
+ return text;
128
+ const all = clusters(text);
129
+ let width = 0;
130
+ let kept = '';
131
+ for (let index = all.length - 1; index >= 0; index--) {
132
+ const clusterWidth = displayWidth(all[index]);
133
+ if (width + clusterWidth > maxWidth)
134
+ break;
135
+ width += clusterWidth;
136
+ kept = all[index] + kept;
137
+ }
138
+ return kept;
139
+ }
140
+ //# sourceMappingURL=displayWidth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"displayWidth.js","sourceRoot":"","sources":["../../src/utils/displayWidth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,OAAO,WAAW,MAAM,cAAc,CAAC;AAEvC;;;GAGG;AACH,MAAM,kBAAkB,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,+FAA+F;AAC/F,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9E,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,QAAgB;IACzD,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7B,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjF,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3D,MAAM,YAAY,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,KAAK,GAAG,YAAY,GAAG,QAAQ;YAAE,MAAM;QAC3C,KAAK,IAAI,YAAY,CAAC;QACtB,IAAI,IAAI,OAAO,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB;IAC5D,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7B,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjF,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,IAAI,KAAK,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,YAAY,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9C,IAAI,KAAK,GAAG,YAAY,GAAG,QAAQ;YAAE,MAAM;QAC3C,KAAK,IAAI,YAAY,CAAC;QACtB,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -61,26 +61,46 @@ export interface CommitCoAuthor {
61
61
  email?: string;
62
62
  }
63
63
  /**
64
- * GS2-35: append the commit co-authoring rule to the composed code-mode system prompt.
64
+ * GS2-35/EXT-83: append the commit-writing rules to the composed code-mode system prompt.
65
65
  *
66
66
  * Gaunt Sloth has **no dedicated git-commit tool** — the agent commits by calling
67
- * `run_shell_command` with `git commit`, composing the message (including any trailer) itself. Left
68
- * unguided, models emit `Co-Authored-By: <their own model name>` (e.g. `Claude`, `GPT`, `Gemini`)
69
- * from trained habit, which is **factually wrong**: the commit was produced by *Gaunt Sloth*, not by
70
- * the model. This note is the fix at the correct layer first-party prompt guidance that (a) states
71
- * the exact trailer to emit and (b) forbids a model-name co-author.
67
+ * `run_shell_command` with `git commit`, composing the message (including any trailer) itself. That
68
+ * leaves two things it must be told, and both live here because both are about committing:
69
+ *
70
+ * 1. **WHO the co-author is.** Left unguided, models emit their own model name from trained habit,
71
+ * which is factually wrong: the commit was produced by *Gaunt Sloth*, not by the model. The note
72
+ * states the exact trailer to emit. EXT-83 — rather than enumerate model names not to write (a
73
+ * denylist is stale the day a new vendor ships, and an enumeration beside a catch-all teaches the
74
+ * model that the list is the rule), the correct name is SUPPLIED: the resolved
75
+ * {@link ResolvedModelIdentity} decorates the DEFAULT name as `Gaunt Sloth (provider:model)`, so
76
+ * the real model is named while the authorship stays Gaunt Sloth's.
77
+ * 2. **HOW the message reaches git.** A commit message passed inline in a double-quoted shell
78
+ * argument is EXPANDED BY THE SHELL before git runs, so a message that quotes code the way
79
+ * ordinary technical prose does is executed as a command. The note states that mechanism rather
80
+ * than merely forbidding the construct — naming a construct without its mechanism has been
81
+ * measured not to work. A file path carries no shell metacharacters, so the file form removes the
82
+ * failure mode instead of asking the model to avoid it.
83
+ *
84
+ * The note's prose carries **no backtick and no other markup** — including no angle-bracket
85
+ * placeholder: it is the one piece of guidance whose subject is how to write a commit message, so
86
+ * quoting its own examples in backticks would demonstrate the exact style rule 2 exists to stop, and
87
+ * an angle-bracket placeholder copied literally is itself a shell input redirect. The `<email>` of
88
+ * the trailer line is the exception the RFC form requires, and is scoped out of the scan.
72
89
  *
73
90
  * The identity is config-driven (`commit.coAuthor` in {@link import('#src/config/types.js').GthConfig}).
74
91
  * Each field falls back INDEPENDENTLY to the Gaunt Sloth account
75
92
  * ({@link DEFAULT_COMMIT_CO_AUTHOR_NAME} / {@link DEFAULT_COMMIT_CO_AUTHOR_EMAIL}) — so a partial
76
93
  * override (name only, or a config that bypassed the loader) still yields a complete trailer, and a
77
- * fully-absent config yields the default account. Blank/whitespace values are treated as unset.
94
+ * fully-absent config yields the default account. Blank/whitespace values are treated as unset. An
95
+ * EXPLICITLY CONFIGURED name is emitted verbatim, with no identity spliced in — the user asked for
96
+ * that string; the identity decorates only the default. An unresolvable identity (`undefined`) falls
97
+ * back to the plain default name, never to a placeholder.
78
98
  *
79
99
  * Backend-agnostic: composed through the shared code path so BOTH the lean `GthLangChainAgent` and
80
100
  * the deep `GthDeepAgent` inject it (the git-commit capability rides on `run_shell_command`, which
81
101
  * both backends expose in code mode). Returns the note alone when there is no base prompt.
82
102
  */
83
- export declare function appendCommitCoAuthorNote(systemPrompt: string | undefined, coAuthor?: CommitCoAuthor): string;
103
+ export declare function appendCommitCoAuthorNote(systemPrompt: string | undefined, coAuthor?: CommitCoAuthor, modelIdentity?: ResolvedModelIdentity): string;
84
104
  /**
85
105
  * GS2-34/GS2-53 — the resolved active-model identity, as a STRUCTURED value.
86
106
  *