Commit Graph
12 Commits
Author SHA1 Message Date
steve 285789c89b test(agent): enumerate the orientations instead of patching them one by one
CI / Tidy (pull_request) Successful in 9m21s
CI / Build & Test (pull_request) Successful in 10m3s
Three consecutive rounds found the same rule broken in a different
orientation — content after the pointer, then content before it in the
same sentence — because each round's cases only covered the direction
that round was about, and each fix was then tested only in the direction
I had just thought of. A fourth patch was not the answer; the missing
thing was a harness.

TestBareAbovePointerOrientations crosses every pointer form with every
position content can occupy: alone, before in the same sentence, before
in its own sentence, on its own line, as a list item, after in each of
those, and behind a filler opener. 36 cells, one assertion — bare IFF
there is no content — and each cell first asserts the input really is a
pointer, so a mistyped fixture fails loudly instead of passing vacuously.

It earned itself immediately: three cells failed on the first run, and
the bug was mine and shipped. clauseBoundaryChars gained the em dash last
round, and the cut did start = k + 1 — but LastIndexAny returns the BYTE
index of the boundary rune, and an em dash is three bytes. "Done — as
shown above." sliced mid-rune, left a stray continuation byte in the
remainder that no trim removes, and a genuinely bare pointer stopped
being recovered. Now advances by the rune's width.

Break-check: sixteen mutations, each killed by a named test, control
survives. Two had to be reformulated after the harness started failing
mutations that were only "killed" by breaking the build — including the
one for this very fix.
2026-08-22 00:32:45 -04:00
steve 184627c570 fix(agent): gadfly round 4 — the answer can come BEFORE the pointer
CI / Tidy (pull_request) Successful in 9m22s
CI / Build & Test (pull_request) Successful in 9m46s
My round-3 fix has the same defect it fixed, mirrored. bareAbovePointer
cut back to the previous FULL STOP, not the previous clause, so anything
sharing the reference's sentence ahead of it was swallowed:

    "Ship Tuesday, as shown above."                  bare=true
    "OK. The verdict is guilty, as detailed above."  bare=true
    "The answer is sixty minutes, as computed above." bare=true

All three read as pure pointers and were replaced by an earlier turn.
The comment called it a clause cut; the code cut a sentence. It survived
a round because every case I had written put the answer AFTER the
pointer — I fixed the direction I had thought of and tested only that.
clauseBoundaryChars now includes , ; : and the em dash.

Three more, all real:

- pointsAbove and bareAbovePointer each did their own FindStringIndex
  plus offset check, so the two could disagree about what a pointer is.
  One aboveRefLoc now answers that.
- fillerWords was extracted to stop drift while its separator class was
  still copy-pasted beside it — the same defect one token over. fillerSep
  shares it too.
- A missing blank comment line ran two paragraphs together.

The padded 96-byte "bare pointer" fixture is gone rather than bent a
third time. Its premise does not survive the rule: a genuinely bare
pointer is its own clause and nothing else, so it is SHORT, and the two
recovery bars cannot disagree about a short terminal. I had twice
reshaped that string to keep a case alive whose scenario the code no
longer admits. The modeBackRef path stays covered by the bare-pointer
cases in TestIsWeakFinal and the "(Already answered above.)" end-to-end.

Break-check: fifteen mutations including a regression to the sentence
cut, each killed by a named test; the harness now also fails a mutation
that only breaks the build.
2026-08-22 00:28:53 -04:00
steve 1756910ef0 fix(agent): gadfly round 3 — the pointer must be all there is
CI / Tidy (pull_request) Successful in 9m22s
CI / Build & Test (pull_request) Successful in 9m48s
Round 2 fixed this class on isSummaryCloser and left its twin standing
on isWeakFinal. Three of the four findings are that twin, and every one
of them reproduced:

    "That's the chain above. Ship Tuesday."          weak=true
    "See the summary above. Option B wins."          weak=true
    "Anything above 100 boils. See the note above."  weak=true
    "Anything above, say, 40 degrees is a problem."  weak=true

Each of those is 37-58 bytes with the answer sitting right next to the
pointer, and each was disposable — a >=200-byte earlier turn replaced it
and "Ship Tuesday" went in the bin. pointsAbove asks whether a pointer is
PRESENT; disposability needs it to be ALL THERE IS.

