Skip to content

chore(gateway): rename single-shot CompletionReason "objectiveMet" to "singleShot" (#552)#569

Merged
sytone merged 1 commit into
mainfrom
chore/552-singleshot-rename
May 27, 2026
Merged

chore(gateway): rename single-shot CompletionReason "objectiveMet" to "singleShot" (#552)#569
sytone merged 1 commit into
mainfrom
chore/552-singleshot-rename

Conversation

@sytone

@sytone sytone commented May 27, 2026

Copy link
Copy Markdown
Owner

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.7PASS 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-fix(gateway): cancellation must not seal cross-world relay session (#553) #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

… "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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Gateway] Rename single-shot CompletionReason 'objectiveMet' -> 'singleShot'

1 participant