Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e603b95
fix(agent): the incompletion detector keeps its allowances from swall…
gnanam1990 Aug 16, 2026
b6ad1fc
fix(agent): keep the subjectless stem, and stop blocked-work markers …
gnanam1990 Aug 16, 2026
5ad7344
fix(agent): verb-anchor the objective markers, and stop a tool caveat…
gnanam1990 Aug 16, 2026
732c760
fix(agent): an absence is a finding only when the object makes it one
gnanam1990 Aug 16, 2026
2fbb5ff
fix(agent): the consequence may be the next sentence, and "blocked" m…
gnanam1990 Aug 16, 2026
a257b7f
fix(agent): an explicit failure state outranks a recognised absence o…
gnanam1990 Aug 16, 2026
b486a88
fix(agent): a failure state inside the negated proposition is not the…
gnanam1990 Aug 17, 2026
a5b0a31
fix(agent): recognise a tool caveat in its copula forms
gnanam1990 Aug 17, 2026
f2d98e7
fix(agent): repair two dead marker lists this PR shipped
gnanam1990 Aug 18, 2026
a5b2ed6
fix(agent): only the "any" forms make an observation verb a successfu…
gnanam1990 Aug 19, 2026
d035884
test(agent): check tail reachability in the direction the runtime uses
gnanam1990 Aug 19, 2026
4438180
fix(agent): a tool named as an excuse does not exempt the failure it …
gnanam1990 Aug 19, 2026
d232e8c
fix(agent): a tool caveat exempts a failure by position, not by conne…
gnanam1990 Aug 20, 2026
84b5d10
fix(agent): keep tool caveats from hiding admissions
gnanam1990 Aug 28, 2026
b7828a0
fix(agent): make completion admission checks clause-aware
gnanam1990 Aug 28, 2026
a02d829
fix(agent): scope completion evidence to clauses
gnanam1990 Aug 28, 2026
c12695f
fix(agent): scope completion fallback evidence
gnanam1990 Aug 28, 2026
04627e7
fix(agent): tighten capability completion exemptions
gnanam1990 Aug 28, 2026
6a5f750
fix(agent): bind fallbacks to completed operations
gnanam1990 Aug 29, 2026
aca0693
fix(agent): classify completion claims by obligation
gnanam1990 Aug 29, 2026
55f410e
fix(agent): coordinate blocked completion outcomes
gnanam1990 Aug 30, 2026
64a1444
fix(agent): preserve completion obligation scope
gnanam1990 Aug 31, 2026
1bdf520
fix(agent): reject incomplete completion reports
gnanam1990 Aug 31, 2026
7d98f1e
fix(agent): close completion classifier gaps
gnanam1990 Sep 1, 2026
f5b76f5
fix(agent): preserve completion obligation semantics
gnanam1990 Sep 2, 2026
4c41d4e
fix(agent): preserve completion claim semantics
gnanam1990 Sep 7, 2026
367831f
fix(agent): classify incomplete consequence structures
gnanam1990 Sep 12, 2026
2f3d28c
fix(agent): classify incomplete consequences structurally
gnanam1990 Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions internal/agent/completion_gate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,182 @@ func TestMaxTurnsCutoffIsIncompleteUnderGate(t *testing.T) {
t.Fatalf("IncompleteReason = %q, want it to cite max-turns", result.IncompleteReason)
}
}