bareAbovePointer cuts the reference's own clause — from the end of the
previous sentence through the match — and requires what remains to be
filler: nothing, punctuation, or a throat-clearing "Done —". Cutting the
clause rather than testing position is what makes the other two shapes
safe for free: a mixed terminal keeps its first sentence, and a
comparative "above," with an interjection keeps the rest of its own.

The fourth finding (kimi) is the same idea one level up:
compressionMarkerRe matched anywhere, so "Given the analysis above, the
bottom line is that we need a different vendor" read as an announced
summary when it is a conclusion. The marker must now OPEN a sentence.

Both classes now enforce one rule from opposite ends: a terminal is
disposable only when it carries no answer of its own — proved in
isWeakFinal by nothing standing beside the pointer, and in
isSummaryCloser by the model declaring what stands beside it a
compression.

One of my own round-1b fixtures had to change: "…is above, so there is
no point repeating it" is a pure pointer to a human, but prose after the
reference is indistinguishable from an answer, so the rule correctly
stops treating it as disposable. Replaced with a genuinely bare 96-byte
pointer, still sized so the two recovery bars disagree about it.

Break-check: thirteen mutations, each killed by a named test, control
survives. M12 initially "passed" by failing to compile — reformulated so
it builds, and TestIsWeakFinal/prepositional-then-deictic-with-content
kills it properly.
2026-08-22 00:16:47 -04:00
steve 3f1c016e74 fix(agent): gadfly round 2 — a pointer is not a compression
CI / Tidy (pull_request) Successful in 9m22s
CI / Build & Test (pull_request) Successful in 9m47s
Four findings, all real.

**A deictic closer now needs a compression marker too** (opus,
correctness). The pointsAbove branch of isSummaryCloser required only a
back-reference, so a terminal like "Given the analysis above, I
recommend option B because X" — a pointer followed by a CONCLUSION the
earlier turn never contained — would be discarded in favour of that
turn, throwing away the answer. The ack shape does not have this problem
because "Citations are logged." carries nothing; a bare pointer does not
carry that guarantee.

