summaryrefslogtreecommitdiff
path: root/modules/core/terminal.js
Commit message (Collapse)AuthorAgeFilesLines
* fix(newos4): `ui` now works in classic — it was being answered by the AI ↵Hermes SysOp3 days1-1/+52
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (v4.6.168) Operator report: "Ui command don't work". It was worse than a broken command: typing `ui` in the classic terminal printed Security: Unknown command "ui" — routing to AI (did you mean "ai"?)... → Routing to AI assistant... and an AI turn answered it. Three layers each looked healthy on its own: 1. `ui` is registered by UIShellModule, which since the v4.6.162 gate only loads when the visitor has already opted in — so the command that switches the interface did not exist in the state you need it from. 2. `_classify()` decides "known" from the command registry (module commands + a short builtin list), not from the safety whitelist, so `ui` was `unknown: true`. At the default security level 1 an unknown command that is not whitelisted is offered as a typo and then routed to the AI. `ui` was not whitelisted. 3. Nothing printed an error anyone would recognise as failure — it looked like the terminal was answering you. Fix: * `'ui'` joins `_baseWhitelist` (114 -> 115). It is a navigation command: safe, non-destructive, and it must never depend on the interface it navigates to. * `dispatchCommand` gets an `if (cmd === 'ui')` fallback that mounts the shell on demand via a new `_mountUIShell()` (register + init, the same two steps the boot loop performs) and then opens the picker. It also writes `webx11_ui_experimental=1`, so the opt-in survives the reload. * The mount is deliberately NOT `_safeRegister('UIShellModule', …)`: that bare call is the boot gate and `verify-newos4-ui-gate.py` fails on any occurrence of it. A guard that grants one exception has to grant whatever is written next to it, so the on-demand path is separate by name. Guard: new check `ui_command_in_classic` — executes `ui` on the served page with no flag and waits for `webx11.uiShell` + `#wx11-ui-picker`. Deliberately behavioural: `_classify('ui').known` reads FALSE even when the command works (registry vs whitelist), so a text or classifier assertion would have been a lie in both directions. The RED case is on record: the AI-routing line above, captured in the live scrollback before the fix. Verified on v4.6.168 (served): `ui` -> picker with 7 options; `ui easy` -> easy; `ui cockpit` -> cockpit; `ui classic` -> classic; `ui nonsense` -> "Unknown mode 'nonsense'. Available: classic, easy, cockpit" and stays put. Guard 8/8 rc=0. maintain OK.
* fix(newos4): ?ui=<mode> is an opt-in too — the gate had killed every ↵Hermes SysOp3 days1-1/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | shared mode link (v4.6.163) v4.6.162 shipped the opt-in gate as `get('ui') === '1'`. But the UI shell has always treated `?ui=<mode>` as a REQUEST — `module-uishell.js` reads `q = get('ui')` and honours it when the value is one of `lazyIds()` (easy|chat|studio|workspace|commander|cockpit), with `_fromQuery` set so the request never becomes a stored preference. So every shared link of the form `main.html?ui=chat` stopped mounting the shell entirely and booted classic, silently, with no error: the gate required a literal '1' that the shell's own convention never sends. Found by an independent verification pass that tried `probe-ui-modes.py --cycle` against prod and got `FAIL easy@390x844: wait_for_selector timeout` — a false red on a working build, which traced back to this. The probe's own breakage was mine (I had just changed its default base to `?ui=1`, and it appends `?ui=<mode>`, producing `?ui=1?ui=easy`); that is fixed in the next commit. Gate now: opt in on ANY `ui` value except an explicit off ('0'/'false'), plus the localStorage key. q = get('ui'); if (q !== null && q !== '' && q !== '0' && q !== 'false') return true; return localStorage.getItem('webx11_ui_experimental') === '1'; Verified on the served page (v4.6.163): * `?ui=chat` -> uiShell object, data-ui-mode=chat, body wx11-ui-chat, #chat-stream present, _fromQuery=true, picker NOT shown, and localStorage UNCHANGED (webx11_ui_experimental=null, webx11_ui_mode=null) — a link is not a preference. * no flag -> uiShell undefined, 0 #ui-root, no picker (unchanged from v4.6.162). * `?ui=1` -> shell + picker (guard's browser assertion still passes). Served core-bundle.js byte-identical to disk; guard green (7/7 checks, rc=0); probe-ui-modes.py --cycle ALL PASS 13 checks; maintain.py --fix PASS.
* feat(newos4): re-enable the six UI modes behind an opt-in gate (v4.6.162)Hermes SysOp3 days1-9/+16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The six interface modes (v4.6.152–156) were removed from the startup path outright in v4.6.157 after crash reports. The crash was never reproduced: F2 in the rollback plan (2026-11-09_224409) drove all six modes on one page in headless Chromium at 390×844 and recorded crashes: [], pageerrors: [], heap flat at 10 MB over a 25 s idle. The two defects the investigation DID find were fixed instead — F3 (nginx 413 on a >1 MB agent context) and F4 (36 tool handlers where a model-supplied non-string reached .trim/.split/.replace). v4.6.159–161 also removed the last navigator.onLine verdicts, which is what made "it died mid-session" look like a UI crash. So the modes come back, but only for a visitor who asks for them. 1. The gate (modules/core/terminal.js, plan Task 9) - Registration is now conditional: const _uiExp = ... URLSearchParams(location.search).get('ui') === '1' || localStorage.getItem('webx11_ui_experimental') === '1'; if (_uiExp) this._safeRegister('UIShellModule', 'UI shell'); - Without the flag nothing changes: the shell is not registered, no mode can mount (the ui-*.js files all begin `if (!terminal.uiShell) return;`), no first-run picker appears, `data-ui-mode` is absent and `webx11.uiShell` is undefined. - The gate is in the bundle, so it cannot be bypassed by a stale cached ext file. 2. What was verified on the served page (v4.6.162, live Chromium) - Classic, no flag: NEWOS4_VERSION v4.6.162, 48 modules, uiShell undefined, 0 #ui-root, no #wx11-ui-picker, no data-ui-mode, terminal visible, `help` renders 18,124 chars, 0 console warnings, 0 JS errors, 55 console messages. - Opt-in `?ui=1`: uiShell object, #ui-root + #ui-foot, data-ui-mode="classic", 49 modules, picker present (opt-in only), 0 warnings, 0 JS errors. - All six modes driven through uiShell.apply() with a 1.2 s settle each, and back to classic: chat (body wx11-ui-chat, 6 mode nodes), cockpit (3), easy (4), commander (4), studio (3), workspace (5), classic (0). #terminal-container survived every switch, zero throws, heap 6.3 MB against 3.3 MB classic. - Served bytes identical to disk for core-bundle.js; `_uiExp` appears twice in the served bundle and the registration line is the guarded one (core-bundle.js:4111). 3. Not done, deliberately (plan Task 9 step 4) - `webx11_ui_experimental` is NOT defaulted on for new visitors. The plan asks for one week of clean use first, with the crash matrix re-run on the then-current bundle. Guard for this gate: admin/scripts/verify-newos4-ui-gate.py, wired above `exit $FAIL` in smoke-auth.sh — a bare (unconditional) registration is the failure it exists to catch.
* fix(newos4): honest size/network measurements + checkpoint and ↵Hermes SysOp3 days1-2/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | phantom-delete fixes (v4.6.160) Applies the three 2026-11-09 patches after review, and fixes two more navigator.onLine lies found while verifying them. Each patch was checked against the live code first; one of its claims did not survive. 1. virtual-fs — phantom deletes and checkpoint bloat - deleteCheckpoint('nope') returned true and printed "Deleted checkpoint" for a name that did not exist: _resolvePath() validates intermediate segments only, so the leaf was never checked. New _requireLeaf() refuses a resolved path whose leaf is absent, and deleteCheckpoint uses it. - CHECKPOINT SIZE: checkpoint() cloned the whole root minus _snapshots/_sessions/.trash, so _mods (module originals) and .semantic_index were duplicated into EVERY checkpoint — measured 2,101,686 bytes for a 2 MiB _mods. Both are now excluded on write and preserved across rollback (without the keep-list change a rollback would delete the live module originals). Measured after: 185 bytes. - Claim corrected: the patch also claimed deleteFile() reported success on a phantom path. It did not — live code already required `node && node.type === 'file'` and returned false. The guard is kept as defence-in-depth, and the test asserts the real behaviour rather than the claimed one. 2. module-fs — vfs-size reported ~2x the real size - It measured JSON.stringify(root).length: UTF-16 code units PLUS every structural character of the serialisation. A 1046-byte tree printed as 1961; anything under ~5 KB printed as "0.00 MB". - Every number is now stored bytes (UTF-8 of file content): one measure() walk for the total, the per-file table, the top-directory table and the system dirs, so the rows add up to the total. The patch's arithmetic was still off in seven places, all fixed here: a UTF-16 fallback that reintroduced the bug when Blob was missing, a top-5 table that could never sum to the total (now top 10 + an aggregated remainder row), the top-dirs table dropping files stored at root (now a "/(root files)" row), a 2-decimal MB total that printed 0.00 for small VFS, three duplicate hand-rolled walks, readDir() used for dir sizes, and "1 files". 3. module-system — status/doctor no longer trust navigator.onLine - status printed "Network: Offline" from navigator.onLine, which an Android WebView reports false while fully connected. - doctor now awaits a real HEAD probe (AbortController, 4 s): only 'reachable' is healthy, a throw is HIGH "Network: unreachable", a non-ok answer is MEDIUM and names the status (405 to HEAD is not "offline"). - THE PATCH'S THRESHOLDS COULD NEVER FIRE: it compared localStorage bytes against navigator.storage.estimate().quota, which is the whole-origin quota (GBs, mostly Cache Storage), while localStorage has its own ~5 MB cap — a 4.5 MB localStorage reported "All systems healthy" against a 2 GB quota. Now: localStorage measured as (key+value)*2 UTF-16 bytes with HIGH > 4 MiB / MEDIUM > 3 MiB, VFS measured in UTF-8 bytes with HIGH > 3 MiB and > 80% of the measured localStorage bytes. The origin quota appears only in one informational line and decides no severity. - Also removed navigator.onLine from version, bug-report and bug (a grep for it is now 0 in the module), and added a "Measured:" line so a clean report still shows what was measured. 4. terminal.js / module-servermon.js — the same lie, where the user looks - The status bar printed "❌ Offline" in red from navigator.onLine; on a WebView that is false while online. It now shows "📶 Network?" with a title pointing at `doctor` unless the browser claims online. - `net` test line now reads "Browser reports: offline (unreliable)" as a warning instead of a red failure — the real verdict comes from the probes that follow. Verified: 40/40 node tests (3 new files: 36 + 53 + 34 assertions, each proven red against the pre-patch code; the doctor test runs against the PATCH's threshold logic and shows the 4.5 MB case staying silent). An independent orchestrator probe re-derived all of it: phantom delete false, 2 MB _mods absent from the checkpoint and preserved through rollback, vfs-size exact bytes for ASCII/é/☃ nested trees, 4.5 MB localStorage HIGH despite a 2 GB quota, failing probe unreachable, status never claiming Online/Offline. Served bytes identical for core-bundle.js / main.html / module-system.js / virtual-fs.js. Live Chromium on v4.6.160: vfs-size "85.8 KB (87880 bytes) across 40 files", doctor "Measured: VFS 0.08 MB UTF-8 · localStorage 0.07 MB UTF-16 in 14 keys", a stubbed failing probe reports HIGH unreachable, the status bar reads "📶 Network?" when the browser lies, deleteCheckpoint on a missing name returns false, and the console has 0 warnings and 0 JS errors.
* fix(newos4): one arg-coercion boundary + binary-safe gunzip + clean boot log ↵Hermes SysOp3 days1-8/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (v4.6.159) 1. Tool arguments are coerced once, at the dispatch boundary (module-aiassist.js) - _coerceToolArgs(name, a) at :1584 reads the tool's own JSON schema and, for every declared string prop, converts number/boolean with String() and object/array with JSON.stringify (never "[object Object]"); declared integer/number props accept unambiguous numeric strings. null/undefined stay absent so the handlers' defaults and _pathArg still own "no value given". - Called at both dispatch boundaries: runTool (:2650) and runModuleTool (:3565 — reachable from view_module and from ship_module -> test_module/health_check, which have their own parse+switch). - A JSON body of "null" or "[1,2]" parses to a non-object; both sites normalise to {} before any handler reads a.field. - The 26 handlers keep their `a.x || ''` reads: `|| ''` catches only FALSY, which is how a truthy number or object reached .trim()/.split()/.replace() and filed one auto-report per tool (user_get "key.split is not a function", create_skill, patch_skill, delegate_task "task.substring is not a function", create_mindmap, web_fetch, list_skills, ship_module). 2. gunzip is binary-safe (cryptogzip.js — plan 2026-11-03 T3, the last open task) - _cmdGunzip used _vfsRead + _gunzipString (base64-only), so a real .gz fetched with wget reported "not valid base64 input (illegal character \u001f)" and could not be decompressed at all. It now reads bytes (_vfsReadBytes handles both the module's base64 output and raw Latin-1 bytes), checks the 1f 8b magic, and reports "not a gzip file" for non-gzip input without writing or deleting anything. The module's own gzip output still round-trips. 3. Boot log is clean (terminal.js) - Seven _safeRegister() calls for Editor/Shell/Alias/Finder/FileManager/Diff/BusyBox were guaranteed no-ops: those classes are declared in critical-ext-bundle.js, a <script> after module-loader.js, so they do not exist yet when new WebX11Terminal() runs; adding them to the registry map would still resolve to undefined. Removed with a comment — the ext bundle registers each one on load. - SimpleGUIModule IS a core class but was missing from _safeRegister's registry map, so the fallback could never restore it. Added. 4. New guard: admin/scripts/verify-plan-age.py + profile wrapper plan-age-guard.sh (cron newos4-plan-age-guard, no_agent, daily 07:10). Silent when healthy; reports plans marked <!-- autonomy: review --> with no execution stamp older than 72 h — the process gap that let four fix plans sit for six days before v4.6.158. Read-only by construction (148 real plans byte+mtime identical before/after a run), rc=2 when the directory cannot be scanned, --strict opt-in so a report is never recorded as a crashed guard. Verified: 37/37 node tests, 3 new test files and 631 new assertions (613 arg-coercion, 18 gunzip); test-cryptogzip-guard's C8 updated because it pinned the old misleading "not valid base64" message. Independent orchestrator checks: all 66 declared string props across 58 tool defs coerced under 4 hostile shapes; positive cases untouched (string content not double-encoded, numbers still numbers, null still absent, unknown tool identity preserved). Served bytes byte-identical for core-bundle.js, main.html and cryptogzip.js. Live Chromium on v4.6.159: write_file with object content wrote 27 chars of readable JSON (not "[object Object]"), read_file with offset:"0"/limit:"200" coerced to numbers, user_set with key 12345 -> "12345" and object value -> JSON, and the boot log now carries 0 SafeRegister warnings (52 messages, 0 JS errors) with SimpleGUIModule registered.
* fix(newos4): take the UI shell off the startup path until the crash reports ↵Agando6 days1-1/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | are explained Symptom: operator reports newOS4 "started to crash since the last ui update"; a user (Max) reports his browser crashed. Order: roll back, do not re-enable until it is verified in a dev environment. Root cause: not yet established, and the log work rules out the obvious suspect. - 0 requests for modules/ext/ui-*.js in the entire access log: no interface mode has ever mounted in production, so a mode's own chrome cannot be the cause. - all six modes driven on one page in headless Chromium at 390x844: no crash, no page errors, one #terminal-container, heap flat at 10 MB over a 25 s idle. - the six errors.json entries that read like a crash report (SPA is not defined, DOM harness FAILED: N, "...reading 'makeInstance'") exist in no shipped file - grep on the served 1.29 MB bundle returns 0 - so they came from a harness run inside that one browser session. Two real defects were found instead, both planned not bundled here: - /ollama/api/chat inherits nginx's 1 MB default client_max_body_size and refused a 1,063,595-byte agent context with 413 (only 413 in 7 days). - module-aiassist.js:2576 const originalPath = a.path || '' then .replace on a model-supplied non-string: TypeError, 3 events on tool:list_files, 32 sites with the same shape. Fix: the UIShellModule registration in terminal.js is commented out with a dated note and the revert recipe; bundles rebuilt from source; LATEST_VER v4.6.156 -> v4.6.157; main.html's three ?v= refs bumped. The six modes are lazy ext files whose first line is `if (!terminal.uiShell) return;`, so an accidental load is a no-op and no first-run picker appears. Verified: served core-bundle.js?v=v4.6.157 is cmp-identical to disk (1,298,695 B) and the registration exists only as a comment (core-bundle.js:4082). Playwright 390x844: shellPresent false, uiShellModuleRegistered false, uiRoot false, pickerNode false, moduleCount 47, help renders 21,802 chars, pageerrors []. Workaround: none needed - users get classic only. Re-enable = restore the line, rebuild, bump LATEST_VER + main.html, re-run admin/scripts/probe-ui-modes.py. Plan: .hermes/plans/2026-11-09_224409-newos4-ui-rollback-dev-verify.md
* fix(newos4): second review round — identity for every stock node, load ↵Agando8 days1-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | dedup, sweep race, a11y Four models (kimi-k2.7-code, deepseek-v4-pro, minimax-m3, glm-5.3) reviewed the shipped change set. Review log with every accepted and rejected finding: .hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md - identity guard now covers every snapshot node, not just #terminal-container: terminal.js also binds #output, #command-input, #input-area and #status-bar, so a cloned one leaves the terminal writing into a detached node while the page looks perfect - _restoreStock anchors on the next stock node already home, then on the recorded trailing sibling; a raw child index was measured wrong (terminal,input,status-bar,quick-actions) - _loading map: a double-clicked card or a repeated 'ui easy' loads the mode once - _sweepPending returns while that id is in flight instead of declaring failure at 6 s and overwriting the stored preference - _syncChrome() on every switch (quick-actions was left in the tab order behind the mode root) - picker: role=dialog/aria-modal/labelledby/focus, shown after 600 ms instead of waiting for a registration that never happens with lazy modes, and suppressed when ?ui= already applied - ?ui= must be one of the six; _loadMode only accepts packages['ui-<id>']; hasOwnProperty on modes - VFS names are shell-quoted before reaching api.exec (cat/edit) - refresh generation no longer bumps per tick (slow captures left later panes at '...') - easy: padding shorthand reset the 34 px footer reserve (footer overlapped the composer) - cockpit panes get tabIndex and focus; F1/F12 left to the browser; nothing swallowed in inputs - api.el lost its unused innerHTML sink; the test lost its _parseArgs stub (it hid the regression) - v4.6.155
* feat(newos4): six interface modes (easy -> cockpit) over one terminalAgando8 days1-1/+20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Symptom: newOS4 had exactly one layout. A first-time user had no way in that did not require learning commands, and an operator had no surface showing live runtime state; the only way to see heap/VFS/tool-call telemetry was to type cat /sys/... Root cause: no UI layer existed. main.html hard-codes one flex column, and terminal.js binds the stock nodes by id at construction, so an alternative layout had to either duplicate the output stream or reparent the real nodes. Fix: - modules/core/module-uishell.js (new): mode registry, #ui-root, stock-node snapshot/restore, identity check that reverts to classic when a mode removes or clones #terminal-container, shell-docked #ui-foot (the AI stop button exists in every mode), ui command, gated first-run picker, ?ui= honoured only if the mode actually registers, lazy mode loading via the same loader ext modules use. - modules/ext/ui-{easy,chat,studio,workspace,commander,cockpit}.js (new): six reparenting skins with scoped CSS. Workspace/commander/cockpit read real /sys/* mounts and captured command output; no value on screen is invented. - modules/test/test-ui-shell.js (new): 25 assertions - teardown, clone rejection, failed-mount fallback, footer docking, destructive-mode rejection. - terminal.js: register the shell; make one failing ext module stop aborting the rest of the serial load loop (it did, silently, which is why six ui-* files at the end of extKeys never loaded). - module-loader.js: LATEST_VER v4.6.152. The six ui-* keys are deliberately NOT in extKeys - see the rate-limit note below. - admin/scripts/probe-ui-modes.py (new): 6 modes x 2 viewports plus a --cycle pass that switches all seven states on ONE page and asserts the output node keeps its identity. Also fixed, found while testing (pre-existing, production-affecting): - nginx limit_req zone=webx11 burst 50 -> 200. One newOS4 boot fans out ~40 assets; the bucket emptied mid-boot and every later module got 503. Measured in webx11.error.log: "limiting requests, excess: 50.696 by zone webx11" against modules/ext/ui-chat.js. A reload within ~50 s silently broke modules. Sustained rate unchanged at 60r/m. Verified: node modules/test/test-ui-shell.js -> 25/25; removing both _restoreStock() calls measures 20/25 with five FAILs; probe-ui-modes.py --cycle against production -> ALL PASS, 13 checks; served bytes: v4.6.152 x3 in main.html, class UIShellModule x1 in core-bundle.js, six ui-* packages in packages.json.
* feat(newos4): register `test` / `[` so chains decide locally instead of ↵Agando2026-10-291-0/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | asking a model (v4.6.135) `[ -f "/qt.txt" ] && echo exists` — the probe that closed the v4.6.132 batch — was not a shell test at all: both commands were unregistered, so the line fell through to the AI fallback and a model was asked whether a file exists. `_evalTest()` has implemented `-f -d -e -z -n ! = !=` for the if/while parser since the script runner was written; only the command was missing. * `test` and `[` registered in ShellModule. Success resolves silently; false rejects with a `silent` error so `&&`/`||` skip it — POSIX prints nothing on exit 1. * `[` requires the closing bracket (`[ : missing ']'`), and that message is NOT silent. * `test` already names the on-demand regression-suite runner ("test auto" / "test list" in help.html). A argv that does not look like a test expression (`-f`/`-d`/`-e`/`-z`/`-n`/`!`, or `a = b` / `a != b`) is delegated to the handler registered before us, so `test auto` still runs the suite and the two meanings cannot collide. * terminal.dispatchCommand() now honours `err.silent` — without it a top-level `test -f /nope` printed `Error in ShellModule.test:` (measured on v4.6.134, fixed in v4.6.135). * help.html documents the bracket form next to the existing `test` row. Three tags were minted minutes apart — v4.6.133 (commands), v4.6.134 (suite delegation), v4.6.135 (silent top-level) — each a real cache-bust for a fix that superseded the previous build; the net change is this commit. Verification node modules/test/test-shell-test-cmd.js -> 24/24 pass (RED against HEAD: "test command registered" fails, "m.commands.test is not a function") node modules/test/test-pipe-guard.js -> all seven flags true node modules/test/test-parse-args.js -> 28/28 · daily job -> node tests 22/22 pass newos4-maintain.py -> atlas 355 commands (was 354), modules-index 349, help 364/364 live v4.6.135, read from the terminal DOM: [ -f /qt.txt ] && echo F135 -> F135 test -f /nope || echo M135 -> M135 [ -d /nope ] && echo BAD || echo N134-> N134 (false branch skipped, nothing printed) test -> no output (suite not loaded; POSIX false) test -f /nope -> no output js "a b".length | wc -c -> 1 (v4.6.132 pipe fix still intact) The AI fallback is no longer reachable for a bracketed file test.
* fix(newos4): pipe stage quoting, maintain ordering, help cost row, plan ↵Agando2026-10-291-13/+21
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | status (v4.6.131) Four items closed in one batch; three ran as parallel subagents on disjoint files, the bundle was rebuilt once by the parent (rebuild-bundles.py serialises the working tree, which carries other sessions' work — see 161372f). 1. PIPE STAGES KEEP THEIR QUOTES. `js "a b".length | wc -c` reached eval as `a b.length` and printed a ReferenceError. terminal.js executePipeChain() now keeps `stages[i]` aligned 1:1 with `pipeParts[i]` (the `.filter(argv => argv.length > 0)` shifted the index; replaced by a lastStage scan + `if (argv.length === 0) continue`) and forwards the stage's own text to the dispatcher. The core path alone was dead code: ShellModule.init() replaces terminal.executePipeChain (shell.js:144), and that override is what runs in the browser (`executePipeChain.toString()` contained `cmdText.split`, not `lastStage`). Both now pass `cmdText`/`pipeParts[i]`. test-pipe-guard.js was rewritten for the new shape and now also asserts the shell override forwards raw text — the assertion that would have caught the shadowing. RED on HEAD: both flags false; GREEN now. 2. MAINTENANCE --fix CAN CLEAR ITS OWN WARN. check_index() ran before check_atlas(), which is what regenerates course/atlas.json, so --fix rebuilt modules-index.json from the stale atlas and the same WARN reappeared every run. check_atlas() now runs immediately before check_index(). modules-index.json regenerated: cost/usage present, 349 commands, version 2026-10-29-atlas. 3. `cost` DOCUMENTED. modules/core/module-billing.js registers `cost [on|off]` (an alias of `usage`); help.html had no row. Added above the `usage` row. help coverage 362/363 -> 363/363. 4. SHIPPED PLANS STOP ASKING FOR A DECISION. The applier only flips review<->safe, so every executed review-gated plan kept counting as an open decision (5 of 5). newos4-feedback-collect.py now reads the body: a `## Status` / `## Implementation` heading followed by shipped|executed plus a version, commit or date is `status: done`; those stay in `plans` (visible as shipped) but leave plans_review and awaiting_decision. Counts: plans_review 5 -> 0, awaiting_decision 4 -> 1 (the remaining one is a real open report, RPT-20261027-160751). admin/feedback.html read `it.status[0]`, which would render `d` for a string status; it now prefers `status_lines[0]`. Also carried, authored by earlier uncommitted sessions and already live in the bundle (attributed, not silently absorbed): modules/core/module-billing.js (new, BillingModule: cost/usage), its CORE_FILES entry in scripts/rebuild-bundles.py, and the BillingModule registry entry in terminal.js. The bundle was rebuilt from HEAD + this batch with ollama-manager.js taken from HEAD, so another session's in-flight `_httpError` refactor is deliberately NOT in the shipped bundle (grep -c _httpError core-bundle.js = 0). Verification node modules/test/test-pipe-guard.js -> {"empty":[],"blank":[],"classifyGuarded":true,"pipeGuarded":true,"shellForwardsRaw":true} node modules/test/test-parse-args.js -> 28/28 pass maintained job -> node tests 21/21 pass scripts/newos4-maintain.py -> maintenance result: OK (modules-index 349, help 363/363, changelog current v4.6.131) live v4.6.131 (real DOM, no print monkey-patch): js "a b".length | wc -c -> 1 (js printed 3, one byte) js "a b".length -> 3 js "abc".toUpperCase() -> ABC echo "two words" -> two words node -e "import('fs')" -> red error + Node-builtin hint js 6*7 -> 42 unhandledrejection -> 0 livePipeImpl: shell-override, livePipeForwardsRaw: true, costRegistered: true
* fix(newos4): js keeps its string literals — dispatcher forwards the raw ↵Agando2026-10-291-3/+11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | command line (v4.6.130) _parseArgs() treats quote characters as delimiters and drops them, so every string literal in a js expression died before eval: `js "abc".toUpperCase()` reached the module as `abc.toUpperCase()` → ReferenceError: abc is not defined. Same for `js import('fs')` → `import(fs)`. Found while verifying the v4.6.128 console fix (plan 2026-10-29_112000-newos4-parseargs-strips-js-string-literals.md). - terminal.js: checkAndRun() records the untokenised line (`_currentRaw`), _dispatch() forwards it, dispatchCommand(cmd, args, rawText) passes it to the module. Security classification still runs on the tokenised form, and pipes/AI/script-runner calls pass nothing, so they behave as before. - console.js: `js` evaluates _expr(args, rawText) — the raw remainder. `node -e` deliberately does NOT: the tokenizer already keeps `'…'` inside `"…"`, and the raw form would hand eval the string literal `"import('fs')"` instead of the import (caught by the live probe, not by review). js-run keeps tokenised paths. - modules/test/test-parse-args.js: tokenizer table (records the deletion as the current contract), dispatcher plumbing asserts, _expr cases, and the user-visible paths. 27 assertions. - module-loader.js + main.html + bundle: v4.6.130 (129 was minted, then console.js changed again — re-minting so no cached client keeps the intermediate). Verification node modules/test/test-parse-args.js -> 27/27 pass same file against HEAD sources (git show HEAD:...) -> TypeError: _expr is not a function (RED) scripts/lint-no-undef.py terminal.js console.js -> clean, 122 cross-file globals core-bundle.js rebuilt from HEAD sources + this patch in /tmp/iso (the working tree carries another session's uncommitted ollama-manager.js `_httpError`); diff vs HEAD bundle = 14 lines, all mine. critical-ext-bundle.js byte-identical. Live page v4.6.130 (real executeCommand, output captured): js "abc".toUpperCase() -> ABC js 'a b'.split(' ').length -> 2 js JSON.stringify({a:1}) -> {"a":1} js 6*7 -> 42 js import('fs') -> red "Failed to resolve module specifier 'fs'" + Node-builtin hint node -e "import('fs')" -> same error + hint (was printing the expression as a string) echo "two words" -> two words js-run /nope.js -> js-run: /nope.js: No such file unhandledrejection count -> 0 The sibling's parked terminal.js hunk (BillingModule registry entry) was stashed for this commit and is restored in the working tree afterwards; it is not part of this change.
* fix(newos4): one error counter for errors/dashboard/world-model (v4.6.61)Agando2026-10-231-0/+32
| | | | | | | | | | | | | | | | | | | | | Packet 2 (three surfaces, three answers). Measured first: `errors` scanned /logs/current.log for [ERROR] lines, but that file only ever receives explicit terminal.log('ERROR', ...) calls -- exactly one call site in the whole app -- so it read 0. The dashboard added DebugSupervisor._errorDB plus failed tool calls (18), and the world model read the debug DB alone (11). - terminal.countErrors() / countLogErrors(): the single counter, exposed on the one object all three surfaces already hold - `errors` prints the canonical total with its breakdown, the entry list (hash, count, severity, message), and reports the log scan separately as a mirror - dashboard health.errors and health.errorBreakdown derive from it - world model telemetry.recentErrors derives from it, plus errorsBySource - terminal.log('ERROR', ...) now also records into DebugSupervisor, so the log is a human-readable mirror of one store instead of a second store almost nobody wrote Verified live on v4.6.61: with a seeded debug entry + failed tool call, all three report 2; a single log('ERROR') call moves debug 1->2 and the total by exactly 1 (the mirror line is counted once, in the log, and reported separately).
* fix(newos4): refuse shell composition, wc takes multiple paths (v4.6.58)Agando2026-10-231-0/+26
| | | | | | | | | | | | | | | | | | | Packet 1 and 3 (silent wrong answers from a shell-less terminal). - detectComposition() in the security gate refuses ; && || | before dispatch. `;` used to fail loudly, but `&&`/`||` folded the right-hand side into the left command's argv (wc -l f && grep -c x f returned "42 42"), and a `|` arriving from the AI run_command tool bypassed splitPipe entirely and degraded the pattern to empty -- which matches every line. Operators inside quotes are untouched. - wc accepts multiple paths: one line per file plus a total, per-file errors for missing paths. `wc -c a b` used to print only `a` and exit 0. - help.html: "There is no shell" callout at the top of the command reference, plus the v4.6.57/58 changelog entry. Verified live on v4.6.58 by replaying the exact call sequence used by both paths: interactive `wc -c a b` -> "14 a / 10 b / 24 total"; `&&` and `;` refused; `echo "a && b"` still echoes; AI run_command semantics with `|` and `&&` refused; single command and multi-file wc still return real output.
* fix(newos4): empty-pipeline crash, stacks in reports, frame in digest + ↵Agando2026-10-231-7/+29
| | | | | | | | | | | | | | | | | | | | | | | | | | dashboard (v4.6.55) Root cause: splitPipe() splits on every '|', so `help |`, `| wc -l`, `a || b` produced a stage with no argv and executePipeChain() called parts[0].toLowerCase() on undefined — 989 occurrences, first filed 2026-10-20, unfixable from the report because bug reports dropped the stack that named executePipeChain:3955. Same unguarded read existed in _classify(). - terminal.js: _classify() returns an inert classification for empty argv; executePipeChain() parses each stage once, filters stages with no argv (so the last real stage stays final), warns only when the whole pipeline is empty. Last-stage test now compares against the filtered length. - module-system.js: `bug` diagnostics include the stack (10 frames, 1200 chars); auto-filed reports carry it too — they are the majority of the inbox and were previously message-only. - report.php: per-stack 2 KB cap; fingerprint pinned to message/source/count so adding `stack` cannot break dedupe against reports filed before today. - ingest.php: first_frame() parsed once on ingest, stored as `frame` on each signature. - newos4-triage.py: FRAME| line with the first application frame. - dashboard.php: frame under each Top Errors row. - modules/test/test-pipe-guard.js: ordering tripwire (guard must precede the parts[0] read). Verified: test RED exit=1 -> GREEN exit=0; browser matrix on v4.6.55 (help |, | wc -l, echo hi || echo bye, ls | | wc -l, |) -> 0 new error signatures, "|" warns; auto-filed report now carries stack (RPT-...f3e31124, archived after the probe); frame = modules/core/core-bundle.js:3955:42; maintain OK; skill audit 7/7.
* fix(newos4): dashboard shows the real world model and stops dying after 10 s ↵Agando2026-10-221-11/+31
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | (v4.6.51) Two defects, both verified live before/after: 1. The World Model panel always said "Not synthesized yet". The page had a renderer for data.worldModel and _collectDashboardData() never set it -- the model was right there in WorldModelModule._model. Now collected and mapped to the renderer's exact keys (model has modules.loaded/skills.count/memory.nodes, the page reads modules/skills/memoryNodes), plus changesSinceLastSnapshot and the recent-error sample, with an honest "not synthesized" fallback. 2. The page discarded any payload older than 10 s and its own 5 s auto-refresh then showed "Cannot connect to newOS4". Stale payloads are now rendered with their age ("Data as of 1:20:14 AM - 3m ago"), labelled amber past 5 min. Also: the Refresh button wrote a localStorage key nobody read -> the terminal now listens for it and re-collects (page says "No terminal tab answered" when no terminal tab is open); storage events repaint instantly; the dead /api/dashboard fetch (404 on all three paths, 5 s stall) is gone; `dashboard --watch` / `--stop` re-collects every 5 s; header says the data is this browser's instance. Verified v4.6.51: panel rows "Synthesized 34s ago / v4.6.51 / 46-22 / 136-3 / 1 nodes 0 edges / OK / none recorded"; 130 s-old payload still renders with "- 3m ago" and no banner (previously "Cannot connect"); storage event moves the footer 13s -> 0s; watch timer advances the payload, --stop clears it; mobile 360/390 px all OK; 0 console errors.
* newOS4: interactive AI course (terminal module, portal, atlas, telemetry, ↵Agando2026-10-181-2/+10
| | | | daily maintenance+cron) + free store card
* newOS4 v4.6.15: reentrant print-capture stack, AI tail -f block, read-only ↵Agando2026-09-291-3/+22
| | | | sed in safeRead, security test harness (21 checks)
* fix: parse per-call captured output for unknown-cmd error (race-free under ↵Agando2026-09-291-3/+0
| | | | Promise.all)
* newOS4 v4.6.14: race-safe AI loop guard (depth counter), cover _capture* ↵Agando2026-09-291-2/+7
| | | | entry points, structured JSON tool error
* newOS4 v4.6.13: typo suggestion bar (one-tap fix), consistent unknown-cmd ↵Agando2026-09-291-13/+41
| | | | messages, AI loop guard
* newOS4: Ctrl+C/Esc stops tail -f follow; security whitelist gains sedAgando2026-09-291-7/+22
|
* fix: add kanban to packages.json, update cache version to 2026-07-29Agando2026-07-291-1/+1
|
* auto: 2026-07-09_06:00 changesAgando2026-07-091-58/+233
|
* auto: 2026-06-24_06:01 changesAgando2026-06-241-0/+55
|
* auto: 2026-06-23_18:00 changesAgando2026-06-231-0/+2
|
* auto: 2026-06-23_06:00 changesAgando2026-06-231-24/+144
|
* auto: 2026-06-22_18:00 changesAgando2026-06-221-5/+5
|
* auto: 2026-06-18_18:00 changesAgando2026-06-181-10/+32
|
* auto: 2026-06-18_06:00 changesAgando2026-06-181-1/+15
|
* auto: 2026-06-17_06:01 changesAgando2026-06-171-39/+58
|
* auto: 2026-06-16_06:00 changesAgando2026-06-161-2/+11
|
* auto: 2026-06-14_06:00 changesAgando2026-06-141-30/+81
|
* auto: 2026-06-13_18:00 changesAgando2026-06-131-13/+47
|
* auto: 2026-06-10_18:00 changesAgando2026-06-101-16/+54
|
* auto: 2026-06-10_06:00 changesAgando2026-06-101-3/+3
|
* auto: 2026-06-09_18:00 changesAgando2026-06-091-1/+1
|
* auto: 2026-06-09_06:00 changesAgando2026-06-091-7/+30
|
* auto: 2026-06-08_18:00 changesAgando2026-06-081-3/+3
|
* auto: 2026-06-07_18:00 changesAgando2026-06-071-1/+1
|
* auto: 2026-06-06_18:01 changesAgando2026-06-061-2/+6
|
* auto: 2026-06-06_06:00 changesAgando2026-06-061-11/+339
|
* auto: 2026-06-05_18:01 changesAgando2026-06-051-0/+4
|
* auto: 2026-06-04_06:00 changesAgando2026-06-041-1/+4
|
* auto: 2026-06-02_18:00 changesAgando2026-06-021-2/+5
|
* auto: 2026-06-02_06:00 changesAgando2026-06-021-46/+71
|
* auto: 2026-06-01_18:00 changesAgando2026-06-011-6/+64
|
* auto: 2026-06-01_06:00 changesAgando2026-06-011-14/+44
|
* auto: 2026-05-31_06:00 changesAgando2026-05-311-10/+87
|
* auto: 2026-05-30_06:00 changesAgando2026-05-301-3/+62
|
* auto: 2026-05-29_18:01 changesAgando2026-05-291-1/+129
|