@lazyingart/agintiflow 0.20.247 → 0.20.249

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.
@@ -330,3 +330,52 @@ event sequence, a project handoff with exact status and verification commands,
330
330
  and no lingering job tmux session. No source patch or npm release was needed;
331
331
  this run validates the published package's general long-job, resume, context,
332
332
  deduplication, and cleanup contracts without task-specific routing.
333
+
334
+ ### Host-managed response roles and permission integrity
335
+
336
+ `permission-resilient-synthesis-030` traced a real LabCanvas career-report
337
+ failure to the integration boundary rather than weakening AgInTiFlow's
338
+ permission guard. LabCanvas invoked the host-managed report under the role
339
+ `career_research`, while its response-only classifier recognized only the older
340
+ `career_daily` alias. The model was consequently offered file tools in a safe,
341
+ read-only run; when it tried to save its already synthesized report,
342
+ AgInTiFlow correctly persisted a `permission_required` pause.
343
+
344
+ LabCanvas now classifies the full `career-research-*` and
345
+ `daily-organizer-*` role families as host-managed response turns. Those turns
346
+ use AgInTi's `chatops` profile with shell, file, and auxiliary tools disabled;
347
+ the host owns persistence, compilation, quality validation, and delivery.
348
+ General worker roles retain their existing writable Docker contract, and safe
349
+ mode still blocks genuine unapproved writes.
350
+
351
+ A fresh installed `0.20.248` DeepSeek run completed the imperfect synthesis
352
+ prompt in one model turn. Independent event inspection found zero tool calls,
353
+ zero permission events, one normal `session.finished`, a complete 1,893-byte
354
+ answer, and no task artifact mutation. This was an AgenticApp integration fix;
355
+ no AgInTiFlow runtime or npm release change was required.
356
+
357
+ ### Reader-facing report quality and local editorial routing
358
+
359
+ `research-pdf-quality-034` exercised a normal scheduled-research packet through
360
+ the AgInTi-backed LabCanvas worker. DeepSeek revised an existing local report
361
+ into a 17,557-byte Chinese scientific review with nine traceable sources,
362
+ source-level methods/results/limitations, cross-source synthesis and tensions,
363
+ explicit evidence boundaries, actionable experiments, and references. The
364
+ first host compile exposed a nearly empty final page, and the prior agent repair
365
+ claimed success without changing the source.
366
+
367
+ LabCanvas now treats the PDF as a different deliverable from the concise chat
368
+ brief. It audits every configured content dimension, keeps orchestration
369
+ provenance out of the reader document, extracts text per page, renders private
370
+ page previews, rejects orphan pages, retries one conservative compact layout,
371
+ rebuilds stale sibling PDFs, and can deterministically adopt a corrected
372
+ host-built PDF during stored-result repair. The accepted four-page PDF has
373
+ embedded CJK fonts, page body counts `1584, 1988, 1416, 2058`, and no visual or
374
+ layout issues. Stored replay covered every task item with no model rerun or
375
+ external write.
376
+
377
+ The scenario also exposed two general AgInTi routing false positives. Explicit
378
+ local report paths and existing-document revision language now count as local
379
+ workspace intent, so surrounding research policy does not force a new deep
380
+ research route. The phrase `page-safe` no longer creates a browser-evidence
381
+ requirement. Focused routing/evidence smokes and the full npm suite pass.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.247",
3
+ "version": "0.20.249",
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",
@@ -49,6 +49,20 @@ async function runCliIn(cwd, args, envOverrides = {}) {
49
49
  return result.stdout;
50
50
  }
51
51
 
