mixdog 0.9.120 → 0.9.121

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": "mixdog",
3
- "version": "0.9.120",
3
+ "version": "0.9.121",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -19,7 +19,8 @@
19
19
  project-relative paths and omit optional scopes equal to its root; explicit
20
20
  paths may be outside cwd only for targets outside the project.
21
21
  - Plan the fewest dependent rounds, then the fewest calls. Known state —
22
- anything the task supplied, a tool returned, or a check already proved —
22
+ anything the task supplied, a tool returned (applied patches and envelope
23
+ hints included), or a check already proved —
23
24
  is never re-found, re-derived, or re-verified; a change to its subject
24
25
  re-opens it. Batch calls iff none needs
25
26
  another's output or can change another's inputs/state; otherwise
@@ -37,16 +38,23 @@
37
38
  covers only what returned spans cannot, as an anchored offset/limit
38
39
  window. A conclusive result ends its facet; evidence that determines the
39
40
  answer, edit, or deliverable ends retrieval — patch if needed.
41
+ - Inspect read-only; writable handles, repair, or cleanup only when the
42
+ deliverable requires them, copying what they could destroy. Never clear
43
+ an obstacle or unexpected state by mutating it; unrecoverably lost
44
+ evidence ends its search — report best effort.
40
45
  - Once the edit or deliverable is determined, finish in one assistant turn:
41
46
  before `apply_patch`, obtain every target hunk's exact current content and
42
- anchor from `grep`, `code_graph`, or `read`; never infer patch context from
47
+ anchor from `grep`, `code_graph`, `read`, or a prior successful patch
48
+ envelope; never infer patch context from
43
49
  another file, a sample, or expected text. Then issue `apply_patch` calls
44
50
  serially, never in parallel; use one cohesive call with one file section per
45
51
  target, all patches first, then their one batched verification `shell` in
46
52
  the same turn — the runtime runs it after every patch and only if all
47
53
  succeeded, so verification never needs its own turn. It runs the real
48
54
  required postconditions on every changed file and produced artifact,
49
- never echoing a claim; a postcondition that did not actually run is
55
+ never echoing a claim; changes made through `shell` verify under the same
56
+ one-batch contract — one script proving every postcondition, value-level
57
+ included, never one check per round; a postcondition that did not actually run is
50
58
  unresolved, not passed. Retry only failed envelopes; rerun a failed check only
51
59
  after a change that can alter its outcome — commands alike; else switch
52
60
  route or report it unresolved.
@@ -1,6 +1,7 @@
1
1
  import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
2
- import { unlinkSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
2
+ import { accessSync, constants as fsConstants, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { constants as osConstants, tmpdir } from 'node:os';
4
+ import { delimiter as pathDelimiter } from 'node:path';
4
5
  import { join as pathJoin } from 'node:path';
5
6
  import { isLegitimateShellExit } from '../../session/result-classification.mjs';
6
7
  import { makeToolEnvelope } from '../../session/tool-envelope.mjs';
@@ -246,10 +247,69 @@ export function _shellFailureStatus(result, timeout) {
246
247
  ? `[timeout: ${timeout}ms${signalDetail || ' signal: unknown'}${causeDetail}]${timeoutHint}`
247
248
  : (signal
248
249
  ? `[signal: ${signal}${causeDetail}]`
249
- : (exitCode !== 0 && exitCode !== null ? `[exit code: ${exitCode}]` : '')));
250
+ : (exitCode !== 0 && exitCode !== null ? `[exit code: ${exitCode}]${_exitClassDiagnostic(exitCode, result.stderr)}` : '')));
250
251
  return { signal, exitCode, shellToolFailed, statusDetail };
251
252
  }
252
253
 
254
+ // Deterministic POSIX exit-class facts only (127 not-found, 126 not
255
+ // executable, 128+N signal). Per-command meanings (grep 1 = no match, test
256
+ // runner 1 = failures) stay uninterpreted — that would need a per-command
257
+ // dictionary, which is banned steering. 127 additionally names verified
258
+ // same-prefix executables actually present on PATH (fact statement, no
259
+ // substitution suggestion) so the model skips the "then what exists?" probe.
260
+ const _SIGNAL_NAME_BY_NUMBER = new Map(
261
+ Object.entries(osConstants.signals || {}).map(([name, num]) => [num, name]).reverse(),
262
+ );
263
+ function _missingCommandFrom(stderr) {
264
+ const text = String(stderr || '');
265
+ const m = /(?:^|\n)[^\n]*?(?:line \d+:\s*)?([A-Za-z0-9._+-]+):\s*(?:command )?not found/.exec(text)
266
+ || /The term '([^']+)' is not recognized/.exec(text);
267
+ return m ? m[1] : null;
268
+ }
269
+ function _pathPrefixExecutables(cmd, limit = 5) {
270
+ const needle = String(cmd || '').toLowerCase();
271
+ const hits = [];
272
+ if (!needle) return hits;
273
+ const seenDirs = new Set();
274
+ for (const dir of String(process.env.PATH || '').split(pathDelimiter)) {
275
+ if (!dir || seenDirs.has(dir)) continue;
276
+ seenDirs.add(dir);
277
+ if (seenDirs.size > 64) break;
278
+ let entries;
279
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
280
+ for (const ent of entries) {
281
+ if (ent.isDirectory()) continue;
282
+ if (!ent.name.toLowerCase().startsWith(needle)) continue;
283
+ if (process.platform !== 'win32') {
284
+ try { accessSync(pathJoin(dir, ent.name), fsConstants.X_OK); } catch { continue; }
285
+ }
286
+ hits.push(`${ent.name} (${dir.replace(/\\/g, '/')})`);
287
+ if (hits.length >= limit) return hits;
288
+ }
289
+ }
290
+ return hits;
291
+ }
292
+ export function _exitClassDiagnostic(exitCode, stderr) {
293
+ // Not gated on 127: compound chains (`a && b; c`) and pipelines mask the
294
+ // 127 into the chain's final code (observed as exit 1 in 6/28 bench
295
+ // cases), so the stderr fact decides, not the exit code.
296
+ {
297
+ const cmd = _missingCommandFrom(stderr);
298
+ if (cmd) {
299
+ const hits = _pathPrefixExecutables(cmd);
300
+ return hits.length
301
+ ? ` — '${cmd}' is not on PATH; PATH does have: ${hits.join(', ')}`
302
+ : ` — '${cmd}' is not on PATH and no '${cmd}*' executable exists on PATH`;
303
+ }
304
+ }
305
+ if (exitCode === 126) return ' — 126: command found but not executable (permission or format)';
306
+ if (exitCode > 128 && exitCode < 165) {
307
+ const name = _SIGNAL_NAME_BY_NUMBER.get(exitCode - 128);
308
+ if (name) return ` — 128+${exitCode - 128}: terminated by ${name}`;
309
+ }
310
+ return '';
311
+ }
312
+
253
313
  export function _composeShellFailure(statusMarker, errorPrefix, warningBlock, payload) {
254
314
  return `${errorPrefix}${statusMarker}${warningBlock ? `\n${warningBlock}` : ''}\n\n${payload}`;
255
315
  }