// These cases exercise the public run loop rather than the classifier helpers.
// Each final answer used to pass the headless completion gate even though it
// explicitly admitted that the requested work was not complete.
func TestCompletionGateRejectsCurrentHeadReviewAdmissions(t *testing.T) {
admissions := []string{
"I could not find the root cause.",
"I could not find where to apply the fix.",
"I could not locate the migration script.",
"I was unable to determine where the regression was introduced, so the change was never applied.",
"I could not locate the file. The change was never applied.",
"I do not have the network access I needed, but the task is complete.",
"I do not have a write tool available in this context and could not complete the task.",
"Unable to complete the task (1):",
"**Unable to verify (1):** - the migration was never run",
"Unable to deploy (1): - production deployment failed.",
"**Unable to verify (1):** - the migration did not run",
"I could not run the migration because no migration tool is available, so I ran it manually instead, but it failed.",
"I could not run the migration because no migration tool is available, so I ran it manually instead. It failed.",
"I could not run the migration because no migration tool is available, so I ran it manually instead and it did not work.",
"I could not deploy to our production cluster because no deployment tool is available, so I deployed to our staging cluster manually instead.",
"I could not deploy to the main environment because no deployment tool is available, so I deployed to the staging environment manually instead.",
"I could not deploy to the primary production environment because no deployment tool is available, so I deployed to the primary staging environment manually instead.",
"I could not publish to an internal production registry because no release tool is available, so I published to an internal staging registry manually instead.",
"I could not deploy to prod because no deployment tool is available, so I deployed to dev manually instead.",
"I do not have the API key or the tools available in this session.",
}

for _, admission := range admissions {
t.Run(admission, func(t *testing.T) {
provider := &mockProvider{turns: [][]zeroruntime.StreamEvent{textTurn(admission)}}
result, err := Run(context.Background(), "complete the requested operation", provider, Options{
Registry: tools.NewRegistry(),
MaxTurns: 2,
RequireCompletionSignal: true,
})
if err != nil {
t.Fatal(err)
}
if !result.Incomplete {
t.Fatalf("admission passed the production completion gate: %q", admission)
}
})
}
}

func TestCompletionGateKeepsCurrentHeadReviewControlsComplete(t *testing.T) {
complete := []string{
"I could not find where the regression was introduced; the source is the parser boundary.",
"**Unable to verify (1):** - MCP #3 claim was truncated.",
"I don't have an update_plan tool available in this specialist context; only read-only exploration tools were provided.",
"I could not deploy to our production cluster because no deployment tool is available, so I deployed to our production cluster manually instead.",
"I could not deploy to the primary production environment because no deployment tool is available, so I deployed to the primary production environment manually instead.",
}

for _, answer := range complete {
t.Run(answer, func(t *testing.T) {
provider := &mockProvider{turns: [][]zeroruntime.StreamEvent{textTurn(answer)}}
result, err := Run(context.Background(), "complete the requested operation", provider, Options{
Registry: tools.NewRegistry(),
MaxTurns: 2,
RequireCompletionSignal: true,
})
if err != nil {
t.Fatal(err)
}
if result.Incomplete {
t.Fatalf("completed control was rejected: %q (%s)", answer, result.IncompleteReason)
}
})
}
}

