@lazyingart/agintiflow 0.20.316 → 0.20.318
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-dynamic-step-budget.js +295 -0
- package/src/agent-runner.js +533 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.318",
|
|
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",
|
|
@@ -6132,6 +6132,242 @@ try {
|
|
|
6132
6132
|
sandboxMode: "host",
|
|
6133
6133
|
}
|
|
6134
6134
|
);
|
|
6135
|
+
const scopedRelativeTaskRoot = path.relative(workspace, scopedTaskRoot).replace(/\\/g, "/");
|
|
6136
|
+
const liveScopedGroupTestLogCommand = {
|
|
6137
|
+
toolName: "run_command",
|
|
6138
|
+
ok: true,
|
|
6139
|
+
exitCode: 0,
|
|
6140
|
+
args: {
|
|
6141
|
+
command:
|
|
6142
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6143
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6144
|
+
`{ echo "=== focused test ==="; python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6145
|
+
`> "$LOG" 2>&1; tail -n 6 "$LOG"; echo "LOG_OK=$([ -s "$LOG" ] && echo yes || echo no)"`,
|
|
6146
|
+
},
|
|
6147
|
+
projectMutationPaths: [],
|
|
6148
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\nLOG_OK=yes\n",
|
|
6149
|
+
stderr: "",
|
|
6150
|
+
};
|
|
6151
|
+
recordProjectVerificationOutcome(
|
|
6152
|
+
scopedTaskState,
|
|
6153
|
+
liveScopedGroupTestLogCommand,
|
|
6154
|
+
{
|
|
6155
|
+
commandCwd: workspace,
|
|
6156
|
+
taskProfile: "qa",
|
|
6157
|
+
allowShellTool: true,
|
|
6158
|
+
sandboxMode: "host",
|
|
6159
|
+
}
|
|
6160
|
+
);
|
|
6161
|
+
const liveScopedTeeTestLogCommand = {
|
|
6162
|
+
toolName: "run_command",
|
|
6163
|
+
ok: true,
|
|
6164
|
+
exitCode: 0,
|
|
6165
|
+
args: {
|
|
6166
|
+
command:
|
|
6167
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6168
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6169
|
+
`{ echo "=== focused test ==="; python3 -m unittest discover -s tests -p 'test_reader.py' -v 2>&1; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6170
|
+
`| tee "$LOG"; grep -q '^OK$' "$LOG"; echo "DONE_EXIT=$?"`,
|
|
6171
|
+
},
|
|
6172
|
+
projectMutationPaths: [],
|
|
6173
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\nDONE_EXIT=0\n",
|
|
6174
|
+
stderr: "",
|
|
6175
|
+
};
|
|
6176
|
+
recordProjectVerificationOutcome(
|
|
6177
|
+
scopedTaskState,
|
|
6178
|
+
liveScopedTeeTestLogCommand,
|
|
6179
|
+
{
|
|
6180
|
+
commandCwd: workspace,
|
|
6181
|
+
taskProfile: "qa",
|
|
6182
|
+
allowShellTool: true,
|
|
6183
|
+
sandboxMode: "host",
|
|
6184
|
+
}
|
|
6185
|
+
);
|
|
6186
|
+
const liveScopedOutsideStatusTestLogCommand = {
|
|
6187
|
+
toolName: "run_command",
|
|
6188
|
+
ok: true,
|
|
6189
|
+
exitCode: 0,
|
|
6190
|
+
args: {
|
|
6191
|
+
command:
|
|
6192
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6193
|
+
`TASK=${scopedRelativeTaskRoot} && ` +
|
|
6194
|
+
`{ date -u +"RUN_AT=%FT%TZ"; PYTHONPATH=src python -m pytest tests/test_wechat_document_reader.py -q; } ` +
|
|
6195
|
+
`> "$TASK/document-reader-test.log" 2>&1; rc=$?; echo "PYTEST_EXIT=$rc"; ` +
|
|
6196
|
+
`cat "$TASK/document-reader-test.log"`,
|
|
6197
|
+
},
|
|
6198
|
+
projectMutationPaths: [],
|
|
6199
|
+
stdout: "PYTEST_EXIT=0\n....... [100%]\n7 passed in 0.04s\n",
|
|
6200
|
+
stderr: "",
|
|
6201
|
+
};
|
|
6202
|
+
recordProjectVerificationOutcome(
|
|
6203
|
+
scopedTaskState,
|
|
6204
|
+
liveScopedOutsideStatusTestLogCommand,
|
|
6205
|
+
{
|
|
6206
|
+
commandCwd: workspace,
|
|
6207
|
+
taskProfile: "qa",
|
|
6208
|
+
allowShellTool: true,
|
|
6209
|
+
sandboxMode: "host",
|
|
6210
|
+
}
|
|
6211
|
+
);
|
|
6212
|
+
const conditionalTestCommand =
|
|
6213
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6214
|
+
`if PYTHONPATH=src python -c "import pytest" 2>/dev/null; then ` +
|
|
6215
|
+
`PYTHONPATH=src python -m pytest tests/test_wechat_document_reader.py -q; else ` +
|
|
6216
|
+
`PYTHONPATH=src python -m unittest tests.test_wechat_document_reader -v; fi`;
|
|
6217
|
+
const conditionalTestState = { meta: { goalContract: { revision: 1 } } };
|
|
6218
|
+
const conditionalTestResult = {
|
|
6219
|
+
toolName: "run_command",
|
|
6220
|
+
ok: true,
|
|
6221
|
+
exitCode: 0,
|
|
6222
|
+
args: { command: conditionalTestCommand },
|
|
6223
|
+
projectMutationPaths: [],
|
|
6224
|
+
stdout: "....... [100%]\n7 passed in 0.04s\n",
|
|
6225
|
+
stderr: "",
|
|
6226
|
+
commandPolicy: classifyCommand(conditionalTestCommand),
|
|
6227
|
+
};
|
|
6228
|
+
recordProjectVerificationOutcome(
|
|
6229
|
+
conditionalTestState,
|
|
6230
|
+
conditionalTestResult,
|
|
6231
|
+
{
|
|
6232
|
+
commandCwd: workspace,
|
|
6233
|
+
taskProfile: "qa",
|
|
6234
|
+
allowShellTool: true,
|
|
6235
|
+
sandboxMode: "host",
|
|
6236
|
+
}
|
|
6237
|
+
);
|
|
6238
|
+
const mutatingConditionalTestCommand = conditionalTestCommand.replace(
|
|
6239
|
+
"tests.test_wechat_document_reader -v; fi",
|
|
6240
|
+
"tests.test_wechat_document_reader -v; touch src/agent-runner.js; fi"
|
|
6241
|
+
);
|
|
6242
|
+
const mutatingConditionalTestState = { meta: { goalContract: { revision: 1 } } };
|
|
6243
|
+
const mutatingConditionalTestResult = {
|
|
6244
|
+
toolName: "run_command",
|
|
6245
|
+
ok: true,
|
|
6246
|
+
exitCode: 0,
|
|
6247
|
+
args: { command: mutatingConditionalTestCommand },
|
|
6248
|
+
projectMutationPaths: [],
|
|
6249
|
+
stdout: "7 passed in 0.04s\n",
|
|
6250
|
+
stderr: "",
|
|
6251
|
+
commandPolicy: classifyCommand(mutatingConditionalTestCommand),
|
|
6252
|
+
};
|
|
6253
|
+
recordProjectVerificationOutcome(
|
|
6254
|
+
mutatingConditionalTestState,
|
|
6255
|
+
mutatingConditionalTestResult,
|
|
6256
|
+
{
|
|
6257
|
+
commandCwd: workspace,
|
|
6258
|
+
taskProfile: "qa",
|
|
6259
|
+
allowShellTool: true,
|
|
6260
|
+
allowDestructive: true,
|
|
6261
|
+
sandboxMode: "host",
|
|
6262
|
+
}
|
|
6263
|
+
);
|
|
6264
|
+
const unsafeScopedLogState = {
|
|
6265
|
+
goal: scopedTaskGoal,
|
|
6266
|
+
commandCwd: workspace,
|
|
6267
|
+
meta: {
|
|
6268
|
+
goalContract: {
|
|
6269
|
+
revision: 1,
|
|
6270
|
+
taskGoal: scopedTaskGoal,
|
|
6271
|
+
currentRequest: "Create and verify output/result.json.",
|
|
6272
|
+
},
|
|
6273
|
+
},
|
|
6274
|
+
};
|
|
6275
|
+
const outsideScopedGroupTestLogCommand = {
|
|
6276
|
+
toolName: "run_command",
|
|
6277
|
+
ok: true,
|
|
6278
|
+
exitCode: 0,
|
|
6279
|
+
args: {
|
|
6280
|
+
command:
|
|
6281
|
+
`cd ${JSON.stringify(workspace)} && LOG="source-tree-test.log" && ` +
|
|
6282
|
+
`{ python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6283
|
+
`> "$LOG" 2>&1; tail -n 6 "$LOG"`,
|
|
6284
|
+
},
|
|
6285
|
+
projectMutationPaths: [],
|
|
6286
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\n",
|
|
6287
|
+
stderr: "",
|
|
6288
|
+
};
|
|
6289
|
+
recordProjectVerificationOutcome(
|
|
6290
|
+
unsafeScopedLogState,
|
|
6291
|
+
outsideScopedGroupTestLogCommand,
|
|
6292
|
+
{
|
|
6293
|
+
commandCwd: workspace,
|
|
6294
|
+
taskProfile: "qa",
|
|
6295
|
+
allowShellTool: true,
|
|
6296
|
+
sandboxMode: "host",
|
|
6297
|
+
}
|
|
6298
|
+
);
|
|
6299
|
+
const sourceWriteAfterScopedLogState = {
|
|
6300
|
+
goal: scopedTaskGoal,
|
|
6301
|
+
commandCwd: workspace,
|
|
6302
|
+
meta: {
|
|
6303
|
+
goalContract: {
|
|
6304
|
+
revision: 1,
|
|
6305
|
+
taskGoal: scopedTaskGoal,
|
|
6306
|
+
currentRequest: "Create and verify output/result.json.",
|
|
6307
|
+
},
|
|
6308
|
+
},
|
|
6309
|
+
};
|
|
6310
|
+
const sourceWriteAfterScopedLogCommand = {
|
|
6311
|
+
toolName: "run_command",
|
|
6312
|
+
ok: true,
|
|
6313
|
+
exitCode: 0,
|
|
6314
|
+
args: {
|
|
6315
|
+
command:
|
|
6316
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6317
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6318
|
+
`{ python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6319
|
+
`> "$LOG" 2>&1; touch src/agent-runner.js; tail -n 6 "$LOG"`,
|
|
6320
|
+
},
|
|
6321
|
+
projectMutationPaths: [],
|
|
6322
|
+
stdout: "Ran 7 tests in 0.01s\nOK\nEXIT=0\n",
|
|
6323
|
+
stderr: "",
|
|
6324
|
+
};
|
|
6325
|
+
recordProjectVerificationOutcome(
|
|
6326
|
+
sourceWriteAfterScopedLogState,
|
|
6327
|
+
sourceWriteAfterScopedLogCommand,
|
|
6328
|
+
{
|
|
6329
|
+
commandCwd: workspace,
|
|
6330
|
+
taskProfile: "qa",
|
|
6331
|
+
allowShellTool: true,
|
|
6332
|
+
sandboxMode: "host",
|
|
6333
|
+
}
|
|
6334
|
+
);
|
|
6335
|
+
const spoofedCapturedStatusState = {
|
|
6336
|
+
goal: scopedTaskGoal,
|
|
6337
|
+
commandCwd: workspace,
|
|
6338
|
+
meta: {
|
|
6339
|
+
goalContract: {
|
|
6340
|
+
revision: 1,
|
|
6341
|
+
taskGoal: scopedTaskGoal,
|
|
6342
|
+
currentRequest: "Create and verify output/result.json.",
|
|
6343
|
+
},
|
|
6344
|
+
},
|
|
6345
|
+
};
|
|
6346
|
+
const spoofedCapturedStatusCommand = {
|
|
6347
|
+
toolName: "run_command",
|
|
6348
|
+
ok: true,
|
|
6349
|
+
exitCode: 0,
|
|
6350
|
+
args: {
|
|
6351
|
+
command:
|
|
6352
|
+
`cd ${JSON.stringify(workspace)} && ` +
|
|
6353
|
+
`TASK=${scopedRelativeTaskRoot} && LOG="$TASK/document-reader-test.log" && ` +
|
|
6354
|
+
`{ python3 -m unittest discover -s tests -p 'test_reader.py' -v; RC=$?; echo "EXIT=$RC"; } ` +
|
|
6355
|
+
`> "$LOG" 2>&1; tail -n 6 "$LOG"; echo "EXIT=0"`,
|
|
6356
|
+
},
|
|
6357
|
+
projectMutationPaths: [],
|
|
6358
|
+
stdout: "FAILED (failures=1)\nEXIT=1\nEXIT=0\n",
|
|
6359
|
+
stderr: "",
|
|
6360
|
+
};
|
|
6361
|
+
recordProjectVerificationOutcome(
|
|
6362
|
+
spoofedCapturedStatusState,
|
|
6363
|
+
spoofedCapturedStatusCommand,
|
|
6364
|
+
{
|
|
6365
|
+
commandCwd: workspace,
|
|
6366
|
+
taskProfile: "qa",
|
|
6367
|
+
allowShellTool: true,
|
|
6368
|
+
sandboxMode: "host",
|
|
6369
|
+
}
|
|
6370
|
+
);
|
|
6135
6371
|
const nullRedirectValidationCommand = {
|
|
6136
6372
|
toolName: "run_command",
|
|
6137
6373
|
ok: true,
|
|
@@ -6169,6 +6405,25 @@ try {
|
|
|
6169
6405
|
scopedManifestCommand.scopedTaskArtifactWrite === true &&
|
|
6170
6406
|
pythonScopedManifestCommand.scopedTaskArtifactWrite === true &&
|
|
6171
6407
|
scopedCompoundTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6408
|
+
liveScopedGroupTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6409
|
+
liveScopedGroupTestLogCommand.projectTest?.passed === true &&
|
|
6410
|
+
liveScopedGroupTestLogCommand.projectTest?.explicitExitStatus === 0 &&
|
|
6411
|
+
liveScopedTeeTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6412
|
+
liveScopedTeeTestLogCommand.projectTest?.passed === true &&
|
|
6413
|
+
liveScopedTeeTestLogCommand.projectTest?.explicitExitStatus === 0 &&
|
|
6414
|
+
liveScopedOutsideStatusTestLogCommand.scopedTaskArtifactWrite === true &&
|
|
6415
|
+
liveScopedOutsideStatusTestLogCommand.projectTest?.passed === true &&
|
|
6416
|
+
liveScopedOutsideStatusTestLogCommand.projectTest?.explicitExitStatus === 0 &&
|
|
6417
|
+
conditionalTestState.meta.projectVerification?.mutationRevision === 0 &&
|
|
6418
|
+
conditionalTestResult.boundedReadOnlyTestWrapper === true &&
|
|
6419
|
+
conditionalTestResult.projectTest?.passed === true &&
|
|
6420
|
+
mutatingConditionalTestState.meta.projectVerification?.mutationRevision === 1 &&
|
|
6421
|
+
mutatingConditionalTestResult.projectTest === undefined &&
|
|
6422
|
+
unsafeScopedLogState.meta.projectVerification?.mutationRevision === 1 &&
|
|
6423
|
+
outsideScopedGroupTestLogCommand.scopedTaskArtifactWrite !== true &&
|
|
6424
|
+
sourceWriteAfterScopedLogState.meta.projectVerification?.mutationRevision === 1 &&
|
|
6425
|
+
sourceWriteAfterScopedLogCommand.scopedTaskArtifactWrite !== true &&
|
|
6426
|
+
spoofedCapturedStatusCommand.projectTest === undefined &&
|
|
6172
6427
|
nullRedirectValidationCommand.readOnlyArtifactValidation === true,
|
|
6173
6428
|
`task-scoped delivery bookkeeping invalidated requested artifact evidence: ${JSON.stringify({
|
|
6174
6429
|
requestedOutputRevision,
|
|
@@ -6181,6 +6436,46 @@ try {
|
|
|
6181
6436
|
pythonScopedManifestCommand.scopedTaskArtifactWrite,
|
|
6182
6437
|
scopedCompoundTestLogCommand:
|
|
6183
6438
|
scopedCompoundTestLogCommand.scopedTaskArtifactWrite,
|
|
6439
|
+
liveScopedGroupTestLogCommand: {
|
|
6440
|
+
scopedTaskArtifactWrite: liveScopedGroupTestLogCommand.scopedTaskArtifactWrite,
|
|
6441
|
+
projectTest: liveScopedGroupTestLogCommand.projectTest,
|
|
6442
|
+
},
|
|
6443
|
+
liveScopedTeeTestLogCommand: {
|
|
6444
|
+
scopedTaskArtifactWrite: liveScopedTeeTestLogCommand.scopedTaskArtifactWrite,
|
|
6445
|
+
projectTest: liveScopedTeeTestLogCommand.projectTest,
|
|
6446
|
+
},
|
|
6447
|
+
liveScopedOutsideStatusTestLogCommand: {
|
|
6448
|
+
scopedTaskArtifactWrite:
|
|
6449
|
+
liveScopedOutsideStatusTestLogCommand.scopedTaskArtifactWrite,
|
|
6450
|
+
projectTest: liveScopedOutsideStatusTestLogCommand.projectTest,
|
|
6451
|
+
},
|
|
6452
|
+
conditionalTestResult: {
|
|
6453
|
+
mutationRevision:
|
|
6454
|
+
conditionalTestState.meta.projectVerification?.mutationRevision,
|
|
6455
|
+
boundedReadOnlyTestWrapper:
|
|
6456
|
+
conditionalTestResult.boundedReadOnlyTestWrapper,
|
|
6457
|
+
projectTest: conditionalTestResult.projectTest,
|
|
6458
|
+
},
|
|
6459
|
+
mutatingConditionalTestResult: {
|
|
6460
|
+
mutationRevision:
|
|
6461
|
+
mutatingConditionalTestState.meta.projectVerification?.mutationRevision,
|
|
6462
|
+
projectTest: mutatingConditionalTestResult.projectTest,
|
|
6463
|
+
},
|
|
6464
|
+
outsideScopedGroupTestLogCommand: {
|
|
6465
|
+
mutationRevision:
|
|
6466
|
+
unsafeScopedLogState.meta.projectVerification?.mutationRevision,
|
|
6467
|
+
scopedTaskArtifactWrite:
|
|
6468
|
+
outsideScopedGroupTestLogCommand.scopedTaskArtifactWrite,
|
|
6469
|
+
},
|
|
6470
|
+
sourceWriteAfterScopedLogCommand: {
|
|
6471
|
+
mutationRevision:
|
|
6472
|
+
sourceWriteAfterScopedLogState.meta.projectVerification?.mutationRevision,
|
|
6473
|
+
scopedTaskArtifactWrite:
|
|
6474
|
+
sourceWriteAfterScopedLogCommand.scopedTaskArtifactWrite,
|
|
6475
|
+
},
|
|
6476
|
+
spoofedCapturedStatusCommand: {
|
|
6477
|
+
projectTest: spoofedCapturedStatusCommand.projectTest,
|
|
6478
|
+
},
|
|
6184
6479
|
nullRedirectValidationCommand:
|
|
6185
6480
|
nullRedirectValidationCommand.readOnlyArtifactValidation,
|
|
6186
6481
|
})}`
|
package/src/agent-runner.js
CHANGED
|
@@ -4967,6 +4967,407 @@ function normalizeProjectCommand(command = "") {
|
|
|
4967
4967
|
return canonicalizeShellCommand(command);
|
|
4968
4968
|
}
|
|
4969
4969
|
|
|
4970
|
+
function shellBraceGroups(value = "") {
|
|
4971
|
+
const text = String(value || "");
|
|
4972
|
+
const groups = [];
|
|
4973
|
+
let quote = "";
|
|
4974
|
+
let escaped = false;
|
|
4975
|
+
let start = -1;
|
|
4976
|
+
let depth = 0;
|
|
4977
|
+
|
|
4978
|
+
const isGroupOpen = (index) => {
|
|
4979
|
+
if (text[index] !== "{" || text[index - 1] === "$") return false;
|
|
4980
|
+
const before = text[index - 1] || "";
|
|
4981
|
+
const after = text[index + 1] || "";
|
|
4982
|
+
return (!before || /[\s;&|()]/u.test(before)) && /\s/u.test(after);
|
|
4983
|
+
};
|
|
4984
|
+
const isGroupClose = (index) => {
|
|
4985
|
+
if (text[index] !== "}") return false;
|
|
4986
|
+
const before = text[index - 1] || "";
|
|
4987
|
+
const after = text[index + 1] || "";
|
|
4988
|
+
return /\s/u.test(before) && (!after || /[\s;&|<>)]/u.test(after));
|
|
4989
|
+
};
|
|
4990
|
+
|
|
4991
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
4992
|
+
const char = text[index];
|
|
4993
|
+
if (quote === "'") {
|
|
4994
|
+
if (char === "'") quote = "";
|
|
4995
|
+
continue;
|
|
4996
|
+
}
|
|
4997
|
+
if (escaped) {
|
|
4998
|
+
escaped = false;
|
|
4999
|
+
continue;
|
|
5000
|
+
}
|
|
5001
|
+
if (char === "\\") {
|
|
5002
|
+
escaped = true;
|
|
5003
|
+
continue;
|
|
5004
|
+
}
|
|
5005
|
+
if (quote === '"') {
|
|
5006
|
+
if (char === '"') quote = "";
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
if (char === "'" || char === '"') {
|
|
5010
|
+
quote = char;
|
|
5011
|
+
continue;
|
|
5012
|
+
}
|
|
5013
|
+
if (start < 0 && isGroupOpen(index)) {
|
|
5014
|
+
start = index;
|
|
5015
|
+
depth = 1;
|
|
5016
|
+
continue;
|
|
5017
|
+
}
|
|
5018
|
+
if (start >= 0 && isGroupOpen(index)) {
|
|
5019
|
+
depth += 1;
|
|
5020
|
+
continue;
|
|
5021
|
+
}
|
|
5022
|
+
if (start >= 0 && isGroupClose(index)) {
|
|
5023
|
+
depth -= 1;
|
|
5024
|
+
if (depth > 0) continue;
|
|
5025
|
+
groups.push({
|
|
5026
|
+
start,
|
|
5027
|
+
end: index,
|
|
5028
|
+
body: text.slice(start + 1, index).trim(),
|
|
5029
|
+
});
|
|
5030
|
+
start = -1;
|
|
5031
|
+
depth = 0;
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
return groups;
|
|
5035
|
+
}
|
|
5036
|
+
|
|
5037
|
+
function unquotedShellControlDelimiter(value = "", keyword = "", startIndex = 0) {
|
|
5038
|
+
const text = String(value || "");
|
|
5039
|
+
const expected = String(keyword || "").toLowerCase();
|
|
5040
|
+
let quote = "";
|
|
5041
|
+
let escaped = false;
|
|
5042
|
+
for (let index = Math.max(0, Number(startIndex || 0)); index < text.length; index += 1) {
|
|
5043
|
+
const char = text[index];
|
|
5044
|
+
if (quote === "'") {
|
|
5045
|
+
if (char === "'") quote = "";
|
|
5046
|
+
continue;
|
|
5047
|
+
}
|
|
5048
|
+
if (escaped) {
|
|
5049
|
+
escaped = false;
|
|
5050
|
+
continue;
|
|
5051
|
+
}
|
|
5052
|
+
if (char === "\\") {
|
|
5053
|
+
escaped = true;
|
|
5054
|
+
continue;
|
|
5055
|
+
}
|
|
5056
|
+
if (quote === '"') {
|
|
5057
|
+
if (char === '"') quote = "";
|
|
5058
|
+
continue;
|
|
5059
|
+
}
|
|
5060
|
+
if (char === "'" || char === '"') {
|
|
5061
|
+
quote = char;
|
|
5062
|
+
continue;
|
|
5063
|
+
}
|
|
5064
|
+
if (char !== ";") continue;
|
|
5065
|
+
const suffix = text.slice(index + 1).match(/^\s*([A-Za-z]+)\b/u);
|
|
5066
|
+
if (String(suffix?.[1] || "").toLowerCase() === expected) return index;
|
|
5067
|
+
}
|
|
5068
|
+
return -1;
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
function boundedTestAvailabilityProbe(value = "") {
|
|
5072
|
+
let command = normalizeProjectCommand(value);
|
|
5073
|
+
if (!command || /(?:`|\$\()/u.test(command)) return false;
|
|
5074
|
+
command = command
|
|
5075
|
+
.replace(/(?:^|\s)(?:\d*>>?|&>>?)\s*(?:"\/dev\/null"|'\/dev\/null'|\/dev\/null)/gu, " ")
|
|
5076
|
+
.trim()
|
|
5077
|
+
.replace(/^(?:[A-Za-z_]\w*=(?:"[^"\n]*"|'[^'\n]*'|[^\s;&|]+)\s*)+/u, "")
|
|
5078
|
+
.trim();
|
|
5079
|
+
const tokens = tokenizeShellWords(command);
|
|
5080
|
+
if (
|
|
5081
|
+
["command", "which"].includes(String(tokens[0] || "")) &&
|
|
5082
|
+
(tokens[0] !== "command" || tokens[1] === "-v")
|
|
5083
|
+
) {
|
|
5084
|
+
const target = tokens[0] === "command" ? tokens[2] : tokens[1];
|
|
5085
|
+
return Boolean(target && /^[A-Za-z0-9_.+-]+$/u.test(target) && tokens.length <= 3);
|
|
5086
|
+
}
|
|
5087
|
+
const executable = String(tokens[0] || "").split(/[\\/]/u).at(-1) || "";
|
|
5088
|
+
const codeIndex = tokens.indexOf("-c");
|
|
5089
|
+
if (!/^python(?:\d+(?:\.\d+)*)?$/iu.test(executable) || codeIndex < 1) return false;
|
|
5090
|
+
const code = String(tokens[codeIndex + 1] || "").trim();
|
|
5091
|
+
if (!code || codeIndex + 2 !== tokens.length) return false;
|
|
5092
|
+
return /^(?:import\s+[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*|from\s+[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s+import\s+[A-Za-z_]\w*)$/u.test(
|
|
5093
|
+
code
|
|
5094
|
+
);
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
function boundedTestCommandClassification(value = "") {
|
|
5098
|
+
const command = normalizeProjectCommand(value);
|
|
5099
|
+
const executableCommand = command
|
|
5100
|
+
.replace(/^(?:[A-Za-z_]\w*=(?:"[^"\n]*"|'[^'\n]*'|[^\s;&|]+)\s*)+/u, "")
|
|
5101
|
+
.trim();
|
|
5102
|
+
return {
|
|
5103
|
+
command,
|
|
5104
|
+
executableCommand,
|
|
5105
|
+
policy: classifyCommand(executableCommand),
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5109
|
+
function parseBoundedConditionalTestWrapper(value = "") {
|
|
5110
|
+
let normalized = normalizeProjectCommand(value);
|
|
5111
|
+
if (!normalized) return null;
|
|
5112
|
+
normalized = normalized.replace(
|
|
5113
|
+
/^cd\s+(?:"[^"\n]+"|'[^'\n]+'|[^\s;&|]+)\s*&&\s*/u,
|
|
5114
|
+
""
|
|
5115
|
+
);
|
|
5116
|
+
if (!/^if\s+/u.test(normalized)) return null;
|
|
5117
|
+
const thenAt = unquotedShellControlDelimiter(normalized, "then", 3);
|
|
5118
|
+
if (thenAt < 0) return null;
|
|
5119
|
+
const thenMatch = normalized.slice(thenAt + 1).match(/^\s*then\b/u);
|
|
5120
|
+
if (!thenMatch) return null;
|
|
5121
|
+
const thenBodyStart = thenAt + 1 + thenMatch[0].length;
|
|
5122
|
+
const elseAt = unquotedShellControlDelimiter(normalized, "else", thenBodyStart);
|
|
5123
|
+
if (elseAt < 0) return null;
|
|
5124
|
+
const elseMatch = normalized.slice(elseAt + 1).match(/^\s*else\b/u);
|
|
5125
|
+
if (!elseMatch) return null;
|
|
5126
|
+
const elseBodyStart = elseAt + 1 + elseMatch[0].length;
|
|
5127
|
+
const fiAt = unquotedShellControlDelimiter(normalized, "fi", elseBodyStart);
|
|
5128
|
+
if (fiAt < 0 || !/^\s*fi\s*$/u.test(normalized.slice(fiAt + 1))) return null;
|
|
5129
|
+
|
|
5130
|
+
const condition = normalized.slice(3, thenAt).trim();
|
|
5131
|
+
const thenCommand = normalizeProjectCommand(
|
|
5132
|
+
normalized.slice(thenBodyStart, elseAt).trim()
|
|
5133
|
+
);
|
|
5134
|
+
const elseCommand = normalizeProjectCommand(
|
|
5135
|
+
normalized.slice(elseBodyStart, fiAt).trim()
|
|
5136
|
+
);
|
|
5137
|
+
if (!boundedTestAvailabilityProbe(condition)) return null;
|
|
5138
|
+
const branches = [thenCommand, elseCommand];
|
|
5139
|
+
const classifications = branches.map((candidate) =>
|
|
5140
|
+
boundedTestCommandClassification(candidate)
|
|
5141
|
+
);
|
|
5142
|
+
const policies = classifications.map((candidate) => candidate.policy);
|
|
5143
|
+
if (
|
|
5144
|
+
branches.some((candidate) => {
|
|
5145
|
+
const sequence = parseTopLevelShellSequence(candidate);
|
|
5146
|
+
return (
|
|
5147
|
+
!candidate ||
|
|
5148
|
+
sequence.openQuote ||
|
|
5149
|
+
sequence.trailingEscape ||
|
|
5150
|
+
sequence.trailingSeparator ||
|
|
5151
|
+
sequence.commands.length !== 1
|
|
5152
|
+
);
|
|
5153
|
+
}) ||
|
|
5154
|
+
policies.some((policy) => policy.substantiveTest !== true)
|
|
5155
|
+
) {
|
|
5156
|
+
return null;
|
|
5157
|
+
}
|
|
5158
|
+
return {
|
|
5159
|
+
command: thenCommand,
|
|
5160
|
+
alternateCommand: elseCommand,
|
|
5161
|
+
wrappedCommand: normalizeProjectCommand(value),
|
|
5162
|
+
statusSource: "process",
|
|
5163
|
+
semanticallyReadOnly: policies.every(
|
|
5164
|
+
(policy, index) =>
|
|
5165
|
+
!commandCanMutateProjectContent(
|
|
5166
|
+
classifications[index].executableCommand,
|
|
5167
|
+
policy
|
|
5168
|
+
)
|
|
5169
|
+
),
|
|
5170
|
+
};
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
function capturedStatusEcho(value = "", statusVariable = "") {
|
|
5174
|
+
const label =
|
|
5175
|
+
"(?:(?:[A-Z][A-Z0-9_]{0,31}_)?(?:EXIT|STATUS|RESULT)(?:_CODE)?)";
|
|
5176
|
+
const variable = String(statusVariable || "");
|
|
5177
|
+
const statusOperand = variable
|
|
5178
|
+
? `\\$(?:\\{${variable.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\}|${variable.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")})`
|
|
5179
|
+
: "\\$\\?";
|
|
5180
|
+
return String(value || "").trim().match(
|
|
5181
|
+
new RegExp(
|
|
5182
|
+
`^echo\\s+(?:-n\\s+)?["']?(?<label>${label})\\s*[:=]\\s*${statusOperand}["']?$`,
|
|
5183
|
+
"iu"
|
|
5184
|
+
)
|
|
5185
|
+
);
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
function boundedReadOnlyTestPrefixCommand(value = "") {
|
|
5189
|
+
const command = normalizeProjectCommand(value);
|
|
5190
|
+
if (!command) return false;
|
|
5191
|
+
const tokens = tokenizeShellWords(command);
|
|
5192
|
+
if (
|
|
5193
|
+
["date", "echo", "printf"].includes(String(tokens[0] || "")) &&
|
|
5194
|
+
!/(?:`|\$\()/u.test(command)
|
|
5195
|
+
) {
|
|
5196
|
+
return true;
|
|
5197
|
+
}
|
|
5198
|
+
if (
|
|
5199
|
+
["cat", "grep", "head", "stat", "tail", "wc"].includes(
|
|
5200
|
+
String(tokens[0] || "")
|
|
5201
|
+
) &&
|
|
5202
|
+
!/(?:`|\$\(|[|<>])/u.test(command)
|
|
5203
|
+
) {
|
|
5204
|
+
return true;
|
|
5205
|
+
}
|
|
5206
|
+
const policy = classifyCommand(command);
|
|
5207
|
+
return (
|
|
5208
|
+
policy.writesWorkspace !== true &&
|
|
5209
|
+
policy.mayMutateProject !== true &&
|
|
5210
|
+
policy.substantiveTest !== true
|
|
5211
|
+
);
|
|
5212
|
+
}
|
|
5213
|
+
|
|
5214
|
+
function parseCapturedTestExitStatusWrapper(value = "") {
|
|
5215
|
+
const normalized = normalizeProjectCommand(value);
|
|
5216
|
+
if (!normalized) return null;
|
|
5217
|
+
const label = "(?:EXIT|STATUS|RESULT)(?:_CODE)?";
|
|
5218
|
+
for (const group of shellBraceGroups(normalized)) {
|
|
5219
|
+
const sequence = parseTopLevelShellSequence(group.body);
|
|
5220
|
+
if (
|
|
5221
|
+
sequence.openQuote ||
|
|
5222
|
+
sequence.trailingEscape ||
|
|
5223
|
+
(sequence.trailingSeparator && sequence.trailingSeparator !== ";") ||
|
|
5224
|
+
sequence.commands.length < 1
|
|
5225
|
+
) {
|
|
5226
|
+
continue;
|
|
5227
|
+
}
|
|
5228
|
+
for (let index = 1; index < sequence.commands.length - 1; index += 1) {
|
|
5229
|
+
const statusAssignment = String(sequence.commands[index] || "")
|
|
5230
|
+
.trim()
|
|
5231
|
+
.match(/^([A-Za-z_]\w*)\s*=\s*\$\?$/u);
|
|
5232
|
+
if (!statusAssignment) continue;
|
|
5233
|
+
const statusVariable = statusAssignment[1];
|
|
5234
|
+
const escapedVariable = statusVariable.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
5235
|
+
const statusEcho = String(sequence.commands[index + 1] || "").trim();
|
|
5236
|
+
const echoPattern = new RegExp(
|
|
5237
|
+
`^echo\\s+(?:-n\\s+)?["']?(?<label>${label})\\s*[:=]\\s*\\$(?:\\{${escapedVariable}\\}|${escapedVariable})["']?$`,
|
|
5238
|
+
"iu"
|
|
5239
|
+
);
|
|
5240
|
+
const echoMatch = statusEcho.match(echoPattern);
|
|
5241
|
+
if (!echoMatch) continue;
|
|
5242
|
+
const outsideGroup = `${normalized.slice(0, group.start)} ${normalized.slice(group.end + 1)}`;
|
|
5243
|
+
if (
|
|
5244
|
+
/\b(?:echo|printf)\b[^;&|\n]{0,240}\b(?:EXIT|STATUS|RESULT)(?:_CODE)?\s*[:=]/iu.test(
|
|
5245
|
+
outsideGroup
|
|
5246
|
+
)
|
|
5247
|
+
) {
|
|
5248
|
+
continue;
|
|
5249
|
+
}
|
|
5250
|
+
const testCommand = normalizeProjectCommand(sequence.commands[index - 1]);
|
|
5251
|
+
if (boundedTestCommandClassification(testCommand).policy.substantiveTest !== true) continue;
|
|
5252
|
+
return {
|
|
5253
|
+
command: testCommand,
|
|
5254
|
+
wrappedCommand: normalized,
|
|
5255
|
+
label: String(echoMatch.groups?.label || "").toUpperCase(),
|
|
5256
|
+
statusSource: "captured-group",
|
|
5257
|
+
statusVariable,
|
|
5258
|
+
};
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
for (let index = 0; index < sequence.commands.length - 1; index += 1) {
|
|
5262
|
+
const testCommand = normalizeProjectCommand(sequence.commands[index]);
|
|
5263
|
+
const directEcho = capturedStatusEcho(sequence.commands[index + 1]);
|
|
5264
|
+
if (
|
|
5265
|
+
!directEcho ||
|
|
5266
|
+
boundedTestCommandClassification(testCommand).policy.substantiveTest !== true
|
|
5267
|
+
) {
|
|
5268
|
+
continue;
|
|
5269
|
+
}
|
|
5270
|
+
if (
|
|
5271
|
+
sequence.commands.slice(0, index).some(
|
|
5272
|
+
(candidate) => !boundedReadOnlyTestPrefixCommand(candidate)
|
|
5273
|
+
) ||
|
|
5274
|
+
sequence.commands.length !== index + 2
|
|
5275
|
+
) {
|
|
5276
|
+
continue;
|
|
5277
|
+
}
|
|
5278
|
+
const outsideGroup = `${normalized.slice(0, group.start)} ${normalized.slice(group.end + 1)}`;
|
|
5279
|
+
if (
|
|
5280
|
+
/\b(?:echo|printf)\b[^;&|\n]{0,240}\b(?:EXIT|STATUS|RESULT)(?:_CODE)?\s*[:=]/iu.test(
|
|
5281
|
+
outsideGroup
|
|
5282
|
+
)
|
|
5283
|
+
) {
|
|
5284
|
+
continue;
|
|
5285
|
+
}
|
|
5286
|
+
return {
|
|
5287
|
+
command: testCommand,
|
|
5288
|
+
wrappedCommand: normalized,
|
|
5289
|
+
label: String(directEcho.groups?.label || "").toUpperCase(),
|
|
5290
|
+
statusSource: "captured-group",
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
const bodyCommands = sequence.commands.map((candidate) =>
|
|
5295
|
+
normalizeProjectCommand(candidate)
|
|
5296
|
+
);
|
|
5297
|
+
const testCommand = bodyCommands.at(-1) || "";
|
|
5298
|
+
if (
|
|
5299
|
+
boundedTestCommandClassification(testCommand).policy.substantiveTest !== true ||
|
|
5300
|
+
bodyCommands.slice(0, -1).some(
|
|
5301
|
+
(candidate) => !boundedReadOnlyTestPrefixCommand(candidate)
|
|
5302
|
+
)
|
|
5303
|
+
) {
|
|
5304
|
+
continue;
|
|
5305
|
+
}
|
|
5306
|
+
const suffix = normalized
|
|
5307
|
+
.slice(group.end + 1)
|
|
5308
|
+
.replace(
|
|
5309
|
+
/^\s*(?:(?:\d*>>?|&>>?)\s*(?:"[^"\n]+"|'[^'\n]+'|&\d+|[^\s;&|]+)\s*)+/u,
|
|
5310
|
+
""
|
|
5311
|
+
)
|
|
5312
|
+
.replace(/^\s*;\s*/u, "");
|
|
5313
|
+
const suffixSequence = parseTopLevelShellSequence(suffix);
|
|
5314
|
+
if (
|
|
5315
|
+
!suffix ||
|
|
5316
|
+
suffixSequence.openQuote ||
|
|
5317
|
+
suffixSequence.trailingEscape ||
|
|
5318
|
+
suffixSequence.trailingSeparator ||
|
|
5319
|
+
suffixSequence.commands.length < 2
|
|
5320
|
+
) {
|
|
5321
|
+
continue;
|
|
5322
|
+
}
|
|
5323
|
+
const statusAssignment = String(suffixSequence.commands[0] || "")
|
|
5324
|
+
.trim()
|
|
5325
|
+
.match(/^([A-Za-z_]\w*)\s*=\s*\$\?$/u);
|
|
5326
|
+
if (!statusAssignment) continue;
|
|
5327
|
+
const statusEcho = capturedStatusEcho(
|
|
5328
|
+
suffixSequence.commands[1],
|
|
5329
|
+
statusAssignment[1]
|
|
5330
|
+
);
|
|
5331
|
+
if (!statusEcho) continue;
|
|
5332
|
+
if (
|
|
5333
|
+
suffixSequence.commands.slice(2).some((candidate) => {
|
|
5334
|
+
if (
|
|
5335
|
+
/\b(?:echo|printf)\b[^;&|\n]{0,240}\b(?:[A-Z][A-Z0-9_]{0,31}_)?(?:EXIT|STATUS|RESULT)(?:_CODE)?\s*[:=]/iu.test(
|
|
5336
|
+
candidate
|
|
5337
|
+
)
|
|
5338
|
+
) {
|
|
5339
|
+
return true;
|
|
5340
|
+
}
|
|
5341
|
+
return !boundedReadOnlyTestPrefixCommand(candidate);
|
|
5342
|
+
})
|
|
5343
|
+
) {
|
|
5344
|
+
continue;
|
|
5345
|
+
}
|
|
5346
|
+
return {
|
|
5347
|
+
command: testCommand,
|
|
5348
|
+
wrappedCommand: normalized,
|
|
5349
|
+
label: String(statusEcho.groups?.label || "").toUpperCase(),
|
|
5350
|
+
statusSource: "captured-group",
|
|
5351
|
+
statusVariable: statusAssignment[1],
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
5354
|
+
return null;
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
function projectTestCommandWrapper(command = "") {
|
|
5358
|
+
return (
|
|
5359
|
+
parseCapturedTestExitStatusWrapper(command) ||
|
|
5360
|
+
parseBoundedConditionalTestWrapper(command)
|
|
5361
|
+
);
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
function projectExitStatusWrapper(command = "") {
|
|
5365
|
+
return (
|
|
5366
|
+
parseNonMutatingExitStatusWrapper(command) ||
|
|
5367
|
+
parseCapturedTestExitStatusWrapper(command)
|
|
5368
|
+
);
|
|
5369
|
+
}
|
|
5370
|
+
|
|
4970
5371
|
function normalizeLeadingWorkspaceCd(command = "", config = {}) {
|
|
4971
5372
|
const normalized = normalizeProjectCommand(command);
|
|
4972
5373
|
const sequence = parseTopLevelShellSequence(normalized);
|
|
@@ -5037,7 +5438,8 @@ function isRetainedExactVerificationCommand(command = "", config = {}) {
|
|
|
5037
5438
|
export function isSubstantiveTestCommand(command = "", config = {}) {
|
|
5038
5439
|
if (isRetainedExactVerificationCommand(command, config)) return true;
|
|
5039
5440
|
const normalized = normalizeProjectCommand(normalizeCommandForPolicy(command, config));
|
|
5040
|
-
|
|
5441
|
+
if (projectTestCommandWrapper(normalized)) return true;
|
|
5442
|
+
const text = projectExitStatusWrapper(normalized)?.command || normalized;
|
|
5041
5443
|
if (!text) return false;
|
|
5042
5444
|
const classification = classifyCommand(text);
|
|
5043
5445
|
if (classification.substantiveTest !== true) return false;
|
|
@@ -5142,7 +5544,7 @@ function testRunRepresentsInvalidInvocation(testRun = {}) {
|
|
|
5142
5544
|
|
|
5143
5545
|
function explicitExitProbeStatus(command = "", result = {}) {
|
|
5144
5546
|
const normalizedCommand = normalizeProjectCommand(command);
|
|
5145
|
-
const exitProbe =
|
|
5547
|
+
const exitProbe = projectExitStatusWrapper(normalizedCommand);
|
|
5146
5548
|
if (!exitProbe) {
|
|
5147
5549
|
return {
|
|
5148
5550
|
present: false,
|
|
@@ -5152,9 +5554,26 @@ function explicitExitProbeStatus(command = "", result = {}) {
|
|
|
5152
5554
|
};
|
|
5153
5555
|
}
|
|
5154
5556
|
|
|
5557
|
+
let status = parseExplicitExitStatus(result.stdout || "");
|
|
5558
|
+
if (status === null && exitProbe.statusSource === "captured-group") {
|
|
5559
|
+
const escapedLabel = String(exitProbe.label || "")
|
|
5560
|
+
.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
5561
|
+
const matches = [
|
|
5562
|
+
...String(result.stdout || "").matchAll(
|
|
5563
|
+
escapedLabel
|
|
5564
|
+
? new RegExp(
|
|
5565
|
+
`(?:^|\\n)\\s*${escapedLabel}\\s*[:=]\\s*(-?\\d+)\\s*(?=\\n|$)`,
|
|
5566
|
+
"giu"
|
|
5567
|
+
)
|
|
5568
|
+
: /(?:^|\n)\s*(?:EXIT|STATUS|RESULT)(?:_CODE)?\s*[:=]\s*(-?\d+)\s*(?=\n|$)/giu
|
|
5569
|
+
),
|
|
5570
|
+
];
|
|
5571
|
+
if (matches.length) status = Number(matches.at(-1)[1]);
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5155
5574
|
return {
|
|
5156
5575
|
present: true,
|
|
5157
|
-
status
|
|
5576
|
+
status,
|
|
5158
5577
|
command: exitProbe.command,
|
|
5159
5578
|
wrappedCommand: exitProbe.wrappedCommand || exitProbe.command,
|
|
5160
5579
|
};
|
|
@@ -6719,6 +7138,7 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
|
|
|
6719
7138
|
|
|
6720
7139
|
if (toolName === "run_command") {
|
|
6721
7140
|
const command = normalizeProjectCommand(toolResult.args?.command || "");
|
|
7141
|
+
const testCommandWrapper = projectTestCommandWrapper(command);
|
|
6722
7142
|
const retainedExactVerification = isRetainedExactVerificationCommand(
|
|
6723
7143
|
command,
|
|
6724
7144
|
config
|
|
@@ -6832,6 +7252,7 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
|
|
|
6832
7252
|
command &&
|
|
6833
7253
|
toolResult.blocked !== true &&
|
|
6834
7254
|
!retainedExactVerification &&
|
|
7255
|
+
testCommandWrapper?.semanticallyReadOnly !== true &&
|
|
6835
7256
|
!disposableGeneratedCleanup &&
|
|
6836
7257
|
!disposableGeneratedVerificationSideEffects &&
|
|
6837
7258
|
!scopedTaskArtifactWrite &&
|
|
@@ -6856,6 +7277,9 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
|
|
|
6856
7277
|
if (readOnlyArtifactValidation) {
|
|
6857
7278
|
toolResult.readOnlyArtifactValidation = true;
|
|
6858
7279
|
}
|
|
7280
|
+
if (testCommandWrapper?.semanticallyReadOnly === true) {
|
|
7281
|
+
toolResult.boundedReadOnlyTestWrapper = true;
|
|
7282
|
+
}
|
|
6859
7283
|
let requiredBatch = currentRequiredCommandBatch(verification, requiredCommands);
|
|
6860
7284
|
const activeExecutionContract = state.meta?.activeExecutionContract;
|
|
6861
7285
|
const activeTurnCommands =
|
|
@@ -7396,7 +7820,7 @@ export function recordAlreadyCommittedRepositoryRepair(state = {}, toolResult =
|
|
|
7396
7820
|
|
|
7397
7821
|
function projectTestCommandKey(command = "") {
|
|
7398
7822
|
const normalized = normalizeProjectCommand(command);
|
|
7399
|
-
const exitProbe =
|
|
7823
|
+
const exitProbe = projectExitStatusWrapper(normalized);
|
|
7400
7824
|
const invocation = normalizeProjectCommand(exitProbe?.command || normalized)
|
|
7401
7825
|
// These wrappers alter bytecode/output handling, not the tests selected or
|
|
7402
7826
|
// executed. Treating them as distinct suites can leave an old failure
|
|
@@ -12777,40 +13201,106 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12777
13201
|
for (const match of normalized.matchAll(
|
|
12778
13202
|
/(?:^|[\s;&|])([A-Za-z_]\w*)\s*=\s*("[^"\n]+"|'[^'\n]+'|[^\s;&|]+)/g
|
|
12779
13203
|
)) {
|
|
12780
|
-
assignments.set(
|
|
12781
|
-
match[1],
|
|
12782
|
-
String(match[2] || "").replace(/^["']|["']$/g, "")
|
|
12783
|
-
);
|
|
13204
|
+
assignments.set(match[1], String(match[2] || "").replace(/^["']|["']$/g, ""));
|
|
12784
13205
|
}
|
|
12785
|
-
const resolveTarget = (value = "") => {
|
|
12786
|
-
|
|
13206
|
+
const resolveTarget = (value = "", seen = new Set()) => {
|
|
13207
|
+
let target = String(value || "")
|
|
12787
13208
|
.trim()
|
|
12788
13209
|
.replace(/^["']|["']$/g, "");
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
13210
|
+
if (!target || /\$\(|`/u.test(target)) return "";
|
|
13211
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
13212
|
+
let unresolved = false;
|
|
13213
|
+
let changed = false;
|
|
13214
|
+
const expanded = target.replace(
|
|
13215
|
+
/\$(?:\{([A-Za-z_]\w*)\}|([A-Za-z_]\w*))/gu,
|
|
13216
|
+
(match, bracedName, bareName) => {
|
|
13217
|
+
const name = bracedName || bareName;
|
|
13218
|
+
if (!assignments.has(name) || seen.has(name)) {
|
|
13219
|
+
unresolved = true;
|
|
13220
|
+
return match;
|
|
13221
|
+
}
|
|
13222
|
+
const nextSeen = new Set(seen);
|
|
13223
|
+
nextSeen.add(name);
|
|
13224
|
+
const resolved = resolveTarget(assignments.get(name), nextSeen);
|
|
13225
|
+
if (!resolved) {
|
|
13226
|
+
unresolved = true;
|
|
13227
|
+
return match;
|
|
13228
|
+
}
|
|
13229
|
+
changed = true;
|
|
13230
|
+
return resolved;
|
|
13231
|
+
}
|
|
13232
|
+
);
|
|
13233
|
+
target = expanded;
|
|
13234
|
+
if (unresolved) return "";
|
|
13235
|
+
if (!changed) return target;
|
|
13236
|
+
}
|
|
13237
|
+
return "";
|
|
12793
13238
|
};
|
|
12794
|
-
const tokens =
|
|
12795
|
-
|
|
13239
|
+
const evidenceTargetsFromTee = (tokens = []) => {
|
|
13240
|
+
if (tokens[0] !== "tee") return [];
|
|
12796
13241
|
let index = 1;
|
|
12797
13242
|
if (["-a", "--append"].includes(tokens[index])) index += 1;
|
|
12798
13243
|
if (tokens[index] === "--") index += 1;
|
|
12799
|
-
|
|
13244
|
+
return tokens.slice(index).map((target) => resolveTarget(target));
|
|
13245
|
+
};
|
|
13246
|
+
const evidenceTargetsFromMkdir = (tokens = []) =>
|
|
13247
|
+
tokens[0] === "mkdir"
|
|
13248
|
+
? tokens
|
|
13249
|
+
.slice(1)
|
|
13250
|
+
.filter((token) => token !== "-p" && token !== "--")
|
|
13251
|
+
.map((target) => resolveTarget(target))
|
|
13252
|
+
: [];
|
|
13253
|
+
const boundedEchoProbeIsReadOnly = (segment = "") => {
|
|
13254
|
+
const source = String(segment || "").trim();
|
|
13255
|
+
const sourceTokens = tokenizeShellWords(source);
|
|
13256
|
+
if (!["echo", "printf"].includes(String(sourceTokens[0] || ""))) return false;
|
|
13257
|
+
if (source.includes("`") || !source.includes("$(")) return !source.includes("`");
|
|
13258
|
+
const substitutions = [];
|
|
13259
|
+
const remainder = source.replace(/\$\(([^()]*)\)/gu, (match, body) => {
|
|
13260
|
+
substitutions.push(String(body || ""));
|
|
13261
|
+
return "";
|
|
13262
|
+
});
|
|
13263
|
+
if (!substitutions.length || remainder.includes("$(")) return false;
|
|
13264
|
+
return substitutions.every((body) => {
|
|
13265
|
+
const sequence = parseTopLevelShellSequence(body);
|
|
13266
|
+
if (
|
|
13267
|
+
sequence.openQuote ||
|
|
13268
|
+
sequence.trailingEscape ||
|
|
13269
|
+
sequence.trailingSeparator ||
|
|
13270
|
+
!sequence.commands.length
|
|
13271
|
+
) {
|
|
13272
|
+
return false;
|
|
13273
|
+
}
|
|
13274
|
+
return sequence.commands.every((candidate) => {
|
|
13275
|
+
const candidateTokens = tokenizeShellWords(candidate);
|
|
13276
|
+
if (["echo", "printf"].includes(String(candidateTokens[0] || ""))) {
|
|
13277
|
+
return !String(candidate || "").includes("$(") && !String(candidate || "").includes("`");
|
|
13278
|
+
}
|
|
13279
|
+
if (["[", "test"].includes(String(candidateTokens[0] || ""))) {
|
|
13280
|
+
const operands = candidateTokens.filter(
|
|
13281
|
+
(token) => !["[", "]", "test", "!", "-e", "-f", "-r", "-s"].includes(token)
|
|
13282
|
+
);
|
|
13283
|
+
return operands.length > 0 && operands.every((operand) => Boolean(resolveTarget(operand)));
|
|
13284
|
+
}
|
|
13285
|
+
const policy = classifyCommand(candidate);
|
|
13286
|
+
return policy.writesWorkspace !== true && policy.mayMutateProject !== true;
|
|
13287
|
+
});
|
|
13288
|
+
});
|
|
13289
|
+
};
|
|
13290
|
+
const tokens = tokenizeShellWords(normalized);
|
|
13291
|
+
if (tokens[0] === "tee") {
|
|
13292
|
+
const targets = evidenceTargetsFromTee(tokens);
|
|
12800
13293
|
return targets.length > 0 && targets.every(pathMatches);
|
|
12801
13294
|
}
|
|
12802
13295
|
if (tokens[0] === "mkdir") {
|
|
12803
|
-
const targets = tokens
|
|
12804
|
-
.slice(1)
|
|
12805
|
-
.filter((token) => token !== "-p" && token !== "--")
|
|
12806
|
-
.map(resolveTarget);
|
|
13296
|
+
const targets = evidenceTargetsFromMkdir(tokens);
|
|
12807
13297
|
return targets.length > 0 && targets.every(pathMatches);
|
|
12808
13298
|
}
|
|
12809
13299
|
|
|
12810
13300
|
let strippedEvidenceRedirect = false;
|
|
12811
13301
|
let rejectedRedirect = false;
|
|
12812
13302
|
const withoutEvidenceRedirects = normalized.replace(
|
|
12813
|
-
/(^|\s)(?:\d*>>?|&>>?)\s*("[^"]+"|'[^']+'
|
|
13303
|
+
/(^|\s)(?:\d*>>?|&>>?)\s*("[^"]+"|'[^']+'|&\d+|[^\s;&|]+)/g,
|
|
12814
13304
|
(match, prefix, target) => {
|
|
12815
13305
|
const resolvedTarget = resolveTarget(target);
|
|
12816
13306
|
if (/^(?:&\d+|\/dev\/null)$/u.test(resolvedTarget)) return prefix;
|
|
@@ -12822,15 +13312,19 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12822
13312
|
return prefix;
|
|
12823
13313
|
}
|
|
12824
13314
|
).trim();
|
|
12825
|
-
if (rejectedRedirect || !
|
|
13315
|
+
if (rejectedRedirect || !withoutEvidenceRedirects) {
|
|
12826
13316
|
return false;
|
|
12827
13317
|
}
|
|
12828
13318
|
const sequence = parseTopLevelShellSequence(withoutEvidenceRedirects);
|
|
12829
13319
|
if (sequence.openQuote || sequence.trailingEscape || sequence.trailingSeparator) {
|
|
12830
13320
|
return false;
|
|
12831
13321
|
}
|
|
12832
|
-
|
|
13322
|
+
let sawEvidenceWrite = strippedEvidenceRedirect;
|
|
13323
|
+
const safeSequence = sequence.commands.every((segment) => {
|
|
12833
13324
|
const withoutAssignments = String(segment || "")
|
|
13325
|
+
.trim()
|
|
13326
|
+
.replace(/^\{\s*/u, "")
|
|
13327
|
+
.replace(/\s*\}$/u, "")
|
|
12834
13328
|
.trim()
|
|
12835
13329
|
.replace(
|
|
12836
13330
|
/^(?:[A-Za-z_]\w*=(?:"[^"\n]*"|'[^'\n]*'|[^\s;&|]+)\s*)+/u,
|
|
@@ -12839,6 +13333,20 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12839
13333
|
.trim();
|
|
12840
13334
|
if (!withoutAssignments) return true;
|
|
12841
13335
|
const commandTokens = tokenizeShellWords(withoutAssignments);
|
|
13336
|
+
if (boundedReadOnlyTestPrefixCommand(withoutAssignments)) return true;
|
|
13337
|
+
const teeTargets = evidenceTargetsFromTee(commandTokens);
|
|
13338
|
+
if (teeTargets.length) {
|
|
13339
|
+
const safe = teeTargets.every(pathMatches);
|
|
13340
|
+
if (safe) sawEvidenceWrite = true;
|
|
13341
|
+
return safe;
|
|
13342
|
+
}
|
|
13343
|
+
const mkdirTargets = evidenceTargetsFromMkdir(commandTokens);
|
|
13344
|
+
if (mkdirTargets.length) {
|
|
13345
|
+
const safe = mkdirTargets.every(pathMatches);
|
|
13346
|
+
if (safe) sawEvidenceWrite = true;
|
|
13347
|
+
return safe;
|
|
13348
|
+
}
|
|
13349
|
+
if (boundedEchoProbeIsReadOnly(withoutAssignments)) return true;
|
|
12842
13350
|
if (
|
|
12843
13351
|
["cat", "cd", "head", "ls", "stat", "tail", "wc"].includes(
|
|
12844
13352
|
String(commandTokens[0] || "")
|
|
@@ -12852,6 +13360,7 @@ function commandWritesOnlyEvidenceMatching(command = "", pathMatches = () => fal
|
|
|
12852
13360
|
underlyingPolicy.mayMutateProject !== true
|
|
12853
13361
|
);
|
|
12854
13362
|
});
|
|
13363
|
+
return safeSequence && sawEvidenceWrite;
|
|
12855
13364
|
}
|
|
12856
13365
|
|
|
12857
13366
|
function commandWritesOnlyPrivateVerificationEvidence(command = "") {
|