52
+ async function runCliAllowStopped(args, envOverrides = {}) {
53
+ try {
54
+ const stdout = await runCli(args, envOverrides);
55
+ return { stdout, stderr: "", exitCode: 0 };
56
+ } catch (error) {
57
+ if (!Number.isInteger(error?.code)) throw error;
58
+ return {
59
+ stdout: String(error.stdout || ""),
60
+ stderr: String(error.stderr || ""),
61
+ exitCode: error.code,
62
+ };
63
+ }
64
+ }
65
+
52
66
  try {
53
67
  await runCli(["init"]);
54
68
  const agintiMd = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
@@ -188,7 +202,7 @@ try {
188
202
  const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
189
203
  assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
190
204
  assert(doctor.project.instructionsPresent, "doctor --capabilities did not report AGINTI.md");
191
- const envSandboxRun = await runCli(
205
+ const envSandboxRun = await runCliAllowStopped(
192
206
  ["--provider", "mock", "--routing", "manual", "--model", "mock-agent", "--max-steps", "1", "env sandbox smoke"],
193
207
  {
194
208
  SANDBOX_MODE: "host",
@@ -196,8 +210,19 @@ try {
196
210
  USE_DOCKER_SANDBOX: "false",
197
211
  }
198
212
  );
199
- assert(envSandboxRun.includes("Shell: host policy=allow"), "one-shot CLI did not respect host sandbox env defaults");
200
- assert(!envSandboxRun.includes("Docker workspace:"), "one-shot CLI forced Docker despite host sandbox env defaults");
213
+ assert(envSandboxRun.exitCode === 1, "a step-budget-stopped one-shot CLI run did not report failure");
214
+ assert(
215
+ envSandboxRun.stderr.includes("Stopped after 1 steps without finish()."),
216
+ "a step-budget-stopped one-shot CLI run did not expose its terminal reason"
217
+ );
218
+ assert(
219
+ envSandboxRun.stdout.includes("Shell: host policy=allow"),
220
+ "one-shot CLI did not respect host sandbox env defaults"
221
+ );
222
+ assert(
223
+ !envSandboxRun.stdout.includes("Docker workspace:"),
224
+ "one-shot CLI forced Docker despite host sandbox env defaults"
225
+ );
201
226
 
202
227
  console.log(
203
228
  JSON.stringify(
@@ -415,11 +415,126 @@ function boundedOutputReadPair(index, generation) {
415
415
  ];
416
416
  }
417
417
 
418
+ function exactInputEvidencePair(index) {
419
+ const id = `exact-input-${index}`;
420
+ const startLine = 1 + (index - 1) * 45;
421
+ return [
422
+ {
423
+ role: "assistant",
424
+ content: "",
425
+ reasoning_content: "Read one bounded exact-input evidence range.",
426
+ tool_calls: [
427
+ {
428
+ id,
429
+ type: "function",
430
+ function: {
431
+ name: "read_file",
432
+ arguments: JSON.stringify({
433
+ path: "tmp/reliability-evidence-pass.md",
434
+ startLine,
435
+ lineLimit: 45,
436
+ }),
437
+ },
438
+ },
439
+ ],
440
+ },
441
+ {
442
+ role: "tool",
443
+ tool_call_id: id,
444
+ content: JSON.stringify({
445
+ ok: true,
446
+ toolName: "read_file",
447
+ path: "tmp/reliability-evidence-pass.md",
448
+ startLine,
449
+ lineLimit: 45,
450
+ lineCount: 180,
451
+ bytes: 24000,
452
+ sha256: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
453
+ contentTruncated: false,
454
+ content: `EXACT-INPUT-RANGE-${index}\n${`source evidence ${index} `.repeat(220)}`,
455
+ }),
456
+ },
457
+ ];
458
+ }
459
+
460
+ function readOnlyDiagnosticPair(index) {
461
+ const id = `diagnostic-${index}`;
462
+ const command = `python3 -c "print('diagnostic ${index}')"`;
463
+ return [
464
+ {
465
+ role: "assistant",
466
+ content: "",
467
+ reasoning_content: "Inspect one diagnostic without changing task outputs.",
468
+ tool_calls: [
469
+ {
470
+ id,
471
+ type: "function",
472
+ function: { name: "run_command", arguments: JSON.stringify({ command }) },
473
+ },
474
+ ],
475
+ },
476
+ {
477
+ role: "tool",
478
+ tool_call_id: id,
479
+ content: JSON.stringify({
480
+ ok: true,
481
+ toolName: "run_command",
482
+ args: { command },
483
+ exitCode: 0,
484
+ stdout: `DIAGNOSTIC-${index}\n${"read-only shell output ".repeat(80)}`,
485
+ }),
486
+ },
487
+ ];
488
+ }
489
+
490
+ const exactInputCoverageState = {
491
+ goal: [
492
+ "Read tmp/reliability-evidence-pass.md as the exact read-only input.",
493
+ "Rewrite agent-reliability-evidence-review.md.",
494
+ "Rebuild sources.json.",
495
+ ].join("\n"),
496
+ plan: "Use retained evidence and create the two outputs.",
497
+ meta: {
498
+ scs: {
499
+ taskContract: {
500
+ exactInputPaths: ["tmp/reliability-evidence-pass.md"],
501
+ exactOutputPaths: ["agent-reliability-evidence-review.md", "sources.json"],
502
+ },
503
+ },
504
+ },
505
+ messages: [
506
+ { role: "system", content: `SYSTEM-INPUT-COVERAGE\n${"policy ".repeat(5000)}` },
507
+ { role: "user", content: "Create a source-grounded reader-facing report." },
508
+ ...Array.from({ length: 4 }, (_, index) => exactInputEvidencePair(index + 1)).flat(),
509
+ ...Array.from({ length: 10 }, (_, index) => readOnlyDiagnosticPair(index + 1)).flat(),
510
+ ...Array.from({ length: 6 }, (_, index) => boundedOutputReadPair(index + 1, 9)).flat(),
511
+ ],
512
+ };
513
+ const exactInputCoverage = buildContextBudgetCompactionMessages(
514
+ exactInputCoverageState,
515
+ { ...config, provider: "deepseek", model: "deepseek-chat" },
516
+ { title: "", url: "" },
517
+ 20,
518
+ { reason: "preserve all exact-input source ranges over diagnostics" }
519
+ );
520
+ const exactInputCoverageText = exactInputCoverage.map((message) => message.content || "").join("\n");
521
+ for (let index = 1; index <= 4; index += 1) {
522
+ assert.ok(
523
+ exactInputCoverageText.includes(`EXACT-INPUT-RANGE-${index}`),
524
+ `compaction lost exact input evidence range ${index}`
525
+ );
526
+ }
527
+ assert.ok(
528
+ estimateMessageTokens(exactInputCoverage) <= 12288,
529
+ "exact-input evidence retention exceeded the bounded retry target"
530
+ );
531
+
418
532
  const twiceCompactedState = {
419
533
  ...compactionState,
420
534
  meta: {
421
535
  scs: {
422
536
  taskContract: {
537
+ exactInputPaths: ["reports/reliability.md"],
423
538
  exactOutputPaths: ["agent-reliability-evidence-review.md", "sources.json"],
424
539
  },
425
540
  },
@@ -427,6 +542,7 @@ const twiceCompactedState = {
427
542
  messages: [
428
543
  ...deepSeekRuntimeMessages,
429
544
  ...Array.from({ length: 14 }, (_, index) => noisyFullReadPair(index + 1, 2)).flat(),
545
+ ...Array.from({ length: 8 }, (_, index) => readOnlyDiagnosticPair(index + 1)).flat(),
430
546
  ...Array.from({ length: 6 }, (_, index) => boundedOutputReadPair(index + 1, 2)).flat(),
431
547
  ],
432
548
  };
@@ -9,6 +9,7 @@ import { flushHousekeeping } from "../src/housekeeping.js";
9
9
  import { requestNextStep, toolChoiceForProvider } from "../src/model-client.js";
10
10
  import { providerStructuredOutputAttempts } from "../src/provider-contract.js";
11
11
  import {
12
+ hasExplicitDeepResearchSuppression,
12
13
  hasExplicitDeepResearchIntent,
13
14
  hasLocalResearchWorkspaceIntent,
14
15
  shouldStartWithDeepResearch,
@@ -107,6 +108,50 @@ async function main() {
107
108
  shouldStartWithDeepResearch("Write a deep web research report comparing three primary papers."),
108
109
  "standalone deep research no longer starts with the bounded research workflow"
109
110
  );
111
+ const retainedResearchGoal = [
112
+ "Continue the interrupted evidence-review task from its saved state.",
113
+ "Do not run deep_research again. Reuse the completed research artifact.",
114
+ "Rewrite agent-reliability-evidence-review.md from the retained evidence.",
115
+ ].join("\n");
116
+ assert(
117
+ hasExplicitDeepResearchSuppression(retainedResearchGoal),
118
+ "an explicit completed-research reuse instruction was not detected"
119
+ );
120
+ assert(
121
+ !shouldStartWithDeepResearch(retainedResearchGoal, inspectedLocalEvidenceMessages),
122
+ "an explicit prohibition still narrowed the next turn to deep_research"
123
+ );
124
+ const coordinatedSuppressionGoal = [
125
+ "Resume the saved evidence-review task.",
126
+ "Do not restart the task, run deep_research, or reopen broad discovery.",
127
+ "Use the retained completed evidence and rebuild sources.json.",
128
+ ].join("\n");
129
+ assert(
130
+ hasExplicitDeepResearchSuppression(coordinatedSuppressionGoal),
131
+ "a coordinated do-not clause did not suppress deep_research"
132
+ );
133
+ assert(
134
+ !shouldStartWithDeepResearch(localEvidenceGoal, [
135
+ { role: "user", content: coordinatedSuppressionGoal },
136
+ ...inspectedLocalEvidenceMessages.slice(1),
137
+ ]),
138
+ "a resumed retained-evidence repair was forced back into deep_research after inspection"
139
+ );
140
+ assert(
141
+ shouldStartWithDeepResearch(localEvidenceGoal, [
142
+ ...inspectedLocalEvidenceMessages,
143
+ { role: "user", content: "Run a fresh deep research pass now; the retained evidence is stale." },
144
+ {
145
+ role: "assistant",
146
+ tool_calls: [{ id: "fresh-report", function: { name: "read_file", arguments: '{"path":"agent-reliability-evidence-review.md"}' } }],
147
+ },
148
+ {
149
+ role: "assistant",
150
+ tool_calls: [{ id: "fresh-sources", function: { name: "read_file", arguments: '{"path":"sources.json"}' } }],
151
+ },
152
+ ]),
153
+ "an older completed-research context suppressed a newer explicit refresh request"
154
+ );
110
155
  assert(
111
156
  !hasExplicitDeepResearchIntent("Create a phone-friendly document from this folder.", [
112
157
  {
@@ -1364,6 +1364,33 @@ try {
1364
1364
  `a status wrapper changed the inner command's mutation identity: ${command}`
1365
1365
  );
1366
1366
  }
1367
+ const readOnlyValidatorState = { meta: { goalContract: { revision: 1 } } };
1368
+ const readOnlyValidatorResult = {
1369
+ toolName: "run_command",
1370
+ ok: true,
1371
+ exitCode: 0,
1372
+ args: {
1373
+ command:
1374
+ 'python3 tmp/external_agent_reliability_quality.py . ; echo "EXIT=$?"',
1375
+ },
1376
+ stdout: "agent reliability research contract passed\nEXIT=1\n",
1377
+ stderr: "agent reliability research quality failed\n",
1378
+ commandPolicy: {
1379
+ category: "toolchain",
1380
+ writesWorkspace: true,
1381
+ mayMutateProject: false,
1382
+ substantiveTest: false,
1383
+ },
1384
+ };
1385
+ recordProjectVerificationOutcome(readOnlyValidatorState, readOnlyValidatorResult, {
1386
+ commandCwd: workspace,
1387
+ taskProfile: "writing",
1388
+ });
1389
+ assert(
1390
+ readOnlyValidatorState.meta.projectVerification?.mutationRevision === 0 &&
1391
+ readOnlyValidatorResult.projectMutationRevision === 0,
1392
+ "a semantically read-only validator fabricated project mutation progress"
1393
+ );
1367
1394
  const shellMutationState = { meta: { goalContract: { revision: 1 } } };
1368
1395
  const shellMutationResult = {
1369
1396
  toolName: "run_command",
@@ -2479,6 +2506,13 @@ try {
2479
2506
  args: { command: "npm test && git pull --ff-only" },
2480
2507
  stdout: "1 test passed",
2481
2508
  stderr: "",
2509
+ commandPolicy: {
2510
+ category: "git-remote",
2511
+ writesWorkspace: true,
2512
+ mayMutateProject: false,
2513
+ substantiveTest: true,
2514
+ gitOnly: false,
2515
+ },
2482
2516
  };
2483
2517
  recordProjectVerificationOutcome(testBeforePullState, testBeforePullResult, {
2484
2518
  commandCwd: workspace,
@@ -3067,6 +3101,21 @@ try {
3067
3101
  shouldResetStaticDiscoveryPhase({ ok: true, toolName: "write_file", args: { path: "report.md" } }),
3068
3102
  "successful output creation should reset static discovery convergence"
3069
3103
  );
3104
+ assert(
3105
+ !shouldResetStaticDiscoveryPhase({
3106
+ ok: true,
3107
+ toolName: "run_command",
3108
+ args: { command: "python3 - <<'PY'\nprint('inspect only')\nPY" },
3109
+ commandPolicy: {
3110
+ writesWorkspace: true,
3111
+ mayMutateProject: false,
3112
+ substantiveTest: false,
3113
+ },
3114
+ exitCode: 0,
3115
+ stdout: "inspect only",
3116
+ }),
3117
+ "a read-only diagnostic shell probe reset static discovery because of a conservative write heuristic"
3118
+ );
3070
3119
  const uniqueDiscovery = {};
3071
3120
  recordStaticDiscoveryProgress(uniqueDiscovery, "read_file:/reference/A.md");
3072
3121
  recordStaticDiscoveryProgress(uniqueDiscovery, "read_file:/reference/A.md");
@@ -3095,6 +3144,32 @@ try {
3095
3144
  JSON.stringify(compactedDiscoveryState.meta.toolLoop.warned) === JSON.stringify(["run_command:keep"]),
3096
3145
  "context recovery did not clear only stale static-read warnings"
3097
3146
  );
3147
+ const retainedDiscoveryState = {
3148
+ meta: {
3149
+ toolLoop: {
3150
+ recent: [],
3151
+ warned: ["file-read:/reference/A.md"],
3152
+ staticCounts: { "file-read:/reference/A.md": 2 },
3153
+ staticOrder: ["file-read:/reference/A.md"],
3154
+ staticTotal: 1,
3155
+ staticCallTotal: 2,
3156
+ },
3157
+ },
3158
+ };
3159
+ resetStaticDiscoveryAfterContextLoss(
3160
+ retainedDiscoveryState,
3161
+ "proactive-context-compaction",
3162
+ { preserveStaticEvidence: true }
3163
+ );
3164
+ assert(
3165
+ retainedDiscoveryState.meta.toolLoop.staticTotal === 1 &&
3166
+ retainedDiscoveryState.meta.toolLoop.staticCounts["file-read:/reference/A.md"] === 2,
3167
+ "lossless context compaction reopened already completed discovery"
3168
+ );
3169
+ assert(
3170
+ retainedDiscoveryState.meta.toolLoop.lastContextRecovery?.preservedStaticEvidence === true,
3171
+ "lossless context compaction did not record preserved discovery evidence"
3172
+ );
3098
3173
  const exactReadSignature = staticToolCallSignature("read_file", { path: "/reference/A.md" }, {
3099
3174
  commandCwd: workspace,
3100
3175
  });
@@ -6065,6 +6140,35 @@ try {
6065
6140
  events: [],
6066
6141
  });
6067
6142
  assert(!staticOnlyDecision.approved, "budget gate treated static discovery alone as implementation progress");
6143
+ const readOnlyShellDecision = decideStepBudgetExtension({
6144
+ config: { scsActive: true, commandCwd: "/tmp/workspace" },
6145
+ budget: createStepBudgetState(
6146
+ { provider: "localllm", maxSteps: 12, dynamicSteps: "on", dynamicStepExtensionLimit: 2, scsActive: true },
6147
+ { meta: {}, stepsCompleted: 0 }
6148
+ ),
6149
+ step: 11,
6150
+ state: {
6151
+ messages: Array.from({ length: 5 }, (_, index) =>
6152
+ toolMessage({
6153
+ toolName: "run_command",
6154
+ ok: true,
6155
+ args: { command: `python3 -c "print('inspect ${index}')"` },
6156
+ commandPolicy: {
6157
+ writesWorkspace: true,
6158
+ mayMutateProject: false,
6159
+ substantiveTest: false,
6160
+ },
6161
+ exitCode: 0,
6162
+ stdout: `inspection ${index}`,
6163
+ })
6164
+ ),
6165
+ },
6166
+ events: [],
6167
+ });
6168
+ assert(
6169
+ !readOnlyShellDecision.approved,
6170
+ "budget gate extended a run containing only read-only diagnostic shell output"
6171
+ );
6068
6172
 
6069
6173
  const mockAutoBudget = createStepBudgetState({ provider: "mock", maxSteps: 4, dynamicSteps: "auto" }, { meta: {}, stepsCompleted: 0 });
6070
6174
  assert(!mockAutoBudget.enabled, "mock provider should not auto-extend unless explicitly enabled");
@@ -1515,6 +1515,43 @@ sameNames(
1515
1515
  ["deep_research", "finish"],
1516
1516
  "local evidence research did not enter the bounded research engine after inspection"
1517
1517
  );
1518
+ const retainedEvidenceManifestRepair = selectProgressiveTools(allTools, {
1519
+ config: { provider: "deepseek" },
1520
+ goal:
1521
+ "Investigate the reliability problem in this folder, write an evidence review and sources.json, then commit the intentional work.",
1522
+ profile: "research",
1523
+ messages: [
1524
+ {
1525
+ role: "user",
1526
+ content: [
1527
+ "Resume the saved evidence-review task.",
1528
+ "Do not restart the task, run deep_research, or reopen broad discovery.",
1529
+ "Use the retained completed evidence and rebuild sources.json.",
1530
+ ].join("\n"),
1531
+ },
1532
+ {
1533
+ role: "assistant",
1534
+ tool_calls: [{ id: "retained-sources", function: { name: "read_file", arguments: '{"path":"sources.json"}' } }],
1535
+ },
1536
+ {
1537
+ role: "assistant",
1538
+ tool_calls: [{ id: "retained-evidence", function: { name: "read_file", arguments: '{"path":"tmp/reliability-evidence-pass.md"}' } }],
1539
+ },
1540
+ ],
1541
+ });
1542
+ assert(
1543
+ names(retainedEvidenceManifestRepair).includes("write_file"),
1544
+ "retained-evidence manifest repair omitted write_file after inspection"
1545
+ );
1546
+ assert(
1547
+ names(retainedEvidenceManifestRepair).includes("read_file"),
1548
+ "retained-evidence manifest repair omitted bounded source reads"
1549
+ );
1550
+ assert(
1551
+ !(names(retainedEvidenceManifestRepair).length === 2 &&
1552
+ names(retainedEvidenceManifestRepair)[0] === "deep_research"),
1553
+ "explicit retained-evidence reuse was forced back into deep_research"
1554
+ );
1518
1555
  const localEvidenceAfterDeepResearchCompaction = selectProgressiveTools(allTools, {
1519
1556
  config: { provider: "deepseek" },
1520
1557
  goal:
@@ -1610,6 +1647,22 @@ assert(
1610
1647
  "surrounding policy prose incorrectly forced a scoped artifact task into deep research"
1611
1648
  );
1612
1649
 
1650
+ const scopedExistingReportEditPrompt = `Generic research worker policy.
1651
+ AGINTI_EVIDENCE_SCOPE_JSON: {"mode":"task","request":"Revise the exact existing research report at output/wechat_worker/task/report.md. Preserve the evidence, write the revised Markdown under this task's artifact directory, and use page-safe tables. The host owns PDF compilation."}
1652
+ Surrounding routine text mentions literature review, evidence review, research report, and web sources.`;
1653
+ const scopedExistingReportEditTools = selectProgressiveTools(allTools, {
1654
+ config: { provider: "deepseek", progressiveTools: true },
1655
+ goal: scopedExistingReportEditPrompt,
1656
+ profile: "auto",
1657
+ messages: [{ role: "user", content: scopedExistingReportEditPrompt }],
1658
+ });
1659
+ assert(names(scopedExistingReportEditTools).includes("read_file"), "scoped existing-report edit omitted read_file");
1660
+ assert(names(scopedExistingReportEditTools).includes("write_file"), "scoped existing-report edit omitted write_file");
1661
+ assert(
1662
+ !(names(scopedExistingReportEditTools).length === 2 && names(scopedExistingReportEditTools)[0] === "deep_research"),
1663
+ "scoped existing-report edit was incorrectly forced into deep_research"
1664
+ );
1665
+
1613
1666
  const scopedDeepResearchPrompt = `Generic workspace policy.
1614
1667
  AGINTI_EVIDENCE_SCOPE_JSON: {"mode":"task","request":"Write a deep research evidence review comparing three primary papers."}`;
1615
1668
  const scopedDeepResearchTools = selectProgressiveTools(allTools, {
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { machineRunPayload } from "../src/cli.js";
7
+ import { machineRunPayload, runResultExitCode } from "../src/cli.js";
8
8
  import { showProjectSession } from "../src/project.js";
9
9
 
10
10
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -36,6 +36,12 @@ const stoppedPayload = machineRunPayload({
36
36
  if (stoppedPayload.ok !== false || stoppedPayload.failed !== true || stoppedPayload.stopped !== true) {
37
37
  throw new Error(`stopped machine run was reported as success: ${JSON.stringify(stoppedPayload)}`);
38
38
  }
39
+ if (runResultExitCode({ stopped: true, reason: "tool_contract_violation" }) !== 1) {
40
+ throw new Error("a stopped non-JSON agent run still reports shell success");
41
+ }
42
+ if (runResultExitCode({ stopped: false, result: "done" }) !== 0) {
43
+ throw new Error("a successful agent run was assigned a failing shell exit status");
44
+ }
39
45
 
40
46
  async function runMachine(label, commandArgs, stdin = "") {
41
47
  const command = [cliPath, ...commandArgs];
@@ -36,6 +36,26 @@ function fakeStudentClient(json) {
36
36
  };
37
37
  }
38
38
 
39
+ const pageSafeReportContract = deriveScsTaskContract({
40
+ goal: [
41
+ "AGINTI_EVIDENCE_SCOPE_JSON: {\"mode\":\"task\",\"request\":\"Revise the exact existing research report at output/wechat_worker/task/report.md, write a complete Markdown report, and use page-safe tables. The host owns PDF compilation.\"}",
42
+ "Surrounding browser and research policy text is not part of the exact request.",
43
+ ].join("\n"),
44
+ taskProfile: "auto",
45
+ });
46
+ assert(
47
+ pageSafeReportContract.requiredEvidence.some((item) => item.category === "file"),
48
+ "a scoped existing-report edit did not require file evidence"
49
+ );
50
+ assert(
51
+ pageSafeReportContract.requiredEvidence.some((item) => item.category === "artifact"),
52
+ "a scoped existing-report edit did not require artifact evidence"
53
+ );
54
+ assert(
55
+ !pageSafeReportContract.requiredEvidence.some((item) => item.category === "browser"),
56
+ "the editorial phrase page-safe incorrectly required browser evidence"
57
+ );
58
+
39
59
  const noEvidenceProgress = {
40
60
  role: "student",
41
61
  decision: "reject_phase",
@@ -264,6 +284,27 @@ assert(
264
284
  "excluding output filenames also removed a real required text term"
265
285
  );
266
286
 
287
+ const wrappedManifestRepairContract = deriveScsTaskContract({
288
+ goal: [
289
+ "The only unresolved content work",
290
+ "is rebuilding the stale sources.json for the current report.",
291
+ "The verified claims are retained in",
292
+ "tmp/reliability-evidence-pass.md. Then write",
293
+ "sources.json immediately. Do not read any other file.",
294
+ ].join("\n"),
295
+ taskProfile: "research",
296
+ });
297
+ assert.deepEqual(
298
+ wrappedManifestRepairContract.exactOutputPaths,
299
+ ["sources.json"],
300
+ "an inflected, soft-wrapped output instruction did not classify its manifest as an exact output"
301
+ );
302
+ assert.deepEqual(
303
+ wrappedManifestRepairContract.exactInputPaths,
304
+ ["tmp/reliability-evidence-pass.md"],
305
+ "a mutable exact output leaked into exact inputs through a later negated read clause"
306
+ );
307
+
267
308
  const wordDocumentContract = deriveScsTaskContract({
268
309
  goal: "Create an editable DOCX and a phone-friendly PDF, then verify both outputs.",
269
310
  taskProfile: "word",
@@ -1018,7 +1018,7 @@ function compactRetainedToolPayload(toolName, payload = {}, args = {}) {
1018
1018
  return redactValue(result);
1019
1019
  }
1020
1020
 
1021
- function retainedPathMatchesOutput(sourcePath = "", outputPaths = []) {
1021
+ function retainedPathMatchesAny(sourcePath = "", candidatePaths = []) {
1022
1022
  const normalize = (value = "") =>
1023
1023
  String(value || "")
1024
1024
  .replace(/\\/g, "/")
@@ -1027,13 +1027,13 @@ function retainedPathMatchesOutput(sourcePath = "", outputPaths = []) {
1027
1027
  .replace(/\/$/, "");
1028
1028
  const source = normalize(sourcePath);
1029
1029
  if (!source) return false;
1030
- return outputPaths.some((candidate) => {
1030
+ return candidatePaths.some((candidate) => {
1031
1031
  const output = normalize(candidate);
1032
1032
  return output && (source === output || source.endsWith(`/${output}`) || output.endsWith(`/${source}`));
1033
1033
  });
1034
1034
  }
1035
1035
 
1036
- function retainedToolRecordPriority(record = {}, outputPaths = []) {
1036
+ function retainedToolRecordPriority(record = {}, outputPaths = [], inputPaths = []) {
1037
1037
  const name = String(record.name || "");
1038
1038
  const args = record.args || {};
1039
1039
  const payload = record.payload || {};
@@ -1048,12 +1048,13 @@ function retainedToolRecordPriority(record = {}, outputPaths = []) {
1048
1048
  const range = retainedReadRange(payload, args);
1049
1049
  priority = range.lineLimit > 0 ? 750 : 600;
1050
1050
  const sourcePath = String(payload.path || args.path || "").trim();
1051
- if (retainedPathMatchesOutput(sourcePath, outputPaths)) priority -= 220;
1051
+ if (retainedPathMatchesAny(sourcePath, inputPaths)) priority += 260;
1052
+ if (retainedPathMatchesAny(sourcePath, outputPaths)) priority -= 220;
1052
1053
  } else if (["search_files", "list_files"].includes(name)) priority = 400;
1053
1054
  return priority + Math.min(0.999, Math.max(0, Number(record.ordinal) || 0) / 100000);
1054
1055
  }
1055
1056
 
1056
- function retainedToolStateMessages(messages = [], limit = 12, outputPaths = []) {
1057
+ function retainedToolStateMessages(messages = [], limit = 12, outputPaths = [], inputPaths = []) {
1057
1058
  const callsById = new Map();
1058
1059
  const recordsByKey = new Map();
1059
1060
  let ordinal = 0;
@@ -1103,7 +1104,8 @@ function retainedToolStateMessages(messages = [], limit = 12, outputPaths = [])
1103
1104
  const selected = [...records]
1104
1105
  .sort(
1105
1106
  (left, right) =>
1106
- retainedToolRecordPriority(right, outputPaths) - retainedToolRecordPriority(left, outputPaths) ||
1107
+ retainedToolRecordPriority(right, outputPaths, inputPaths) -
1108
+ retainedToolRecordPriority(left, outputPaths, inputPaths) ||
1107
1109
  right.ordinal - left.ordinal
1108
1110
  )
1109
1111
  .slice(0, Math.max(1, Number(limit) || 12))
@@ -1135,8 +1137,8 @@ function retainedToolStateMessages(messages = [], limit = 12, outputPaths = [])
1135
1137
  });
1136
1138
  }
1137
1139
 
1138
- function retainedToolStateTextMessages(messages = [], limit = 12, outputPaths = []) {
1139
- const nativeMessages = retainedToolStateMessages(messages, limit, outputPaths);
1140
+ function retainedToolStateTextMessages(messages = [], limit = 12, outputPaths = [], inputPaths = []) {
1141
+ const nativeMessages = retainedToolStateMessages(messages, limit, outputPaths, inputPaths);
1140
1142
  const retained = [];
1141
1143
  for (let index = 0; index < nativeMessages.length; index += 2) {
1142
1144
  const assistantMessage = nativeMessages[index];
@@ -1156,13 +1158,17 @@ function retainedToolStateTextMessages(messages = [], limit = 12, outputPaths =
1156
1158
  return retained;
1157
1159
  }
1158
1160
 
1159
- function retainedToolPairPriority(pair = [], order = 0, outputPaths = []) {
1161
+ function retainedToolPairPriority(pair = [], order = 0, outputPaths = [], inputPaths = []) {
1160
1162
  const assistantCall = pair[0]?.tool_calls?.[0];
1161
1163
  const retained = pair.length === 1 ? parseRetainedToolEvidenceMessage(pair[0]) : null;
1162
1164
  const name = String(retained?.name || assistantCall?.function?.name || "");
1163
1165
  const args = retained?.args || safeParseToolContent(assistantCall?.function?.arguments) || {};
1164
1166
  const payload = retained?.payload || safeParseToolContent(pair[1]?.content) || {};
1165
- return retainedToolRecordPriority({ name, args, payload, ordinal: order }, outputPaths);
1167
+ return retainedToolRecordPriority(
1168
+ { name, args, payload, ordinal: order },
1169
+ outputPaths,
1170
+ inputPaths
1171
+ );
1166
1172
  }
1167
1173
 
1168
1174
  function isRuntimeCompactionRequest(content = "") {
@@ -1345,9 +1351,10 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
1345
1351
  // context instead of fabricating assistant reasoning.
1346
1352
  const deepSeekCompaction = normalizeProviderId(config.provider, "") === "deepseek";
1347
1353
  const exactOutputPaths = exactOutputPathsForState(state);
1354
+ const exactInputPaths = exactInputPathsForState(state);
1348
1355
  const retainedToolMessages = deepSeekCompaction
1349
- ? retainedToolStateTextMessages(messages, 12, exactOutputPaths)
1350
- : retainedToolStateMessages(messages, 12, exactOutputPaths);
1356
+ ? retainedToolStateTextMessages(messages, 12, exactOutputPaths, exactInputPaths)
1357
+ : retainedToolStateMessages(messages, 12, exactOutputPaths, exactInputPaths);
1351
1358
  const snapshotSummary = {
1352
1359
  step,
1353
1360
  maxSteps: config.maxSteps,
@@ -1425,7 +1432,7 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
1425
1432
  }));
1426
1433
  const boundedContent = compactTextForTokenBudget(
1427
1434
  compactedContent,
1428
- Math.max(1024, Math.floor(targetTokens * (retainedToolMessages.length ? 0.4 : 0.52))),
1435
+ Math.max(1024, Math.floor(targetTokens * (retainedToolMessages.length ? 0.28 : 0.52))),
1429
1436
  { headFraction: 0.58 }
1430
1437
  );
1431
1438
  const baseMessages = [
@@ -1441,7 +1448,12 @@ function buildCompactedRuntimeMessages(state, config, snapshot, step, options =
1441
1448
  retainedPairs.push({
1442
1449
  pair,
1443
1450
  order: retainedPairs.length,
1444
- priority: retainedToolPairPriority(pair, retainedPairs.length, exactOutputPaths),
1451
+ priority: retainedToolPairPriority(
1452
+ pair,
1453
+ retainedPairs.length,
1454
+ exactOutputPaths,
1455
+ exactInputPaths
1456
+ ),
1445
1457
  });
1446
1458
  }
1447
1459
  const selectedPairs = [];
@@ -4356,7 +4368,12 @@ export function recordProjectVerificationOutcome(state = {}, toolResult = {}, co
4356
4368
  // authorize a new invocation of the same command. A recognized trailing
4357
4369
  // status probe reports evidence but does not change the inner command's
4358
4370
  // mutation capability.
4359
- const commandPolicy = classifyCommand(mutationCommand);
4371
+ const commandPolicy = {
4372
+ ...classifyCommand(mutationCommand),
4373
+ ...(toolResult.commandPolicy && typeof toolResult.commandPolicy === "object"
4374
+ ? toolResult.commandPolicy
4375
+ : {}),
4376
+ };
4360
4377
  const requiredCommands = effectiveRequiredProjectCommands(state, verification, config);
4361
4378
  const requiredCommand = requiredCommands.find(
4362
4379
  (candidate) => projectCommandsEquivalent(candidate, exitProbe.command || command, config)
@@ -5335,16 +5352,31 @@ function expectedRepeatedObservationCommand(command = "") {
5335
5352
  );
5336
5353
  }
5337
5354
 
5355
+ function runCommandResultHasDurableProgress(toolResult = {}) {
5356
+ const policy = toolResult.commandPolicy || {};
5357
+ const policyAllowsMutation =
5358
+ policy.mayMutateProject === true ||
5359
+ (policy.mayMutateProject === undefined && policy.writesWorkspace === true);
5360
+ return Boolean(
5361
+ policyAllowsMutation ||
5362
+ policy.substantiveTest === true ||
5363
+ (Array.isArray(toolResult.verifiedGeneratedOutputPaths) &&
5364
+ toolResult.verifiedGeneratedOutputPaths.length > 0)
5365
+ );
5366
+ }
5367
+
5338
5368
  function isStaticDiscoveryToolResult(toolResult = {}) {
5339
5369
  if (isStaticDiscoveryToolCall(toolResult.toolName, toolResult.args || {})) return true;
5340
5370
  if (toolResult.toolName !== "run_command") return false;
5341
- if (toolResult.commandPolicy?.writesWorkspace !== false) return false;
5371
+ if (runCommandResultHasDurableProgress(toolResult)) return false;
5342
5372
  return !expectedRepeatedObservationCommand(toolResult.args?.command);
5343
5373
  }
5344
5374
 
5345
5375
  function successfulToolStateProgress(toolResult = {}) {
5346
5376
  if (!toolResult || toolResult.done || toolResult.ok === false || toolResult.blocked || toolResult.skipped) return false;
5347
- if (toolResult.toolName === "run_command") return toolResult.commandPolicy?.writesWorkspace === true;
5377
+ if (toolResult.toolName === "run_command") {
5378
+ return runCommandResultHasDurableProgress(toolResult);
5379
+ }
5348
5380
  if (["write_file", "apply_patch"].includes(String(toolResult.toolName || ""))) {
5349
5381
  return successfulProjectMutationPaths(toolResult).length > 0;
5350
5382
  }
@@ -5365,7 +5397,7 @@ function noProgressOutcomeFingerprint(toolResult = {}) {
5365
5397
  toolResult?.toolName !== "run_command" ||
5366
5398
  toolResult?.ok === false ||
5367
5399
  toolResult?.blocked ||
5368
- toolResult?.commandPolicy?.writesWorkspace === true ||
5400
+ successfulToolStateProgress(toolResult) ||
5369
5401
  expectedRepeatedObservationCommand(toolResult?.args?.command)
5370
5402
  ) {
5371
5403
  return "";
@@ -6179,6 +6211,16 @@ function commandWritesOnlyPrivateVerificationEvidence(command = "") {
6179
6211
  }
6180
6212
 
6181
6213
  function commandCanMutateProjectContent(command = "", commandPolicy = {}) {
6214
+ // The classifier may conservatively mark an interpreter or compound shell
6215
+ // command as workspace-writing while still proving that this exact command
6216
+ // cannot mutate project content. Preserve that stronger semantic result so
6217
+ // validators and inspection probes do not fabricate mutation progress. Git
6218
+ // sequences remain structurally inspected because an aggregate Git policy
6219
+ // can be conservative even when one segment changes the worktree.
6220
+ const category = String(commandPolicy.category || "");
6221
+ const requiresGitMutationInspection =
6222
+ ["git-workflow", "git-remote"].includes(category);
6223
+ if (commandPolicy.mayMutateProject === false && !requiresGitMutationInspection) return false;
6182
6224
  if (commandPolicy.writesWorkspace !== true && commandPolicy.mayMutateProject !== true) return false;
6183
6225
  const sequence = parseTopLevelShellSequence(String(command || ""));
6184
6226
  if (
@@ -6192,7 +6234,6 @@ function commandCanMutateProjectContent(command = "", commandPolicy = {}) {
6192
6234
  );
6193
6235
  }
6194
6236
  if (commandWritesOnlyPrivateVerificationEvidence(command)) return false;
6195
- const category = String(commandPolicy.category || "");
6196
6237
  if (!["git-workflow", "git-remote"].includes(category)) return true;
6197
6238
  if (/\bgit\s+clone\b/i.test(String(command || ""))) return true;
6198
6239
  // An aggregate Git category can still contain a non-Git build/generator
@@ -6700,6 +6741,18 @@ function exactOutputPathsForState(state = {}) {
6700
6741
  ])].slice(0, 32);
6701
6742
  }
6702
6743
 
6744
+ function exactInputPathsForState(state = {}) {
6745
+ const scsInputPaths = Array.isArray(state.meta?.scs?.taskContract?.exactInputPaths)
6746
+ ? state.meta.scs.taskContract.exactInputPaths.filter(Boolean)
6747
+ : [];
6748
+ const outputPaths = new Set(
6749
+ exactOutputPathsForState(state).map((item) => String(item).replace(/\\/g, "/").replace(/^\.\//, ""))
6750
+ );
6751
+ return [...new Set(scsInputPaths)]
6752
+ .filter((item) => !outputPaths.has(String(item).replace(/\\/g, "/").replace(/^\.\//, "")))
6753
+ .slice(0, 32);
6754
+ }
6755
+
6703
6756
  async function hashExactOutputFile(absolutePath) {
6704
6757
  return await new Promise((resolve, reject) => {
6705
6758
  const digest = crypto.createHash("sha256");
@@ -7314,7 +7367,11 @@ export function recordStaticDiscoveryProgress(toolLoop = {}, signature = "") {
7314
7367
  };
7315
7368
  }
7316
7369
 
7317
- export function resetStaticDiscoveryAfterContextLoss(state = {}, reason = "context-compaction") {
7370
+ export function resetStaticDiscoveryAfterContextLoss(
7371
+ state = {},
7372
+ reason = "context-compaction",
7373
+ options = {}
7374
+ ) {
7318
7375
  state.meta = state.meta || {};
7319
7376
  const toolLoop = state.meta.toolLoop && typeof state.meta.toolLoop === "object"
7320
7377
  ? state.meta.toolLoop
@@ -7323,6 +7380,16 @@ export function resetStaticDiscoveryAfterContextLoss(state = {}, reason = "conte
7323
7380
  const priorCounts = toolLoop.staticCounts && typeof toolLoop.staticCounts === "object"
7324
7381
  ? toolLoop.staticCounts
7325
7382
  : {};
7383
+ if (options.preserveStaticEvidence === true) {
7384
+ toolLoop.lastContextRecovery = {
7385
+ reason: String(reason || "context-compaction"),
7386
+ at: new Date().toISOString(),
7387
+ priorStaticTotal: Number(priorOrder.length),
7388
+ preservedStaticEvidence: true,
7389
+ };
7390
+ state.meta.toolLoop = toolLoop;
7391
+ return toolLoop.lastContextRecovery;
7392
+ }
7326
7393
  if (priorOrder.length || Object.keys(priorCounts).length) {
7327
7394
  const history = Array.isArray(toolLoop.staticHistory) ? toolLoop.staticHistory : [];
7328
7395
  history.push({
@@ -11043,7 +11110,9 @@ export async function runAgent(config) {
11043
11110
  const tokensAfter = estimateMessageTokens(compactMessages);
11044
11111
  if (charsAfter < contextDecision.charsBefore) {
11045
11112
  state.messages = compactMessages;
11046
- resetStaticDiscoveryAfterContextLoss(state, "proactive-context-compaction");
11113
+ resetStaticDiscoveryAfterContextLoss(state, "proactive-context-compaction", {
11114
+ preserveStaticEvidence: true,
11115
+ });
11047
11116
  state.meta.contextBudget = recordContextCompaction(contextBudget, {
11048
11117
  step,
11049
11118
  charsBefore: contextDecision.charsBefore,
@@ -11159,7 +11228,9 @@ export async function runAgent(config) {
11159
11228
  };
11160
11229
  state.messages = compactMessages;
11161
11230
  requestMessages = compactMessages;
11162
- resetStaticDiscoveryAfterContextLoss(state, "local-context-budget-retry");
11231
+ resetStaticDiscoveryAfterContextLoss(state, "local-context-budget-retry", {
11232
+ preserveStaticEvidence: true,
11233
+ });
11163
11234
  state.meta.localContextBudgetRetries = {
11164
11235
  ...contextRetriedSteps,
11165
11236
  [retryKey]: true,
@@ -11207,7 +11278,9 @@ export async function runAgent(config) {
11207
11278
  };
11208
11279
  state.messages = compactMessages;
11209
11280
  requestMessages = compactMessages;
11210
- resetStaticDiscoveryAfterContextLoss(state, "model-timeout-retry");
11281
+ resetStaticDiscoveryAfterContextLoss(state, "model-timeout-retry", {
11282
+ preserveStaticEvidence: true,
11283
+ });
11211
11284
  state.meta.modelTimeoutRetries = {
11212
11285
  ...retriedSteps,
11213
11286
  [retryKey]: true,
package/src/cli.js CHANGED
@@ -383,6 +383,10 @@ export function machineRunPayload(run, fallbackSessionId = "", metadata = {}) {
383
383
  };
384
384
  }
385
385
 
386
+ export function runResultExitCode(run = {}) {
387
+ return run?.stopped === true || run?.failed === true ? 1 : 0;
388
+ }
389
+
386
390
  function printMachineRunResult(run, fallbackSessionId = "", metadata = {}) {
387
391
  const payload = machineRunPayload(run, fallbackSessionId, metadata);
388
392
  console.log(JSON.stringify(payload));
@@ -2507,6 +2511,7 @@ export async function main(argv = process.argv.slice(2)) {
2507
2511
  ...(jsonFlag ? { onConsole: () => {} } : {}),
2508
2512
  });
2509
2513
  const run = await runAgent(config);
2514
+ if (runResultExitCode(run) !== 0) process.exitCode = 1;
2510
2515
  if (jsonFlag) {
2511
2516
  printMachineRunResult(run, runArgs.sessionId, {
2512
2517
  provider: config.provider,
@@ -2604,6 +2609,7 @@ export async function main(argv = process.argv.slice(2)) {
2604
2609
  config.expectedRuntimeRevision = preparedRuntime.expectedRuntimeRevision;
2605
2610
  if (preparedRuntime.runtimePatch) config.runtimePatch = preparedRuntime.runtimePatch;
2606
2611
  const run = await runAgent(config);
2612
+ if (runResultExitCode(run) !== 0) process.exitCode = 1;
2607
2613
  if (jsonFlag) {
2608
2614
  printMachineRunResult(run, sessionId, {
2609
2615
  provider: config.provider,
@@ -2765,5 +2771,6 @@ export async function main(argv = process.argv.slice(2)) {
2765
2771
  config.expectedRuntimeRevision = preparedRuntime.expectedRuntimeRevision;
2766
2772
  if (preparedRuntime.runtimePatch) config.runtimePatch = preparedRuntime.runtimePatch;
2767
2773
  }
2768
- await runAgent(config);
2774
+ const run = await runAgent(config);
2775
+ if (runResultExitCode(run) !== 0) process.exitCode = 1;
2769
2776
  }
@@ -89,18 +89,38 @@ export function hasLocalResearchWorkspaceIntent(goal = "", messages = []) {
89
89
  const text = `${scopedChatopsEvidenceGoal(goal)}\n${recent}`;
90
90
  return (
91
91
  /\b(?:this|current|existing|project|workspace|local)\s+(?:folder|directory|repo(?:sitory)?|files?|notes?|sources?|artifacts?)\b/i.test(text) ||
92
+ /(?:^|[\s`'"(])(?:~\/|\.{1,2}\/|\/)?(?:[^\s`'"()\/]+\/)+[^\s`'"()\/]+\.(?:bib|csv|docx?|json|md|pdf|tex|txt|ya?ml)\b/i.test(text) ||
93
+ /\b(?:edit|revise|rewrite|proofread|correct|polish|read|inspect|update)\b[^.\n;]{0,160}\b(?:exact|existing|current|saved|local)\b[^.\n;]{0,80}\b(?:document|file|manuscript|markdown|notes?|report|source)\b/i.test(text) ||
92
94
  /\b(?:task|project|source|research|evidence|notes?|manifest|readme)[-_A-Za-z0-9]*\.(?:md|json|ya?ml|txt|csv|bib|tex)\b/i.test(text) ||
93
95
  /\b(?:inspect|read|reconcile|correct|rewrite|update)\b.{0,120}\b(?:workspace|folder|directory|repo(?:sitory)?|local files?|project notes?|existing notes?)\b/i.test(text) ||
94
96
  /\b(?:git\s+)?commit\b/i.test(text)
95
97
  );
96
98
  }
97
99
 
100
+ export function hasExplicitDeepResearchSuppression(goal = "", messages = []) {
101
+ const current = currentIntentMessages(messages);
102
+ const latestUserIntent = [...current]
103
+ .reverse()
104
+ .find((message) => message?.role === "user" && !isRuntimeUserMessage(messageText(message.content)));
105
+ const text = `${scopedChatopsEvidenceGoal(goal)}\n${scopedChatopsEvidenceGoal(
106
+ messageText(latestUserIntent?.content)
107
+ )}`;
108
+ return (
109
+ /\b(?:do not|don't|must not|never)\s+(?:run|rerun|re-run|repeat|restart|invoke|call|start)\s+(?:the\s+)?(?:deep[_ -]?research|research workflow)\b/i.test(text) ||
110
+ /\b(?:do not|don't|must not|never)\b[^.!?\n]{0,140}\b(?:run|rerun|re-run|repeat|restart|invoke|call|start)\s+(?:the\s+)?(?:deep[_ -]?research|research workflow)\b/i.test(text) ||
111
+ /\b(?:reuse|use|continue from|recover from)\b.{0,140}\b(?:completed|existing|retained|saved)\b.{0,180}\b(?:deep[_ -]?research|research (?:result|artifact|evidence|pass))\b/i.test(text) ||
112
+ /(?:不要|无需|不必|禁止).{0,20}(?:重新|再次|重复)?(?:运行|调用|启动)?(?:深度研究|深入研究|deep[_ -]?research)/iu.test(text) ||
113
+ /(?:ディープリサーチ|深い調査).{0,20}(?:再実行しない|繰り返さない|呼び出さない)/u.test(text)
114
+ );
115
+ }
116
+
98
117
  export function toolWasRequested(messages = [], toolName = "") {
99
118
  return requestedToolCount(messages, toolName) > 0;
100
119
  }
101
120
 
102
121
  export function shouldStartWithDeepResearch(goal = "", messages = []) {
103
122
  if (!hasExplicitDeepResearchIntent(goal, messages)) return false;
123
+ if (hasExplicitDeepResearchSuppression(goal, messages)) return false;
104
124
  const current = currentIntentMessages(messages);
105
125
  if (toolWasRequested(current, "deep_research")) return false;
106
126
  if (hasLocalResearchWorkspaceIntent(goal, messages) && !localWorkspaceInspectionReady(current)) return false;
@@ -373,9 +373,9 @@ function inferExactOutputPaths(goal = "") {
373
373
  "gi"
374
374
  );
375
375
  const directOutputAction =
376
- /\b(save|write|rewrite|output|create|rebuild|replace|regenerate|generate|store|update|modify|edit)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/i;
376
+ /\b(?:sav(?:e|es|ing)|writ(?:e|es|ing)|rewrit(?:e|es|ing)|output(?:s|ting)?|creat(?:e|es|ing)|rebuild(?:s|ing)?|replac(?:e|es|ing)|regenerat(?:e|es|ing)|generat(?:e|es|ing)|stor(?:e|es|ing)|updat(?:e|es|ing)|modif(?:y|ies|ying)|edit(?:s|ing)?)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/i;
377
377
  const directOutputActionGlobal =
378
- /\b(save|write|rewrite|output|create|rebuild|replace|regenerate|generate|store|update|modify|edit)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/gi;
378
+ /\b(?:sav(?:e|es|ing)|writ(?:e|es|ing)|rewrit(?:e|es|ing)|output(?:s|ting)?|creat(?:e|es|ing)|rebuild(?:s|ing)?|replac(?:e|es|ing)|regenerat(?:e|es|ing)|generat(?:e|es|ing)|stor(?:e|es|ing)|updat(?:e|es|ing)|modif(?:y|ies|ying)|edit(?:s|ing)?)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/gi;
379
379
  const outputListHeader =
380
380
  /^(?:#+\s*)?(?:(?:required|final|expected|declared|target|pilot|deliverable)\s+)*(?:create|created files?|files? to create|outputs?|output structure|required outputs?|artifacts?|deliverables?|generated files?|writer requirements|renderer requirements|生成文件|输出结构|輸出結構|输出文件|輸出文件|创建文件|建立文件)(?:\s+(?:outputs?|artifacts?|deliverables?))?\s*[::]?\s*$/i;
381
381
  const nonOutputToolLine =
@@ -410,8 +410,15 @@ function inferExactOutputPaths(goal = "") {
410
410
  }
411
411
  return matches[0]?.index ?? -1;
412
412
  };
413
- for (const rawLine of lines) {
414
- const line = String(rawLine || "").trim();
413
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
414
+ const rawLine = lines[lineIndex];
415
+ const currentLine = String(rawLine || "").trim();
416
+ const previousLine = String(lines[lineIndex - 1] || "").trim();
417
+ const wrappedOutputInstruction =
418
+ previousLine &&
419
+ !/[.!?。!?;;]$/.test(previousLine) &&
420
+ directOutputAction.test(previousLine);
421
+ const line = wrappedOutputInstruction ? `${previousLine} ${currentLine}`.trim() : currentLine;
415
422
  if (!line) {
416
423
  if (inOutputList) inOutputList = false;
417
424
  continue;
@@ -468,16 +475,23 @@ function inferExactInputPaths(goal = "") {
468
475
  "gi"
469
476
  );
470
477
  const inputAction =
471
- /\b(use|using|read|load|fill|upload|attach|import|select|choose|reference|input|from|fix|repair|patch|correct)\b|使用|读取|讀取|加载|載入|填写|填入|上传|上傳|附加|导入|導入|选择|選擇|选取|選取|参考|參考|素材|图片|圖片|照片|提示词|提示詞|修复|修正|更正|从|從/i;
478
+ /\b(?:use|using|read|load|fill|upload|attach|import|select|choose|reference|input|from|retain(?:ed|ing)?|fix|repair|patch|correct)\b|使用|读取|讀取|加载|載入|填写|填入|上传|上傳|附加|导入|導入|选择|選擇|选取|選取|参考|參考|素材|图片|圖片|照片|提示词|提示詞|保留|修复|修正|更正|从|從/i;
472
479
  const directOutputAction =
473
- /\b(save|write|rewrite|output|create|rebuild|replace|regenerate|generate|store|update|modify|edit)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/i;
480
+ /\b(?:sav(?:e|es|ing)|writ(?:e|es|ing)|rewrit(?:e|es|ing)|output(?:s|ting)?|creat(?:e|es|ing)|rebuild(?:s|ing)?|replac(?:e|es|ing)|regenerat(?:e|es|ing)|generat(?:e|es|ing)|stor(?:e|es|ing)|updat(?:e|es|ing)|modif(?:y|ies|ying)|edit(?:s|ing)?)\b|保存|写入|寫入|重写|重寫|输出|輸出|创建|建立|重建|替换|替換|重新生成|生成|更新|修改|编辑|編輯/i;
474
481
  const pushPath = (raw = "") => {
475
482
  const cleaned = String(raw || "").trim();
476
483
  if (!cleaned || /[{}]/.test(cleaned)) return;
477
484
  paths.push(cleaned);
478
485
  };
479
- for (const rawLine of lines) {
480
- const fullLine = String(rawLine || "").trim();
486
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
487
+ const rawLine = lines[lineIndex];
488
+ const currentLine = String(rawLine || "").trim();
489
+ const previousLine = String(lines[lineIndex - 1] || "").trim();
490
+ const wrappedInputInstruction =
491
+ previousLine &&
492
+ !/[.!?。!?;;]$/.test(previousLine) &&
493
+ inputAction.test(previousLine);
494
+ const fullLine = wrappedInputInstruction ? `${previousLine} ${currentLine}`.trim() : currentLine;
481
495
  const outputIndex = fullLine.search(directOutputAction);
482
496
  const line = outputIndex > 0 ? fullLine.slice(0, outputIndex).trim() : fullLine;
483
497
  if (!line || !inputAction.test(line)) continue;
@@ -779,7 +793,17 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
779
793
  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)) {
780
794
  categories.add("artifact");
781
795
  }
782
- if (textHas(mandatoryEvidenceText, /\b(browser|chrome|chromium|cdp|devtools|playwright|selenium|web[- ]?ui|website|page|tab|composer|click|type|upload|attach|submit|form)\b/) || /浏览器|网页|页面|上传|提交|附件|资产库/.test(mandatoryEvidenceText)) {
796
+ if (
797
+ textHas(
798
+ mandatoryEvidenceText,
799
+ /\b(browser|chrome|chromium|cdp|devtools|playwright|selenium|web[- ]?(?:ui|page)|website|tab|composer|click|type|upload|attach|submit|form)\b/
800
+ ) ||
801
+ textHas(
802
+ mandatoryEvidenceText,
803
+ /\b(?:browse|navigate|open|refresh|visit)\b[^.\n;]{0,60}\b(?:page|site)\b|\b(?:page|site)\b[^.\n;]{0,60}\b(?:click|open|submit|upload)\b/
804
+ ) ||
805
+ /浏览器|网页|页面|上传|提交|附件|资产库/.test(mandatoryEvidenceText)
806
+ ) {
783
807
  categories.add("browser");
784
808
  }
785
809
  if (textHas(mandatoryEvidenceText, /\b(screenshot|visible|visual|see|inspect image|open image|read_image|thumbnail)\b/) || /截图|可见|缩略图/.test(mandatoryEvidenceText)) {
@@ -208,7 +208,25 @@ export function staticToolCallSignature(toolName, args = {}, context = {}) {
208
208
 
209
209
  function isStaticDiscoveryResult(result = {}) {
210
210
  if (!result || result.ok === false || result.blocked || result.done) return false;
211
- return isStaticDiscoveryToolCall(result.toolName, result.args || {});
211
+ if (isStaticDiscoveryToolCall(result.toolName, result.args || {})) return true;
212
+ if (result.toolName !== "run_command") return false;
213
+ if (/\b(?:watch|poll|status|queue|sleep)\b|tail\s+-f|tmux\s+capture-pane/i.test(String(result.args?.command || ""))) {
214
+ return false;
215
+ }
216
+ return !runCommandHasConcreteProgress(result);
217
+ }
218
+
219
+ function runCommandHasConcreteProgress(result = {}) {
220
+ const policy = result.commandPolicy || {};
221
+ const policyAllowsMutation =
222
+ policy.mayMutateProject === true ||
223
+ (policy.mayMutateProject === undefined && policy.writesWorkspace === true);
224
+ return Boolean(
225
+ policyAllowsMutation ||
226
+ policy.substantiveTest === true ||
227
+ (Array.isArray(result.verifiedGeneratedOutputPaths) &&
228
+ result.verifiedGeneratedOutputPaths.length > 0)
229
+ );
212
230
  }
213
231
 
214
232
  export function summarizeRepeatedStaticDiscovery(recentToolResults = [], context = {}) {
@@ -251,7 +269,7 @@ function hasConcreteProgress(recentToolResults = [], events = []) {
251
269
  if (result.ok === false || result.blocked || result.done) return false;
252
270
  if (isStaticDiscoveryToolCall(result.toolName, result.args || {})) return false;
253
271
  if (!PROGRESS_TOOL_NAMES.has(result.toolName)) return false;
254
- if (result.toolName === "run_command") return Boolean(result.stdout || result.stderr || result.exitCode === 0);
272
+ if (result.toolName === "run_command") return runCommandHasConcreteProgress(result);
255
273
  return Boolean(
256
274
  result.path ||
257
275
  result.artifactPath ||