func TestCompletionGateStructuralReviewerMatrix(t *testing.T) {
cases := []struct {
name string
answer string
incomplete bool
}{
{name: "capability note only", answer: "I don't have an update_plan tool available in this specialist context; only read-only exploration tools were provided."},
{name: "capability note followed by subject-elided failure", answer: "I don't have a write tool available in this context and could not apply the required fix.", incomplete: true},
{name: "same publish object completed", answer: "I could not publish the package because no publishing tool is available, so I published the package manually instead."},
{name: "different publish object", answer: "I could not publish the package because no publishing tool is available, so I published the release notes manually instead.", incomplete: true},
{name: "same destination completed", answer: "I could not publish the package for production because no publishing tool is available, so I published the package for production manually instead."},
{name: "different destination", answer: "I could not publish the package for production because no publishing tool is available, so I published the package for staging manually instead.", incomplete: true},
{name: "all coordinated tests completed", answer: "I could not run the unit and integration tests because no test tool is available, so I ran the unit and integration tests manually instead."},
{name: "coordinated test subset", answer: "I could not run the unit and integration tests because no test tool is available, so I ran the unit tests manually instead.", incomplete: true},
{name: "every test completed", answer: "I could not run every test because no test tool is available, so I ran every test manually instead."},
{name: "smoke substituted for every test", answer: "I could not run every test because no test tool is available, so I ran a smoke test manually instead.", incomplete: true},
{name: "affirmative fallback", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead."},
{name: "explicitly successful fallback", answer: "I could not deploy the release because no deployment tool is available, so I successfully deployed it manually instead."},
{name: "past-perfect affirmative fallback", answer: "I could not deploy the release because no deployment tool is available, so I had deployed it manually instead."},
{name: "qualified affirmative fallback", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead, but it was successful."},
{name: "negated fallback", answer: "I could not deploy the release because no deployment tool is available, so I never deployed it manually instead.", incomplete: true},
{name: "partial fallback", answer: "I could not deploy the release because no deployment tool is available, so I partially deployed it manually instead.", incomplete: true},
{name: "unsuccessful fallback", answer: "I could not deploy the release because no deployment tool is available, so I unsuccessfully deployed it manually instead.", incomplete: true},
{name: "attempted fallback", answer: "I could not deploy the release because no deployment tool is available, so I attempted to deploy it manually instead.", incomplete: true},
{name: "fallback crashed", answer: "I could not run the migration because no migration tool is available, so I ran it manually instead, but it crashed.", incomplete: true},
{name: "benign counted audit bucket", answer: "**Unable to verify (1):** - MCP #3 claim was truncated."},
{name: "counted operation rejected", answer: "**Unable to publish (1):** - registry rejected the request.", incomplete: true},
{name: "exhaustive negative finding", answer: "I could not find any issues after inspecting every changed path."},
{name: "blocked negative finding", answer: "I could not find any issues due to running out of time.", incomplete: true},
{name: "counterfactual fallback", answer: "I could not deploy the release because no deployment tool is available, so I would have deployed it manually instead.", incomplete: true},
{name: "timed out fallback", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead, but it timed out.", incomplete: true},
{name: "cancelled fallback", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead, but it was cancelled.", incomplete: true},
{name: "unfinished fallback", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead, but it did not finish.", incomplete: true},
{name: "not successful fallback", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead, but it was not successful.", incomplete: true},
{name: "incomplete fallback", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead, but it was incomplete.", incomplete: true},
{name: "unlisted adversative fallback failure", answer: "I could not deploy the release because no deployment tool is available, so I deployed it manually instead, but it expired.", incomplete: true},
{name: "full suite replaced by smoke", answer: "I could not run the full test suite because no test tool is available, so I ran a full smoke test manually instead.", incomplete: true},
{name: "full suite completed", answer: "I could not run the full test suite because no test tool is available, so I ran the full test suite manually instead."},
{name: "modifier-bearing coordinated failure", answer: "I don't have a write tool available in this context and therefore could not apply the required fix.", incomplete: true},
{name: "unlisted modifier coordinated failure", answer: "I don't have a write tool available in this context and consequently could not apply the required fix.", incomplete: true},
{name: "short modifier coordinated failure", answer: "I don't have a write tool available in this context and thus could not apply the required fix.", incomplete: true},
{name: "multiword modifier coordinated failure", answer: "I don't have a write tool available in this context and as a result could not apply the required fix.", incomplete: true},
{name: "subject elided negative action", answer: "I don't have a write tool available in this context and so never applied the required fix.", incomplete: true},
{name: "subject elided unlisted never action", answer: "I don't have a write tool available in this context and so never touched the file.", incomplete: true},
{name: "subject elided unlisted did-not action", answer: "I don't have a write tool available in this context and accordingly did not land it.", incomplete: true},
{name: "reordered coordinated failure", answer: "I could not apply the required fix, and I don't have a write tool available in this context.", incomplete: true},
{name: "separately punctuated coordinated failure", answer: "I don't have a write tool available in this context. I could not apply the required fix.", incomplete: true},
{name: "direct capability footnote", answer: "I don't have an update_plan tool available in this specialist context; only read-only exploration tools were provided."},
{name: "completion declaration cannot self certify", answer: "I could not run the migration because no migration tool is available, but the task is complete.", incomplete: true},
{name: "plan capability note remains complete", answer: "I could not call update_plan because that tool is unavailable, but the task is complete."},
{name: "multiline counted operation failure", answer: "**Unable to deploy (1):**\n- production deployment failed.", incomplete: true},
{name: "multiline asterisk counted operation failure", answer: "**Unable to deploy (2):**\n* production deployment failed.\n* staging deployment failed.", incomplete: true},
{name: "multiline ordered counted operation failure", answer: "**Unable to deploy (1):**\n1. production deployment failed.", incomplete: true},
{name: "multiline counted operation after blank line", answer: "**Unable to deploy (1):**\n\n+ production deployment failed.", incomplete: true},
{name: "multiline benign audit bucket", answer: "**Unable to verify (1):**\n- MCP #3 source record was truncated."},
{name: "multiline benign audit asterisk after blank line", answer: "**Unable to verify (1):**\n\n* MCP #3 source record was truncated."},
{name: "multiline benign audit ordered entry", answer: "**Unable to verify (1):**\n1. MCP #3 source record was truncated."},
{name: "same-line benign audit ordered entry", answer: "**Unable to verify (1):** 1. MCP #3 source record was truncated."},
{name: "multiline benign audit bullets", answer: "**Unable to verify (2):**\n- MCP #3 source record was truncated.\n- MCP #4 evidence was omitted."},
{name: "multiline later failed bullet", answer: "**Unable to verify (2):**\n- MCP #3 source record was truncated.\n- the operation failed.", incomplete: true},
{name: "bounded observation task not done", answer: "I could not find where the flag is set, so the task is not done.", incomplete: true},
{name: "bounded observation fix unapplied", answer: "I could not find where the flag is set, so the fix has not been applied.", incomplete: true},
{name: "bounded observation made no change", answer: "I could not find the value being set, so I made no change.", incomplete: true},
{name: "bounded observation remains unfixed", answer: "I could not find where the flag is set, so it remains unfixed.", incomplete: true},
{name: "bounded observation nothing changed", answer: "I could not find where the flag is set, so nothing changed.", incomplete: true},
{name: "bounded observation unfinished consequence", answer: "I could not find where the flag is set, leaving the task unfinished.", incomplete: true},
{name: "bounded observation change never made", answer: "I could not find where the flag is set; the change was never made.", incomplete: true},
{name: "bounded observation stands incomplete", answer: "I could not find where the flag is set, and so the work stands incomplete.", incomplete: true},
{name: "bounded observation relative consequence", answer: "I could not find where the flag is set, which leaves it broken.", incomplete: true},
{name: "bounded observation gerund consequence", answer: "I could not find where the flag is set, meaning nothing was changed.", incomplete: true},
{name: "bounded observation coordinated consequence", answer: "I could not find where the flag is set and it stays wrong.", incomplete: true},
{name: "bounded observation next-sentence consequence", answer: "I could not find where the flag is set. It is still wrong.", incomplete: true},
{name: "bounded observation result", answer: "I could not find where the flag is set after inspecting every registration path."},
{name: "bounded observation comma-qualified result", answer: "I could not find where the flag is set, after inspecting every registration path."},
{name: "bounded observation exhaustive evidence variant", answer: "I could not find where the flag is set, after carefully reviewing all registration paths."},
{name: "bounded observation failed exhaustive evidence", answer: "I could not find where the flag is set, after failing every attempt.", incomplete: true},
{name: "bounded observation identifies source", answer: "I could not find where the regression was introduced; the source is the parser boundary."},
{name: "bounded observation disclaims concern", answer: "I could not find the flag being set anywhere outside tests, so the concern does not apply."},
{name: "failed noun is not asserted outcome", answer: "I could not find any issues; the failed test in CI is a known flake."},
{name: "failed predicate is asserted outcome", answer: "I could not find any issues; the operation failed.", incomplete: true},
{name: "read-only no-change report", answer: "I could not find any remaining issues. I did not modify any files."},
{name: "same-sentence read-only no-change report", answer: "I could not find any remaining issues; I did not modify any files."},
{name: "read-only made-no-change report", answer: "I could not find any remaining issues. I made no changes."},
{name: "affirmative cleanup result", answer: "I could not find any leftover references; the cleanup is complete."},
{name: "required fix left unchanged", answer: "I could not find where to apply the required fix. I did not modify any files.", incomplete: true},
{name: "singular recognized absence", answer: "I could not find a bug after inspecting every changed path."},
{name: "singular deliverable remains admission", answer: "I could not find a solution after inspecting every changed path.", incomplete: true},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
provider := &mockProvider{turns: [][]zeroruntime.StreamEvent{textTurn(tc.answer)}}
result, err := Run(context.Background(), "complete the requested operation", provider, Options{
Registry: tools.NewRegistry(),
MaxTurns: 2,
RequireCompletionSignal: true,
})
if err != nil {
t.Fatal(err)
}
if result.Incomplete != tc.incomplete {
t.Fatalf("Incomplete = %v, want %v for %q (reason: %s)", result.Incomplete, tc.incomplete, tc.answer, result.IncompleteReason)
}
})
}
}
Loading
Loading