@lazyingart/agintiflow 0.20.238 → 0.20.239

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.238",
3
+ "version": "0.20.239",
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",
@@ -14,6 +14,7 @@ import {
14
14
  runAgent,
15
15
  sanitizeToolResult,
16
16
  toolResultForModel,
17
+ shouldPauseForPermissionAdvice,
17
18
  shouldShortCircuitToolBatch,
18
19
  shellDiagnosticHint,
19
20
  skippedAfterBlockedToolResult,
@@ -1311,6 +1312,79 @@ try {
1311
1312
  ),
1312
1313
  "an explicitly requested deletion was incorrectly treated as optional housekeeping"
1313
1314
  );
1315
+ assert(
1316
+ isUnrequestedCleanupCommand(
1317
+ "run_command",
1318
+ { command: "rm -rf /tmp/run-a /tmp/run-b" },
1319
+ {
1320
+ goal:
1321
+ "Continue the deterministic comparison. Do not delete any project or temporary directory; use fresh paths instead.",
1322
+ },
1323
+ {}
1324
+ ),
1325
+ "a negated deletion constraint was mistaken for destructive authorization"
1326
+ );
1327
+ const negatedCleanupAdvice = buildPermissionAdvice({
1328
+ toolName: "run_command",
1329
+ args: { command: "rm -rf /tmp/run-a /tmp/run-b" },
1330
+ guard: {
1331
+ category: "destructive",
1332
+ reason: "Destructive shell commands require Allow destructive actions.",
1333
+ },
1334
+ config: {
1335
+ ...dockerWorkspacePolicy,
1336
+ goal: "Do not delete any project or temporary directory; keep working on the build.",
1337
+ },
1338
+ state: { sessionId: "coding-negated-cleanup-smoke" },
1339
+ });
1340
+ assert(
1341
+ negatedCleanupAdvice.autoRecover === true,
1342
+ "blocked cleanup under a do-not-delete goal should recover without pausing"
1343
+ );
1344
+ const dynamicEvidenceAdvice = buildPermissionAdvice({
1345
+ toolName: "run_command",
1346
+ args: {
1347
+ command:
1348
+ 'STAMP=$(date -u +%Y%m%dT%H%M%SZ); bash build.sh 2>&1 | tee ".aginti/build-${STAMP}.log"',
1349
+ },
1350
+ guard: {
1351
+ category: "destructive",
1352
+ reason:
1353
+ 'Command contains a write-capable or destructive token: tee ".aginti/build-${STAMP}.log"',
1354
+ },
1355
+ config: {
1356
+ ...dockerWorkspacePolicy,
1357
+ goal: "Build twice and retain deterministic evidence without deleting anything.",
1358
+ },
1359
+ state: { sessionId: "coding-dynamic-evidence-smoke" },
1360
+ });
1361
+ assert(
1362
+ dynamicEvidenceAdvice.autoRecover === true &&
1363
+ /literal workspace-relative evidence paths/i.test(dynamicEvidenceAdvice.instruction),
1364
+ "dynamic evidence filename false positive should recover into literal workspace paths"
1365
+ );
1366
+ assert(
1367
+ !shouldPauseForPermissionAdvice({ blocked: true, permissionAdvice: dynamicEvidenceAdvice }),
1368
+ "dynamic evidence filename recovery still produced a permission pause"
1369
+ );
1370
+ const destructiveDynamicEvidenceAdvice = buildPermissionAdvice({
1371
+ toolName: "run_command",
1372
+ args: {
1373
+ command:
1374
+ 'rm -rf output; STAMP=$(date -u +%Y%m%dT%H%M%SZ); bash build.sh 2>&1 | tee ".aginti/build-${STAMP}.log"',
1375
+ },
1376
+ guard: {
1377
+ category: "destructive",
1378
+ reason: "Destructive shell commands require Allow destructive actions.",
1379
+ },
1380
+ config: { ...dockerWorkspacePolicy, goal: "Build and verify the document." },
1381
+ state: { sessionId: "coding-destructive-dynamic-evidence-smoke" },
1382
+ });
1383
+ assert(
1384
+ destructiveDynamicEvidenceAdvice.autoRecover === true &&
1385
+ /Unrequested cleanup was blocked safely/i.test(destructiveDynamicEvidenceAdvice.summary),
1386
+ "a real cleanup token should not be mislabeled as only dynamic evidence formatting"
1387
+ );
1314
1388
  const documentPageBatchGuard = checkToolUse({
1315
1389
  toolName: "read_image",
1316
1390
  args: { imagePaths: ["build/verification/page-1.png", "build/verification/page-2.png"] },
@@ -144,6 +144,8 @@ const runtimeMessages = buildContextBudgetCompactionMessages(
144
144
  toolName: "read_file",
145
145
  path: "/evidence/Musia/SKILL.md",
146
146
  bytes: 2048,
147
+ sha256: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
148
+ contentTruncated: false,
147
149
  content: [
148
150
  "---",
149
151
  "name: musia-music-production",
@@ -187,9 +189,12 @@ assert.ok(runtimeText.includes("Retained source evidence"));
187
189
  assert.ok(runtimeText.includes("/evidence/Musia/SKILL.md"));
188
190
  assert.ok(runtimeText.includes("Create and review songs through the established Musia production workflow"));
189
191
  assert.ok(runtimeText.includes("# Musia Music Production"));
192
+ assert.ok(runtimeText.includes("sha256=1234567890abcdef"));
193
+ assert.ok(runtimeText.includes("content=complete"));
190
194
  assert.ok(runtimeText.includes("node bin/musia.js doctor --json"));
191
195
  assert.ok(runtimeText.includes("scripts/xyq_cdp_browser.py"));
192
196
  assert.ok(!runtimeText.includes("OLD-COMPACTION-MUST-NOT-RECUR"));
193
197
  assert.match(runtimeText, /Do not reread a listed source solely because compaction occurred/);
198
+ assert.match(runtimeText, /never restart a full-file read loop after compaction/);
194
199
 
195
200
  console.log("context budget recovery smoke passed");
@@ -2281,6 +2281,27 @@ try {
2281
2281
  shellMutationState.meta.projectVerification?.mutationRevision === 1,
2282
2282
  "git metadata incorrectly invalidated current project-content verification"
2283
2283
  );
2284
+ const gitMetadataWithObservationsResult = {
2285
+ toolName: "run_command",
2286
+ ok: true,
2287
+ exitCode: 0,
2288
+ args: {
2289
+ command:
2290
+ "git add report.md && git commit -m 'record verified report' && echo '=== status ===' && git status --porcelain && git log -1 --oneline",
2291
+ },
2292
+ stdout: "=== status ===\nabc123 record verified report",
2293
+ stderr: "",
2294
+ };
2295
+ recordProjectVerificationOutcome(shellMutationState, gitMetadataWithObservationsResult, {
2296
+ commandCwd: workspace,
2297
+ taskProfile: "writing",
2298
+ allowShellTool: true,
2299
+ sandboxMode: "host",
2300
+ });
2301
+ assert(
2302
+ shellMutationState.meta.projectVerification?.mutationRevision === 1,
2303
+ "metadata-only Git chain with observational output invalidated current verification"
2304
+ );
2284
2305
  const gitCheckoutResult = {
2285
2306
  toolName: "run_command",
2286
2307
  ok: true,
@@ -2471,6 +2471,25 @@ assert(recoveredMixedBatch.ok, "valid mixed batch could not recover through boun
2471
2471
  assert(recoveredMixedBatch.recoveredSequentially, "mixed batch recovery was not recorded");
2472
2472
  assert(recoveredMixedBatch.acceptedToolCalls.length === 1, "mixed batch recovery dispatched more than one call");
2473
2473
  assert(recoveredMixedBatch.deferredToolCalls.length === 1, "mixed batch recovery did not defer the extra call");
2474
+ const fiveCallMixedBatch = [
2475
+ ...safeReadCalls,
2476
+ contractCall("safe-read-four", "read_file", { path: "fourth.txt" }),
2477
+ contractCall("deferred-write-five", "write_file", { path: "later.txt", content: "later" }),
2478
+ ];
2479
+ const recoveredFiveCallMixedBatch = resolveDispatchableToolCallBatch(
2480
+ fiveCallMixedBatch,
2481
+ createToolContract([...safeReadDescriptors, strictWriteDescriptor])
2482
+ );
2483
+ assert(recoveredFiveCallMixedBatch.ok, "five-call mixed batch was rejected instead of bounded deferral");
2484
+ assert(
2485
+ recoveredFiveCallMixedBatch.acceptedToolCalls.length === 1 &&
2486
+ recoveredFiveCallMixedBatch.deferredToolCalls.length === 4,
2487
+ "five-call mixed batch did not dispatch exactly one call and preserve the suffix"
2488
+ );
2489
+ assert(
2490
+ recoveredFiveCallMixedBatch.deferredToolCalls.at(-1)?.function?.name === "write_file",
2491
+ "bounded mixed recovery lost or executed the deferred write"
2492
+ );
2474
2493
  const oversizedReadCalls = Array.from({ length: 5 }, (_, index) =>
2475
2494
  contractCall(`read-${index}`, "read_file", { path: `file-${index}.txt` })
2476
2495
  );
@@ -2492,6 +2511,13 @@ assert(
2492
2511
  !resolveDispatchableToolCallBatch(excessiveReadBatch, safeReadContract).ok,
2493
2512
  "unbounded safe read batch escaped the reported-call cap"
2494
2513
  );
2514
+ assert(
2515
+ !resolveDispatchableToolCallBatch(
2516
+ [...excessiveReadBatch.slice(0, 12), contractCall("excessive-write", "write_file", { path: "later.txt", content: "later" })],
2517
+ createToolContract([...safeReadDescriptors, strictWriteDescriptor])
2518
+ ).ok,
2519
+ "unbounded mixed batch escaped the reported-call cap"
2520
+ );
2495
2521
 
2496
2522
  for (const [label, call, expectedCode] of [
2497
2523
  [
@@ -826,6 +826,12 @@ function summarizeRetainedSourceEvidence(messages = [], limit = 28) {
826
826
  const parts = [`tool=${toolName}`];
827
827
  if (sourcePath) parts.push(`path=${sourcePath}`);
828
828
  if (Number.isFinite(Number(payload.bytes))) parts.push(`bytes=${Number(payload.bytes)}`);
829
+ if (payload.sha256) parts.push(`sha256=${String(payload.sha256).slice(0, 16)}`);
830
+ if (payload.contentTruncated === true || payload.contentTruncatedByLines === true) {
831
+ parts.push("content=truncated");
832
+ } else if (toolName === "read_file" && typeof payload.content === "string") {
833
+ parts.push("content=complete");
834
+ }
829
835
  if (payload.summary) parts.push(`summary=${compactSingleLine(payload.summary, 220)}`);
830
836
  if (toolName === "read_file") {
831
837
  const semantic = summarizeReadSemanticEvidence(payload);
@@ -1249,6 +1255,7 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
1249
1255
  "",
1250
1256
  "Retained source evidence summaries (already inspected; reread an exact source only when its needed content is absent here):",
1251
1257
  "Do not reread a listed source solely because compaction occurred.",
1258
+ "A content=complete entry is authoritative for that recorded sha256. Use search_files or one bounded range read only when an exact edit anchor is absent; never restart a full-file read loop after compaction.",
1252
1259
  ...(retainedSourceEvidence.length
1253
1260
  ? retainedSourceEvidence.map((item) => `- ${item}`)
1254
1261
  : ["- No structured source evidence was available before compaction."]),
@@ -5986,6 +5993,17 @@ const WORKTREE_CHANGING_GIT_ACTIONS = new Set([
5986
5993
 
5987
5994
  function commandCanMutateProjectContent(command = "", commandPolicy = {}) {
5988
5995
  if (commandPolicy.writesWorkspace !== true && commandPolicy.mayMutateProject !== true) return false;
5996
+ const sequence = parseTopLevelShellSequence(String(command || ""));
5997
+ if (
5998
+ sequence.commands.length > 1 &&
5999
+ !sequence.openQuote &&
6000
+ !sequence.trailingEscape &&
6001
+ !sequence.trailingSeparator
6002
+ ) {
6003
+ return sequence.commands.some((segment) =>
6004
+ commandCanMutateProjectContent(segment, classifyCommand(segment))
6005
+ );
6006
+ }
5989
6007
  const category = String(commandPolicy.category || "");
5990
6008
  if (!["git-workflow", "git-remote"].includes(category)) return true;
5991
6009
  if (/\bgit\s+clone\b/i.test(String(command || ""))) return true;
@@ -11787,9 +11805,9 @@ export async function runAgent(config) {
11787
11805
  content: [
11788
11806
  "Runtime batching note: the valid tool batch exceeded the bounded per-turn dispatch limit.",
11789
11807
  `The first ${toolCalls.length} call(s) ran sequentially; do not repeat them.`,
11790
- "These remaining read-only calls were deferred and did not run:",
11808
+ "These remaining calls were deferred and did not run:",
11791
11809
  ...deferredSummary,
11792
- "Request only the specific deferred reads still needed, in a bounded batch, then move to the requested artifact.",
11810
+ "Review the deferred list, request only the calls still needed in a bounded batch, and do not assume any deferred write or command ran.",
11793
11811
  ].join("\n"),
11794
11812
  });
11795
11813
  }
@@ -67,11 +67,40 @@ export function isOptionalGeneratedPreviewCleanup(toolName = "", args = {}) {
67
67
  );
68
68
  }
69
69
 
70
+ export function isRecoverableDynamicEvidenceWrite(toolName = "", args = {}, guard = {}) {
71
+ if (toolName !== "run_command" || guard?.category !== "destructive") return false;
72
+ const command = String(args.command || args.text || "");
73
+ if (!/\btee\s+/i.test(command)) return false;
74
+ if (
75
+ /(?:^|[;&|\n]\s*)(?:command\s+)?(?:rm|rmdir|mv|chmod|chown)\b/i.test(command) ||
76
+ /\bgit\s+(?:checkout|switch|reset|clean)\b/i.test(command) ||
77
+ /(?:^|\s)-delete(?:\s|$)/i.test(command)
78
+ ) {
79
+ return false;
80
+ }
81
+ const dynamicWorkspaceEvidenceTarget = new RegExp(
82
+ String.raw`\btee\s+(?:--?append\s+|-a\s+)?["']?(?:\.aginti|artifacts|build/verification|output/verification)/[^\n;|]*\$\{?[A-Z_][A-Z0-9_]*\}?`,
83
+ "i"
84
+ );
85
+ return dynamicWorkspaceEvidenceTarget.test(command);
86
+ }
87
+
70
88
  function goalRequestsDeletion(config = {}, state = {}) {
71
89
  const goal = String(config.goal || state.goal || state.meta?.goalContract?.current || "");
72
- return /\b(?:delete|remove|clean\s+up|cleanup|purge|erase|discard|drop)\b|删除|刪除|移除|清理|清除|删掉|刪掉|削除|消去/i.test(
73
- goal
74
- );
90
+ const deletionIntent = /\b(?:delete|remove|clean\s+up|cleanup|purge|erase|discard|drop)\b|删除|刪除|移除|清理|清除|删掉|刪掉|削除|消去/i;
91
+ if (!deletionIntent.test(goal)) return false;
92
+
93
+ // A safety constraint such as "do not delete" is the opposite of
94
+ // authorization. Strip bounded negated phrases before looking for a genuine
95
+ // deletion request elsewhere in the goal.
96
+ const withoutNegatedDeletion = goal
97
+ .replace(
98
+ /\b(?:do\s+not|don't|dont|never|must\s+not|should\s+not|shouldn't|without)\s+(?:retry(?:ing)?\s+or\s+)?(?:delete|remove|clean\s+up|cleanup|purge|erase|discard|drop)(?:\s+any)?\b/gi,
99
+ " "
100
+ )
101
+ .replace(/(?:不要|不可|禁止|无需|無需|不需要)(?:再)?(?:删除|刪除|移除|清理|清除|删掉|刪掉)/g, " ")
102
+ .replace(/(?:削除|消去)(?:しない|するな|不要)/g, " ");
103
+ return deletionIntent.test(withoutNegatedDeletion);
75
104
  }
76
105
 
77
106
  export function isUnrequestedCleanupCommand(toolName = "", args = {}, config = {}, state = {}) {
@@ -291,6 +320,21 @@ function adviceForCategory(category = "", { toolName = "", args = {}, config = {
291
320
  }
292
321
 
293
322
  if (category === "destructive") {
323
+ if (isRecoverableDynamicEvidenceWrite(toolName, args, { category, reason })) {
324
+ return {
325
+ ...base,
326
+ autoRecover: true,
327
+ summary:
328
+ "A generated-evidence command used a shell-expanded output filename that the workspace guard could not prove safe. The command stayed blocked, but no destructive permission is needed.",
329
+ instruction:
330
+ "Do not retry the same command and do not request destructive approval. Reissue the check with fresh literal workspace-relative evidence paths under `.aginti/verification/`; avoid variables, globs, and `/tmp` in tee/redirection targets, then continue the substantive validation.",
331
+ options: [
332
+ "Use a literal timestamp or nonce already written into the command text.",
333
+ "Keep every log, hash file, and render under `.aginti/verification/`.",
334
+ "Split the build and evidence checks into smaller commands if that makes each output path explicit.",
335
+ ],
336
+ };
337
+ }
294
338
  if (
295
339
  isOptionalGeneratedPreviewCleanup(toolName, args) ||
296
340
  isUnrequestedCleanupCommand(toolName, args, config, state)
@@ -354,7 +354,7 @@ function fallbackHardContractPlan(goal = "", contract = {}, studentReason = "",
354
354
  forbiddenTextTerms.length ? `5. Ensure the output does not contain these forbidden term(s): ${forbiddenTextTerms.join(", ")}.` : "",
355
355
  requiredToolCalls.length ? `6. Call these explicitly required tool(s) before finish: ${requiredToolCalls.join(", ")}.` : "",
356
356
  selectedSkillPaths.length
357
- ? `7. Read the selected Markdown guidance at these exact paths before choosing an interface: ${selectedSkillPaths.join(", ")}. Skill IDs are not commands.`
357
+ ? `7. Read the selected Markdown guidance once before choosing an interface: ${selectedSkillPaths.join(", ")}. If retained source evidence records the path as already inspected, use that evidence and do not reread it solely after compaction. Skill IDs are not commands.`
358
358
  : "",
359
359
  readOnlyRoots.length
360
360
  ? `8. Inspect only the exact active read-only roots or their children with structured read tools: ${readOnlyRoots.join(", ")}. Do not pass --read-root to an in-task command.`
@@ -29,8 +29,7 @@ const SAFE_SEQUENTIAL_READ_TOOLS = new Set([
29
29
  const MAX_VALIDATION_ERRORS = 8;
30
30
  const MAX_VALIDATION_NODES = 50_000;
31
31
  const MAX_SAFE_SEQUENTIAL_READ_CALLS = 4;
32
- const MAX_RECOVERABLE_SEQUENTIAL_CALLS = 4;
33
- const MAX_REPORTED_SAFE_READ_CALLS = 12;
32
+ const MAX_REPORTED_SEQUENTIAL_CALLS = 12;
34
33
 
35
34
  function cloneValue(value) {
36
35
  return structuredClone(value);
@@ -436,9 +435,12 @@ export function resolveDispatchableToolCallBatch(toolCalls, contract) {
436
435
  const onlyExceededBatchLimit =
437
436
  errors.length > 0 && errors.every((error) => error?.code === "TOO_MANY_TOOL_CALLS");
438
437
  const safeReadBatch = isSafeSequentialReadBatch(calls);
439
- const recoverableCallLimit = safeReadBatch
440
- ? MAX_REPORTED_SAFE_READ_CALLS
441
- : MAX_RECOVERABLE_SEQUENTIAL_CALLS;
438
+ // The model may report a bounded batch even though the runtime deliberately
439
+ // dispatches only one mixed/mutating call at a time. Validate every reported
440
+ // call against the authenticated contract, then defer the untouched suffix.
441
+ // This keeps writes sequential without turning a harmless fifth call into a
442
+ // whole-turn failure.
443
+ const recoverableCallLimit = MAX_REPORTED_SEQUENTIAL_CALLS;
442
444
  if (
443
445
  !onlyExceededBatchLimit ||
444
446
  calls.length <= 1 ||