@lazyingart/agintiflow 0.20.237 → 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.237",
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");
@@ -3,7 +3,9 @@ import assert from "node:assert/strict";
3
3
  import {
4
4
  evaluateCurrentStateText,
5
5
  evaluateDocumentConsistency,
6
+ evaluateExtractedDocumentText,
6
7
  evaluatePdfPageBalance,
8
+ evaluatePdfTextBounds,
7
9
  extractSupersededLiterals,
8
10
  } from "../src/document-artifact-quality.js";
9
11
 
@@ -122,4 +124,21 @@ const readableOnePage = evaluatePdfPageBalance(
122
124
  );
123
125
  assert.equal(readableOnePage.ok, true, "readable one-page typography was rejected");
124
126
 
127
+ const cleanExtractedText = evaluateExtractedDocumentText("Page one\n\fPage two\tvalue\r\n");
128
+ assert.equal(cleanExtractedText.ok, true, "normal whitespace and PDF form feeds were rejected");
129
+
130
+ const corruptExtractedText = evaluateExtractedDocumentText("Reference detector \u0016 complete \ufffd");
131
+ assert.equal(corruptExtractedText.ok, false, "control and replacement glyphs were accepted");
132
+ assert.deepEqual(corruptExtractedText.codePoints, [0x16, 0xfffd]);
133
+ assert.equal(corruptExtractedText.defects[0]?.code, "corrupt-extracted-text");
134
+
135
+ const boundedText = evaluatePdfTextBounds(`<doc>${page(20)}</doc>`);
136
+ assert.equal(boundedText.ok, true, "text inside readable margins was rejected");
137
+
138
+ const clippedText = evaluatePdfTextBounds(
139
+ '<doc><page width="595" height="842"><word xMin="8" yMin="60" xMax="90" yMax="70">clipped</word></page></doc>'
140
+ );
141
+ assert.equal(clippedText.ok, false, "text outside readable margins was accepted");
142
+ assert.equal(clippedText.defects[0]?.code, "pdf-text-outside-readable-margin");
143
+
125
144
  console.log("document artifact quality smoke test 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,
@@ -29,7 +29,13 @@ import {
29
29
  shouldActivateScs,
30
30
  shouldRequestScsReplan,
31
31
  } from "../src/scs-controller.js";
32
- import { buildScsEvidenceLedger, deriveScsTaskContract, evaluateScsEvidence } from "../src/scs-evidence.js";
32
+ import {
33
+ buildScsEvidenceLedger,
34
+ deriveScsTaskContract,
35
+ evaluateScsEvidence,
36
+ gitActionsSatisfyContract,
37
+ inferSuccessfulGitActionsFromCommandResult,
38
+ } from "../src/scs-evidence.js";
33
39
  import { resolveRuntimeConfig } from "../src/config.js";
34
40
  import { classifyGoalIntent, isDirectAnswerIntent } from "../src/goal-intent.js";
35
41
  import { languageWriterDefaults } from "../src/writing-specialist.js";
@@ -922,6 +928,26 @@ assert(
922
928
  !committedEvaluation.missing.some((item) => item.category === "git"),
923
929
  "a successful git commit did not satisfy the explicit git requirement"
924
930
  );
931
+ const recoveredCommitActions = inferSuccessfulGitActionsFromCommandResult({
932
+ ok: true,
933
+ exitCode: 0,
934
+ args: {
935
+ command:
936
+ 'echo "seed author: $(git log -1 --format=\'%an <%ae>\')"; git config user.name "$(git log -1 --format=\'%an\')"; git commit -m "finish handoff" && git log -1 --oneline',
937
+ },
938
+ stdout: "[main abc1234] finish handoff\n 8 files changed, 499 insertions(+)",
939
+ });
940
+ assert(
941
+ recoveredCommitActions.length === 1 && recoveredCommitActions[0] === "commit",
942
+ "a canonical successful commit was lost because its command had a setup prefix or shell expansion"
943
+ );
944
+ assert(
945
+ gitActionsSatisfyContract(
946
+ { requiredGitActions: ["add", "commit"] },
947
+ recoveredCommitActions
948
+ ),
949
+ "a successful commit did not satisfy the implied staging step after a partially successful add/commit chain"
950
+ );
925
951
  const explainCodeContract = deriveScsTaskContract({
926
952
  goal: "Explain JavaScript closures at a high level.",
927
953
  taskProfile: "code",
@@ -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
  [
@@ -20,3 +20,16 @@ tools:
20
20
  Prefer safe conversions through available tools such as `pandoc`, `libreoffice`, or Python libraries when installed. Preserve originals; write converted or edited outputs to a new file unless overwrite is explicit.
21
21
 
22
22
  If binary `.docx` content cannot be inspected directly, explain the needed converter and create a project-local script or setup note.
23
+
24
+ For a repository handoff or generated document project:
25
+
26
+ - synthesize the authoritative current state instead of narrating superseded values unless history was requested;
27
+ - keep editable source plus the requested reader format, normally DOCX and a phone-readable PDF;
28
+ - provide and document a conventional project-local `build.sh` entry point, even when it delegates to a Python or TeX implementation;
29
+ - make a clean-checkout build provision or clearly verify its dependencies rather than relying silently on the current shell;
30
+ - preserve source inputs byte-for-byte and keep session data, visual-inspection renders, LaTeX intermediates, caches, and other transient evidence ignored; prefer ignore rules over deleting evidence, and never couple cleanup commands to the build or validation command;
31
+ - validate extracted DOCX/PDF text for current facts, stale or private values, encoding damage, and missing action/evidence content;
32
+ - render every PDF page and inspect it for clipping, overlap, orphaned headings, sparse spill pages, readable margins, and phone-scale readability;
33
+ - when the task is in a git repository and requests a finished handoff, commit only intentional project files and finish with a clean worktree unless the user says not to commit.
34
+
35
+ Visual polish does not establish content quality. Cross-check names, dates, totals, counts, statuses, decisions, actions, risks, evidence, and limitations against the source material before completion.
@@ -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
  }
@@ -30,6 +30,7 @@ const INTENTIONAL_SPARSE_PAGE_PATTERN =
30
30
  const HISTORICAL_TRANSITION_PATTERN =
31
31
  /\b(?:formerly|no longer|previously|replac(?:ed|ing)|supersed(?:e|ed|es|ing)|used to be)\b/i;
32
32
  const MIN_READABLE_MEDIAN_WORD_HEIGHT_PT = 8.8;
33
+ const MIN_READABLE_HORIZONTAL_MARGIN_PT = 18;
33
34
  const COUNT_WORDS = new Map([
34
35
  ["zero", 0], ["one", 1], ["two", 2], ["three", 3], ["four", 4], ["five", 5],
35
36
  ["six", 6], ["seven", 7], ["eight", 8], ["nine", 9], ["ten", 10],
@@ -239,6 +240,49 @@ export function evaluatePdfPageBalance(bboxXml = "") {
239
240
  };
240
241
  }
241
242
 
243
+ export function evaluateExtractedDocumentText(text = "") {
244
+ const unexpected = [...String(text || "").matchAll(/[\u0000-\u0008\u000b\u000e-\u001f\u007f-\u009f\ufffd]/gu)]
245
+ .map((match) => match[0].codePointAt(0))
246
+ .filter(Number.isInteger);
247
+ const codePoints = [...new Set(unexpected)].sort((a, b) => a - b);
248
+ const defects = [];
249
+ if (codePoints.length) {
250
+ defects.push({
251
+ code: "corrupt-extracted-text",
252
+ message: `The independently extracted reader text contains unexpected control or replacement glyphs: ${codePoints
253
+ .map((value) => `U+${value.toString(16).toUpperCase().padStart(4, "0")}`)
254
+ .join(", ")}. Repair the document encoding or generator instead of stripping these bytes only during validation.`,
255
+ });
256
+ }
257
+ return { ok: defects.length === 0, defects, codePoints };
258
+ }
259
+
260
+ export function evaluatePdfTextBounds(bboxXml = "", minimumMargin = MIN_READABLE_HORIZONTAL_MARGIN_PT) {
261
+ const pages = parsePdfBboxPages(bboxXml);
262
+ const defects = [];
263
+ for (const [index, page] of pages.entries()) {
264
+ if (!Number.isFinite(page.width) || page.width <= 0) continue;
265
+ const outside = page.words.filter((word) =>
266
+ word.text && (
267
+ !Number.isFinite(word.xMin) ||
268
+ !Number.isFinite(word.xMax) ||
269
+ word.xMin < minimumMargin ||
270
+ word.xMax > page.width - minimumMargin
271
+ )
272
+ );
273
+ if (!outside.length) continue;
274
+ const sample = outside
275
+ .slice(0, 4)
276
+ .map((word) => `${JSON.stringify(word.text)} at ${Number(word.xMin).toFixed(1)}..${Number(word.xMax).toFixed(1)} pt`)
277
+ .join("; ");
278
+ defects.push({
279
+ code: "pdf-text-outside-readable-margin",
280
+ message: `PDF page ${index + 1} places ${outside.length} text item${outside.length === 1 ? "" : "s"} outside the ${minimumMargin} pt horizontal readability margin (${sample}). Reflow the text or table instead of accepting clipping.`,
281
+ });
282
+ }
283
+ return { ok: pages.length > 0 && defects.length === 0, checked: pages.length > 0, defects };
284
+ }
285
+
242
286
  export function evaluateCurrentStateText({ sourceText = "", outputText = "", currentStateRequired = false } = {}) {
243
287
  const supersededLiterals = extractSupersededLiterals(sourceText);
244
288
  const presentSupersededLiterals = supersededLiterals.filter((literal) => containsLiteral(outputText, literal));
@@ -475,9 +519,11 @@ export async function validateWordDocumentArtifacts({
475
519
  try {
476
520
  if (artifact.extension === ".pdf") {
477
521
  const extracted = await extractPdf(artifact);
522
+ const textQuality = evaluateExtractedDocumentText(extracted.text);
478
523
  const semantic = evaluateCurrentStateText({ sourceText, outputText: extracted.text, currentStateRequired });
479
524
  const consistency = evaluateDocumentConsistency(extracted.text);
480
525
  const pageBalance = evaluatePdfPageBalance(extracted.bbox);
526
+ const textBounds = evaluatePdfTextBounds(extracted.bbox);
481
527
  if (!String(extracted.text || "").trim()) {
482
528
  defects.push({
483
529
  code: "empty-pdf-text",
@@ -493,8 +539,10 @@ export async function validateWordDocumentArtifacts({
493
539
  });
494
540
  }
495
541
  defects.push(...semantic.defects.map((item) => ({ ...item, path: artifact.path })));
542
+ defects.push(...textQuality.defects.map((item) => ({ ...item, path: artifact.path })));
496
543
  defects.push(...consistency.defects.map((item) => ({ ...item, path: artifact.path })));
497
544
  defects.push(...pageBalance.defects.map((item) => ({ ...item, path: artifact.path })));
545
+ defects.push(...textBounds.defects.map((item) => ({ ...item, path: artifact.path })));
498
546
  artifactReports.push({
499
547
  path: artifact.path,
500
548
  extension: artifact.extension,
@@ -506,6 +554,7 @@ export async function validateWordDocumentArtifacts({
506
554
  });
507
555
  } else {
508
556
  const text = await extractDocxText(artifact);
557
+ const textQuality = evaluateExtractedDocumentText(text);
509
558
  const semantic = evaluateCurrentStateText({ sourceText, outputText: text, currentStateRequired });
510
559
  const consistency = evaluateDocumentConsistency(text);
511
560
  if (!String(text || "").trim()) {
@@ -516,6 +565,7 @@ export async function validateWordDocumentArtifacts({
516
565
  });
517
566
  }
518
567
  defects.push(...semantic.defects.map((item) => ({ ...item, path: artifact.path })));
568
+ defects.push(...textQuality.defects.map((item) => ({ ...item, path: artifact.path })));
519
569
  defects.push(...consistency.defects.map((item) => ({ ...item, path: artifact.path })));
520
570
  artifactReports.push({
521
571
  path: artifact.path,
@@ -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.`
@@ -204,7 +204,20 @@ function missingRequiredGitActionSequence(required = [], observed = []) {
204
204
  let cursor = 0;
205
205
  for (let index = 0; index < expected.length; index += 1) {
206
206
  const observedIndex = actual.indexOf(expected[index], cursor);
207
- if (observedIndex < 0) return expected.slice(index);
207
+ if (observedIndex < 0) {
208
+ // A successful commit proves that an index was staged, even when the
209
+ // preceding `git add` happened in an earlier partially successful shell
210
+ // chain or the commit used `-a`. Preserve the requested add -> commit
211
+ // order without forcing the model to repeat a completed commit.
212
+ if (expected[index] === "add" && expected[index + 1] === "commit") {
213
+ const commitIndex = actual.indexOf("commit", cursor);
214
+ if (commitIndex >= 0) {
215
+ cursor = commitIndex;
216
+ continue;
217
+ }
218
+ }
219
+ return expected.slice(index);
220
+ }
208
221
  cursor = observedIndex + 1;
209
222
  }
210
223
  if (expected.at(-1) === "push") {
@@ -1070,7 +1083,20 @@ export function inferSuccessfulGitActionsFromCommandResult(payload = {}) {
1070
1083
  return inferGitActionsFromCommand(exitWrapper.command);
1071
1084
  }
1072
1085
  if (payload.ok === false || Number(payload.exitCode ?? 0) !== 0) return [];
1073
- return inferGitActionsFromCommand(command);
1086
+ const inferred = inferGitActionsFromCommand(command);
1087
+ if (inferred.length) return inferred;
1088
+
1089
+ // Shell expansion or a setup prefix can make the full command ambiguous to
1090
+ // the static parser. A canonical successful commit line plus an explicit
1091
+ // top-level `git commit` still provides concrete commit evidence.
1092
+ const output = String(payload.stdout || payload.result || "");
1093
+ if (
1094
+ /(?:^|[;&|]\s*)git\s+commit\b/i.test(command) &&
1095
+ /^\[[^\]\n]+\s+[0-9a-f]{7,}\]\s+\S+/mi.test(output)
1096
+ ) {
1097
+ return ["commit"];
1098
+ }
1099
+ return [];
1074
1100
  }
1075
1101
 
1076
1102
  function observedProjectCommandSatisfies(requiredCommand = "", item = {}) {
@@ -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 ||