Skip to content

Commit 373fc05

Browse files
Let an attempt change the file it created
A write step's kind is resolved once, when the plan is built, from whether the file is on disk, and that stops being true the moment the step's own first call lands. The two write tools then contradicted each other: apply-edit refuses a second wholesale rewrite and tells the run to send a patch instead, on a step whose only permitted tool was apply-edit. There was no call the run could make that would change the file. Ladder rung 9 lost an attempt to it -- the fix for its one failing test written at 41.9s and refused, rewritten at 50.8s and refused again, the attempt ending on tests that did not pass. A creating step now declares both write tools and the tools decide: apply-edit refuses a rewrite it has already accepted once, and apply-patch has nothing to match against in a file that is not there. A patch step is not widened, because there the file exists from the start and a wholesale rewrite is the churn the patch tool was added to stop. Separately, the no-op write detection, the staleness flag and the write counter all asked "== apply-edit", written when that was the only write tool. From the second attempt onward, when every run switches to patching, none of them saw a write -- so a suite answered from the last run told a run "the write did not land" while four patches had applied cleanly and undone each other. It now says which of those two things happened. Change-Log: CL-20260803-148 Dev-Log: DL-20260803-158 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5577f21 commit 373fc05

8 files changed

Lines changed: 448 additions & 25 deletions

File tree

CHANGELOG

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,74 @@ Dev-Log:
5050

5151
Entries
5252
-------
53-
Change-ID: CL-20260803-147
53+
Change-ID: CL-20260803-149
54+
Commit: pending
55+
Date: 2026-08-03
56+
Type: Fix -- a patch could not say which of several identical places it meant,
57+
and trailing text became part of the hunk
58+
Request-or-TODO: Ladder rung 9
59+
Outcome: A patch names the scope a change is inside in one of two ways: a
60+
heading on the "@@" line, or a hunk of nothing but context immediately before
61+
the change. Both were discarded -- the heading as envelope, the context block
62+
as a hunk that changes nothing -- so a hunk whose context was a closing brace
63+
and its indent was searched for across the whole file and matched once per
64+
function. Rung 9 sent the same six-line patch six times in a row, told each
65+
time to add more context, with the context it needed sitting in the hunk above
66+
it. Both forms now travel as a hunk's Anchor: it is never applied and never
67+
required to match, and its only effect is to narrow where the hunk is looked
68+
for, so a stale scope name costs nothing. Separately, the parser accepts an
69+
unprefixed line as context written without its leading space, which is right
70+
inside a hunk and wrong after "*** End Patch": a run that finished its patch
71+
and wrote a stray "EOF" had that EOF folded into the last hunk, which then
72+
asked the file to contain a line no file contains.
73+
Affected-behavior: apply-patch. Ambiguity refusals fall; a patch followed by
74+
trailing text applies.
75+
Compatibility-or-migration: None. No schema change.
76+
Verification: Rung 9 passes in 502s after six failing passes. Its patch failures
77+
by kind: "does not match anything" fell to 6 of 109 after the end-marker fix,
78+
from dominant before it; "matches N places" was 103 of 109 before the anchor
79+
fix. Six new tests cover both anchor forms, the line-range control that must
80+
not become an anchor, an anchor that resolves to nothing, trailing text after
81+
the end marker, and a second envelope whose hunks must not be dropped.
82+
Dev-Log: DL-20260803-159
83+
84+
Change-ID: CL-20260803-148
5485
Commit: pending
5586
Date: 2026-08-03
87+
Type: Fix -- a step could not use the tool its own first call made necessary,
88+
and the suite told a run its writes had not landed when they had
89+
Request-or-TODO: Ladder rung 9
90+
Outcome: A write step's kind is resolved once, when the plan is built, from
91+
whether the file is on disk. That stops being true the moment the step's first
92+
call lands. The two write tools then contradicted each other: apply-edit
93+
refuses a second wholesale rewrite and tells the run to send a patch instead,
94+
on a step whose only permitted tool was apply-edit. There was no call the run
95+
could make that would change the file. Rung 9 lost an attempt to it: the fix
96+
for its one failing test was written at 41.9s, refused, rewritten at 50.8s,
97+
refused again, and the attempt ended reporting that its tests did not pass. A
98+
creating step now declares both write tools and the tools themselves decide --
99+
apply-edit refuses a rewrite it has already accepted once, and apply-patch has
100+
nothing to match against in a file that is not there. A patch step is not
101+
widened, because there the file exists from the start. Separately, the no-op
102+
write detection, the staleness flag and the write counter all asked
103+
"== apply-edit", written when that was the only write tool, so from the second
104+
attempt onward -- when every run switches to patching -- none of them saw a
105+
write. A suite answered from the last run therefore told a run "the write did
106+
not land" while four patches had applied cleanly and undone each other; it now
107+
says which of those two things happened.
108+
Affected-behavior: A run can revise the file it just created. A suite answered
109+
from cache names the real reason it is unchanged.
110+
Compatibility-or-migration: None. An edit step's plan now declares two
111+
completion tools; the loop's contract accepts that pair and no other.
112+
Verification: Rung 9 reached its integration tests passing for the first time in
113+
five passes, and reached path-coverage. New tests cover the pair, the
114+
patch-step control, the loop's acceptance of the pair, both notes, and the
115+
write-tool set.
116+
Dev-Log: DL-20260803-158
117+
118+
Change-ID: CL-20260803-147
119+
Commit: 4237a5d
120+
Date: 2026-08-03
56121
Type: Chore -- remove code nothing calls, and satisfy the atom lint the commit
57122
hook enforces
58123
Request-or-TODO: Unblocking the commit of CL-20260803-140 through -146
@@ -78,7 +143,7 @@ Dev-Log: None: this is cleanup carried out to land the entries above, and it is
78143
nothing.
79144