@@ -76,6 +76,9 @@ export function findFileByBasename(searchRoot, fullPath, { limit = 3, maxDirs =
76
76
  }
77
77
  }
78
78
  }
79
+ // Queue drained without hitting maxDirs/limit → the walk covered the
80
+ // whole (non-vendor, non-hidden) tree; a miss is conclusive.
81
+ matches.exhaustive = queue.length === 0;
79
82
  return matches;
80
83
  } catch { return []; }
81
84
  }
@@ -153,6 +156,7 @@ export function findDirectoryByBasename(searchRoot, fullPath, { limit = 3, maxDi
153
156
  }
154
157
  }
155
158
  }
159
+ matches.exhaustive = queue.length === 0;
156
160
  return matches;
157
161
  } catch { return []; }
158
162
  }
@@ -67,6 +67,11 @@ const NOT_FOUND_CODES = new Set(['ENOENT', 'ENOTDIR']);
67
67
 
68
68
  const ENOENT_FIND_NUDGE = 'Locate with find on the basename before retrying.';
69
69
 
70
+ // Exhaustive-scan miss: the basename BFS drained its queue (whole non-vendor,
71
+ // non-hidden tree walked) with zero hits, so a follow-up find/list re-probe
72
+ // cannot succeed. Declare the miss conclusive instead of nudging one.
73
+ const ENOENT_ABSENT = ' Nothing same-named elsewhere in the project scan — treat the path as absent (create it if the task requires).';
74
+
70
75
  // Per-invocation memo for the ENOENT recovery fs scans. A single grep/glob
71
76
  // ENOENT surfaces the SAME missing path through tryReadFamilyEnoentRedirect
72
77
  // (resolveUniqueEnoentRedirect) AND buildNotFoundHint (which re-runs
@@ -120,7 +125,7 @@ function spaceJoinedPathHint(requestedPath) {
120
125
 
121
126
  function appendEnoentFindNudge(text = '') {
122
127
  const base = String(text || '');
123
- if (base.includes(ENOENT_FIND_NUDGE)) return base;
128
+ if (base.includes(ENOENT_FIND_NUDGE) || base.includes(ENOENT_ABSENT)) return base;
124
129
  const sep = base.length && !/\s$/.test(base) ? ' ' : '';
125
130
  return `${base}${sep}${ENOENT_FIND_NUDGE}`;
126
131
  }
@@ -191,6 +196,7 @@ export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'E
191
196
  if (dirHits.length === 1) {
192
197
  return ` Not found at this path; the same directory name exists at: "${normalizeOutputPath(dirHits[0])}". ${actionVerb} that path directly.`;
193
198
  }
199
+ const absent = elsewhere.exhaustive === true && dirHits.exhaustive === true ? ENOENT_ABSENT : '';
194
200
  const parentRel = nearestExistingParentRel(workDir, missingPath);
195
201
  if (parentRel) {
196
202
  const resolvedParent = parentRel === '.' ? workDir : resolveAgainstCwd(parentRel, workDir);
@@ -200,11 +206,12 @@ export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'E
200
206
  .slice(0, 3);
201
207
  if (siblings.length) {
202
208
  const shown = parentRel === '.' ? '.' : normalizeOutputPath(parentRel);
203
- return ` Not found; under "${shown}" try: ${siblings.map((n) => `"${n}"`).join(', ')}.`;
209
+ return ` Not found; under "${shown}" try: ${siblings.map((n) => `"${n}"`).join(', ')}.${absent}`;
204
210
  }
205
211
  }
212
+ return absent;
206
213
  }
207
- return '';
214
+ return elsewhere.exhaustive === true ? ENOENT_ABSENT : '';
208
215
  }
209
216
 
210
217
  export function relativePathPrefix(pathPrefix, workDir) {