fix(tools): tolerate indent drift + content alias in file_edit; concrete scope-widen remedy
file_edit failed en masse for small models on two ergonomics traps: - replace() rejected calls that sent `content` (the append param) instead of `replacement` — intent unambiguous; now accepted as an alias. - exact-string match died on leading-whitespace drift (model can't reproduce indentation). Added whitespace-flexible line matching + replacement reindent, wired into both the pre-exec validation gate and replace(). Also made the WRITE_SCOPE / PATH_OUTSIDE_MANIFEST block messages emit a literal copy-pasteable task_update(id=..., affected_paths=[...]) call and warn against action=block — the exact wrong turn models kept taking (18x in one session). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rdo9fe7SujNVeyZA8YkpkD
This commit is contained in:
+6
-2
@@ -78,8 +78,12 @@ class ManifestContainmentRule : ToolCallRule {
|
||||
val remedy = if (taskScope.isEmpty()) {
|
||||
" Claim a task whose affected_paths cover this path (task_update), then retry."
|
||||
} else {
|
||||
" If this path is genuinely part of the task, widen its affected_paths via " +
|
||||
"task_update (note why), then retry."
|
||||
val taskId = input.session.activeTask?.taskId
|
||||
val widened = (taskScope + raw).joinToString(", ") { "\"$it\"" }
|
||||
" If this path is genuinely part of the task, widen scope with this exact call — " +
|
||||
"task_update(id=\"$taskId\", affected_paths=[$widened], note=\"why $raw is in " +
|
||||
"scope\") — then retry the write. affected_paths REPLACES the old list, so keep " +
|
||||
"the existing paths. Do NOT use action=block/unblock; that does not change scope."
|
||||
}
|
||||
issues += ValidationIssue(
|
||||
code = "PATH_OUTSIDE_MANIFEST",
|
||||
|
||||
@@ -52,11 +52,15 @@ class WriteScopeRule : ToolCallRule {
|
||||
)
|
||||
|
||||
if (!inScope) {
|
||||
val widened = (active.scope + raw).joinToString(", ") { "\"$it\"" }
|
||||
issues += ValidationIssue(
|
||||
code = RULE_CODE,
|
||||
message = "Tool '${input.request.toolName}' writes '$raw', outside claimed task " +
|
||||
"${active.taskId}'s affected_paths (${active.scope}). If this change is needed, " +
|
||||
"add its path via task_update affected_paths (note why), then retry.",
|
||||
"widen the scope with this exact call — task_update(id=\"${active.taskId}\", " +
|
||||
"affected_paths=[$widened], note=\"why $raw is in scope\") — then retry the write. " +
|
||||
"The affected_paths list REPLACES the old one, so include the existing paths shown " +
|
||||
"above. Do NOT use action=block/unblock; that does not change scope.",
|
||||
severity = ValidationSeverity.ERROR,
|
||||
)
|
||||
disposition = maxAction(disposition, RiskAction.BLOCK)
|
||||
|
||||
@@ -59,6 +59,7 @@ class WriteScopeRuleTest {
|
||||
assertEquals(RiskAction.BLOCK, r.disposition)
|
||||
assertEquals("WRITE_SCOPE", r.issues.single().code)
|
||||
assertTrue(r.issues.single().message.contains("auth-2"))
|
||||
assertTrue(r.issues.single().message.contains("task_update affected_paths"))
|
||||
assertTrue(r.issues.single().message.contains("task_update(id=\"auth-2\""))
|
||||
assertTrue(r.issues.single().message.contains("affected_paths=["))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user