Skip to content

Commit 2f3d28c

Browse files
committed
fix(agent): classify incomplete consequences structurally
1 parent 367831f commit 2f3d28c

2 files changed

Lines changed: 22 additions & 31 deletions

File tree

‎internal/agent/completion_gate_test.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ func TestCompletionGateStructuralReviewerMatrix(t *testing.T) {
319319
{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},
320320
{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},
321321
{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},
322+
{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},
323+
{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},
322324
{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},
323325
{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},
324326
{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."},
@@ -342,15 +344,22 @@ func TestCompletionGateStructuralReviewerMatrix(t *testing.T) {
342344
{name: "bounded observation unfinished consequence", answer: "I could not find where the flag is set, leaving the task unfinished.", incomplete: true},
343345
{name: "bounded observation change never made", answer: "I could not find where the flag is set; the change was never made.", incomplete: true},
344346
{name: "bounded observation stands incomplete", answer: "I could not find where the flag is set, and so the work stands incomplete.", incomplete: true},
347+
{name: "bounded observation relative consequence", answer: "I could not find where the flag is set, which leaves it broken.", incomplete: true},
348+
{name: "bounded observation gerund consequence", answer: "I could not find where the flag is set, meaning nothing was changed.", incomplete: true},
349+
{name: "bounded observation coordinated consequence", answer: "I could not find where the flag is set and it stays wrong.", incomplete: true},
350+
{name: "bounded observation next-sentence consequence", answer: "I could not find where the flag is set. It is still wrong.", incomplete: true},
345351
{name: "bounded observation result", answer: "I could not find where the flag is set after inspecting every registration path."},
346352
{name: "bounded observation comma-qualified result", answer: "I could not find where the flag is set, after inspecting every registration path."},
353+
{name: "bounded observation exhaustive evidence variant", answer: "I could not find where the flag is set, after carefully reviewing all registration paths."},
354+
{name: "bounded observation failed exhaustive evidence", answer: "I could not find where the flag is set, after failing every attempt.", incomplete: true},
347355
{name: "bounded observation identifies source", answer: "I could not find where the regression was introduced; the source is the parser boundary."},
348356
{name: "bounded observation disclaims concern", answer: "I could not find the flag being set anywhere outside tests, so the concern does not apply."},
349357
{name: "failed noun is not asserted outcome", answer: "I could not find any issues; the failed test in CI is a known flake."},
350358
{name: "failed predicate is asserted outcome", answer: "I could not find any issues; the operation failed.", incomplete: true},
351359
{name: "read-only no-change report", answer: "I could not find any remaining issues. I did not modify any files."},
352360
{name: "same-sentence read-only no-change report", answer: "I could not find any remaining issues; I did not modify any files."},
353361
{name: "read-only made-no-change report", answer: "I could not find any remaining issues. I made no changes."},
362+
{name: "affirmative cleanup result", answer: "I could not find any leftover references; the cleanup is complete."},
354363
{name: "required fix left unchanged", answer: "I could not find where to apply the required fix. I did not modify any files.", incomplete: true},
355364
{name: "singular recognized absence", answer: "I could not find a bug after inspecting every changed path."},
356365
{name: "singular deliverable remains admission", answer: "I could not find a solution after inspecting every changed path.", incomplete: true},

‎internal/agent/guardrails.go‎

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ func hasUnexemptedSubjectElidedInability(sentence string, after int) bool {
10341034
return false
10351035
}
10361036

1037-
var subjectElidedMissedWorkPattern = regexp.MustCompile(`^never\s+(?:applied|built|changed|completed|deployed|edited|finished|implemented|migrated|modified|published|ran|reviewed|tested|validated|verified|wrote)\b`)
1037+
var subjectElidedMissedWorkPattern = regexp.MustCompile(`^(?:never|did\s+not|didn't)\s+[[:alpha:]][[:alnum:]_-]*\b`)
10381038

10391039
// subjectElidedFailure finds the failure predicate after a coordinating
10401040
// connector. The subject may be inherited from the first clause and ordinary
@@ -1287,7 +1287,7 @@ var strongAbsenceObjects = []string{
12871287
// absenceQualifiers sit between "any" and the object without changing it.
12881288
var absenceQualifiers = []string{
12891289
"remaining", "other", "further", "more", "additional", "obvious", "such",
1290-
"outstanding", "new", "existing", "actual", "real", "clear", "direct",
1290+
"outstanding", "leftover", "new", "existing", "actual", "real", "clear", "direct",
12911291
}
12921292

12931293
// strongAbsence reports whether tail is an "any"-family absence whose OBJECT
@@ -1435,26 +1435,6 @@ var consequenceBoundaries = []string{
14351435
" because ", " since ", "; ", ": ", " - ", " -- ", " and ", ", ",
14361436
}
14371437

1438-
func explicitConsequenceBoundary(boundary string) bool {
1439-
return strings.HasPrefix(boundary, ";") ||
1440-
strings.Contains(boundary, " so ") || strings.Contains(boundary, " but ") ||
1441-
strings.Contains(boundary, "therefore") || strings.Contains(boundary, "leaving") ||
1442-
strings.Contains(boundary, "because") || strings.Contains(boundary, "since")
1443-
}
1444-
1445-
func hasExplicitConsequenceBoundary(sentence string, stemEnd int) bool {
1446-
if stemEnd < 0 || stemEnd >= len(sentence) {
1447-
return false
1448-
}
1449-
tail := sentence[stemEnd:]
1450-
for _, boundary := range consequenceBoundaries {
1451-
if explicitConsequenceBoundary(boundary) && strings.Contains(tail, boundary) {
1452-
return true
1453-
}
1454-
}
1455-
return false
1456-
}
1457-
14581438
// reportedConsequence returns an asserted outcome after one matched inability.
14591439
// Separators before the stem are irrelevant, and weak separators inside a
14601440
// `that ...` negated proposition remain part of what was not found. Explicit
@@ -1472,7 +1452,10 @@ func reportedConsequence(sentence string, stemEnd int) string {
14721452
if index < 0 {
14731453
continue
14741454
}
1475-
explicit := explicitConsequenceBoundary(boundary)
1455+
explicit := strings.HasPrefix(boundary, ";") ||
1456+
strings.Contains(boundary, " so ") || strings.Contains(boundary, " but ") ||
1457+
strings.Contains(boundary, "therefore") || strings.Contains(boundary, "leaving") ||
1458+
strings.Contains(boundary, "because") || strings.Contains(boundary, "since")
14761459
if thatAt >= 0 && index > thatAt && !explicit {
14771460
continue
14781461
}
@@ -1555,13 +1538,15 @@ var topicShiftMarkers = []string{
15551538
}
15561539

15571540
var affirmativeObservationConsequencePattern = regexp.MustCompile(`\b(?:(?:the\s+)?(?:cause|source|root\s+cause|value|setting|definition|registration|owner|result)\s+(?:is|was)\b|(?:the\s+)?concern\s+(?:does|did)\s+not\s+apply\b|(?:it|the\s+(?:issue|bug|problem|change|fix|guard))\s+(?:is|was|looks|remains)\s+(?:resolved|fixed|complete|completed|done|correct|valid|safe|neutral|unaffected)\b|(?:the\s+)?(?:fix|guard|check)\s+holds\b)`)
1541+
var exhaustiveObservationEvidencePattern = regexp.MustCompile(`^after\s+(?:(?:[[:alpha:]][[:alnum:]_-]*ly)\s+)*(?:auditing|checking|examining|inspecting|reading|reviewing|searching|tracing|validating|verifying)\s+(?:every|all)\b`)
15581542

15591543
// observationConsequenceIsAffirmative is deliberately an allow-list: an
15601544
// unfamiliar consequence must not turn an admitted inability into success just
15611545
// because its failure wording is absent from a deny-list. It recognizes either
15621546
// an explicit completed action or a bounded positive result of the observation.
15631547
func observationConsequenceIsAffirmative(consequence string) bool {
15641548
return affirmativeObservationConsequencePattern.MatchString(consequence) ||
1549+
exhaustiveObservationEvidencePattern.MatchString(consequence) ||
15651550
fallbackOutcomeIsAffirmative(consequence)
15661551
}
15671552

@@ -1571,19 +1556,16 @@ func observationConsequenceIsAffirmative(consequence string) bool {
15711556
// This is the inverse of enumerating every synonym for work that did not land.
15721557
func boundedObservationHasUnresolvedConsequence(claim inabilityClaim) bool {
15731558
consequence := strings.TrimSpace(reportedConsequence(claim.sentence, claim.stemAt+claim.stemLen))
1574-
if consequence != "" && hasExplicitConsequenceBoundary(claim.sentence, claim.stemAt+claim.stemLen) &&
1575-
carriesTheConsequence(consequence) &&
1576-
!observationConsequenceIsAffirmative(consequence) {
1577-
return true
1578-
}
1579-
if containsFailureConsequence(consequence) {
1580-
return true
1559+
if consequence != "" {
1560+
return carriesTheConsequence(consequence) &&
1561+
!observationConsequenceIsAffirmative(consequence)
15811562
}
15821563
if len(claim.blockedContext) <= len(claim.sentence) {
15831564
return false
15841565
}
15851566
next := strings.TrimSpace(claim.blockedContext[len(claim.sentence):])
1586-
return containsFailureConsequence(next)
1567+
return carriesTheConsequence(next) &&
1568+
!observationConsequenceIsAffirmative(next)
15871569
}
15881570

15891571
// countedLabelContent separates a counted markdown label from any content

0 commit comments

Comments
 (0)