@lazyingart/agintiflow 0.20.290 → 0.20.292

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.
@@ -408,3 +408,65 @@ The same real session was replayed against the patched source at goal revision
408
408
  The target stayed at commit `2b35928` with no mutation. Focused planning,
409
409
  truthful-completion, and dynamic-budget regressions pass, as does the complete
410
410
  AgInTiFlow npm suite. The fix is released in AgInTiFlow `0.20.251`.
411
+
412
+ ### Exact source recovery after context loss
413
+
414
+ `database-migration-049` repeated an imperfect SQLite repair task against a
415
+ fresh broken workspace. Automatic routing selected only `qa-testing` and
416
+ `database`, but the first run read the canonical source immediately before
417
+ proactive context compaction. The compacted model correctly requested that
418
+ source again; the retained failed-test read budget exposed a different file,
419
+ so two safe but schema-invalid reads stopped the session without dispatch.
420
+
421
+ AgInTiFlow now treats proactive compaction, local context-window recovery,
422
+ model-timeout compaction, and same-task continuation as bounded context-loss
423
+ boundaries. A current failed-test packet may reopen only its evidence-derived
424
+ production repair paths after such a boundary. Tests, broad discovery,
425
+ arbitrary commands, and unrelated writes remain closed. Once the exact source
426
+ is read in the new context, the ordinary mutation and verification gates apply.
427
+
428
+ The original session resumed against the patched runtime, repaired the
429
+ transactional schema migration, preserved IDs and tags on URL updates, made
430
+ punctuation search literal, added regression coverage, passed four unit tests,
431
+ and committed a clean tree at `b956470`. The independent migration-safety
432
+ contract also passed. The complete npm suite passes, and the runtime fix is
433
+ released in AgInTiFlow `0.20.291`.
434
+
435
+ ### Explicit test evidence and security-review completion
436
+
437
+ `security-labshare-050` asked a fresh DeepSeek Pro session to harden a small
438
+ standard-library laboratory service from an imperfect, outcome-level prompt.
439
+ The agent removed a default credential, bounded artifact and dataset paths,
440
+ kept export execution shell-free, redacted audit credentials, protected the
441
+ public status response, added focused tests, and committed a clean repair. The
442
+ hidden acceptance contract then exposed a remaining audit-log injection path:
443
+ newlines in an actor or artifact field could forge additional physical records.
444
+
445
+ During the retained-session repair, AgInTi fixed the production source but
446
+ committed it before adding the explicitly requested regression test. This was a
447
+ runtime contract defect, not a model-quality failure. The mutation parser did
448
+ not recognize contextual requests such as "add a regression test," and the SCS
449
+ evidence contract did not convert "run the tests" into a fresh test obligation.
450
+ Consequently, source and Git evidence could satisfy the phase too early.
451
+
452
+ AgInTiFlow now recognizes explicit English and Chinese test-file mutations,
453
+ requires fresh test evidence for explicit run/rerun-test requests, and keeps
454
+ task-owned commit completion closed until that evidence exists. The regression
455
+ suite reproduces the stale persisted contract and proves no commit is offered
456
+ after source mutation but before a fresh passing test. It avoids broad keyword
457
+ matching, so phrases such as "create a canvas preview for this smoke test" do
458
+ not invent a test-file mutation.
459
+
460
+ The same release accepts genuinely observational Git evidence when no
461
+ consequential Git action is required, permits only the exact bounded Python
462
+ cache-cleanup forms used by project hygiene, and treats masked values such as
463
+ `token=***` as safe status evidence without weakening detection of real
464
+ credential assignments. The security-review skill now covers control-character
465
+ log injection and requires a standalone note to distinguish deployment
466
+ boundary, threat model, controls, residual risks, non-goals, and verification.
467
+
468
+ The retained session completed `SECURITY.md` after a valid secret-content
469
+ block, passed the exact hidden contract and all 13 visible tests, and left a
470
+ clean target repository at `ef3c099`. Focused regressions and the complete npm
471
+ suite pass. These runtime and skill fixes are released in AgInTiFlow
472
+ `0.20.292`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.290",
3
+ "version": "0.20.292",
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",
@@ -1680,6 +1680,45 @@ try {
1680
1680
  mixedValidationCleanupAdvice.autoRecover === true,
1681
1681
  "unrequested cleanup should be skipped without pausing substantive work"
1682
1682
  );
