chore(gateway): rename single-shot CompletionReason "objectiveMet" to "singleShot" (#552)#569
Merged
Merged
Conversation
… "singleShot" (#552) The wire value `CompletionReason = "objectiveMet"` was emitted by `ResolveCompletionReason` when the caller provided NO `Objective` — i.e. single-shot exchanges that ran for exactly one prompt and stopped because there was nothing to drive toward. The name was a holdover from the pre-Phase 8 prose-substring objective detection (issue #379) that Phase 8 / F-11 replaced. Post-#379 the literal was misleading: it announced "objective met" when no objective was ever set. This PR renames the wire value to `"singleShot"` — the same name the corresponding local variable already uses (`singleShot = true` at `AgentExchangeService.cs:185`) — and adds an architecture fitness function that bans the legacy literal from production source under `src/**/*.cs`. ## Surface map (4 occurrences total) 1. `src/gateway/BotNexus.Gateway/Agents/AgentExchangeService.cs:400-405` (producer; helper funnels both call sites at lines 228 + 394) 2. `src/domain/BotNexus.Domain/Gateway/Models/AgentConversationResult.cs:33-46` (XML doc rewritten to list all 4 current values: `exchangeFinished`, `singleShot`, `maxTurnsReached`, `error`) 3. `tests/gateway/BotNexus.Gateway.Tests/Agents/AgentExchangeServiceTests.cs:671-708` (test renamed in place + assertion + regression-pin message) 4. NEW `tests/architecture/BotNexus.Architecture.Tests/SingleShotWireValueArchitectureTests.cs` (7 tests: 1 fence + 1 vacuity guard + 3 false-positive guards + 2 critique-fold-in false-negative pins) ## No back-compat concerns - BotNexus has no external API consumers (AGENTS.md). The wire value is consumed only by in-process callers (the LLM tool layer). - `CompletionReason` is LOCAL-ONLY: it lives on `AgentExchangeResult` (the return type of `ConverseAsync`) and is JSON-serialised to camelCase by `AgentConverseTool.cs:77-80` for the LLM to read as tool-call response text. - **`CrossWorldRelayResponse` has no `CompletionReason` field** (verified at `src/gateway/BotNexus.Gateway.Contracts/CrossWorld/CrossWorldRelayModels.cs:42-76`). The cross-world wire carries `ExchangeFinished`, `FinishReason`, `FinishSummary` — the computed `CompletionReason` never crosses the federation boundary. - `CompletionReason` is NOT a persisted entity field — no store schema or replay log references the old literal. - A repo-wide grep for `objectiveMet` confirms zero references in `*.json` / `*.md` / `*.yaml` / `*.razor` / `*.resx` — no fixtures or docs to migrate. ## Architecture fence (7 tests) `NoProductionSourceFile_ContainsLegacyObjectiveMetWireValue` scans `src/**/*.cs` for the quoted literal `"objectiveMet"` after comment-stripping. Matches the quoted literal specifically (not bare identifier) to avoid false-positives on variable names. Excludes `bin/` and `obj/`. Supporting tests: - `Fence_IsNotVacuous_AgainstSyntheticRegression` — synthetic post-revert shape must trip. - `Fence_DoesNotFalsePositive_OnCommentMention` — `//` line-comment mention is allowed. - `Fence_DoesNotFalsePositive_OnXmlDocMention` — `///` XML doc mention is allowed. - `Fence_DoesNotFalsePositive_OnUnrelatedToken` — bare identifier `objectiveMet` is allowed. - `Fence_DetectsLegacyLiteral_OnLineWithUrlString` (fold-in) — pins the URL-on-same-line false-negative the critique sweep found. - `Fence_DetectsLegacyLiteral_AfterVerbatimStringWithSlashes` (fold-in) — pins the verbatim-string-with-slashes false-negative. ## Multi-model critique sweep summary Three reviewers ran in parallel after impl (autopilot policy): - **security/gpt-5.5** — 1 LOW (fence bypass-able by concatenation / interpolation / `//` inside string). Adopted the realistic part (`//` inside string) — see fold-in below. Adversarial bypasses (concatenation, interpolation) deferred: the fence is regression detection, not adversarial defense; a developer hand-crafting `"object" + "iveMet"` to evade detection is acting in bad faith. **Critically verified**: no authz/audit/persistence switch on `CompletionReason`; no cross-world wire exposure. - **plan-vs-impl/claude-opus-4.7** — **PASS** on all ACs. All 4 current wire values documented in XML doc. Helper is the single funnel both call sites pass through. Test rename consistent (name + comment + assertion). 2 LOW cosmetic items deferred (close-issue comment + a stale doc in `docs/development/triggers-and-federation.md` describing a fictional `bool ObjectiveMet` field on a long-gone type — out of scope). - **bug-hunt/gpt-5.3-codex** — 1 MEDIUM + 1 LOW. MEDIUM is the same `//`-inside-string false-negative (rated higher than the other two reviewers) — **adopted via fold-in**. LOW (`IsObjectiveMet` comment at `AgentExchangeService.cs:182` and test name `MaxTurnsReachedWithoutObjectiveMet` at `AgentExchangeServiceTests.cs:637`) deferred: the comment is a HISTORICAL reference to the pre-Phase 8 method that the new code fixed; the test asserts `"maxTurnsReached"` not the renamed literal and describes a domain concept (was the objective met) not the wire value. The architecture fence correctly does NOT ban these — it bans the quoted literal only. ## Critique fold-in: string-aware comment stripper Replaced the naive regex `//[^\r\n]*` with a small state-machine lexer that: - Recognises regular strings (`"…"` with `\\` / `\"` escapes). - Recognises verbatim strings (`@"…"` with `""`-escaped quotes). - Recognises char literals (`'…'`). - Only strips `//` and `/* */` when NOT inside any of the above. Two new tests pin the fix: - `Fence_DetectsLegacyLiteral_OnLineWithUrlString` — input `var url = "https://x"; var reason = "objectiveMet";` would be stripped to `var url = "https:` by the old regex, hiding the literal. The new lexer preserves the string and the fence catches the literal. - `Fence_DetectsLegacyLiteral_AfterVerbatimStringWithSlashes` — same shape for verbatim strings (`@"C:\foo//bar"`). No new dependencies (no Roslyn). ~110 lines including XML doc. ## Verification - Build: 0 warnings / 0 errors under `TreatWarningsAsErrors`. - Targeted `SingleShot|NoObjectiveSet`: 1/0. - Targeted `SingleShotWireValue`: 7/0 (5 original + 2 fold-in). - Full `BotNexus.Architecture.Tests`: 76/0 (74 baseline + 2 new). - Full `BotNexus.Gateway.Tests`: 1883/0/1 skipped (unchanged from post-#565 baseline). - Full solution suite: green (pre-fold-in run before laptop reboot). Closes #552. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The wire value
CompletionReason = "objectiveMet"was emitted byResolveCompletionReasonwhen the caller provided NO
Objective— i.e. single-shot exchanges that ran for exactlyone prompt and stopped because there was nothing to drive toward. The name was a holdover
from the pre-Phase 8 prose-substring objective detection (issue #379) that Phase 8 / F-11
replaced. Post-#379 the literal was misleading: it announced "objective met" when no
objective was ever set. This PR renames the wire value to
"singleShot"— the same namethe corresponding local variable already uses (
singleShot = trueatAgentExchangeService.cs:185) — and adds an architecture fitness function that bans thelegacy literal from production source under
src/**/*.cs.Surface map (4 occurrences total)
src/gateway/BotNexus.Gateway/Agents/AgentExchangeService.cs:400-405(producer; helperfunnels both call sites at lines 228 + 394)
src/domain/BotNexus.Domain/Gateway/Models/AgentConversationResult.cs:33-46(XML docrewritten to list all 4 current values:
exchangeFinished,singleShot,maxTurnsReached,error)tests/gateway/BotNexus.Gateway.Tests/Agents/AgentExchangeServiceTests.cs:671-708(testrenamed in place + assertion + regression-pin message)
tests/architecture/BotNexus.Architecture.Tests/SingleShotWireValueArchitectureTests.cs(7 tests: 1 fence + 1 vacuity guard + 3 false-positive guards + 2 critique-fold-in
false-negative pins)
No back-compat concerns
in-process callers (the LLM tool layer).
CompletionReasonis LOCAL-ONLY: it lives onAgentExchangeResult(the return type ofConverseAsync) and is JSON-serialised to camelCase byAgentConverseTool.cs:77-80forthe LLM to read as tool-call response text.
CrossWorldRelayResponsehas noCompletionReasonfield (verified atsrc/gateway/BotNexus.Gateway.Contracts/CrossWorld/CrossWorldRelayModels.cs:42-76).The cross-world wire carries
ExchangeFinished,FinishReason,FinishSummary— thecomputed
CompletionReasonnever crosses the federation boundary.CompletionReasonis NOT a persisted entity field — no store schema or replay logreferences the old literal.
objectiveMetconfirms zero references in*.json/*.md/*.yaml/*.razor/*.resx— no fixtures or docs to migrate.Architecture fence (7 tests)
NoProductionSourceFile_ContainsLegacyObjectiveMetWireValuescanssrc/**/*.csfor thequoted literal
"objectiveMet"after comment-stripping. Matches the quoted literalspecifically (not bare identifier) to avoid false-positives on variable names. Excludes
bin/andobj/.Supporting tests:
Fence_IsNotVacuous_AgainstSyntheticRegression— synthetic post-revert shape must trip.Fence_DoesNotFalsePositive_OnCommentMention—//line-comment mention is allowed.Fence_DoesNotFalsePositive_OnXmlDocMention—///XML doc mention is allowed.Fence_DoesNotFalsePositive_OnUnrelatedToken— bare identifierobjectiveMetis allowed.Fence_DetectsLegacyLiteral_OnLineWithUrlString(fold-in) — pins the URL-on-same-linefalse-negative the critique sweep found.
Fence_DetectsLegacyLiteral_AfterVerbatimStringWithSlashes(fold-in) — pins theverbatim-string-with-slashes false-negative.
Multi-model critique sweep summary
Three reviewers ran in parallel after impl (autopilot policy):
//inside string). Adopted the realistic part (
//inside string) — see fold-in below.Adversarial bypasses (concatenation, interpolation) deferred: the fence is regression
detection, not adversarial defense; a developer hand-crafting
"object" + "iveMet"toevade detection is acting in bad faith. Critically verified: no authz/audit/persistence
switch on
CompletionReason; no cross-world wire exposure.documented in XML doc. Helper is the single funnel both call sites pass through.
Test rename consistent (name + comment + assertion). 2 LOW cosmetic items deferred
(close-issue comment + a stale doc in
docs/development/triggers-and-federation.mddescribing a fictional
bool ObjectiveMetfield on a long-gone type — out of scope).//-inside-stringfalse-negative (rated higher than the other two reviewers) — adopted via fold-in.
LOW (
IsObjectiveMetcomment atAgentExchangeService.cs:182and test nameMaxTurnsReachedWithoutObjectiveMetatAgentExchangeServiceTests.cs:637) deferred:the comment is a HISTORICAL reference to the pre-Phase 8 method that the new code
fixed; the test asserts
"maxTurnsReached"not the renamed literal and describes adomain concept (was the objective met) not the wire value. The architecture fence
correctly does NOT ban these — it bans the quoted literal only.
Critique fold-in: string-aware comment stripper
Replaced the naive regex
//[^\r\n]*with a small state-machine lexer that:"…"with\\/\"escapes).@"…"with""-escaped quotes).'…').//and/* */when NOT inside any of the above.Two new tests pin the fix:
Fence_DetectsLegacyLiteral_OnLineWithUrlString— inputvar url = "https://x"; var reason = "objectiveMet";would be stripped tovar url = "https:by the old regex, hiding the literal. The new lexer preserves thestring and the fence catches the literal.
Fence_DetectsLegacyLiteral_AfterVerbatimStringWithSlashes— same shape for verbatimstrings (
@"C:\foo//bar").No new dependencies (no Roslyn). ~110 lines including XML doc.
Verification
TreatWarningsAsErrors.SingleShot|NoObjectiveSet: 1/0.SingleShotWireValue: 7/0 (5 original + 2 fold-in).BotNexus.Architecture.Tests: 76/0 (74 baseline + 2 new).BotNexus.Gateway.Tests: 1883/0/1 skipped (unchanged from post-fix(gateway): cancellation must not seal cross-world relay session (#553) #565 baseline).Closes #552.
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com