neoagent 2.5.2-beta.16 → 2.5.2-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.5.2-beta.16",
3
+ "version": "2.5.2-beta.18",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 5acb38c4eeee836df0a631d5ed3f863e
1
+ f7852ae0fc3e8f369c66383642692e12
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1920445875" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1291084377" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -134794,7 +134794,7 @@ r===$&&A.b()
134794
134794
  p.push(A.jP(q,A.j9(!1,new A.a_(B.uG,A.d8(new A.cA(B.jt,new A.a7N(r,q),q),q,q),q),!1,B.H,!0),q,q,0,0,0,q))}r=!1
134795
134795
  if(!s.ay)if(!s.ch){r=s.e
134796
134796
  r===$&&A.b()
134797
- r=B.b.u("mqgdktti-6bc08d8").length!==0&&r.b}if(r){r=s.d
134797
+ r=B.b.u("mqgevy6k-3f81df2").length!==0&&r.b}if(r){r=s.d
134798
134798
  r===$&&A.b()
134799
134799
  r=r.aP&&!r.ai?84:0
134800
134800
  s=s.e
@@ -140506,7 +140506,7 @@ $S:0}
140506
140506
  A.a_6.prototype={}
140507
140507
  A.SQ.prototype={
140508
140508
  nb(a){var s=this
140509
- if(B.b.u("mqgdktti-6bc08d8").length===0||s.a!=null)return
140509
+ if(B.b.u("mqgevy6k-3f81df2").length===0||s.a!=null)return
140510
140510
  s.AU()
140511
140511
  s.a=A.on(B.RH,new A.bc8(s))},
140512
140512
  AU(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -140524,7 +140524,7 @@ if(!t.f.b(k)){s=1
140524
140524
  break}i=J.a3(k,"buildId")
140525
140525
  h=i==null?null:B.b.u(J.p(i))
140526
140526
  j=h==null?"":h
140527
- if(J.bi(j)===0||J.d(j,"mqgdktti-6bc08d8")){s=1
140527
+ if(J.bi(j)===0||J.d(j,"mqgevy6k-3f81df2")){s=1
140528
140528
  break}n.b=!0
140529
140529
  n.F()
140530
140530
  p=2
@@ -140541,7 +140541,7 @@ case 2:return A.i(o.at(-1),r)}})
140541
140541
  return A.k($async$AU,r)},