80145
Change-ID: CL-20260803-146
81-
Commit: pending
146+
Commit: 5577f21
82147
Date: 2026-08-03
83148
Type: Checkpoint -- unrelated in-flight work swept in at the user's request
84149
Request-or-TODO: User asked for everything in the working tree to be committed
@@ -101,7 +166,7 @@ Dev-Log: None. This work has no development-log entry because this session did
101166
not do it.
102167

103168
Change-ID: CL-20260803-145
104-
Commit: pending
169+
Commit: 6a5aaa9
105170
Date: 2026-08-03
106171
Type: Fix -- the patch tool applied a two-file patch to whichever file was named
107172
last, and searched every hunk against the whole file
@@ -133,7 +198,7 @@ Verification: Rung 7's patch record across four passes: 4 of 25 landing, then 10
133198
Dev-Log: DL-20260803-155
134199

135200
Change-ID: CL-20260803-144
136-
Commit: pending
201+
Commit: a965e8b
137202
Date: 2026-08-03
138203
Type: Fix -- four hard gates were measured only after the loop that was meant to
139204
inform them, and two of them said things that were not true
@@ -168,7 +233,7 @@ Verification: Rungs 4, 6 and 7 pass, each having failed on the gate the change
168233
Dev-Log: DL-20260803-153, DL-20260803-154, DL-20260803-155
169234

170235
Change-ID: CL-20260803-143
171-
Commit: pending
236+
Commit: 658a746
172237
Date: 2026-08-03
173238
Type: Fix -- the attempt budget and the stall tracker disagreed with themselves
174239
Request-or-TODO: Ladder rungs 5 and 9
@@ -195,7 +260,7 @@ Verification: Rung 5 passes. New tests in
195260
Dev-Log: DL-20260803-155
196261

197262
Change-ID: CL-20260803-142
198-
Commit: pending
263+
Commit: ae366b1
199264
Date: 2026-08-03
200265
Type: Fix -- the loop spent whole attempts on mistakes a round could correct
201266
Request-or-TODO: Ladder rungs 1, 4, 5 and 9
@@ -222,7 +287,7 @@ Verification: Rungs 1, 5, 6, 7 and 8 pass. New tests in internal/agent cover eac
222287
Dev-Log: DL-20260803-150, DL-20260803-152, DL-20260803-155
223288