1683
+ const pythonCacheCleanupArgs = {
1684
+ command:
1685
+ "find . -type d -name __pycache__ -prune -exec rm -rf {} + ; find . -type f -name '*.pyc' -delete; git status --short",
1686
+ };
1687
+ assert(
1688
+ isUnrequestedCleanupCommand(
1689
+ "run_command",
1690
+ pythonCacheCleanupArgs,
1691
+ { goal: "Repair the service, run its tests, and commit the intentional work." },
1692
+ {}
1693
+ ),
1694
+ "post-acceptance Python cache deletion was not recognized as optional housekeeping"
1695
+ );
1696
+ const pythonCacheCleanupAdvice = buildPermissionAdvice({
1697
+ toolName: "run_command",
1698
+ args: pythonCacheCleanupArgs,
1699
+ guard: {
1700
+ category: "destructive",
1701
+ reason: "Destructive shell commands require Allow destructive actions.",
1702
+ },
1703
+ config: {
1704
+ ...dockerWorkspacePolicy,
1705
+ goal: "Repair the service, run its tests, and commit the intentional work.",
1706
+ },
1707
+ state: { sessionId: "coding-python-cache-cleanup-smoke" },
1708
+ });
1709
+ assert(
1710
+ pythonCacheCleanupAdvice.autoRecover === true,
1711
+ "ignored Python cache cleanup should be skipped without pausing a completed task"
1712
+ );
1713
+ assert(
1714
+ !isUnrequestedCleanupCommand(
1715
+ "run_command",
1716
+ { command: "find . -type f -delete" },
1717
+ { goal: "Repair the service and commit the result." },
1718
+ {}
1719
+ ),
1720
+ "broad find deletion was incorrectly classified as optional Python cache cleanup"
1721
+ );
1683
1722
  assert(
1684
1723
  !isUnrequestedCleanupCommand(
1685
1724
  "run_command",
@@ -13546,6 +13546,91 @@ Do not prefix, suffix, wrap, redirect, pipe, or combine that validator command.`
13546
13546
  "a partial multi-artifact task entered task-owned Git completion before its deliverables existed"
13547
13547
  );
13548
13548
 
13549
+ const securityWorkspace = path.join(tempRoot, "security-test-before-commit");
13550
+ await fs.mkdir(securityWorkspace, { recursive: true });
13551
+ await fs.writeFile(path.join(securityWorkspace, "labshare.py"), "def record_access():\n pass\n");
13552
+ const securityGoal = [
13553
+ "Independent acceptance failed: audit fields permit newline log injection.",
13554
+ "Add a focused regression test, fix the root cause, run the full visible tests,",
13555
+ "review the diff, and commit only this corrective work.",
13556
+ ].join(" ");
13557
+ const securityState = {
13558
+ goal: securityGoal,
13559
+ commandCwd: securityWorkspace,
13560
+ plan: "Patch the source and regression test, run the full suite, then commit the verified work.",
13561
+ messages: [
13562
+ {
13563
+ role: "tool",
13564
+ content: JSON.stringify({
13565
+ ok: true,
13566
+ toolName: "apply_patch",
13567
+ path: "labshare.py",
13568
+ goalRevision: 1,
13569
+ projectMutationRevision: 1,
13570
+ }),
13571
+ },
13572
+ {
13573
+ role: "tool",
13574
+ content: JSON.stringify({
13575
+ ok: true,
13576
+ toolName: "run_command",
13577
+ exitCode: 0,
13578
+ args: { command: "git status --short" },
13579
+ stdout: " M labshare.py\n",
13580
+ goalRevision: 1,
13581
+ projectMutationRevision: 1,
13582
+ }),
13583
+ },
13584
+ ],
13585
+ meta: {
13586
+ taskProfile: "security",
13587
+ goalContract: {
13588
+ revision: 1,
13589
+ currentRequest: securityGoal,
13590
+ taskGoal: securityGoal,
13591
+ activeGoal: securityGoal,
13592
+ activeGoalRevision: 1,
13593
+ history: [{ revision: 1, refreshExecutionContract: true }],
13594
+ lifecycle: [{ at: new Date(Date.now() - 2000).toISOString() }],
13595
+ },
13596
+ // Reproduce the pre-fix persisted contract that recognized a mutation
13597
+ // but failed to classify the requested regression test as file work.
13598
+ activeExecutionContract: {
13599
+ revision: 1,
13600
+ startedMutationRevision: 0,
13601
+ materialMutationRevision: 1,
13602
+ requiresWorkspaceMutation: true,
13603
+ requiresFileMutation: false,
13604
+ },
13605
+ projectVerification: {
13606
+ mutationRevision: 1,
13607
+ mutationHistory: [
13608
+ {
13609
+ revision: 1,
13610
+ at: new Date().toISOString(),
13611
+ toolName: "apply_patch",
13612
+ paths: ["labshare.py"],
13613
+ goalRevision: 1,
13614
+ },
13615
+ ],
13616
+ },
13617
+ },
13618
+ };
13619
+ const securityCompletionContract = completionTaskContract(
13620
+ { goal: securityGoal, taskProfile: "security", commandCwd: securityWorkspace },
13621
+ securityState
13622
+ );
13623
+ const preTestSecurityRuntime = nextStepRuntimeConfig(
13624
+ { goal: securityGoal, taskProfile: "security", commandCwd: securityWorkspace },
13625
+ securityState
13626
+ );
13627
+ assert(
13628
+ securityCompletionContract.requiresFileMutation === true &&
13629
+ securityCompletionContract.requiredEvidence.some((item) => item.category === "test") &&
13630
+ preTestSecurityRuntime.taskOwnedCommitPending !== true,
13631
+ "a regression-test repair entered task-owned Git completion before fresh test evidence"
13632
+ );
13633
+
13549
13634
  await fs.rm(tempRoot, { recursive: true, force: true });
13550
13635
  console.log("smoke-dynamic-step-budget ok");
13551
13636
  } catch (error) {
@@ -930,6 +930,22 @@ const refreshedSecurityContract = deriveScsTaskContract({
930
930
  goal: "Fix the security issue and run the regression tests.",
931
931
  taskProfile: "security",
932
932
  });
933
+ const correctiveSecurityContract = deriveScsTaskContract({
934
+ goal: [
935
+ "Independent acceptance failed: audit fields permit newline log injection.",
936
+ "Reopen the exact repair, add a focused regression test, fix the root cause,",
937
+ "run the full visible tests, review the diff, and commit only this corrective work.",
938
+ ].join(" "),
939
+ taskProfile: "security",
940
+ });
941
+ assert(
942
+ correctiveSecurityContract.requiresWorkspaceMutation === true &&
943
+ correctiveSecurityContract.requiresFileMutation === true &&
944
+ correctiveSecurityContract.requiredEvidence.some((item) => item.category === "file") &&
945
+ correctiveSecurityContract.requiredEvidence.some((item) => item.category === "test") &&
946
+ correctiveSecurityContract.requiredGitActions.includes("commit"),
947
+ "a requested regression-test repair could enter Git completion without fresh file and test evidence"
948
+ );
933
949
  const staleContractFinish = await reviewScsFinish(
934
950
  { mock: true },
935
951
  { provider: "mock", model: "mock-agent", taskProfile: "security" },
@@ -964,6 +980,11 @@ const staleContractFinish = await reviewScsFinish(
964
980
  exitCode: 0,
965
981
  args: { command: "python -m unittest discover -s tests -v" },
966
982
  stdout: "Ran 10 tests\nOK",
983
+ projectTest: {
984
+ passed: true,
985
+ command: "python -m unittest discover -s tests -v",
986
+ mutationRevision: 0,
987
+ },
967
988
  }),
968
989
  },
969
990
  ],
@@ -1056,6 +1077,20 @@ assert(
1056
1077
  evaluateScsEvidence(commitContract, gitStatusLedger).missingGitActions.includes("commit"),
1057
1078
  "the completion deficit did not identify the missing commit"
1058
1079
  );
1080
+ const readOnlyGitContract = {
1081
+ requiresExternalEvidence: true,
1082
+ requiredEvidence: [
1083
+ { id: "command", category: "command", description: "read-only verification" },
1084
+ { id: "git", category: "git", description: "existing commit verification" },
1085
+ ],
1086
+ requiredToolCalls: [],
1087
+ requiredGitActions: [],
1088
+ requiredProjectCommands: [],
1089
+ };
1090
+ assert(
1091
+ evaluateScsEvidence(readOnlyGitContract, gitStatusLedger).ok,
1092
+ "read-only Git evidence was rejected when the contract required no consequential Git action"
1093
+ );
1059
1094
  const committedEvaluation = evaluateScsEvidence(commitContract, gitCommitLedger);
1060
1095
  assert(
1061
1096
  committedEvaluation.missingGitActions.length === 0 &&
@@ -1231,7 +1266,14 @@ const checkedCodeEval = evaluateScsEvidence(
1231
1266
  messages: [
1232
1267
  {
1233
1268
  role: "tool",
1234
- content: JSON.stringify({ toolName: "run_command", ok: true, exitCode: 0, args: { command: "npm test" }, stdout: "ok" }),
1269
+ content: JSON.stringify({
1270
+ toolName: "run_command",
1271
+ ok: true,
1272
+ exitCode: 0,
1273
+ args: { command: "npm test" },
1274
+ stdout: "ok",
1275
+ projectTest: { passed: true, command: "npm test", mutationRevision: 0 },
1276
+ }),
1235
1277
  },
1236
1278
  ],
1237
1279
  },
@@ -2818,6 +2818,113 @@ assertStrict.deepEqual(
2818
2818
  ["service_ctl.py"],
2819
2819
  "DeepSeek repair reread fallback was not constrained to the canonical source"
2820
2820
  );
2821
+ const postCompactionRepairRuntime = nextStepRuntimeConfig(
2822
+ { provider: "deepseek", taskProfile: "qa" },
2823
+ {
2824
+ ...packetPathReadState,
2825
+ meta: {
2826
+ ...packetPathReadState.meta,
2827
+ failedTestRecoveryPacket: {
2828
+ ...packetPathReadState.meta.failedTestRecoveryPacket,
2829
+ paths: ["tests/test_service_ctl.py", "legacy_fixture.py", "service_ctl.py"],
2830
+ repairPaths: ["legacy_fixture.py", "service_ctl.py"],
2831
+ },
2832
+ toolLoop: {
2833
+ stagnationEpoch: 10,
2834
+ lastContextRecovery: {
2835
+ reason: "proactive-context-compaction",
2836
+ at: "2026-08-24T02:00:13.000Z",
2837
+ preservedStaticEvidence: true,
2838
+ },
2839
+ recent: [
2840
+ ...packetPathReadState.meta.toolLoop.recent,
2841
+ {
2842
+ toolName: "read_file",
2843
+ path: "tests/test_service_ctl.py",
2844
+ ok: true,
2845
+ blocked: false,
2846
+ at: "2026-08-24T02:00:12.000Z",
2847
+ },
2848
+ {
2849
+ toolName: "read_file",
2850
+ path: "legacy_fixture.py",
2851
+ ok: true,
2852
+ blocked: false,
2853
+ at: "2026-08-24T02:00:12.500Z",
2854
+ },
2855
+ ],
2856
+ },
2857
+ },
2858
+ }
2859
+ );
2860
+ assertStrict.deepEqual(
2861
+ postCompactionRepairRuntime.testFailureRepairContextPaths,
2862
+ ["legacy_fixture.py", "service_ctl.py"],
2863
+ "context compaction did not reopen the exact production source lost from model context"
2864
+ );
2865
+ assertStrict.equal(
2866
+ postCompactionRepairRuntime.testFailureRepairNeedsPatchContext,
2867
+ true,
2868
+ "context compaction did not restore one bounded source-context turn before mutation"
2869
+ );
2870
+ const postCompactionRepairTools = selectProgressiveTools(allTools, {
2871
+ config: postCompactionRepairRuntime,
2872
+ goal: "Continue the failed-test repair after proactive context compaction.",
2873
+ profile: "qa",
2874
+ });
2875
+ sameNames(
2876
+ postCompactionRepairTools,
2877
+ ["read_file", "apply_patch", "finish"],
2878
+ "post-compaction repair reopened broad discovery or verification tools"
2879
+ );
2880
+ assertStrict.deepEqual(
2881
+ postCompactionRepairTools[0].function.parameters.properties.path.enum,
2882
+ ["legacy_fixture.py", "service_ctl.py"],
2883
+ "post-compaction repair did not constrain rereads to all canonical production sources"
2884
+ );
2885
+ const sameTaskContinuationRepairRuntime = nextStepRuntimeConfig(
2886
+ { provider: "deepseek", taskProfile: "qa" },
2887
+ {
2888
+ ...packetPathReadState,
2889
+ meta: {
2890
+ ...packetPathReadState.meta,
2891
+ failedTestRecoveryPacket: {
2892
+ ...packetPathReadState.meta.failedTestRecoveryPacket,
2893
+ paths: ["tests/test_service_ctl.py", "legacy_fixture.py", "service_ctl.py"],
2894
+ repairPaths: ["legacy_fixture.py", "service_ctl.py"],
2895
+ },
2896
+ toolLoop: {
2897
+ stagnationEpoch: 11,
2898
+ lastContextRecovery: {
2899
+ reason: "same-task-continuation",
2900
+ at: "2026-08-24T02:01:00.000Z",
2901
+ },
2902
+ recent: [
2903
+ ...packetPathReadState.meta.toolLoop.recent,
2904
+ {
2905
+ toolName: "read_file",
2906
+ path: "tests/test_service_ctl.py",
2907
+ ok: true,
2908
+ blocked: false,
2909
+ at: "2026-08-24T02:00:12.000Z",
2910
+ },
2911
+ {
2912
+ toolName: "read_file",
2913
+ path: "legacy_fixture.py",
2914
+ ok: true,
2915
+ blocked: false,
2916
+ at: "2026-08-24T02:00:12.500Z",
2917
+ },
2918
+ ],
2919
+ },
2920
+ },
2921
+ }
2922
+ );
2923
+ assertStrict.deepEqual(
2924
+ sameTaskContinuationRepairRuntime.testFailureRepairContextPaths,
2925
+ ["legacy_fixture.py", "service_ctl.py"],
2926
+ "same-task continuation did not reopen all retained canonical production sources"
2927
+ );
2821
2928
  const testOnlyPacketState = {
2822
2929
  meta: {
2823
2930
  projectVerification: {
@@ -132,6 +132,12 @@ for (const unrelated of [
132
132
  }
133
133
  assert(selectedIds("debug Docker deployment logs and port config").includes("devops-deployment"), "devops prompt did not select devops-deployment");
134
134
  assert(selectedIds("review auth security and secrets handling").includes("security-review"), "security prompt did not select security-review");
135
+ const securitySkill = skills.find((skill) => skill.id === "security-review");
136
+ assert(
137
+ securitySkill?.body.includes("carriage-return, newline") &&
138
+ securitySkill?.body.includes("residual risks and non-goals"),
139
+ "security skill omitted audit-record injection or structured security-note guidance"
140
+ );
135
141
  assert(selectedIds("make a PowerPoint pitch deck").includes("presentation-slides"), "slides prompt did not select presentation-slides");
136
142
  const presentationSkill = skills.find((skill) => skill.id === "presentation-slides");
137
143
  assert(presentationSkill?.body.includes("Render every slide"), "presentation skill does not require every slide to be rendered");
@@ -71,6 +71,26 @@ const patchResult = await executeWorkspaceTool(
71
71
  );
72
72
  assert(patchResult.ok, "legacy apply_patch replace content with safe credential-loading code context should not be blocked");
73
73
 
74
+ const maskedAuditPatchResult = await executeWorkspaceTool(
75
+ "apply_patch",
76
+ {
77
+ patch: [
78
+ "*** Begin Patch",
79
+ "*** Add File: tests/test_audit_mask.py",
80
+ "+def test_audit_token_is_masked():",
81
+ "+ assert 'token=***' == 'token=' + ('*' * 3)",
82
+ "*** End Patch",
83
+ ].join("\n"),
84
+ },
85
+ config
86
+ );
87
+ assert(maskedAuditPatchResult.ok, "an all-asterisk credential mask in regression code must not be treated as a secret");
88
+ assert.equal(
89
+ redactSensitiveText("token=***"),
90
+ "token=***",
91
+ "all-asterisk credential masks must remain visible as non-secret evidence"
92
+ );
93
+
74
94
  const secretResult = await executeWorkspaceTool(
75
95
  "write_file",
76
96
  { path: "notes/leak-report.txt", content: "DEMO_SECRET_TOKEN=aginti_fake_do_not_use\n", mode: "create" },
@@ -30,7 +30,9 @@ Inspect auth boundaries, file/path handling, shell command construction, depende
30
30
 
31
31
  - Do not label a finding HIGH or CRITICAL from pattern matching alone. A high-severity claim needs either a reproduced exploit, a direct code path proof, or a clearly stated "potential/unverified" label with lower severity.
32
32
  - For path traversal, SSRF, command injection, open redirect, auth bypass, and file disclosure, test a minimal negative case when a safe local command is available. If runtime policy blocks the test, report the blocker and keep severity conservative.
33
+ - Treat logs, audit records, CSV rows, headers, and other line- or delimiter-based outputs as injection boundaries. Check every untrusted field for carriage-return, newline, delimiter, and control-character injection, and add a regression proving one input event cannot forge extra records.
33
34
  - Exclude generated/vendor/session/cache directories such as `node_modules/`, `.sessions/`, `.aginti-sessions/`, `.aginti-thorough-tests/`, build outputs, and coverage when broad scanning. If a scan accidentally includes them, call that out as noisy evidence and do not base severity solely on those hits.
34
35
  - When a policy blocks `.env`, key, credential, or permission checks, treat that as a safety limitation. Do not retry variants to bypass the block and do not imply the file content was inspected.
35
36
  - Prefer bounded commands: `rg`/`grep` with explicit `--exclude-dir`, `head`, `timeout`, and project-relative paths. Avoid unbounded recursive scans over the whole workspace.
36
37
  - Reports should separate confirmed findings, potential findings, false positives, and limitations. Include verification commands and observed results for every high-severity item.
38
+ - A standalone security note should distinguish the deployment boundary, concrete threat model, implemented controls, residual risks and non-goals, and exact verification procedure. Ground each claim in current source or tests; do not substitute a generic security checklist.
@@ -14588,11 +14588,34 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
14588
14588
  Date.parse(String(state.meta?.failedTestRecoveryPacket?.generatedAt || "")),
14589
14589
  Date.parse(String(state.meta?.failedTestFocusedRecovery?.at || "")),
14590
14590
  ].filter(Number.isFinite);
14591
- const recoveryPacketContextAt = Number.isFinite(failureAt)
14591
+ const retainedRecoveryPacketContextAt = Number.isFinite(failureAt)
14592
14592
  ? failureAt
14593
14593
  : recoveryPacketContextMarkers.length
14594
14594
  ? Math.max(...recoveryPacketContextMarkers)
14595
14595
  : Number.NaN;
14596
+ const latestContextRecovery = state.meta?.toolLoop?.lastContextRecovery;
14597
+ const latestContextRecoveryAt = Date.parse(
14598
+ String(latestContextRecovery?.at || "")
14599
+ );
14600
+ const postContextLossRepairContext = Boolean(
14601
+ Number.isFinite(latestContextRecoveryAt) &&
14602
+ (!Number.isFinite(retainedRecoveryPacketContextAt) ||
14603
+ latestContextRecoveryAt > retainedRecoveryPacketContextAt) &&
14604
+ [
14605
+ "proactive-context-compaction",
14606
+ "local-context-budget-retry",
14607
+ "model-timeout-retry",
14608
+ "same-task-continuation",
14609
+ ].includes(String(latestContextRecovery?.reason || ""))
14610
+ );
14611
+ const recoveryPacketContextAt = postContextLossRepairContext
14612
+ ? latestContextRecoveryAt
14613
+ : retainedRecoveryPacketContextAt;
14614
+ const activeRecoveryPacketPaths = postContextLossRepairContext
14615
+ ? recoveryPacketRepairPaths
14616
+ .filter((item) => !failedTestPathIsTestEvidence(item))
14617
+ .slice(0, 6)
14618
+ : recoveryPacketPaths;
14596
14619
  const consumedRecoveryPacketPaths = new Set(
14597
14620
  Number.isFinite(recoveryPacketContextAt)
14598
14621
  ? (Array.isArray(toolLoop.recent) ? toolLoop.recent : [])
@@ -14610,7 +14633,7 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
14610
14633
  .filter(Boolean)
14611
14634
  : []
14612
14635
  );
14613
- const unreadRecoveryPacketPaths = recoveryPacketPaths.filter(
14636
+ const unreadRecoveryPacketPaths = activeRecoveryPacketPaths.filter(
14614
14637
  (item) => !consumedRecoveryPacketPaths.has(item)
14615
14638
  );
14616
14639
  const recoveryPacketReadCounts = new Map();
@@ -14635,7 +14658,7 @@ export function nextStepRuntimeConfig(config = {}, state = {}) {
14635
14658
  }
14636
14659
  }
14637
14660
  const exactRecoveryPacketContextActive = Boolean(
14638
- recoveryPacketPaths.length && Number.isFinite(recoveryPacketContextAt)
14661
+ activeRecoveryPacketPaths.length && Number.isFinite(recoveryPacketContextAt)
14639
14662
  );
14640
14663
  const recoveryPacketHasProductionContext = [
14641
14664
  ...recoveryPacketPaths,
@@ -175,7 +175,14 @@ export function isUnrequestedCleanupCommand(toolName = "", args = {}, config = {
175
175
  return command
176
176
  .split(/(?:&&|;|\n)/)
177
177
  .map((segment) => segment.trim())
178
- .some((segment) => /^(?:command\s+)?rm\s+(?:-[A-Za-z]*[fr][A-Za-z]*\s+|--force\s+)/.test(segment));
178
+ .some(
179
+ (segment) =>
180
+ /^(?:command\s+)?rm\s+(?:-[A-Za-z]*[fr][A-Za-z]*\s+|--force\s+)/.test(segment) ||
181
+ /^find\s+\.\s+-type\s+d\s+-name\s+['"]?__pycache__['"]?\s+-prune\s+-exec\s+rm\s+-rf\s+\{\}\s+\+$/.test(
182
+ segment
183
+ ) ||
184
+ /^find\s+\.\s+-type\s+f\s+-name\s+(['"]?)\*\.pyc\1\s+-delete$/.test(segment)
185
+ );
179
186
  }
180
187
 
181
188
  function quoteShell(value = "") {
package/src/redaction.js CHANGED
@@ -8,7 +8,7 @@ const SECRET_PATTERNS = [
8
8
  ];
9
9
 
10
10
  const SECRET_ASSIGNMENT_PATTERN = /(\b(?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|apiKey|auth[_-]?token|authToken|token|secret|password|passwd|npm[_-]?token|npmToken|_authToken|grsai|venice[_-]?api[_-]?key|veniceApiKey|openrouter[_-]?api[_-]?key)[^\S\r\n]*([:=])[^\S\r\n]*)([^\s\\"'`,;|(){}]+)/gi;