140542
140542
  vE(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
140543
140543
  var $async$vE=A.h(function(a2,a3){if(a2===1){o.push(a3)
140544
- s=p}for(;;)switch(s){case 0:if(B.b.u("mqgdktti-6bc08d8").length===0||n.c){s=1
140544
+ s=p}for(;;)switch(s){case 0:if(B.b.u("mqgevy6k-3f81df2").length===0||n.c){s=1
140545
140545
  break}n.c=!0
140546
140546
  n.F()
140547
140547
  p=4
@@ -86,6 +86,25 @@ const CANDIDATE_KEYS = [
86
86
  'downloadUris',
87
87
  ];
88
88
 
89
+ const GENERIC_CANDIDATE_KEYS = new Set([
90
+ 'path',
91
+ 'paths',
92
+ 'file',
93
+ 'files',
94
+ 'filePath',
95
+ 'filePaths',
96
+ 'fullPath',
97
+ 'fullPaths',
98
+ 'downloadUrl',
99
+ 'downloadUrls',
100
+ 'downloadUri',
101
+ 'downloadUris',
102
+ ]);
103
+
104
+ const EXPLICIT_CANDIDATE_KEYS = new Set(
105
+ CANDIDATE_KEYS.filter((key) => !GENERIC_CANDIDATE_KEYS.has(key))
106
+ );
107
+
89
108
  const ARTIFACT_CONTAINER_KEYS = new Set([
90
109
  'artifact',
91
110
  'artifacts',
@@ -105,8 +124,22 @@ const ARTIFACT_CONTAINER_KEYS = new Set([
105
124
 
106
125
  const CONTAINER_URL_KEYS = new Set(['url', 'urls', 'uri', 'uris', 'href', 'hrefs']);
107
126
 
108
- function isExplicitCandidateKey(keyHint = '', parentKeyHint = '') {
109
- if (CANDIDATE_KEYS.includes(keyHint)) return true;
127
+ const EVIDENCE_RESULT_TOOLS = /^(execute_command|github_|list_|search_|read_|get_|find_|http_request|web_search|browser_get|browser_read|code_navigate|query_structured_data|memory_|session_search|recordings_|read_health_data)/;
128
+
129
+ function allowsGenericCandidateKeys(toolName = '') {
130
+ return !EVIDENCE_RESULT_TOOLS.test(String(toolName || ''));
131
+ }
132
+
133
+ function isExplicitCandidateKey(keyHint = '', parentKeyHint = '', options = {}) {
134
+ if (EXPLICIT_CANDIDATE_KEYS.has(keyHint)) return true;
135
+ if (
136
+ ARTIFACT_CONTAINER_KEYS.has(parentKeyHint)
137
+ && CANDIDATE_KEYS.includes(keyHint)
138
+ && (!GENERIC_CANDIDATE_KEYS.has(parentKeyHint) || options.allowGenericKeys === true)
139
+ ) {
140
+ return true;
141
+ }
142
+ if (GENERIC_CANDIDATE_KEYS.has(keyHint)) return options.allowGenericKeys === true;
110
143
  if (!CONTAINER_URL_KEYS.has(keyHint)) return false;
111
144
  return ARTIFACT_CONTAINER_KEYS.has(parentKeyHint);
112
145
  }
@@ -198,6 +231,7 @@ async function extractArtifactsFromResult(toolName, result) {
198
231
  const seen = new Set();
199
232
  const seenCandidates = new Set();
200
233
  const fallbackKind = inferArtifactKind(toolName, 'artifact');
234
+ const allowGenericKeys = allowsGenericCandidateKeys(toolName);
201
235
 
202
236
  async function pushCandidate(candidate) {
203
237
  const candidateKey = String(candidate || '').trim();
@@ -214,7 +248,7 @@ async function extractArtifactsFromResult(toolName, result) {
214
248
  async function visit(value, keyHint = '', parentKeyHint = '') {
215
249
  if (value == null) return;
216
250
  if (typeof value === 'string') {
217
- const explicit = isExplicitCandidateKey(keyHint, parentKeyHint);
251
+ const explicit = isExplicitCandidateKey(keyHint, parentKeyHint, { allowGenericKeys });
218
252
  if (explicit) {
219
253
  if (normalizePathOrUri(value)) await pushCandidate(value);
220
254
  return;
@@ -240,6 +274,7 @@ async function extractArtifactsFromResult(toolName, result) {
240
274
  }
241
275
 
242
276
  module.exports = {
277
+ allowsGenericCandidateKeys,
243
278
  extractArtifactsFromResult,
244
279
  inferArtifactKind,
245
280
  inferMimeType,
@@ -125,6 +125,9 @@ const {
125
125
  getAvailableTools: getAvailableToolsImpl,
126
126
  isReadOnlyToolCall: isReadOnlyToolCallImpl,
127
127
  } = require('./tool_dispatch');
128
+ const {
129
+ isProgressToolCall,
130
+ } = require('./progress_classification');
128
131
  const {
129
132
  normalizeOutgoingMessage,
130
133
  clampRunContext,
@@ -225,8 +228,17 @@ function buildErrorPatternGuidance(key, count) {
225
228
 
226
229
  const OUTPUT_FINGERPRINT_TOOLS = /^(list_|search_|read_|get_|find_|github_list|github_get|github_search)/;
227
230
 
228
- function fingerprintOutput(toolName, result) {
229
- if (!toolName || !OUTPUT_FINGERPRINT_TOOLS.test(toolName)) return null;
231
+ function fingerprintOutput(toolName, result, toolArgs = {}) {
232
+ const name = String(toolName || '');
233
+ if (
234
+ !name
235
+ || (
236
+ !OUTPUT_FINGERPRINT_TOOLS.test(name)
237
+ && !(name === 'execute_command' && !isProgressToolCall(name, toolArgs))
238
+ )
239
+ ) {
240
+ return null;
241
+ }
230
242
  const raw = typeof result === 'string' ? result : JSON.stringify(result ?? '');
231
243
  if (raw.length < 200) return null;
232
244
  // djb2 hash over first 3000 chars — fast, collision-unlikely for our sizes
@@ -236,18 +248,6 @@ function fingerprintOutput(toolName, result) {
236
248
  return h >>> 0;
237
249
  }
238
250
 
239
- // Tools that represent concrete forward progress (write, create, send, update, run).
240
- // Anything NOT in this set is considered read-only for the analysis-paralysis gate.
241
- // execute_command counts as progress — it can do anything, including modify state.
242
- function isProgressTool(toolName) {
243
- if (!toolName) return false;
244
- // Neutral / bookkeeping — don't count either way
245
- if (toolName === 'activate_tools' || toolName === 'save_widget_snapshot') return false;
246
- // Explicitly read-only patterns
247
- if (/^(list_|search_|read_file|get_file|find_files?|github_list|github_get|github_search|browser_get|browser_read)/.test(toolName)) return false;
248
- return true;
249
- }
250
-
251
251
  function cloneInterimHistory(history = []) {
252
252
  if (!Array.isArray(history)) return [];
253
253
  return history.map((item) => ({
@@ -564,9 +564,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
564
564
 
565
565
  const runTitle = generateTitle(userMessage);
566
566
  const initialRunMetadata = buildInitialRunMetadata(options);
567
- db.prepare(`INSERT OR REPLACE INTO agent_runs(
567
+ db.prepare(`INSERT INTO agent_runs(
568
568
  id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
569
- ) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(
569
+ ) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)
570
+ ON CONFLICT(id) DO UPDATE SET
571
+ status = 'running',
572
+ model = excluded.model,
573
+ updated_at = datetime('now'),
574
+ completed_at = NULL,
575
+ error = NULL,
576
+ metadata_json = COALESCE(agent_runs.metadata_json, excluded.metadata_json)`).run(
570
577
  runId,
571
578
  userId,
572
579
  agentId,
@@ -762,7 +769,14 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
762
769
  let iteration = 0;
763
770
  let totalTokens = 0;
764
771
  let lastContent = '';
765
- let stepIndex = 0;
772
+ let stepIndex = Number(options.messagingRetryStepOffset || 0);
773
+ if (!Number.isFinite(stepIndex) || stepIndex < 0) {
774
+ stepIndex = 0;
775
+ }
776
+ if (options.messagingAutonomousRetryCount > 0) {
777
+ const existingStep = db.prepare('SELECT COALESCE(MAX(step_index), 0) AS maxStep FROM agent_steps WHERE run_id = ?').get(runId);
778
+ stepIndex = Math.max(stepIndex, Number(existingStep?.maxStep || 0));
779
+ }
766
780
  let failedStepCount = 0;
767
781
  let modelFailureRecoveries = 0;
768
782
  let promptMetrics = {};
@@ -1070,7 +1084,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1070
1084
  const urgency = readOnlyCount >= 6 ? 'CRITICAL' : 'ACTION REQUIRED';
1071
1085
  messages.push({
1072
1086
  role: 'system',
1073
- content: `${urgency} — ${readOnlyCount} consecutive read-only turns: You have been gathering information for ${readOnlyCount} turns without writing, creating, sending, or running anything. You must take ONE concrete action this turn (create a file, open a PR, run a command that modifies state, send a message) or call task_complete to report what you found and why you cannot proceed. Do not read or list anything further.`,
1087
+ content: `${urgency} — ${readOnlyCount} consecutive read-only turns: You have been gathering information for ${readOnlyCount} turns without writing, creating, sending, or running anything. Switch method now: establish or reuse a writable checkout, create a task branch, edit files, run verification, open/update a PR, send a concrete progress update, or call task_complete with the real blocker. Do not do more remote tree/list/content scraping first.`,
1074
1088
  });
1075
1089
  }
1076
1090
  }
@@ -1718,7 +1732,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1718
1732
  // Output fingerprint guard: steer away from re-fetching data already seen.
1719
1733
  if (!toolErrorMessage) {
1720
1734
  const currentRunMeta = engine.getRunMeta(runId);
1721
- const fp = fingerprintOutput(toolName, toolResult);
1735
+ const fp = fingerprintOutput(toolName, toolResult, toolArgs);
1722
1736
  if (fp !== null && currentRunMeta?.seenOutputHashes) {
1723
1737
  const prior = currentRunMeta.seenOutputHashes.get(fp);
1724
1738
  if (prior) {
@@ -1728,24 +1742,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1728
1742
  });
1729
1743
  } else {
1730
1744
  currentRunMeta.seenOutputHashes.set(fp, { toolName, iteration });
1731
- // External state: persist large read results to disk so the
1732
- // model can reference them after context compaction without
1733
- // re-fetching. Only for significant payloads.
1734
- const persistRaw = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult ?? '');
1735
- if (persistRaw.length >= 1000 && runId) {
1736
- const persistPath = `/tmp/run-${runId.slice(0, 8)}-${toolName}.json`;
1737
- try {
1738
- require('fs').writeFileSync(persistPath, persistRaw.slice(0, 40000));
1739
- if (!currentRunMeta.persistedDataPaths) currentRunMeta.persistedDataPaths = [];
1740
- if (!currentRunMeta.persistedDataPaths.includes(persistPath)) {
1741
- currentRunMeta.persistedDataPaths.push(persistPath);
1742
- messages.push({
1743
- role: 'system',
1744
- content: `Data from "${toolName}" (iteration ${iteration}) persisted to ${persistPath}. If context compacts and you need this data again, use execute_command with \`cat ${persistPath}\` instead of re-fetching.`,
1745
- });
1746
- }
1747
- } catch { /* non-fatal — disk full or permissions */ }
1748
- }
1749
1745
  }
1750
1746
  }
1751
1747
  }
@@ -1816,7 +1812,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
1816
1812
  && (analysis.mode === 'execute' || analysis.mode === 'plan_execute')) {
1817
1813
  const iterMeta = engine.getRunMeta(runId);
1818
1814
  if (iterMeta) {
1819
- const calledProgress = response.toolCalls.some((tc) => isProgressTool(tc.function?.name || ''));
1815
+ const calledProgress = response.toolCalls.some((tc) => {
1816
+ let parsedArgs = {};
1817
+ try { parsedArgs = JSON.parse(tc.function?.arguments || '{}'); } catch {}
1818
+ return isProgressToolCall(tc.function?.name || '', parsedArgs);
1819
+ });
1820
1820
  iterMeta.consecutiveReadOnlyIterations = calledProgress
1821
1821
  ? 0
1822
1822
  : (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
@@ -2231,6 +2231,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
2231
2231
 
2232
2232
  const retryOptions = {
2233
2233
  ...options,
2234
+ runId,
2235
+ messagingRetryStepOffset: stepIndex,
2234
2236
  messagingAutonomousRetryCount: retryCount + 1,
2235
2237
  messagingRetryState: {
2236
2238
  lastFinalMessage: String(runMeta?.lastSentMessage || options?.messagingRetryState?.lastFinalMessage || '').trim(),
@@ -2257,7 +2259,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
2257
2259
  ].filter(Boolean).join('\n\n')
2258
2260
  }
2259
2261
  };
2260
- delete retryOptions.runId;
2261
2262
 
2262
2263
  return engine.runWithModel(userId, userMessage, retryOptions, _modelOverride);
2263
2264
  }
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const READ_ONLY_COMMANDS = new Set([
4
+ 'awk',
5
+ 'cat',
6
+ 'curl',
7
+ 'diff',
8
+ 'du',
9
+ 'egrep',
10
+ 'env',
11
+ 'fgrep',
12
+ 'find',
13
+ 'git',
14
+ 'grep',
15
+ 'head',
16
+ 'jq',
17
+ 'less',
18
+ 'ls',
19
+ 'pwd',
20
+ 'rg',
21
+ 'sed',
22
+ 'sort',
23
+ 'tail',
24
+ 'tee',
25
+ 'test',
26
+ 'tr',
27
+ 'tree',
28
+ 'wc',
29
+ 'which',
30
+ ]);
31
+
32
+ const GIT_READ_ONLY_SUBCOMMANDS = new Set([
33
+ 'branch',
34
+ 'diff',
35
+ 'grep',
36
+ 'log',
37
+ 'ls-files',
38
+ 'ls-remote',
39
+ 'rev-parse',
40
+ 'show',
41
+ 'status',
42
+ ]);
43
+
44
+ const STATE_CHANGING_COMMANDS = new Set([
45
+ 'apply_patch',
46
+ 'chmod',
47
+ 'chown',
48
+ 'cp',
49
+ 'git-clone',
50
+ 'git-commit',
51
+ 'git-push',
52
+ 'git-switch',
53
+ 'git-checkout',
54
+ 'git-merge',
55
+ 'git-rebase',
56
+ 'install',
57
+ 'mkdir',
58
+ 'mv',
59
+ 'npm',
60
+ 'pnpm',
61
+ 'rm',
62
+ 'rmdir',
63
+ 'touch',
64
+ 'yarn',
65
+ ]);
66
+
67
+ function stripShellNoise(command = '') {
68
+ return String(command || '')
69
+ .replace(/(^|\n)\s*#.*(?=\n|$)/g, '\n')
70
+ .replace(/\s+/g, ' ')
71
+ .trim();
72
+ }
73
+
74
+ function firstToken(segment = '') {
75
+ const match = String(segment || '').trim().match(/^([A-Za-z0-9_./-]+)/);
76
+ return match ? match[1] : '';
77
+ }
78
+
79
+ function normalizeCommandName(token = '') {
80
+ return String(token || '').trim().split('/').pop().toLowerCase();
81
+ }
82
+
83
+ function splitCommandSegments(command = '') {
84
+ return stripShellNoise(command)
85
+ .split(/\s*(?:&&|\|\||;|\||\n)\s*/g)
86
+ .map((segment) => segment.trim())
87
+ .filter(Boolean);
88
+ }
89
+
90
+ function stripEnvAssignments(segment = '') {
91
+ let text = String(segment || '').trim();
92
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(text)) {
93
+ text = text.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s*/, '').trim();
94
+ }
95
+ return text;
96
+ }
97
+
98
+ function gitSubcommand(segment = '') {
99
+ const parts = stripEnvAssignments(segment).split(/\s+/).filter(Boolean);
100
+ if (normalizeCommandName(parts[0]) !== 'git') return '';
101
+ return String(parts[1] || '').toLowerCase();
102
+ }
103
+
104
+ function isReadOnlyGitCommand(segment = '') {
105
+ const subcommand = gitSubcommand(segment);
106
+ if (!subcommand) return false;
107
+ return GIT_READ_ONLY_SUBCOMMANDS.has(subcommand);
108
+ }
109
+
110
+ function isReadOnlyInterpreterCommand(segment = '') {
111
+ const normalized = stripEnvAssignments(segment);
112
+ const commandName = normalizeCommandName(firstToken(normalized));
113
+ if (!['node', 'perl', 'python', 'python3'].includes(commandName)) return false;
114
+ if (/\b(open|write|writefile|appendfile|unlink|rename|mkdir|rmdir|remove|rm|spawn|exec)\b/i.test(normalized)) {
115
+ return false;
116
+ }
117
+ return /\b(print|json\.|json_tool|json\.load|json\.loads|sys\.stdin|process\.exit|console\.log)\b|-m\s+json\.tool/i.test(normalized);
118
+ }
119
+
120
+ function isStateChangingShellSegment(segment = '') {
121
+ const normalized = stripEnvAssignments(segment);
122
+ const command = normalizeCommandName(firstToken(normalized));
123
+ if (!command) return false;
124
+ if (command === 'git') {
125
+ const subcommand = gitSubcommand(normalized);
126
+ return subcommand && !GIT_READ_ONLY_SUBCOMMANDS.has(subcommand);
127
+ }
128
+ return STATE_CHANGING_COMMANDS.has(command);
129
+ }
130
+
131
+ function isClearlyReadOnlyShellCommand(command = '') {
132
+ const segments = splitCommandSegments(command);
133
+ if (segments.length === 0) return false;
134
+ return segments.every((segment) => {
135
+ const normalized = stripEnvAssignments(segment);
136
+ if (isStateChangingShellSegment(normalized)) return false;
137
+ if (isReadOnlyGitCommand(normalized)) return true;
138
+ if (isReadOnlyInterpreterCommand(normalized)) return true;
139
+ const commandName = normalizeCommandName(firstToken(normalized));
140
+ if (!commandName) return false;
141
+ return READ_ONLY_COMMANDS.has(commandName);
142
+ });
143
+ }
144
+
145
+ function isProgressToolCall(toolName, toolArgs = {}) {
146
+ const name = String(toolName || '');
147
+ if (!name) return false;
148
+ if (name === 'activate_tools' || name === 'save_widget_snapshot') return false;
149
+ if (/^(list_|search_|read_file|get_file|find_files?|github_list|github_get|github_search|browser_get|browser_read)/.test(name)) {
150
+ return false;
151
+ }
152
+ if (name === 'http_request') {
153
+ return String(toolArgs?.method || 'GET').toUpperCase() !== 'GET';
154
+ }
155
+ if (name === 'execute_command') {
156
+ return !isClearlyReadOnlyShellCommand(toolArgs?.command || '');
157
+ }
158
+ return true;
159
+ }
160
+
161
+ module.exports = {
162
+ isClearlyReadOnlyShellCommand,
163
+ isProgressToolCall,
164
+ };
@@ -60,11 +60,12 @@ function evaluateProgressLiveness(runMeta, now = Date.now()) {
60
60
 
61
61
  function buildProgressNudge({ stalled = false } = {}) {
62
62
  return [
63
- 'Internal progress check for the active messaging run.',
63
+ 'Mandatory internal progress check for the active messaging run.',
64
64
  stalled
65
65
  ? 'No verified progress has been recorded for the stall threshold.'
66
66
  : 'The originating chat has not received a user-visible update for the progress threshold.',
67
- 'On the next normal agent turn, decide whether to continue silently, send a concise model-authored interim update with send_interim_update, report a real blocker, or finish with the final answer.',
67
+ 'Before starting more tool work, either send a concise model-authored interim update with send_interim_update, report a real blocker, or finish with the final answer.',
68
+ 'Do not continue silently once this check is present unless the immediate next action itself delivers a final answer or explicit no-response decision.',
68
69
  'Do not repeat previous status text and do not treat an interim update as final delivery.',
69
70
  ].join(' ');
70
71
  }
@@ -81,6 +81,7 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
81
81
  ];
82
82
  const EXECUTION_GUIDANCE_ACTION_LINES = [
83
83
  'Act end-to-end. Run independent searches or inspections in parallel when possible. Prefer native integration tools and structured APIs over browser automation or shell scraping. Use exact IDs and required parameters; list or search first when you do not have them.',
84
+ 'For GitHub issue implementation or PR work, fetch the issue once, then establish or reuse a writable local checkout, create a task branch, inspect/edit/test locally, and push/open the PR. Use direct GitHub file mutation tools only as a fallback when a local checkout is unavailable.',
84
85
  'Use send_interim_update sparingly when a short real update or question would help.',
85
86
  'When you must ask for missing required user input, ask once, then wait for the reply instead of re-asking in the same run.',
86
87
  'For outbound messages, calls, emails, shared edits, installs, restarts, or task mutations, verify the action result before claiming it happened. If user confirmation is required and missing, draft or ask instead of sending.',
@@ -8,6 +8,9 @@
8
8
  const { compactToolResult } = require('./toolResult');
9
9
  const { summarizeForLog } = require('./logFormat');
10
10
  const { normalizeOutgoingMessage, clampRunContext } = require('./messagingFallback');
11
+ const {
12
+ isClearlyReadOnlyShellCommand,
13
+ } = require('./loop/progress_classification');
11
14
 
12
15
  // Ordered classification rules mapping a tool name to its evidence "source"
13
16
  // bucket. First matching rule wins, so order is significant. Declared as data
@@ -83,7 +86,11 @@ function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '
83
86
 
84
87
  const evidenceRelevant = evidenceRelevantExact.has(name)
85
88
  || evidenceRelevantPrefixes.some((prefix) => name.startsWith(prefix));
86
- const stateChanged = stateChangingExact.has(name)
89
+ const stateChanged = (
90
+ name === 'execute_command'
91
+ ? !isClearlyReadOnlyShellCommand(toolArgs?.command || '')
92
+ : stateChangingExact.has(name)
93
+ )
87
94
  || name.startsWith('android_')
88
95
  || ['browser_click', 'browser_type', 'browser_evaluate'].includes(name);
89
96