224289
Change-ID: CL-20260803-141
225-
Commit: pending
290+
Commit: 1fb3d24
226291
Date: 2026-08-03
227292
Type: Fix -- the prompt described the run's own files inaccurately
228293
Request-or-TODO: Ladder rungs 1, 4, 5 and 6
@@ -250,7 +315,7 @@ Verification: Rungs 1 through 8 pass. The patch failure rate for a patch written
250315
Dev-Log: DL-20260803-151, DL-20260803-152, DL-20260803-153
251316

252317
Change-ID: CL-20260803-140
253-
Commit: pending
318+
Commit: 004d2f7
254319
Date: 2026-08-03
255320
Type: Fix -- gates that contradicted each other, and a time report that could not
256321
be read

DEVLOG

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,74 @@ Next-safe-step:
2525

2626
Entries
2727
-------
28+
Dev-Log: DL-20260803-159
29+
Date: 2026-08-03
30+
Status: Committed as CL-20260803-149
31+
Change-ID: CL-20260803-149
32+
Request-or-TODO: Ladder rung 9
33+
Goal: Stop the patch tool refusing patches that said exactly where they went
34+
Assumptions: That a hunk of pure context immediately before a change is naming
35+
the scope of that change, rather than being decorative. Checked against the
36+
patches rung 9 actually sent: every one of them used the two-marker form with
37+
the enclosing test function in the first block
38+
Decisions: An anchor never causes a failure. It narrows the search and nothing
39+
else, so an anchor naming something the file no longer has -- a renamed
40+
function, a stale copy -- leaves the hunk exactly as findable as it was
41+
without one. Refusing on a bad anchor would have converted a class of
42+
successful patches into a new class of failures, which is the opposite of the
43+
point. A context block AFTER the last change still anchors nothing and is
44+
still dropped, which is the case the earlier forgiveness was added for
45+
Files-or-schemas: internal/executor/patch_tool.go -- PatchHunk gains Anchor;
46+
ParsePatch folds context-only hunks and "@@" headings into it; ApplyPatch
47+
searches from just past a resolved anchor. No storage schema touched
48+
Validation: internal/executor passes. Six new tests, each run against the
49+
previous implementation first: the context-block anchor records
50+
"hunk 2 matches 2 places", the heading anchor records "does not match
51+
anything", the trailing-EOF case records a hunk whose Before ends in EOF.
52+
Rung 9 passes end to end in 502s
53+
Failures-or-discarded-approaches: First attempt treated the anchor as an
54+
ordinary context-only hunk inside ApplyPatch, which never ran: ParsePatch had
55+
already dropped every context-only hunk before ApplyPatch saw the request.
56+
Second attempt appended a "@@" heading to the hunk's own context, which made
57+
the hunk unmatchable -- the heading and the hunk body are not contiguous in
58+
the file. The anchor has to be a separate field for that reason
59+
Known-limitations: Ambiguity is still possible when neither form is used. The
60+
message asking for more context is unchanged for that case
61+
Next-safe-step: Rung 10
62+
63+
Dev-Log: DL-20260803-158
64+
Date: 2026-08-03
65+
Status: Committed as CL-20260803-148
66+
Change-ID: CL-20260803-148
67+
Request-or-TODO: Ladder rung 9
68+
Goal: Let an attempt change the file it created, and stop misreporting why a
69+
suite result is unchanged
70+
Assumptions: None. Both defects are visible in the rung 9 pass 4 trace: the
71+
refusal at 41.9s naming a tool the round did not offer, and the "unchanged"
72+
answers at 66.9s and 76.1s after four patches had applied
73+
Decisions: Widened the plan rather than relaxing the refusal. Refusing a second
74+
wholesale rewrite is right -- it is the churn the patch tool exists to stop --
75+
and the defect was that the alternative it names was not callable. Only the
76+
edit kind is widened; a patch step's file exists from the start, so nothing
77+
about it changes during the attempt and admitting a rewrite there would
78+
reopen the churn
79+
Files-or-schemas: internal/agent/loop.go -- planStepKindContract gains
80+
alsoCompletedBy and a permits method; internal/coordinator/write_step_kind.go
81+
-- writeToolsFor; agent_planning.go and agent_narration.go call it;
82+
agent_narration.go -- isWriteTool, writesSinceLastTest, and a two-way
83+
unchangedTestNote
84+
Validation: internal/agent passes. New coordinator tests, each run against the
85+
previous implementation first: the pair test records "an edit step permits
86+
[apply-edit], so the run has no way to revise the file its first call
87+
created". Rung 9 reached passing integration tests for the first time
88+
Failures-or-discarded-approaches: Considered relaxing the rewrite refusal when
89+
apply-patch is unavailable. Rejected: on attempt one every write step is an
90+
edit step, so that is the common case, and it would have removed the churn
91+
control entirely rather than fixing the contradiction
92+
Known-limitations: The step kind is still resolved once per attempt. Only the
93+
tool set is now tolerant of the file moving underneath it
94+
Next-safe-step: DL-20260803-159
95+
2896
Dev-Log: DL-20260803-157
2997
Date: 2026-08-03
3098
Status: Committed as CL-20260803-146

