@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.
@@ -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.215",
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,