aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--help.html23
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js74
-rw-r--r--modules/core/module-fs.js48
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/terminal.js26
6 files changed, 147 insertions, 32 deletions
diff --git a/help.html b/help.html
index 9a9a363..86816d7 100644
--- a/help.html
+++ b/help.html
@@ -484,6 +484,18 @@ diff a.txt b.txt</pre>
<section id="commands">
<h2>๐Ÿ“– Full Command Reference</h2>
+ <div class="callout">
+ <strong>โš ๏ธ There is no shell.</strong> The terminal splits your line into argv and hands it to
+ one command โ€” it does not interpret <code>;</code>, <code>&amp;&amp;</code>, <code>||</code> or
+ redirection. Those are now <em>refused</em> with an explicit message instead of being folded
+ into the command's arguments (which used to produce a plausible wrong answer):
+ <code>wc -l a &amp;&amp; grep -c x a</code> โ†’ refused; run two commands.
+ A single <code>cmd | cmd</code> pipeline works in the interactive terminal (not in the AI
+ <code>run_command</code> tool). <code>wc</code>, <code>grep</code> and friends take
+ <strong>one path per call</strong> unless documented otherwise โ€” <code>wc -c a b</code> now
+ prints a line per file plus a <code>total</code>.
+ </div>
+
<h3>System</h3>
<div class="table-wrap"><table>
<tr><th>Command</th><th>Description</th><th>Example</th></tr>
@@ -1660,6 +1672,17 @@ listmods</pre>
<h2>๐Ÿ“‹ Changelog</h2>
<div class="version-item">
+ <div class="v-header"><span class="v-num">v4.6.57/58 โ€” October 2026</span><span class="v-date">2026-10-23</span></div>
+ <ul>
+ <li><strong>No shell โ€” and it now says so</strong> โ€” <code>;</code>, <code>&amp;&amp;</code> and <code>||</code> are refused with an explicit message instead of being folded into the command's argv (where <code>wc -l f &amp;&amp; grep -c x f</code> returned a plausible wrong number). A <code>|</code> arriving from the AI <code>run_command</code> tool is refused too, because that path bypasses the pipe splitter and degraded the pattern to empty. Documented at the top of the command reference.</li>
+ <li><strong>Multiple paths in <code>wc</code></strong> โ€” <code>wc -c a b</code> printed only <code>a</code> and exited clean. It now prints one line per file plus a <code>total</code>, and reports each missing file separately.</li>
+ <li><strong><code>ls</code> prints text</strong> โ€” it rendered only clickable chips (DOM-only), so any capture โ€” the AI <code>run_command</code> tool, the diagnostics sweep โ€” saw an empty result and reported "directory empty".</li>
+ <li><strong><code>view_module</code> shows what is deployed</strong> โ€” it returned the <code>_mods/&lt;Class&gt;.js</code> edit cache first, unversioned, so after a rebuild it showed pre-fix source while the bundle was new. It now fetches the deployed file and prefixes the version; the cache is a labelled fallback.</li>
+ <li><strong>Predictor arithmetic</strong> โ€” the stored field named <code>mape</code> held a sum, so any JSON consumer read a mean that was <em>n</em> times too large (now <code>errorSum</code>); <code>errorPct</code> is stored raw and clamped only at render (a 475ร— miss used to be flattened to 500%); predictions are floored at 1 s and floor-hits are not scored, because one 0.079 s aborted run was rounding the next prediction to 0 s.</li>
+ </ul>
+ </div>
+
+ <div class="version-item">
<div class="v-header"><span class="v-num">v4.6.56 โ€” October 2026</span><span class="v-date">2026-10-23</span></div>
<ul>
<li><strong>Predictor accuracy was structurally 0%</strong> โ€” the denominator counted every observed run while the numerator only moved when a <em>stored prediction</em> matched, and predictions were stored only by the manual <code>predict</code> command. Unpaired runs no longer enter the accuracy denominator; the AI loop now registers a prediction per turn (<code>notePrediction</code>) and <code>predict_task</code> returns a tracked <code>predictionId</code> instead of discarding its estimate.</li>
diff --git a/main.html b/main.html
index 8f9811e..5c2bf8f 100644
--- a/main.html
+++ b/main.html
@@ -1349,13 +1349,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.57"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.58"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.57"></script>
+<script src="modules/core/module-loader.js?v=v4.6.58"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.57"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.58"></script>
<script>
// โ•โ•โ• ONE TRUE VERSION SOURCE โ•โ•โ•
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index c654021..941707e 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -2628,7 +2628,33 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
const unknown = !known;
return { cmd, args, known, destructive, system, unknown, hasSuspicious };
}
+ // v4.6.58: this terminal has no shell. `;` at least failed loudly, but `&&`/`||`
+ // folded the right-hand side into the left command's argv, and a `|` arriving from
+ // the AI run_command tool (which bypasses splitPipe) degraded to an empty pattern
+ // matching every line. Both are silent wrong answers โ€” refuse instead (packet ยง1).
+ detectComposition(input) {
+ const s = String(input || '');
+ let inSingle = false, inDouble = false;
+ for (let i = 0; i < s.length; i++) {
+ const ch = s[i];
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
+ if (inSingle || inDouble) continue;
+ if (ch === '&' && s[i + 1] === '&') return '&&';
+ if (ch === '|' && s[i + 1] === '|') return '||';
+ if (ch === '|') return '|';
+ if (ch === ';') return ';';
+ }
+ return null;
+ }
async checkAndRun(rawInput) {
+ const badOp = this.detectComposition(rawInput);
+ if (badOp) {
+ this.terminal.print('โš ๏ธ No shell here โ€” `' + badOp + '` is not interpreted. ' +
+ 'Run one command per line (the interactive terminal supports `cmd | cmd`; ' +
+ 'AI run_command does not).', 'error');
+ return;
+ }
const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput);
// Level 3 Audit: just log and allow
if (this.level === 3) {
@@ -6766,26 +6792,46 @@ class FileSystemModule {
this.commands['wc'] = (args) => {
// 2026-10-21: accept -l / -w / -c (previously ANY flag was read as a filename,
// so `wc -l file` answered "wc: -l: No such file").
+ // v4.6.58: multiple files. `wc -c a b` used to print only `a` and exit clean โ€”
+ // a silent false negative (packet ยง1).
const want = { l: false, w: false, c: false };
- let file = null;
+ const files = [];
(args || []).forEach(a => {
if (/^-[lwc]+$/.test(a)) a.slice(1).split('').forEach(k => { want[k] = true; });
else if (/^-[a-zA-Z]+$/.test(a)) { /* ignore other flags */ }
- else if (!file) file = a;
+ else files.push(a);
});
- if (!file) return this.terminal.print('Usage: wc [-l] [-w] [-c] <filename>', 'warning');
- const content = this.terminal.fs.readFile(file);
- if (content === null || content === undefined) return this.terminal.print('wc: ' + file + ': No such file', 'error');
- const lines = content.split('\n').length;
- const words = content.split(/\s+/).filter(w => w).length;
- const chars = content.length;
+ if (!files.length) return this.terminal.print('Usage: wc [-l] [-w] [-c] <filename> [...]', 'warning');
const anyFlag = want.l || want.w || want.c;
- const out = [];
- if (want.l || !anyFlag) out.push(lines);
- if (want.w || !anyFlag) out.push(words);
- if (want.c || !anyFlag) out.push(chars);
- if (anyFlag && !(want.l && want.w && want.c)) out.push(file);
- this.terminal.print(out.join(' '));
+ const rows = [];
+ let total = { l: 0, w: 0, c: 0 };
+ files.forEach(file => {
+ const content = this.terminal.fs.readFile(file);
+ if (content === null || content === undefined) {
+ this.terminal.print('wc: ' + file + ': No such file', 'error');
+ return;
+ }
+ const r = {
+ file: file,
+ l: content.split('\n').length,
+ w: content.split(/\s+/).filter(w => w).length,
+ c: content.length
+ };
+ total.l += r.l; total.w += r.w; total.c += r.c;
+ rows.push(r);
+ });
+ if (!rows.length) return;
+ const multi = rows.length > 1;
+ const fmt = (r, name) => {
+ const out = [];
+ if (want.l || !anyFlag) out.push(r.l);
+ if (want.w || !anyFlag) out.push(r.w);
+ if (want.c || !anyFlag) out.push(r.c);
+ if (multi || (anyFlag && !(want.l && want.w && want.c))) out.push(name);
+ return out.join(' ');
+ };
+ rows.forEach(r => this.terminal.print(fmt(r, r.file)));
+ if (multi) this.terminal.print(fmt(total, 'total'));
};
this.commands['touch'] = (args) => {
if(!args[0]) return this.terminal.print('Usage: touch <filename>', 'warning');
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 03c3733..f93553a 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -438,26 +438,46 @@ class FileSystemModule {
this.commands['wc'] = (args) => {
// 2026-10-21: accept -l / -w / -c (previously ANY flag was read as a filename,
// so `wc -l file` answered "wc: -l: No such file").
+ // v4.6.58: multiple files. `wc -c a b` used to print only `a` and exit clean โ€”
+ // a silent false negative (packet ยง1).
const want = { l: false, w: false, c: false };
- let file = null;
+ const files = [];
(args || []).forEach(a => {
if (/^-[lwc]+$/.test(a)) a.slice(1).split('').forEach(k => { want[k] = true; });
else if (/^-[a-zA-Z]+$/.test(a)) { /* ignore other flags */ }
- else if (!file) file = a;
+ else files.push(a);
});
- if (!file) return this.terminal.print('Usage: wc [-l] [-w] [-c] <filename>', 'warning');
- const content = this.terminal.fs.readFile(file);
- if (content === null || content === undefined) return this.terminal.print('wc: ' + file + ': No such file', 'error');
- const lines = content.split('\n').length;
- const words = content.split(/\s+/).filter(w => w).length;
- const chars = content.length;
+ if (!files.length) return this.terminal.print('Usage: wc [-l] [-w] [-c] <filename> [...]', 'warning');
const anyFlag = want.l || want.w || want.c;
- const out = [];
- if (want.l || !anyFlag) out.push(lines);
- if (want.w || !anyFlag) out.push(words);
- if (want.c || !anyFlag) out.push(chars);
- if (anyFlag && !(want.l && want.w && want.c)) out.push(file);
- this.terminal.print(out.join(' '));
+ const rows = [];
+ let total = { l: 0, w: 0, c: 0 };
+ files.forEach(file => {
+ const content = this.terminal.fs.readFile(file);
+ if (content === null || content === undefined) {
+ this.terminal.print('wc: ' + file + ': No such file', 'error');
+ return;
+ }
+ const r = {
+ file: file,
+ l: content.split('\n').length,
+ w: content.split(/\s+/).filter(w => w).length,
+ c: content.length
+ };
+ total.l += r.l; total.w += r.w; total.c += r.c;
+ rows.push(r);
+ });
+ if (!rows.length) return;
+ const multi = rows.length > 1;
+ const fmt = (r, name) => {
+ const out = [];
+ if (want.l || !anyFlag) out.push(r.l);
+ if (want.w || !anyFlag) out.push(r.w);
+ if (want.c || !anyFlag) out.push(r.c);
+ if (multi || (anyFlag && !(want.l && want.w && want.c))) out.push(name);
+ return out.join(' ');
+ };
+ rows.forEach(r => this.terminal.print(fmt(r, r.file)));
+ if (multi) this.terminal.print(fmt(total, 'total'));
};
this.commands['touch'] = (args) => {
if(!args[0]) return this.terminal.print('Usage: touch <filename>', 'warning');
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index b31fd83..0708228 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.57';
+ const LATEST_VER = 'v4.6.58';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index ad4b105..10608d1 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -176,7 +176,33 @@
const unknown = !known;
return { cmd, args, known, destructive, system, unknown, hasSuspicious };
}
+ // v4.6.58: this terminal has no shell. `;` at least failed loudly, but `&&`/`||`
+ // folded the right-hand side into the left command's argv, and a `|` arriving from
+ // the AI run_command tool (which bypasses splitPipe) degraded to an empty pattern
+ // matching every line. Both are silent wrong answers โ€” refuse instead (packet ยง1).
+ detectComposition(input) {
+ const s = String(input || '');
+ let inSingle = false, inDouble = false;
+ for (let i = 0; i < s.length; i++) {
+ const ch = s[i];
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
+ if (inSingle || inDouble) continue;
+ if (ch === '&' && s[i + 1] === '&') return '&&';
+ if (ch === '|' && s[i + 1] === '|') return '||';
+ if (ch === '|') return '|';
+ if (ch === ';') return ';';
+ }
+ return null;
+ }
async checkAndRun(rawInput) {
+ const badOp = this.detectComposition(rawInput);
+ if (badOp) {
+ this.terminal.print('โš ๏ธ No shell here โ€” `' + badOp + '` is not interpreted. ' +
+ 'Run one command per line (the interactive terminal supports `cmd | cmd`; ' +
+ 'AI run_command does not).', 'error');
+ return;
+ }
const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput);
// Level 3 Audit: just log and allow
if (this.level === 3) {