@lazyingart/agintiflow 0.20.215 → 0.20.217
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/supervision-campaign-ledger.md +81 -0
- package/package.json +1 -1
- package/scripts/smoke-coding-tools.js +284 -1
- package/scripts/smoke-dynamic-step-budget.js +5312 -548
- package/scripts/smoke-local-failure-recovery.js +8 -0
- package/scripts/smoke-math-rendering.js +22 -1
- package/scripts/smoke-progressive-tool-selection.js +824 -7
- package/scripts/smoke-runtime-compat.js +4 -1
- package/scripts/smoke-tmux-tools.js +22 -4
- package/scripts/smoke-truthful-completion.js +76 -0
- package/src/agent-runner.js +3546 -239
- package/src/command-policy.js +802 -49
- package/src/progressive-tool-selection.js +412 -11
- package/src/scs-evidence.js +431 -80
- package/src/shell-syntax.js +679 -0
- package/src/tool-contract.js +13 -1
- package/src/workspace-tools.js +7 -6
|
@@ -186,3 +186,84 @@ DeepSeek-to-LocalLLM handoff when local perception is enabled, continue to block
|
|
|
186
186
|
hosted vision without explicit authorization, and block all automatic vision
|
|
187
187
|
when both local and hosted routes are disabled. A focused regression covers the
|
|
188
188
|
guard and the actual LocalLLM client route.
|
|
189
|
+
|
|
190
|
+
### SQLite migration and literal-query safety
|
|
191
|
+
|
|
192
|
+
`database-migration-safety-015` passed on AgInTiFlow `0.20.215` from a normal,
|
|
193
|
+
imperfect maintenance prompt. The DeepSeek-backed database profile diagnosed a
|
|
194
|
+
destructive version-1 migration, `INSERT OR REPLACE` identity loss, and unsafe
|
|
195
|
+
`LIKE` semantics. In one retained session it replaced the migration with an
|
|
196
|
+
in-place transaction, preserved item IDs, tags, and relationships, used an
|
|
197
|
+
identity-preserving UPSERT, escaped `%`, `_`, and backslashes as literal search
|
|
198
|
+
text, added regression tests, and committed target repair `fb97fbd`.
|
|
199
|
+
|
|
200
|
+
The run also exercised recovery behavior without a supervisor rescue prompt.
|
|
201
|
+
DeepSeek initially requested too many tools in one turn; the contract guard
|
|
202
|
+
rejected that batch and the next turn continued with allowed calls. Later, a
|
|
203
|
+
noncanonical `TEST_EXIT:0` shell suffix confused project-test evidence despite
|
|
204
|
+
five passing tests. The agent recognized the discrepancy, reran the canonical
|
|
205
|
+
README command, obtained `passed:true`, cleaned transient Python artifacts, and
|
|
206
|
+
finished normally. No AgInTiFlow product patch was required for this scenario.
|
|
207
|
+
|
|
208
|
+
Independent acceptance used
|
|
209
|
+
`supervision/acceptance/database_migration_safety_contract.py` to verify legacy
|
|
210
|
+
IDs `7`, `12`, and `19`, tag relationships, schema columns, foreign keys,
|
|
211
|
+
idempotent reopening, stable URL updates, literal punctuation, archive
|
|
212
|
+
filtering, absence of destructive SQL, intentional commit history, and a clean
|
|
213
|
+
worktree. The hidden contract passed.
|
|
214
|
+
|
|
215
|
+
### Shell grammar, mutation revision, and output provenance hardening
|
|
216
|
+
|
|
217
|
+
The next campaign phase exercised completion evidence under realistic compound
|
|
218
|
+
commands, multiline acceptance criteria, delegated test runners, Git workflows,
|
|
219
|
+
and pre-existing output files. The reusable repair centralizes shell
|
|
220
|
+
canonicalization, command classification, Git-action intent, and evidence
|
|
221
|
+
tracking instead of adding project- or prompt-specific branches. In particular,
|
|
222
|
+
escaped line continuations and heredocs are parsed structurally; read-only test
|
|
223
|
+
evidence is separated from write capability; later mutations invalidate stale
|
|
224
|
+
validation; commit, pull-request, tag, and push evidence must occur in the
|
|
225
|
+
requested order; and exact output files count only when the current required
|
|
226
|
+
generator created or changed them.
|
|
227
|
+
|
|
228
|
+
Five independent review rounds found and drove regressions for inline mutation
|
|
229
|
+
batches, stale opaque validators, Git grammar and ordering, zero-test runners,
|
|
230
|
+
external executable paths, arithmetic shifts mistaken for heredocs, multiline
|
|
231
|
+
command substitutions, stale exact outputs, and ambiguous Git nouns such as
|
|
232
|
+
"commit message" or "branch diagram". A final fresh hosted Codex review was
|
|
233
|
+
blocked by its rolling quota and the DeepSeek review route was blocked by
|
|
234
|
+
provider balance. A separate read-only `localllm-deep` review completed with no
|
|
235
|
+
actionable findings. The full `npm test` suite, focused dynamic-step-budget,
|
|
236
|
+
coding-tools, SCS-evidence, and syntax checks all pass before packaging
|
|
237
|
+
AgInTiFlow `0.20.216`.
|
|
238
|
+
|
|
239
|
+
### Same-session interruption convergence and bounded completion
|
|
240
|
+
|
|
241
|
+
The `context-interruption-016` campaign exercised a long retained DeepSeek
|
|
242
|
+
session with multiple concrete interruptions, source corrections, an exact
|
|
243
|
+
external acceptance command, and a final clean-repository requirement. The
|
|
244
|
+
agent owned every target edit and commit. Early recovery turns repeatedly read
|
|
245
|
+
the same already-correct diff, showing that token limits alone do not guarantee
|
|
246
|
+
convergence while the available action surface remains open ended.
|
|
247
|
+
|
|
248
|
+
The reusable repair is driven by runtime state and evidence rather than task
|
|
249
|
+
literals. Mutations invalidate stale tests and prior completion evidence;
|
|
250
|
+
same-task interruptions refresh per-turn acceptance while retaining durable
|
|
251
|
+
evidence; repository-state recovery derives task-owned paths since the latest
|
|
252
|
+
successful commit and offers a bounded `commit_project_changes` action; and
|
|
253
|
+
the runtime constructs the path-scoped Git command. A current-revision exact
|
|
254
|
+
validator creates a one-use completion candidate. The following turn compacts
|
|
255
|
+
the evidence and exposes only `finish`; rejection or mutation invalidates the
|
|
256
|
+
candidate.
|
|
257
|
+
|
|
258
|
+
The final live continuation converged in three turns: bounded commit, exact
|
|
259
|
+
validator, and truthful finish. The target ended clean at commit `bd74bbd`.
|
|
260
|
+
Repository-repair context compacted from 156,716 to 5,788 characters, and the
|
|
261
|
+
verified-completion context compacted from 23,658 to 5,157 characters.
|
|
262
|
+
|
|
263
|
+
An independent read-only DeepSeek review then drove three general fixes: a
|
|
264
|
+
dedicated task-owned commit-path validator that supports binary,
|
|
265
|
+
extensionless, and instruction files while rejecting protected paths;
|
|
266
|
+
platform-aware POSIX and Windows command quoting; and recognition of bounded
|
|
267
|
+
stdin `sed -n` pipelines as read-only inspection. Production code contains no
|
|
268
|
+
campaign scenario IDs, target paths, expected prose, commit IDs, or acceptance
|
|
269
|
+
literals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.217",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
buildModelTimeoutRetryMessages,
|
|
8
8
|
genericArtifactFilenameBlock,
|
|
9
9
|
modelTimeoutRetryRoute,
|
|
10
|
+
applyModelTimeoutRetryRoute,
|
|
11
|
+
recoverFocusedTextRewriteWithWritingSpecialist,
|
|
10
12
|
repairModelMessageHistory,
|
|
11
13
|
shouldResetStaticDiscoveryPhase,
|
|
12
14
|
runAgent,
|
|
@@ -16,10 +18,11 @@ import {
|
|
|
16
18
|
shellDiagnosticHint,
|
|
17
19
|
skippedAfterBlockedToolResult,
|
|
18
20
|
} from "../src/agent-runner.js";
|
|
21
|
+
import { createToolContract, resolveDispatchableToolCallBatch } from "../src/tool-contract.js";
|
|
19
22
|
import { formatBehaviorContractForPrompt } from "../src/behavior-contract.js";
|
|
20
23
|
import { resolveRuntimeConfig } from "../src/config.js";
|
|
21
24
|
import { readCodebaseMap } from "../src/codebase-map.js";
|
|
22
|
-
import { evaluateCommandPolicy } from "../src/command-policy.js";
|
|
25
|
+
import { classifyCommand, evaluateCommandPolicy } from "../src/command-policy.js";
|
|
23
26
|
import { shouldReviewToolResult } from "../src/scs-controller.js";
|
|
24
27
|
import {
|
|
25
28
|
engineeringGuidanceForTask,
|
|
@@ -328,6 +331,39 @@ try {
|
|
|
328
331
|
packageInstallPolicy: "block",
|
|
329
332
|
commandCwd: workspace,
|
|
330
333
|
});
|
|
334
|
+
const opaqueInPlaceEditPolicy = evaluateCommandPolicy("sed -i 's/old/new/' report.md", {
|
|
335
|
+
allowShellTool: true,
|
|
336
|
+
sandboxMode: "host",
|
|
337
|
+
packageInstallPolicy: "block",
|
|
338
|
+
commandCwd: workspace,
|
|
339
|
+
allowDestructive: true,
|
|
340
|
+
});
|
|
341
|
+
assert(
|
|
342
|
+
opaqueInPlaceEditPolicy.writesWorkspace === true,
|
|
343
|
+
"an unknown host shell edit was incorrectly classified as read-only"
|
|
344
|
+
);
|
|
345
|
+
const destructiveGitPolicy = evaluateCommandPolicy("git reset --hard HEAD~1", {
|
|
346
|
+
allowShellTool: true,
|
|
347
|
+
sandboxMode: "host",
|
|
348
|
+
packageInstallPolicy: "block",
|
|
349
|
+
commandCwd: workspace,
|
|
350
|
+
allowDestructive: true,
|
|
351
|
+
});
|
|
352
|
+
assert(
|
|
353
|
+
destructiveGitPolicy.category === "destructive" &&
|
|
354
|
+
destructiveGitPolicy.writesWorkspace === true,
|
|
355
|
+
"a destructive git command was not classified as a workspace mutation"
|
|
356
|
+
);
|
|
357
|
+
const safeTagPolicy = evaluateCommandPolicy("git tag v0.20.216", {
|
|
358
|
+
allowShellTool: true,
|
|
359
|
+
sandboxMode: "host",
|
|
360
|
+
packageInstallPolicy: "block",
|
|
361
|
+
commandCwd: workspace,
|
|
362
|
+
});
|
|
363
|
+
assert(
|
|
364
|
+
safeTagPolicy.allowed && safeTagPolicy.category === "git-workflow" && safeTagPolicy.writesWorkspace,
|
|
365
|
+
"a bounded local git tag was not classified as a git workflow action"
|
|
366
|
+
);
|
|
331
367
|
assert(boundedRgPolicy.allowed, "targeted bounded rg should remain allowed");
|
|
332
368
|
const boundedAdvice = buildPermissionAdvice({
|
|
333
369
|
toolName: "run_command",
|
|
@@ -615,6 +651,14 @@ try {
|
|
|
615
651
|
);
|
|
616
652
|
assert(readonlyVersionPipelinePolicy.allowed, "read-only version probe pipelines should not require package-install-policy=allow");
|
|
617
653
|
assert(readonlyVersionPipelinePolicy.category === "read-only", "read-only version probe pipelines should be classified as read-only");
|
|
654
|
+
const readonlyDiffSlicePolicy = evaluateCommandPolicy(
|
|
655
|
+
"git diff -- src/agent-runner.js | sed -n '1,240p'",
|
|
656
|
+
dockerWorkspaceNoInstallsPolicy
|
|
657
|
+
);
|
|
658
|
+
assert(
|
|
659
|
+
readonlyDiffSlicePolicy.allowed && readonlyDiffSlicePolicy.category === "read-only",
|
|
660
|
+
"a bounded sed print filter made a read-only Git diff pipeline require broad shell access"
|
|
661
|
+
);
|
|
618
662
|
const nodeNpmTestPolicy = evaluateCommandPolicy(
|
|
619
663
|
'cd /workspace && node --version && npm test 2>&1; echo "EXIT:$?"',
|
|
620
664
|
dockerWorkspaceNoInstallsPolicy
|
|
@@ -692,6 +736,53 @@ try {
|
|
|
692
736
|
const actualDangerAfterQuotePolicy = evaluateCommandPolicy('echo "rm -rf is text" && rm -rf reports', dockerWorkspacePolicy);
|
|
693
737
|
assert(!actualDangerAfterQuotePolicy.allowed, "actual destructive command after quoted text should still be blocked");
|
|
694
738
|
assert(actualDangerAfterQuotePolicy.category === "destructive", "actual destructive command after quoted text was not classified as destructive");
|
|
739
|
+
for (const command of [
|
|
740
|
+
"npm test &",
|
|
741
|
+
"npm test & sed -i 's/old/new/' report.md",
|
|
742
|
+
]) {
|
|
743
|
+
const classification = classifyCommand(command);
|
|
744
|
+
const policy = evaluateCommandPolicy(command, hostWorkspacePolicy);
|
|
745
|
+
assert(
|
|
746
|
+
!policy.allowed &&
|
|
747
|
+
classification.category !== "test" &&
|
|
748
|
+
classification.substantiveTest !== true &&
|
|
749
|
+
classification.writesWorkspace === true,
|
|
750
|
+
`background execution bypassed bounded test policy: ${command}`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const backgroundPublishPolicy = evaluateCommandPolicy(
|
|
754
|
+
"npm test & npm publish",
|
|
755
|
+
{ ...hostWorkspacePolicy, allowDestructive: true, allowPasswords: true }
|
|
756
|
+
);
|
|
757
|
+
assert(
|
|
758
|
+
!backgroundPublishPolicy.allowed && backgroundPublishPolicy.category === "blocked",
|
|
759
|
+
"a background test bypassed the hard package-publication guard"
|
|
760
|
+
);
|
|
761
|
+
for (const command of [
|
|
762
|
+
`node -e 'require("fs").writeFileSync("report.md", "changed")' --test`,
|
|
763
|
+
"node test/unit.test.js --test",
|
|
764
|
+
]) {
|
|
765
|
+
const classification = classifyCommand(command);
|
|
766
|
+
assert(
|
|
767
|
+
classification.category !== "test" &&
|
|
768
|
+
classification.substantiveTest !== true &&
|
|
769
|
+
classification.writesWorkspace === true,
|
|
770
|
+
`a Node entrypoint fabricated test-runner identity: ${command}`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const redirectedTestPolicy = evaluateCommandPolicy("npm test 2>&1", hostWorkspacePolicy);
|
|
774
|
+
assert(
|
|
775
|
+
redirectedTestPolicy.allowed && redirectedTestPolicy.substantiveTest === true,
|
|
776
|
+
"descriptor redirection was mistaken for background test execution"
|
|
777
|
+
);
|
|
778
|
+
const singleQuoteBackslashPolicy = evaluateCommandPolicy(
|
|
779
|
+
"git status 'x\\\\'; touch report.md",
|
|
780
|
+
dockerWorkspacePolicy
|
|
781
|
+
);
|
|
782
|
+
assert(
|
|
783
|
+
!singleQuoteBackslashPolicy.allowed || singleQuoteBackslashPolicy.category !== "read-only",
|
|
784
|
+
"a literal backslash inside single quotes hid a following workspace mutation"
|
|
785
|
+
);
|
|
695
786
|
const safeChmodAndRunPolicy = evaluateCommandPolicy(
|
|
696
787
|
'chmod +x /workspace/reports/run_bounded_02079_v2.sh && bash /workspace/reports/run_bounded_02079.sh 2>&1; echo "RUN_COMMAND_EXIT: $?"',
|
|
697
788
|
dockerWorkspacePolicy
|
|
@@ -996,6 +1087,18 @@ try {
|
|
|
996
1087
|
assert(sanitizedSmallRead.content === longSmallFile, "small read_file result did not keep full content for the model");
|
|
997
1088
|
assert(sanitizedSmallRead.contentTruncated === false, "small read_file result should not be marked truncated");
|
|
998
1089
|
assert(!("contentPreview" in sanitizedSmallRead), "small read_file result should not replace full content with preview");
|
|
1090
|
+
const sanitizedCommandResult = sanitizeToolResult({
|
|
1091
|
+
ok: true,
|
|
1092
|
+
toolName: "run_command",
|
|
1093
|
+
args: { command: "echo password=private-value" },
|
|
1094
|
+
commandPolicy: { normalizedCommand: "echo password=private-value" },
|
|
1095
|
+
stdout: "password=private-value",
|
|
1096
|
+
});
|
|
1097
|
+
const serializedCommandResult = JSON.stringify(sanitizedCommandResult);
|
|
1098
|
+
assert(
|
|
1099
|
+
!serializedCommandResult.includes("private-value") && serializedCommandResult.includes("[REDACTED]"),
|
|
1100
|
+
"a nested normalized command bypassed tool-event redaction"
|
|
1101
|
+
);
|
|
999
1102
|
const largeModelRead = toolResultForModel({
|
|
1000
1103
|
ok: true,
|
|
1001
1104
|
toolName: "read_file",
|
|
@@ -1216,6 +1319,128 @@ try {
|
|
|
1216
1319
|
localTimeoutRoute.model === "localllm-fast" && localTimeoutRoute.retryTimeoutMs === 90000,
|
|
1217
1320
|
"LocalLLM timeout retry did not switch to its same-boundary fast route"
|
|
1218
1321
|
);
|
|
1322
|
+
const adoptedLocalTimeoutRoute = applyModelTimeoutRetryRoute(
|
|
1323
|
+
{ provider: "localllm", model: "localllm-deep", routingMode: "manual" },
|
|
1324
|
+
localTimeoutRoute
|
|
1325
|
+
);
|
|
1326
|
+
assert(
|
|
1327
|
+
adoptedLocalTimeoutRoute.model === "localllm-fast" &&
|
|
1328
|
+
adoptedLocalTimeoutRoute.modelTimeoutRecoveryActive === true &&
|
|
1329
|
+
/continuing this run/.test(adoptedLocalTimeoutRoute.routeReason),
|
|
1330
|
+
"a successful in-provider timeout retry was not retained for later steps in the same run"
|
|
1331
|
+
);
|
|
1332
|
+
const focusedRewriteDescriptor = {
|
|
1333
|
+
type: "function",
|
|
1334
|
+
function: {
|
|
1335
|
+
name: "rewrite_text_excerpt",
|
|
1336
|
+
description: "Rewrite one evidence-selected excerpt.",
|
|
1337
|
+
parameters: {
|
|
1338
|
+
type: "object",
|
|
1339
|
+
properties: {
|
|
1340
|
+
revisedText: {
|
|
1341
|
+
type: "string",
|
|
1342
|
+
minLength: 1,
|
|
1343
|
+
maxLength: 4000,
|
|
1344
|
+
pattern: "^(?:(?!load)[\\s\\S])+$",
|
|
1345
|
+
description:
|
|
1346
|
+
"Return only the complete revised excerpt. Remove the premature first-match operand while preserving the technical meaning.",
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
required: ["revisedText"],
|
|
1350
|
+
additionalProperties: false,
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
};
|
|
1354
|
+
const focusedRewriteContract = createToolContract([focusedRewriteDescriptor]);
|
|
1355
|
+
const invalidFocusedRewriteCall = {
|
|
1356
|
+
id: "focused-rewrite-smoke",
|
|
1357
|
+
type: "function",
|
|
1358
|
+
function: {
|
|
1359
|
+
name: "rewrite_text_excerpt",
|
|
1360
|
+
arguments: JSON.stringify({ revisedText: "A carefully preloaded technical summary." }),
|
|
1361
|
+
},
|
|
1362
|
+
};
|
|
1363
|
+
const invalidFocusedRewrite = resolveDispatchableToolCallBatch(
|
|
1364
|
+
[invalidFocusedRewriteCall],
|
|
1365
|
+
focusedRewriteContract
|
|
1366
|
+
);
|
|
1367
|
+
assert(
|
|
1368
|
+
!invalidFocusedRewrite.ok &&
|
|
1369
|
+
invalidFocusedRewrite.errors.some((error) => error.code === "ARGUMENT_PATTERN_MISMATCH"),
|
|
1370
|
+
"focused rewrite smoke input did not exercise the semantic pattern failure"
|
|
1371
|
+
);
|
|
1372
|
+
const focusedWriterCalls = [];
|
|
1373
|
+
const focusedRewriteState = {
|
|
1374
|
+
meta: {
|
|
1375
|
+
failedTestDiagnostic: {
|
|
1376
|
+
mutationRevision: 7,
|
|
1377
|
+
failureSignature: "generic-first-occurrence-relation",
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
};
|
|
1381
|
+
const focusedRewriteConfig = {
|
|
1382
|
+
provider: "localllm",
|
|
1383
|
+
model: "localllm-fast",
|
|
1384
|
+
baseURL: "http://127.0.0.1:8008/v1",
|
|
1385
|
+
testFailureSignature: "generic-first-occurrence-relation",
|
|
1386
|
+
testFailureRepairPatchTargets: [{
|
|
1387
|
+
path: "notes/handoff.md",
|
|
1388
|
+
search: "A carefully preloaded technical summary.",
|
|
1389
|
+
}],
|
|
1390
|
+
writingClientFactory: (writingConfig) => {
|
|
1391
|
+
focusedWriterCalls.push(writingConfig);
|
|
1392
|
+
return {
|
|
1393
|
+
chat: {
|
|
1394
|
+
completions: {
|
|
1395
|
+
create: async () => ({
|
|
1396
|
+
choices: [{
|
|
1397
|
+
message: {
|
|
1398
|
+
content: JSON.stringify({
|
|
1399
|
+
draft: "A carefully prepared technical summary.",
|
|
1400
|
+
revision_notes: [],
|
|
1401
|
+
continuity_notes: [],
|
|
1402
|
+
format_handoff: {},
|
|
1403
|
+
quality_checks: ["constraint satisfied"],
|
|
1404
|
+
questions: [],
|
|
1405
|
+
}),
|
|
1406
|
+
},
|
|
1407
|
+
}],
|
|
1408
|
+
}),
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
};
|
|
1412
|
+
},
|
|
1413
|
+
};
|
|
1414
|
+
const recoveredFocusedRewrite = await recoverFocusedTextRewriteWithWritingSpecialist(
|
|
1415
|
+
focusedRewriteConfig,
|
|
1416
|
+
focusedRewriteState,
|
|
1417
|
+
[invalidFocusedRewriteCall],
|
|
1418
|
+
focusedRewriteContract,
|
|
1419
|
+
invalidFocusedRewrite
|
|
1420
|
+
);
|
|
1421
|
+
assert(recoveredFocusedRewrite?.ok, "writing specialist did not recover the bounded semantic rewrite");
|
|
1422
|
+
assert(
|
|
1423
|
+
recoveredFocusedRewrite.recoveredFocusedTextRewrite === true &&
|
|
1424
|
+
JSON.parse(recoveredFocusedRewrite.acceptedToolCalls[0].function.arguments).revisedText ===
|
|
1425
|
+
"A carefully prepared technical summary.",
|
|
1426
|
+
"writing specialist recovery did not return a schema-valid focused rewrite call"
|
|
1427
|
+
);
|
|
1428
|
+
assert(
|
|
1429
|
+
focusedWriterCalls.length === 1 &&
|
|
1430
|
+
focusedWriterCalls[0].provider === "localllm" &&
|
|
1431
|
+
focusedWriterCalls[0].model === "localllm-fast",
|
|
1432
|
+
"focused rewrite recovery crossed the active provider/model boundary"
|
|
1433
|
+
);
|
|
1434
|
+
assert(
|
|
1435
|
+
(await recoverFocusedTextRewriteWithWritingSpecialist(
|
|
1436
|
+
focusedRewriteConfig,
|
|
1437
|
+
focusedRewriteState,
|
|
1438
|
+
[invalidFocusedRewriteCall],
|
|
1439
|
+
focusedRewriteContract,
|
|
1440
|
+
invalidFocusedRewrite
|
|
1441
|
+
)) === null && focusedWriterCalls.length === 1,
|
|
1442
|
+
"focused rewrite recovery repeated for the same retained failure state"
|
|
1443
|
+
);
|
|
1219
1444
|
const defaultLocalTimeoutRoute = modelTimeoutRetryRoute({
|
|
1220
1445
|
provider: "localllm",
|
|
1221
1446
|
model: "localllm-fast",
|
|
@@ -1420,6 +1645,25 @@ try {
|
|
|
1420
1645
|
const unifiedText = await fs.readFile(path.join(workspace, "unified-target.txt"), "utf8");
|
|
1421
1646
|
assert(unified.ok && unifiedText === "alpha\nnew\nomega\n", "unified apply_patch did not update expected file");
|
|
1422
1647
|
|
|
1648
|
+
await fs.writeFile(path.join(workspace, "no-op-patch-target.txt"), "already correct\n", "utf8");
|
|
1649
|
+
const noOpPatchError = await executeWorkspaceTool(
|
|
1650
|
+
"apply_patch",
|
|
1651
|
+
{
|
|
1652
|
+
path: "no-op-patch-target.txt",
|
|
1653
|
+
search: "already correct",
|
|
1654
|
+
replace: "already correct",
|
|
1655
|
+
expectedReplacements: 1,
|
|
1656
|
+
},
|
|
1657
|
+
{ commandCwd: workspace, allowFileTools: true }
|
|
1658
|
+
)
|
|
1659
|
+
.then(() => "")
|
|
1660
|
+
.catch((error) => String(error?.message || error));
|
|
1661
|
+
assert(
|
|
1662
|
+
/patch made no changes/i.test(noOpPatchError) &&
|
|
1663
|
+
(await fs.readFile(path.join(workspace, "no-op-patch-target.txt"), "utf8")) === "already correct\n",
|
|
1664
|
+
"an exact byte-identical replacement was reported as a successful mutation"
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1423
1667
|
await fs.writeFile(path.join(workspace, "hybrid-patch-target.txt"), "alpha\nold\nomega\n", "utf8");
|
|
1424
1668
|
const hybridPatch = await executeWorkspaceTool(
|
|
1425
1669
|
"apply_patch",
|
|
@@ -1444,6 +1688,44 @@ try {
|
|
|
1444
1688
|
"hybrid wrapped unified apply_patch did not update expected file"
|
|
1445
1689
|
);
|
|
1446
1690
|
|
|
1691
|
+
await fs.mkdir(path.join(workspace, "a"), { recursive: true });
|
|
1692
|
+
await fs.writeFile(path.join(workspace, "report.md"), "root report\n", "utf8");
|
|
1693
|
+
await fs.writeFile(path.join(workspace, "a", "report.md"), "nested old\n", "utf8");
|
|
1694
|
+
const nestedCustomPatch = await executeWorkspaceTool(
|
|
1695
|
+
"apply_patch",
|
|
1696
|
+
{
|
|
1697
|
+
patch: [
|
|
1698
|
+
"*** Begin Patch",
|
|
1699
|
+
"*** Update File: a/report.md",
|
|
1700
|
+
"@@",
|
|
1701
|
+
"-nested old",
|
|
1702
|
+
"+nested new",
|
|
1703
|
+
"*** End Patch",
|
|
1704
|
+
].join("\n"),
|
|
1705
|
+
},
|
|
1706
|
+
{ commandCwd: workspace, allowFileTools: true }
|
|
1707
|
+
);
|
|
1708
|
+
assert(
|
|
1709
|
+
nestedCustomPatch.ok &&
|
|
1710
|
+
(await fs.readFile(path.join(workspace, "a", "report.md"), "utf8")) === "nested new\n" &&
|
|
1711
|
+
(await fs.readFile(path.join(workspace, "report.md"), "utf8")) === "root report\n",
|
|
1712
|
+
"custom patch path canonicalization confused a real a/ directory with a unified-diff prefix"
|
|
1713
|
+
);
|
|
1714
|
+
const nestedCustomDelete = await executeWorkspaceTool(
|
|
1715
|
+
"apply_patch",
|
|
1716
|
+
{ patch: ["*** Begin Patch", "*** Delete File: a/report.md", "*** End Patch"].join("\n") },
|
|
1717
|
+
{ commandCwd: workspace, allowFileTools: true }
|
|
1718
|
+
);
|
|
1719
|
+
assert(nestedCustomDelete.ok, "custom patch could not delete its exact nested target");
|
|
1720
|
+
const nestedStillExists = await fs
|
|
1721
|
+
.stat(path.join(workspace, "a", "report.md"))
|
|
1722
|
+
.then(() => true)
|
|
1723
|
+
.catch(() => false);
|
|
1724
|
+
assert(
|
|
1725
|
+
!nestedStillExists && (await fs.readFile(path.join(workspace, "report.md"), "utf8")) === "root report\n",
|
|
1726
|
+
"custom delete stripped a real a/ directory and deleted the wrong root file"
|
|
1727
|
+
);
|
|
1728
|
+
|
|
1447
1729
|
await fs.writeFile(path.join(workspace, "repair-report.md"), "old report\n", "utf8");
|
|
1448
1730
|
const ordinaryAddExistingError = await executeWorkspaceTool(
|
|
1449
1731
|
"apply_patch",
|
|
@@ -1728,6 +2010,7 @@ try {
|
|
|
1728
2010
|
"node_profile_cli_package_manifest",
|
|
1729
2011
|
"python_profile_helper_test_report",
|
|
1730
2012
|
"model_timeout_compact_retry_messages",
|
|
2013
|
+
"focused_text_rewrite_specialist_recovery",
|
|
1731
2014
|
],
|
|
1732
2015
|
},
|
|
1733
2016
|
null,
|