@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 +1 -1
- package/scripts/smoke-coding-tools.js +74 -0
- package/scripts/smoke-context-budget-recovery.js +5 -0
- package/scripts/smoke-document-artifact-quality.js +19 -0
- package/scripts/smoke-dynamic-step-budget.js +21 -0
- package/scripts/smoke-model-roles.js +27 -1
- package/scripts/smoke-progressive-tool-selection.js +26 -0
- package/skills/word-documents/SKILL.md +13 -0
- package/src/agent-runner.js +20 -2
- package/src/document-artifact-quality.js +50 -0
- package/src/permission-advice.js +47 -3
- package/src/scs-controller.js +1 -1
- package/src/scs-evidence.js +28 -2
- package/src/tool-contract.js +7 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
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 {
|
|
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.
|
package/src/agent-runner.js
CHANGED
|
@@ -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
|
|
11808
|
+
"These remaining calls were deferred and did not run:",
|
|
11791
11809
|
...deferredSummary,
|
|
11792
|
-
"
|
|
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,
|
package/src/permission-advice.js
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
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)
|
package/src/scs-controller.js
CHANGED
|
@@ -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
|
|
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.`
|
package/src/scs-evidence.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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 = {}) {
|
package/src/tool-contract.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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 ||
|