Both halves are now required: the pointer says the full answer is
elsewhere, and compressionMarkerRe ("Short version:", "TL;DR", "In
short") is the model saying what sits beside it is a condensation rather
than new reasoning. #1611's closer has both. A marker without a pointer
stays out of scope for the reason already documented — a user who asked
for brevity gets exactly that shape. Unmatched closers keep today's
behaviour, so the narrowing fails closed.

**CRLF** (opus, error-handling): "above\r\n" was not clause-final, so a
CRLF transcript quietly lost every line-final deictic. CR joins LF in the
terminator set.

**A comment wrapped mid-phrase** so that "// -style" read as a list
marker (sonnet) — reflowed as part of rewriting that doc block.

**Process provenance in a test comment** (sonnet): "(gadfly, 3 models)"
is an execution log, not an invariant. The lesson survives, the
attribution does not.

Break-check is ten mutations now — dropping CR and dropping the
compression requirement are each killed by their own named cases — with
the control surviving.
2026-08-22 00:03:15 -04:00
steve 9534e442fb test(agent): gadfly round 1b — pin the exact bound and the bar asymmetry
CI / Tidy (pull_request) Successful in 9m23s
CI / Build & Test (pull_request) Successful in 10m49s
Sonnet's three findings, all on the tests rather than the rule:

- The "at the bound" case sat at index 115 (and 101 after the first
  round), never at 120 itself, so <= vs < was never exercised. Both sides
  of the boundary are now pinned — index 120 accepted, 121 rejected — and
  an off-by-one mutation is killed by the new case.
- A SHORT (<=120-byte) deictic closer takes the modeBackRef bar, not the
  summary closer's mandatory dwarf ratio, even though it can carry a
  scrap of answer content. That asymmetry is deliberate and pre-existing
  (a "see above" closer has always had it): inside 120 bytes there is no
  room for both a pointer and a real answer. Pinned with a case sized so
  the two bars actually DISAGREE — 3x98 = 294 > the 275-byte prior turn,
  so the summary bar would reject what the back-ref bar accepts. The
  first draft of this fixture was 83 bytes and passed under either bar,
  proving nothing.
- The isBackRef doc finding was sonnet's fourth independent report of it;
  already fixed in the previous commit.

Break-check now runs eight mutations, including the off-by-one and a
swap of the classifier ordering; each is killed by a named test and the
control survives.
2026-08-21 23:44:52 -04:00
steve f97c2b78c2 fix(agent): gadfly round 1 — the hyphen made "above-board" a back-reference
CI / Tidy (pull_request) Successful in 9m25s
CI / Build & Test (pull_request) Successful in 10m43s
Three of four reviewers independently found the same defect: `\b` holds
between "above" and "-", so the literal hyphen in aboveRefRe's terminator
class made every hyphenated compound clause-final. "above-average",
"above-board", "above-ground" all read as deictic pointers, putting a
legitimate short closer at risk of being discarded. My own test used the
SPACE-separated "above average" — it tested the neighbour, not the named
path, which is exactly why the class survived a round of review. Hyphen
dropped from the class; the four compounds are now table cases.

Two more findings, both real:

- backRefHeadChars and weakFinalMaxChars were two unlinked 120 literals
  that the comment called "the same guard". Now defined by reference,
  with a test pinning the identity.
- isBackRef claimed to be the shared extension point for both shapes but
  had a single caller — isSummaryCloser deliberately uses pointsAbove
  directly, because backRefRe's fixed phrases match ANYWHERE and are only
  safe under the 120-byte weak cap. Folding the two together for
  tidiness would widen the gate, not deduplicate it. Helper deleted, the
  doc moved onto pointsAbove where the real sharing is, and it now says
  why backRefRe is not shared.

Also shared the front-loaded-analysis fixture between the table and the
end-to-end test (kimi), and de-hollowed the offset-bound cases: they were
sized as backRefHeadChars±n, so they moved with the constant they were
meant to pin — a break-check that widened the bound to 100000 sailed
through. Literal lengths now, plus an explicit identity assertion.

Break-check: six mutations, each killed by a named test; control survives.
2026-08-21 23:41:46 -04:00
steve bcba9667bd fix(agent): recover the answer behind a bare "above" pointer
Gadfly review (reusable) / review (pull_request) Failing after 1s
Adversarial Review (Gadfly) / review (pull_request) Failing after 2s
CI / Tidy (pull_request) Successful in 9m28s
CI / Build & Test (pull_request) Successful in 9m54s
mort issue #1611: run 8eea3e82 front-loaded a 2,245-char analysis into
its cite-call turn and closed with 220 bytes — "Done — that's the full
chain above. Short version: …". The user got the 220 bytes and a
pointer at a chain that was never posted.

finalOutput already had three shapes for this pathology, and the closer
matched none of them: too long for the weak-final cap (220 > 120), no
citations heading, and no "Citations are logged." ack to open the
summary-closer class. So it was delivered verbatim.

The three shapes were each a separate vocabulary of ack phrases, which
is why a fourth phrasing walked straight through. Two of them are really
one signal — the terminal DEFERS, telling us the answer is somewhere the
user cannot see — differing only in whether the terminal also carries
content of its own. That signal is now isBackRef, shared by both, so a
new phrasing is added once and covered in the bare and the
"+ compression" variant at the same time.

Its open-ended half is aboveRefRe: a DEICTIC "above", separated from the
preposition by what follows the word. The deictic use ends its clause
("that's the full chain above.", "as shown above,"); the preposition
always continues into a noun phrase ("above 100°C", "above the fold",
"above all, …"). pointsAbove additionally requires the reference in the
terminal's first 120 bytes — with almost no text before it in THIS
message, it cannot be pointing at the message's own content.

isSummaryCloser now opens on either the citations ack or pointsAbove.
Recovery is unchanged: the mandatory dwarf ratio and the user-message
scan boundary still gate it, so a closer only loses to a prior turn in
the same user turn that is clearly the fuller original.

Break-checked: reverting either classifier, dropping the clause-final
rule, or dropping the offset bound each kills a named test; the
unmutated control survives.
2026-08-21 23:20:11 -04:00
steveandClaude Fable 5 1bbbdaa1e5 refactor(agent): gadfly round 2 — shared leadingMarkers, explicit mode, comment altitude
CI / Tidy (pull_request) Successful in 9m24s
CI / Build & Test (pull_request) Successful in 9m52s
All tidiness, no behavior change: the leading-marker class is one shared
constant for citationLabelRe and summaryCloserRe (hand-copying it is how
'+' went missing the first time); the deliberate 'all' duplication across
summaryCopulas/summaryArticle is now stated at both sites; the weak-final
switch case assigns modeBackRef explicitly; test comments state the
constraint they guard instead of which reviewer asked for them.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-05 20:29:19 -04:00
steveandClaude Fable 5 21b4775d16 fix(agent): gadfly round 1 — user-boundary scan, back-ref precedence, regex legibility
CI / Tidy (pull_request) Successful in 9m39s
CI / Build & Test (pull_request) Successful in 10m5s
Two behavioral fixes from the review:

- modeSummary's backward scan now stops at the most recent user message.
  With the dwarf ratio rejecting the current turn's 1x-3x answer, the old
  unbounded scan could walk into WithHistory content and resurrect a stale
  answer to a DIFFERENT question — strictly worse than keeping the closer
  (opus, correctness). Other modes keep their historical unbounded scan.
- A terminal matching BOTH the ack shape and a back-reference is now
  classified back-ref: it carries no answer content, so the looser bar is
  the right one (opus, error-handling).

Plus the nits: summaryCloserRe assembled from named fragments, the leading
marker class gains '+' (parity with citationLabelRe), verb-first form takes
'all the', dwarf ratio hoisted into one named local, and the 151-vs-153
char/byte comment inaccuracy corrected.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-05 20:17:56 -04:00
steveandClaude Fable 5 127966bb3a fix(agent): recover the front-loaded answer over a summary closer
CI / Build & Test (pull_request) Successful in 9m49s
CI / Tidy (pull_request) Successful in 10m28s
Gadfly review (reusable) / review (pull_request) Successful in 11m49s
Adversarial Review (Gadfly) / review (pull_request) Successful in 11m49s
A third degenerate terminal shape from the glm-5.2 cite pattern: the model
front-loads its full answer into the cite-call turn, then closes with a
bookkeeping ack plus a one-line compression ("Citations are logged. Short
version: ..."). mort run b3cb9ee9 delivered 151 chars of a 2,089-char
answer this way — the closer was neither a back-reference (over the 120
cap, no back-ref phrase) nor a citations addendum (no label-colon, no
links), so finalOutput let it stand.

isSummaryCloser keys on the ack sentence alone (the verb must end the
sentence, so prose about citations never matches; a compression marker
without the ack is deliberately out of scope), and the new modeSummary
recovery bar makes the 3x dwarf ratio mandatory at every length: unlike a
back-reference this closer carries real answer content, so it is only
displaced by the clearly-fuller original it compressed.

The citations/back-ref bool becomes a three-way recoveryMode; existing
behavior for both old modes is unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-05 08:47:40 -04:00
steveandClaude Opus 4.8 6abb399f5b fix(agent): recover front-loaded answer past a citations-only terminal turn (#11)
CI / Tidy (push) Successful in 9m35s
CI / Build & Test (push) Successful in 10m31s
finalOutput now recovers the front-loaded answer when the terminal turn is a
sources/citations-only addendum ("Sources: [x](url), ..."), not just when it is
empty or a back-reference. It recovers the prior substantive answer and appends
the (real) citations below it. Guards: citation-DOMINANCE (a prose answer that
merely opens with "Source: ... http://..." is left as the answer), ^-anchored
heading, citations recovery decoupled from the terminal-length ratio (concise
answers recover too), preamble filter applied only in the borderline band
(long answers opening with "Sure,"/"Let me" are not vetoed), and a dedup that
ignores <url> angle-bracket wrappers. Healthy terminal answers unchanged; zero
extra model calls.

Fixes mort #1418. Gadfly-reviewed (6 reviewers) + adversarially pre-verified;
all findings graded, real ones addressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-10 16:47:32 +00:00
steveandClaude Opus 4.8 1fd7109a42 fix(agent): recover front-loaded answer when terminal turn is degenerate
CI / Tidy (pull_request) Successful in 9m31s
CI / Build & Test (pull_request) Successful in 10m14s
CI / Tidy (push) Successful in 9m26s
CI / Build & Test (push) Successful in 10m19s
The agent loop took the final answer only from the terminal (no-tool-call)
turn. Models that "front-load" their answer into an earlier turn that also
calls a tool — then close with a trivial pointer like "(Already answered
above.)" — had their real answer discarded and the pointer delivered. This
recurs across several open-weight models (glm-5.2, etc.); well-behaved models
(Claude/GPT) defer their answer to the terminal turn and are unaffected.

finalOutput() now falls back to the last substantive assistant content in the
transcript when the terminal text is weak (empty, or a short back-reference).
The predicate is narrow and back-reference-gated so short-but-correct answers
("42", "It's down, restarting now.") are never overridden; recovery only picks
a prior turn that reads like a real answer, not a preamble. Zero extra model
calls. Terminal-answer behavior for normal runs is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 18:37:38 -04:00