internal/agent/loop.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,18 +1336,40 @@ func requestArgument(
13361336
}
13371337

13381338
type planStepKindContract struct {
1339-
completionTool executor.ToolName
1339+
completionTool executor.ToolName
1340+
// alsoCompletedBy is the one other tool this kind may declare, or empty
1341+
// when the kind admits exactly one. It exists for the write kinds, where
1342+
// which tool is right depends on whether the file is there, and that
1343+
// changes during the attempt rather than between attempts.
1344+
alsoCompletedBy executor.ToolName
13401345
materialEdit bool
13411346
validationRequired bool
13421347
}
13431348

1349+
// permits reports whether a declared completion-tool set is the one this kind
1350+
// allows.
1351+
//
1352+
// The primary tool is required and must come first, because it is the tool the
1353+
// step is named for and the one a run reaches for by default. The alternate is
1354+
// optional: a plan may declare it or not, and both are the same kind of step.
1355+
func (contract planStepKindContract) permits(declared []executor.ToolName) bool {
1356+
if len(declared) == 0 || declared[0] != contract.completionTool {
1357+
return false
1358+
}
1359+
if len(declared) == 1 {
1360+
return true
1361+
}
1362+
return len(declared) == 2 &&
1363+
contract.alsoCompletedBy != "" &&
1364+
declared[1] == contract.alsoCompletedBy
1365+
}
1366+
13441367
func validatePlanStepContract(step PlanStep) error {
13451368
contract, ok := planStepContract(step.Kind)
13461369
if !ok {
13471370
return fmt.Errorf("%w: step kind is invalid", ErrPlanContract)
13481371
}
1349-
if len(step.CompletionTools) != 1 ||
1350-
step.CompletionTools[0] != contract.completionTool {
1372+
if !contract.permits(step.CompletionTools) {
13511373
return fmt.Errorf(
13521374
"%w: a %s step is completed by %s and this one declares %v",
13531375
ErrPlanContract, step.Kind, contract.completionTool,
@@ -1425,7 +1447,15 @@ func planStepContract(kind StepKind) (planStepKindContract, bool) {
14251447
case StepKindEdit:
14261448
return planStepKindContract{
14271449
completionTool: executor.ToolApplyEdit,
1428-
materialEdit: true, validationRequired: true,
1450+
// A step is planned as an edit because the file it names does not
1451+
// exist yet, and it stops being true the moment the step's first
1452+
// call lands. The rest of the attempt is revising a file that is
1453+
// now there, which is what apply-patch is for, so an edit step
1454+
// carries both and the write tools themselves decide which is
1455+
// right: apply-edit refuses a second wholesale rewrite, and
1456+
// apply-patch cannot create a file it has no context for.
1457+
alsoCompletedBy: executor.ToolApplyPatch,
1458+
materialEdit: true, validationRequired: true,
14291459
}, true
14301460
case StepKindPatch:
14311461
return planStepKindContract{

0 commit comments

Comments
 (0)