11
- const SAFE_ASSIGNMENT_VALUE_PATTERN = /^(?:false|true|null|none|unset|missing|not[-_ ]?set|\[REDACTED\])$/i;
11
+ const SAFE_ASSIGNMENT_VALUE_PATTERN = /^(?:false|true|null|none|unset|missing|not[-_ ]?set|\[REDACTED\]|\*{3,})$/i;
12
12
  const SOURCE_TYPE_ANNOTATION_PATTERN = /^(?:str|string|bytes|bytearray|int|float|complex|bool|dict|list|tuple|set|frozenset|object|any|unknown|never|void|path|(?:typing\.)?(?:Any|Optional|Union|Literal|Annotated|Sequence|Mapping|MutableMapping|Callable|Type|ClassVar|Final|List|Dict|Tuple|Set|FrozenSet)\[[^\r\n]{1,120}\])$/i;
13
13
  const SOURCE_CALL_HEAD_PATTERN = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
14
14
 
@@ -266,7 +266,11 @@ export function gitActionsSatisfyContract(contract = {}, actions = []) {
266
266
  ? contract.requiredGitActions.map((item) => String(item || "").toLowerCase()).filter(Boolean)
267
267
  : [];
268
268
  if (required.length) return missingRequiredGitActionSequence(required, observed).length === 0;
269
- return observed.some((action) => !isObservationalGitAction(action));
269
+ // A Git evidence requirement can be purely observational, for example when
270
+ // a continuation asks to verify an existing commit while explicitly
271
+ // forbidding another commit. Consequential actions remain governed by the
272
+ // ordered requiredGitActions contract above.
273
+ return observed.length > 0;
270
274
  }
271
275
 
272
276
  const PROJECT_TEST_PROFILES = new Set([
@@ -1233,14 +1237,37 @@ function codeProfileRequiresCommand(goal = "") {
1233
1237
  return substantiveCodeWork && !simpleDocumentWrite;
1234
1238
  }
1235
1239
 
1240
+ function goalRequestsExplicitTestMutation(text = "") {
1241
+ return (
1242
+ /\b(?:add|create|implement|write)\b(?:\s+(?:a|an|the|focused|new|additional|specific|security|unit|integration|regression))*\s+(?:regression\s+)?(?:tests?|test cases?)\b/.test(
1243
+ text
1244
+ ) ||
1245
+ /\b(?:edit|fix|modify|patch|repair|update)\b(?:\s+(?:a|an|the|focused|new|existing|current|failing|specific|security|unit|integration|regression))*\s+(?:regression\s+)?(?:tests?|test cases?)\b/.test(
1246
+ text
1247
+ ) ||
1248
+ /(?:添加|新增|创建|编写|编辑|修复|修改|更新)(?:一个|新的|现有的|失败的|专门的|回归|单元|集成)*测试(?:用例)?/.test(
1249
+ text
1250
+ )
1251
+ );
1252
+ }
1253
+
1236
1254
  function goalRequestsWorkspaceMutation(goal = "", taskProfile = "") {
1237
1255
  const text = normalizedText(stripCompletedWorkNarration(stripForbiddenLanguage(goal)));
1238
- if (String(taskProfile || "").trim().toLowerCase() === "review") {
1239
- return /\b(?:append|copy|create|delete|edit|fix|implement|modify|move|patch|refactor|remove|rename|repair|replace|rewrite|save|update|write)\b/.test(
1256
+ const explicitAddMutation =
1257
+ goalRequestsExplicitTestMutation(text) ||
1258
+ /\badd\b(?:\s+(?:a|an|the|new|additional|specific))*\s+(?:code|documents?|files?|notes?|readme|scripts?|source|workspace)\b/.test(
1240
1259
  text
1260
+ ) || /(?:添加|新增)(?:一个|新的|额外的|特定的)*(?:文件|文档|代码|脚本|源码)/.test(text);
1261
+ if (String(taskProfile || "").trim().toLowerCase() === "review") {
1262
+ return (
1263
+ explicitAddMutation ||
1264
+ /\b(?:append|copy|create|delete|edit|fix|implement|modify|move|patch|refactor|remove|rename|repair|replace|rewrite|save|update|write)\b/.test(
1265
+ text
1266
+ )
1241
1267
  );
1242
1268
  }
1243
1269
  return (
1270
+ explicitAddMutation ||
1244
1271
  /\b(?:append|build|convert|copy|create|delete|edit|fix|generate|implement|modify|move|patch|refactor|remove|rename|repair|replace|rewrite|save|update|write)\b/.test(
1245
1272
  text
1246
1273
  ) ||
@@ -1251,7 +1278,9 @@ function goalRequestsWorkspaceMutation(goal = "", taskProfile = "") {
1251
1278
  function goalRequestsFileMutation(goal = "", taskProfile = "") {
1252
1279
  const text = normalizedText(stripCompletedWorkNarration(stripForbiddenLanguage(goal)));
1253
1280
  if (!goalRequestsWorkspaceMutation(text, taskProfile)) return false;
1281
+ const explicitTestMutation = goalRequestsExplicitTestMutation(text);
1254
1282
  return (
1283
+ explicitTestMutation ||
1255
1284
  /\b(?:code|codebase|document(?:ation)?|files?|notes?|path|readme|repo(?:sitory)?|script|source|workspace)\b/.test(
1256
1285
  text
1257
1286
  ) ||
@@ -1263,6 +1292,17 @@ function goalRequestsFileMutation(goal = "", taskProfile = "") {
1263
1292
  );
1264
1293
  }
1265
1294
 
1295
+ function goalRequestsTestExecution(goal = "") {
1296
+ const text = normalizedText(stripCompletedWorkNarration(stripForbiddenLanguage(goal)));
1297
+ return (
1298
+ /\b(?:run|rerun|re-run|execute|invoke)\b[^.\n;]{0,140}\b(?:tests?|test suite)\b/.test(text) ||
1299
+ /\b(?:tests?|test suite)\b[^.\n;]{0,120}\b(?:pass|passing|green|run|rerun|re-run|execute)\b/.test(text) ||
1300
+ /(?:运行|执行|重跑|重新运行)[^。;\n]{0,100}(?:测试|测试套件)|(?:测试|测试套件)[^。;\n]{0,80}(?:通过|运行|执行)/.test(
1301
+ text
1302
+ )
1303
+ );
1304
+ }
1305
+
1266
1306
  function profileRequirementsForGoal(taskProfile = "", goal = "") {
1267
1307
  const profile = String(taskProfile || "").toLowerCase();
1268
1308
  const defaults = PROFILE_REQUIREMENTS[profile] || [];
@@ -1367,6 +1407,9 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
1367
1407
  if (directCommandSignal || (validationSignal && codeProfileRequiresCommand(positiveGoal))) {
1368
1408
  categories.add("command");
1369
1409
  }
1410
+ if (goalRequestsTestExecution(positiveGoal)) {
1411
+ categories.add("test");
1412
+ }
1370
1413
  if (textHas(mandatoryEvidenceText, /\b(artifact|canvas|pdf|image|video|screenshot|cover|plot|chart|figure|docx|archive|copy to|export|generated|generate|draft)\b/) || /输出|产物|图片|视频|截图|封面|生成/.test(mandatoryEvidenceText)) {
1371
1414
  categories.add("artifact");
1372
1415
  }
@@ -367,7 +367,7 @@ function stripSafeCredentialReferences(content) {
367
367
  const codeKeyName = String.raw`(?:api[_-]?key|apiKey|token|secret|password|passwd|npmToken|authToken|grsai|veniceApiKey|venice_api_key)`;
368
368
  const codeExpression = String.raw`[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\([^"'\n]*\))?`;
369
369
  const envName = String.raw`[A-Z][A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD|GRSAI|VENICE)[A-Z0-9_]*`;
370
- const safeStatus = String.raw`(?:false|true|null|none|unset|missing|not[-_ ]?set|\[REDACTED\])`;
370
+ const safeStatus = String.raw`(?:false|true|null|none|unset|missing|not[-_ ]?set|\[REDACTED\]|\*{3,})`;
371
371
 
372
372
  text = text.replace(
373
373
  new RegExp(String.raw`\b${keyName}\s*[:=]\s*${safeStatus}\b`, "gi"),