aboutsummaryrefslogtreecommitdiff
path: root/modules/core
Commit message (Collapse)AuthorAgeFilesLines
* fix(newos4): stop reporting network and VFS size as facts we do not have ↡Hermes SysOp3 days5-17/+289
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (v4.6.161) 1. The last two `navigator.onLine`-as-fact payloads - module-aiassist.js: the debug/health payload carried `online: navigator.onLine`, consumed by downloads/dashboard.html, which rendered "πŸ”΄ Offline" β€” on an Android WebView (reports false while fully connected) that told working users they were offline. The key is gone; the payload carries `network:'unknown'`, `networkKnown:false` and `networkHint`. - module-world-model.js: the snapshot had the same field, and it is also handed to the LLM through `_modelDigest()`/`ask()`, so the model was told the user was offline. Now `network:'unknown'` + `networkKnown:false`. - `_loadFromVFS()` scrubs `system.online` from a model persisted by an older build β€” otherwise the stale boolean would survive inside the digest for the life of the VFS. - downloads/dashboard.html: `networkText()` renders reachable/unreachable/http-* only when doctor produced them ("(doctor)"), `unprobed` as "β€” not probed", and everything else as "β€” unknown (run doctor for a live probe)". A legacy payload's `online:false` lands in "unknown", never "Offline". The row label is "Network", not "Online". - Neither payload invents a probe: SystemModule stores no last-doctor verdict, so 'unknown' is the honest value until one exists. 2. `vfsSize` in both payloads was a different unit again - aiassist: `JSON.stringify(fs.root).length / 1024`; world-model: `JSON.stringify(fs.root).length`. Same lie v4.6.160 fixed in `vfs-size` β€” UTF-16 code units plus serialisation structure. Both now sum UTF-8 bytes of file content (`_utf8Len`/`_measureVfs`, Blob β†’ TextEncoder β†’ code-point fallback, depth/node capped) and carry `sizeBytes` beside the human string so the rows reconcile. - world-model's `_gatherFileInfo().size` used a THIRD unit on an adjacent line (`JSON.stringify(fs.root).length/1024`); now the same measurement, plus `sizeBytes`. - `showModel()` renders exact bytes and tolerates a pre-fix persisted string. 3. Predictor made a guaranteed-401 request on every boot (UI-plan finding F6) - `_wdConfig()` returns `user:''`/`pass:''` when the saved WebDAV config has a URL but no credentials, and `_pullFromServer`/`_pushToServer` fetched the auth-gated `/spaces/<user>/_data/predictor.json` anyway β€” 107 Γ— 401 in one day, logged by the browser as console errors. Both now refuse without credentials (`cache-only (no credentials)`). Verified: 42/42 node tests, 2 new files (38 payload/honesty assertions, 8 predictor-store assertions); the predictor suite is 4/8 against the pre-fix source, and the payload suite fails 14 assertions against HEAD (`online:false` present, vfsSize === JSON.stringify length). Live Chromium on v4.6.161, on the served dashboard: a legacy payload with `online:false` renders "Network β€” unknown (run doctor for a live probe)", the current payload renders "85.8 KB (87880 B)" and the same unknown, and a doctor-produced `unreachable` renders "⚠️ unreachable (doctor)". Served bytes identical for core-bundle.js, main.html and downloads/dashboard.html. `maintain.py --fix` all PASS.
* fix(newos4): honest size/network measurements + checkpoint and ↡Hermes SysOp3 days7-106/+570
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 days4-17/+141
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (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): six crash classes from the feedback queue (v4.6.158)Hermes SysOp6 days4-30/+92
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Each item is symptom -> cause -> fix, re-verified against the served bytes. 1. debug-ai panel threw "Cannot read properties of undefined (reading 'substring')" (RPT-20261108-231622). _updateDebugPanel and two sibling previews called JSON.stringify(entry.args).substring(); JSON.stringify(undefined) returns undefined, and _toolCallLog entries carry args: undefined for any tool call with no arguments. All three previews now go through _formatToolArgs(), which renders "null" and never throws. 2. test_module threw "Cannot convert undefined or null to object" (RPT-20261108-232127). Object.keys(instance.commands) assumed every module initialises a command map. The throw landed after loadModuleFromCode had already succeeded, so the rolledBack:true branches were skipped and an unverified module stayed loaded. Both sites now require instance.commands. 3. list_files threw "originalPath.replace is not a function" (RPT-20261109-172742, 3 occurrences in errors.json). A model can send a non-string path despite the schema. The path family (list_files/read_file/write_file/make_directory/ delete_file) now coerces through _pathArg() and answers the tool's own honest error instead of a TypeError. 4. wpm search failed for EVERY keyword (RPT-20261109-123907): packages.json entry `kanban` used `description` while the other 31 use `desc`, and search() called pkg.desc.toLowerCase() mid-loop. Catalog reads now go through _pkg() / _packageMap() (desc || description, values coerced to strings), and the kanban entry carries name/version/desc/commands. Re-running the original loop for pdf/git/zzz is clean. 5. wx/webxai/webx turned a chat-lock timeout into an unhandled rejection (RPT-20261107-150726): forwardToAI() called chat(args) in a synchronous function without returning or catching the promise, so the 10 s busy throw never reached dispatchCommand's catch. It now returns the promise with a .catch that prints "AI error: ..." through terminal.print β€” console.error is hooked by the debug supervisor and would re-file the same report. 6. CryptoGzip._base64ToBytes threw a raw DOMException on non-base64 input (RPT-20261102-203616). It now validates first and throws Error("not valid base64 input (<reason>)"). The duplicate shadowing definitions at the bottom of the class were deleted β€” the later copy is the one that carried the unguarded atob(). Also regenerated modules/core/core-bundle.js (generated, never hand-edited) and bumped the build v4.6.157 -> v4.6.158 (main.html x3 + module-loader.js). Verified: 35/35 node tests, including 6 new regression tests each proven red against the pre-fix source; an independent orchestrator harness 36/36; and newos4-maintain.py --fix all PASS after regenerating the stale changelog page. Served bytes are byte-identical for the bundle, main.html and both ext modules, and a live headless-Chromium run against v4.6.158 shows WpmModule.search('pdf') returning 1 result, the debug panel rendering a no-arg entry as "null", _base64ToBytes('grüße') raising a plain Error that names base64, and 0 JS errors on load. m.pull (RPT-20261102-211438) is closed as a usage error: no module in the tree defines pull(); the real interfaces are the webdav-pull and model-pull commands.
* fix(newos4): take the UI shell off the startup path until the crash reports ↡Agando6 days3-3/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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): workspace re-opened the modules panel every tick; picker lost ↡Agando8 days3-17/+41
| | | | | | | | | | | | | | | | | | | | | | | | | | | | two cards; commander was black Three defects reported against v4.6.155, each reproduced before touching code: 1. 'the modules tab keeps opening' (workspace). api.capture() runs dispatchCommand() for real, and the Modules card called bare 'modules' β€” whose only effect is terminal.openModuleManager(). Every 15 s refresh re-opened the panel (measured: #module-manager display:flex at t+9s and t+18s). The card now captures 'listmods' (same inventory as text), and api.capture() refuses UI-only commands outright so a future card cannot reintroduce it. 2. 'the picker got reduced to 4 items'. showPicker() used this.list() once four modes were registered β€” but modes are lazy, so that is 'modes you have already visited'. It now always renders the six in level order, taking registered specs for label/tagline. 3. 'commander is mostly black' on a phone. The base sheet's #ui-root rule used body[data-ui-mode]:not(...) #ui-root (1,2,1) and therefore beat every mode's own body[data-ui-mode='x'] #ui-root (1,1,1): commander/studio/cockpit asked for display:grid and got flex column. Commander collapsed to a 253 px centre column in an 844 px page with both rails hidden. Base specificity is now (1,0,0) with an explicit !important hide for classic/boot, and the commander phone query pins grid-template-rows: minmax(0,1fr) so the single row fills. Measured after: picker 6 cards after visiting four modes; #module-manager stays display:none across a refresh cycle; commander display:grid, phone centre 390x810 with a 673 px terminal, desktop 232/788/260 columns. probe-ui-modes.py --cycle -> ALL PASS, 13 checks; shell test 25/25. v4.6.156
* fix(newos4): second review round β€” identity for every stock node, load ↡Agando8 days4-59/+107
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* chore(newos4): v4.6.153 β€” cache-bust the review fixesAgando8 days1-1/+1
|
* fix(newos4): four-model review β€” lazy-load race, restore order, a11y, key ↡Agando8 days2-22/+106
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | handling Reviewed the shipped change set with kimi-k2.7-code, deepseek-v4-pro, glm-5.3 and minimax-m3. Findings below are the ones that survived verification against file:line; the rest are in .hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md Accepted and fixed: - apply() had no generation token: a lazy mode load that finished after the user switched to another mode force-applied the abandoned one (`apply(want,{force:true})` in the .then). Every apply() now bumps `_loadSeq` and the callback acts only while its token is newest. Same token also removes a double mount: registerMode() already applies the mode as it registers, so the callback was mounting every lazy mode twice (teardown+cleanup twice). - _restoreStock() used the recorded `nextSibling` as insert anchor. Every real mode moves SEVERAL stock nodes (terminal, input, status bar), so the recorded sibling is inside the mode container and every insert fell through to append. Now anchors on the next stock node that is already back home. A raw child index also fails (elements removed shift the indices) β€” measured order terminal,input,status-bar,quick-actions. - _abortToClassic() left the broken mode persisted and `_pending` set, so the next reload retried the same failure with no way out. It now clears both and stores 'classic'. - ?ui= reached classList.add('wx11-ui-' + id) unvalidated: a space or colon throws a DOMException and boot dies. Validated against /^[a-z][a-z0-9-]{0,15}$/ and no longer persisted (the comment always claimed it was a request, not a preference). - Modes hide .prompt-label, so #command-input had no accessible name at all. The shell sets aria-label once when a mode reparents it. - cockpit: preventDefault on all F1-F8 killed help, the address bar and devtools. F1/F12 are left to the browser and nothing is swallowed when focus is in a text field. - commander: Ctrl/Cmd+K opened the palette even while typing in the composer. Ignored from inside editable elements now. - picker: role=dialog + aria-modal + aria-labelledby + focus on the first card. - api.capture(): strip the quoting characters from argv (capturePipeOutput takes argv). - stale `wx11-ui-*` body class removed on switch instead of accumulating. Rejected with evidence: - "loadModuleFromURL may not return a boolean" β€” modules/core/module-manager.js:56 returns true on success and :60 false in the catch. Verified by reading the function. - "_abortToClassic does not clear _pending" β€” it did not; that one is accepted above. The sibling claim that it "leaves the broken mode persisted" was verified by reading the function: it never touched localStorage, so also accepted. - "mm.LATEST_VER / webx11_cache_ver are never set, so the mode URL falls back to ?v=0" β€” modules/core/module-loader.js:12 sets window.webx11.moduleManager.LATEST_VER and :14-15 writes webx11_cache_ver. Verified live: the mode fetch carries ?v=v4.6.152. - "moduleManager.modules may be a Map" β€” it is a plain object (terminal.moduleManager.modules .CryptoGzipModule is the documented access path), so ui-commander's Object.keys works. Verified: node modules/test/test-ui-shell.js -> 25/25 (the test now moves TWO stock nodes; a raw-index restore fails it with "terminal-container,input-area,status-bar,quick-actions"); probe-ui-modes.py --cycle against production -> ALL PASS, 13 checks.
* fix(newos4): mode layout constraints, capture() arg split, suppress classic ↡Agando8 days2-10/+52
| | | | | | | | | | | | | welcome modal - #ui-root is a flex column (it was display:block, so every mode's flex:1 was inert and the terminal collapsed to 0px while the composer sat at the bottom of a 4600px flow) - overflow:clip on #ui-root and body: overflow:hidden is still a scroll container, and the terminal's scrollIntoView() on new output scrolled the mode's own chrome off-screen - terminal min-height 100px: in the tabbed/panel modes its grid parent is not a flex parent - api.capture() split arguments itself; term._parseArgs does not exist, so every panel showed 'unavailable: term._parseArgs is not a function' - the classic Let's Get Started modal is suppressed in non-classic modes: it covered easy's cards
* feat(newos4): six interface modes (easy -> cockpit) over one terminalAgando8 days4-3/+1062
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* release: v4.6.151 β€” wget that actually saves, web_fetch that actually pagesAgando9 days4-25/+135
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | S1 (reported by agent-os4-maint-01, wrongly refuted by the chat auto-reply): wget existed but was unusable and dishonest. - Cause 1: _cmdWget used the page's own fetch(url), so every cross-origin URL died on CORS ("Failed to fetch"). Measured in the served v4.6.150 page: fetch(cgit README) threw, /api/fetch returned 200/1954 bytes. - Cause 2: _vfsWriteBytes ignored writeFile()'s boolean, so the command printed a green tick when the target directory did not exist. Measured: fs.writeFile('nosuchdir/x.txt') === false. - Cause 3: bytes were round-tripped through String.fromCharCode, mangling UTF-8. - Fix: fetch through /api/fetch (raw source), mkdir -p, writeFile + read-back verification, error instead of a tick on failure. Register curl with -s/-o parsing delegating to the same writer so the two cannot drift. S2 (same reporter): web_fetch could not page and said it could. - Cause: the handler honoured max_chars up to 50 000, then _clipToolResult kept toolResultChars-200 = 11 800, so 38 200 chars vanished silently; offset was not in the schema and was never read, so next_offset pointed at a page the caller could not request again. - Fix: offset/next_offset/total_chars/source_bytes in the contract; the page is built, measured against the clip budget and shrunk until the envelope fits, so next_offset - returned_chars is always 0. - Also fixed: _clipToolResult appended "0 of N chars withheld" when the content already fitted, claiming a loss that did not happen. C1: fetch <url> --save=FILE β€” the only terminal path from a remote URL to the VFS, so a 90 KB source page no longer has to be read through 3 000-char scrollback. Proxy: /api/extract now returns source_bytes (raw page size) and content_chars_utf16. Python len() counts code points while JS String.length counts UTF-16 units; 7 astral characters in the changelog left a 7-char gap at the end of a 90 KB page (measured). _html_ok declares charset=utf-8. Verified: node --check on all four modules; py_compile; bundles rebuilt; served bytes identical to disk for all five assets at ?v=v4.6.151; live in the browser: curl -s -o review/main.md <cross-origin> saved 1954 chars with the correct head, a 404 wrote no file, fetch --save wrote 91 518 chars, web_fetch p1 returned 11 754 with next_offset 11 754 (gap 0), page 2 started at a different offset with next 23 496.
* release(v4.6.150): commit the module work that has been live but uncommittedAgando9 days4-65/+179
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Operator commit-approve, 2026-11-06. These changes have been served since 2026-11-02/03 while git HEAD still stamped v4.6.146, so the mirror published a tree four builds behind the running instance and the release tag named the wrong build. module-orchestrator.js β€” orchestrate-parallel reported success it could not prove. Results are now recorded by sub-task id through a resultSink handed to delegateTask (harvest matched by currentTask/lastTask strings, which a later delegation on the same agent overwrote, losing finished work as "agent not found"); the tally counts summaries with status ok instead of Promise.allSettled resolutions (a spawn refusal fulfils with undefined and used to print green); and a busy agent is no longer reused at pool cap, where two siblings erased each other's lease. Guard: modules/test/test-orchestrator-parallel-harvest.js (16 cases, reproduces the one-agent three-sub-task cap case that printed 3/3 succeeded). module-billing.js β€” the balance said "not signed in β€” newOS4 sends no session token", which was already false: the browser sends the HttpOnly cookie and parse_bearer_token() falls back to it. `cost` is on by default (the flag was opt-in and nobody knew the command existed; `cost off` opts out) and the command now prints today's platform spend from GET /api/billing/usage?days=1 alongside the session estimate. modules/ext/webdav.js β€” the inline WebView detector missed "Version/4.0" Android WebViews, including the operator's own phone, so the login offered a Telegram widget that cannot complete there. It now delegates to modules/wx11-tg-webview.js when the page loaded it and keeps a matching fallback (wv token, Version/4.0-Android, Telegram, FBAN/Instagram/Line). modules/ext/media.js β€” text analysis model renamed deepseek-v4-flash -> deepseek-v4.1-flash in both the field and the printed label. main.html + module-loader.js β€” v4.6.146 -> v4.6.150 in LATEST_VER and the three asset queries; core-bundle.js rebuilt (rebuild-bundles.py reports it already in sync, 1,250,569 bytes). Verified: node --check on all six scripts and the new test; the 16-case orchestrator guard passes against the deployed bundle (ORCH_SRC=...); bundle rebuild is a no-op; core-bundle.js?v=v4.6.150 answers 200.
* refactor(dashboard): one dashboard β€” port billing/API keys, 301 the duplicateAgando13 days3-3/+3
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two dashboards existed and had drifted apart: root /dashboard.html (118 KB single file with Subscription & Usage + API Keys) and /spaces/dashboard.html (34 KB shell + assets/js/dashboard-*.js). The nginx log settles which one is real: 77.211.5.129 requests /spaces/dashboard.html and never the root page. Tonight's fixes landed on the root page twice before that was noticed. Neither page was a superset, so deleting outright would have lost features: root only -> Subscription & Usage card, API Keys card (create/revoke/reveal) spaces only -> 2FA/TOTP, change password, delete space, email change, files, logs assets/js/dashboard-billing.js + assets/css/dashboard-billing.css (new) loadSubscription / loadApiKeys / cancelSubscription / createApiKey / revokeApiKey / copyNewApiKey / dismissNewKey + isProPlusPlan / capPlan, lifted verbatim from the root page, wrapped in an IIFE that re-exports the onclick handlers on window; getAuthHeaders() reads the modular app's localStorage.webx11_session_id; PRO_PLUS_PLANS carried over (its absence threw "PRO_PLUS_PLANS is not defined" and silently hid both cards on the first attempt) spaces/dashboard.html the two cards inserted after the Activity Logs card, plus the css/js includes dashboard-auth.js showDashboard() calls loadSubscription() once the view is up retirement root dashboard.html deleted (git rm); `location = /dashboard.html { return 301 /spaces/dashboard.html$is_args$args; }` so bookmarks keep working and ?next= survives links repointed in ai-api.html (6), ai-course.html (3), models.html (1), newOS4 hint string (v4.6.146 + rebuilt bundles so the cached bundle cannot serve the old URL) Measured (playwright, 390x844 WebView UA, agando's real session): subscriptionCard display block, height 315px, plan "Free", price "EUR0/mo", badge "Free", usage "$0.00 / inf", API-keys card hidden for a free plan, 0 console warnings /dashboard.html?next=/skills.html -> 301 -> /spaces/dashboard.html?next=/skills.html, 200, login card + Telegram button present
* fix(auth-ui): stop offering a Telegram widget where it cannot workAgando13 days1-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two hosts cannot complete the widget flow and both are in use: an Android WebView / in-app browser (the operator's phone UA ends "; wv)") and a desktop browser that blocks the popup or hands the link to the Telegram app. The widget needs a popup that can postMessage back into the opener; in those hosts the link lands in a browser tab or in Chrome with no opener, onTelegramAuth never fires, and the login card looks dead β€” "it opens my default browser" (reported twice: phone, then Max's laptop). modules/wx11-tg-webview.js (new) WX11TG.isWebView() -> "; wv)", Telegram in-app, Meta in-app UAs WX11TG.dropWidgets()-> removes already-rendered oauth.telegram.org iframes/scripts dashboard.html code box is open by default and drops the nested <details> WebView: widget removed + "cannot work inside an app window" hint desktop: if the widget was clicked and onTelegramAuth has not fired in 12 s, open the code flow with a hint instead of leaving a dead card spaces/newos4 (webdav-login panel) in a WebView the widget is never injected, the mount is hidden and the device-code flow starts by itself; desktop keeps the widget unchanged version v4.6.145 Measured with playwright (390x844 WebView UA and 1440x900 Chrome): WebView -> 0 Telegram iframes, hint shown, code box open, real WX11- code minted desktop -> widget still rendered, code box open, real WX11- code minted newOS4 WebView -> no widget injected, code area auto-visible, code WX11-..., no JS errors newOS4 desktop -> widget still offered
* fix(auth): one session writer for every login path; expire dead credentialsAgando13 days3-1/+41
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Third occurrence of the same class of bug, and the first fix is why: commit e95b7be (2026-11-01, "multi-session .newsession v3") touched exactly ONE hunk of auth-file-api.py β€” handle_login. handle_device_poll, handle_telegram_login and handle_register kept overwriting the whole .newsession with a single-session v2 payload, so a phone sign-in logged the desktop out and the loser held a session id no store knew: permanent `401 Invalid session or API key` (newOS4 asked for a login it could never complete). Measured: agando's store held 1 entry (sha8 45cb7819) while the phone sent 9ac607f1 for 3 h; 26 device-polls in 2 s from one page minted 26 cookies and only the last write survived, which also drained the shared nginx `webx11` zone (11x 503 on /admin/api/*). api/webx11_auth.py - add_session() is now THE writer: v3 `sessions[]` (cap 5) under a per-user `flock` (LOCK_DIR outside the spaces), plus the legacy `.session` mirror - module-level import so no handler has to reach for it lazily api/auth-file-api.py - device-poll / telegram-login / register / recover-by-backup-code all call add_session(); a bare write_newsession_file() is now only the admin-gated handle_create_session - handle_device_poll runs its approved branch under a per-code `flock` and mints exactly one session per approved code (idempotent under a poll storm), with a cheap pre-lock reject so unvalidated codes cannot mint lock files - handle_me_cookie expires an unknown session cookie (Set-Cookie Max-Age=0, code SESSION_DEAD): the cookie is HttpOnly, so no page could ever clear a dead credential and every load re-sent it api/newcrypto.py - write_newsession_file() is atomic (tmp + fsync + os.replace); the temp name carries pid+tid because a fixed `<file>.tmp` lets two concurrent writers of one store steal each other's temp file (reproduced: FileNotFoundError) admin-login.html single-flight poll (polling/finished guards) β€” 26 calls -> 1 dashboard.html device-code fallback when the Telegram popup has no opener spaces/newos4/* 401 -> session-repair notice + endpoint normalisation, v4.6.144 nginx /api/auth/ + /api/security/ on zone wx11auth (120r/m, burst 60) admin/scripts/verify-session-writers.py NEW static guard, wired into smoke-auth.sh Proof (live, after restart): 5 concurrent polls on one approved code -> 5x approved, 1 distinct session id, store v3 with 2 entries, pre-fix session still valid; dead cookie -> 401 + Set-Cookie expiry, valid cookie -> 200; 200-call auth burst -> 503s only on /api/auth/, /admin/ stayed 302; smoke-auth.sh PASS. Guard RED (3 offenders) -> GREEN. Backups: /home/darkbot/backups/authfix-20261102_214424
* fix(newos4): read_file pages were silently truncated; delegation no longer ↡Agando13 days4-33/+127
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | invents "completed" Two defects found by re-deriving a report that claimed both were still open. read_file paging (module-aiassist.js) - A page larger than the result clip was clipped AFTER read_file had computed next_offset. On the last page next_offset is null, so the model was told truncated:false while its tail had been removed: measured 11 890 of 19 999 chars delivered, returned:19999, no pointer to the rest - 8 109 chars lost with no way to ask for them. * the clip now sets truncated=true and next_offset=offset+keep when it removes the tail of a page that had no continuation (and never splits a surrogate pair) * read_file caps a page at the result budget (min(pageCap, toolResultChars-200)), so a page is always delivered whole and next_offset is exactly contiguous Live v4.6.143: 19 999-char file -> page 11 800 (next_offset 11 800), page 2 8 199 (next_offset null): 11 800 + 8 199 = 19 999, nothing withheld. A model asking for limit:32000 is clamped to 11 800 and told truncated:true. The ~500-char symptom was the 500-char floor: a caller passing limit:200 gets 500, which is why targeted sed -n reads looked better than the tool. Delegation reporting (module-orchestrator.js, module-aiassist.js) - chat() returning {content} was already fixed in 8a9333b, but three other places still substituted the word "completed" for an absent answer, so the symptom survived: delegateTask's outbox wrote status:'completed' even when the worker returned nothing; the delegate_task tool returned {success:true, result:"completed"}; orchestrate-parallel summarised an unmatched agent as {status:'ok', result:'completed (agent recycled)'} and a missing outbox as "completed (no outbox)". - Now: an empty/undefined answer is status 'failed' with error 'agent produced no answer' / 'no result in outbox' / 'agent not found β€” result unverifiable', and the tool returns success:false with result:null. Tests: test-delegation-reporting.js 6 (RED 3/6 against HEAD: D3/D4 success:true, D5 the literal "completed"), test-tool-context-bound 26 with E6-E8 (RED 24/2: truncated:false, next_offset null). maintain 27/27.
* fix(newos4): memory-graph loads stop rewriting user data; unknown sizes say ↡Agando13 days4-21/+53
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | unknown Three follow-ups from the merge review, each with the assertion that pins it. module-memory-graph.js - _normalizeEdges no longer mutates stored weights. It capped legacy 'failed' edges from 0.8 to 0.2 in memory, and the next debounced save made that permanent - an irreversible migration of the user's graph with no schema version. The cap moved to _effectiveWeight(), which ranking (showStats "Strongest relationships"), the session view and prune() now use; the stored record is left exactly as written. - config.json is stamped version: 2 so a future format change can migrate deliberately. ext/webdav.js - the two fallback parsers emit size: null instead of 0. They cannot see the size column, and 0 rendered as "0 B" - the same "every file looks empty" symptom as the parseInt bug fixed in e639c2d, one regex miss away. - _formatSize(null) renders 'β€”' (unknown) rather than '0 B'; 0 still renders '0 B'. module-aiassist.js - the lock timeout now says what to do: "AI is still busy with another turn (waited Ns). Wait for it to finish, or run 'ai-stop' and retry - the running turn's lock is left alone." It used to be a dead-end string. Tests: test-memory-graph 39 (T6.4 rewritten to the read-time contract: stored weight stays 0.8, effective 0.2, success unaffected), test-webdav-sizes 28 (A14d null -> 'β€”'). maintain 26/26, live v4.6.141: _effectiveWeight 0.2 with the edge object unchanged, _formatSize(null) 'β€”', '0 B' still '0 B'.
* fix(newos4): WebDAV listing sizes + pull, and five MemoryGraph defectsAgando13 days3-35/+179
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Closes STEER-20261102-205416-facd and STEER-20261102-204031-f82a. WebDAV (modules/ext/webdav.js): - listings showed "21 B" for a 21 KB file: nginx emits humanised sizes (autoindex_exact_size off, vhost :412) and _parseAutoindex did parseInt("21K"). _parseAutoindexSize() handles K/M/G, decimals, lowercase and "-"; PROPFIND's exact getcontentlength path is untouched. - webdav-pull into a missing local directory printed "Pulled: ... (N bytes)" and wrote nothing: it ignored fs.writeFile's false return (virtual-fs.js:561). It now creates the parent chain, checks the write, and only reports success after a confirmed write. The byte count uses TextEncoder, not .length (a Cyrillic file under-reported ~2x). - _formatSize gained the GB branch the parser already accepted (3 GB printed "3072.0 MB") and no longer prints "NaN MB" for a missing size. MemoryGraph (modules/core/module-memory-graph.js): - addEdge averages weights, so every repeat pulled a strong edge toward the call site's constant (0.9 -> 0.7 -> 0.6). Now Math.max, with decay left to applyDecay(); prune() untouched. - task ids were `task-${Date.now()}`, so re-submitting a task never deduped: now a hash of agent + normalised text. - recordFileAccess no longer records the graph's own /.memory_graph/ store. - failed tool calls were weighted 0.8 vs success 0.5, so showStats ranked failures as strongest: now 0.2. Legacy 0.8 failures are capped on load. - concept ids keep punctuation ("concept:(v2!),"): slugged, and the slug uses \p{L}\p{N} so Cyrillic/CJK/accented text still yields nodes -- an a-z0-9 slug silently produced none. Review findings folded in from the regression pass: the non-Latin regression (a-z0-9 slug), the GB formatter asymmetry, and the pull byte count were all found by review, not by the patch authors, and each now has a test. Tests: test-webdav-sizes.js 27 (was RED 15/24 before the fix), test-memory-graph.js 37 (RED 17/32), both auto-discovered by newos4-maintain -- 26/26 node tests. Live on v4.6.140: _parseAutoindexSize('21K') 21504, _formatSize -> "21.0 KB", _formatSize(2GiB) "2.0 GB", slug Cyrillic "ΠΈΡΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ-парсСр-ошибок", 0 console errors.
* perf(newos4): delegated agent runs no longer serialise; a waiter can't steal ↡Agando13 days3-21/+63
| | | | | | | | | | | | | | | | | | | | | | | | | | | a lock Closes the second half of STEER-20261102-201045-ab08. - chat() gated the main-turn lock on nothing, so orchestrate-parallel's fan-out (each sub-task calling chat() on the same instance with its own agentTag) ran strictly one at a time. Measured 2x300ms delegated runs: 617ms before, 306ms now; the two 200ms user turns still take 400ms (main path stays serialised). - the 10s lock timeout called this._chatLock = false and printed "force released", which cleared a lock the waiter did not own - that released the main turn's lock while it was still running. A waiter now throws and the owner is the only writer. - delegated runs no longer clear the user's cancel flag on entry (an agent start used to reset this._cancelled, disarming a Stop the user had pressed). - tool-log entries carry the run they belong to and _recentToolLog(tag, n) is what feeds the run log line and _postRunReview, so concurrent runs no longer consume each other's entries. Known limit, unchanged: _currentTag is a single field, so under true concurrency an entry can be attributed to the wrong run (telemetry only - the answers are per-session). Namespacing it properly needs per-call state threading. Test: modules/test/test-chat-concurrency.js, 7 assertions, RED before (H1 617ms, H3 force-released, H5 no tags) and GREEN now; maintain 24/24.
* fix(newos4): chat() threw on every tool-using turn and never returned its answerAgando13 days3-3/+17
| | | | | | | | | | | | | | | | | | | | | | | | | Regression from 32608bc, caught by the .chat() caller sweep before it aged: - `recent` was referenced in chat()'s finally block but never declared, so any turn with toolTurns > 0 threw ReferenceError there and chat() REJECTED. delegateTask's catch turned that into "agent failed"; the 'completed' fallback was only ever reachable on tool-free turns. - chat() still had no return at all, so a tool-free turn resolved undefined and module-orchestrator.js:1250 fell through to `|| 'completed'`. Both fixed: `const recent` is now built from _toolCallLog, and chat() returns { content: finalAnswer, toolTurns } before the archive block (a return at the end of the method would let _archiveSession swallow the answer). Verified with /tmp/probe-chat-hotfix.js, 6/6: tool-using turn resolves {content:'WORKER ANSWER', toolTurns:2}, tool-free turn resolves the answer, the orchestrator's own projection returns the answer instead of 'completed', and _chatLock is released. Run against 32608bc the same probe says "recent is not defined" - i.e. it reproduces the shipped defect. Not included: the _chatLock serialisation (orchestrate-parallel still runs sub-tasks one at a time and the 10s force-release can clear the main turn's lock). That stays open in STEER-20261102-201045-ab08.
* feat(newos4): configurable tool budgets, per-model defaults, real tool telemetryAgando13 days5-53/+406
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Closes STEER-20261102-201234-7200 (read_file capped at 6000 while the schema promised 32000). Budgets (module-aiassist.js + new modules/core/model-profiles.js): - one resolver, precedence: call arg -> loop profile -> model profile -> ai-config -> code default. Keys: read-page-chars, read-result-chars, tool-context-budget, fetch-chars. - model classes tiny/small/mid/large keyed by id then by size token; `mid` reproduces the old numbers so nothing moves for a mid model. - _clipToolResult now clips the `content` field, never the serialised JSON envelope: clipping the envelope deleted truncated/next_offset, so a 16k page lost its continuation pointer (the reported symptom). - read_file's schema no longer promises a number the model class may not give. Telemetry: - _recordToolCall keeps sizes, duration, ok and a 120-char arg preview instead of the whole payload; ai-stats prints calls / ok% / avg ms / KB / truncation from localStorage webx11_tool_stats. Server (api/proxy-api.py): - /api/extract honours max_chars (default 12000, cap 100000) and returns content_chars + truncated; the bs4 branch is no longer uncapped while the regex fallback was silently cut at 10 000. BEHAVIOUR CHANGE: num_ctx for a model with no explicit setting now follows the class default (16384 -> 65536 for cloud/large, 8192 for tiny). Cloud models bill on tokens used, so cost is unchanged; local models get a bigger KV cache. Two build traps, both now documented in AGENTS.md: - model-profiles.js must be the FIRST entry in CORE_FILES and must export the MODEL_PROFILES global (a TDZ typeof throws; without the global every budget silently falls back to the code default). - a budget key needs the setter branch AND the constructor loader at module-aiassist.js:49 - persisting without restoring looks fine until reload. Verified: test-tool-context-bound 23/23 (RED before: E1-E4 + TypeError on _toolLimit); live bundle read_page 32000 / result 12000 / budget 48000 / fetch 12000 and _ctxTokens 65536 for gemma4:31b-cloud; /api/extract 28970 -> 500 chars with truncated true; ai-config write+restore probe 6/6.
* feat(billing): no model-class gate; free allowance becomes $0.10/dayAgando14 days3-15/+40
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Operator policy 2026-11-01: every subscription may call any model, and the free tier resets daily. Model gate removed (api/ai-gateway.py) `plan == 'free' and model_weight > 1 -> 403 "Advanced models require Pro"` ran before any money logic, so a free-plan account holding $1086 of purchased credit was refused and told to upgrade. A model tier is not something we sell: the allowance and the USD balance bound spend. A caller who cannot pay now gets the honest 429 (with period + needed amount). model_weight survives only for track_usage() analytics. Free allowance: $0.50/month -> $0.10/day ~$3/month retail = $2.14/month of provider cost at 1.4x markup, ~6x the previous giveaway and still bounded per member (the 50-slot invite cap bounds the total; at full occupancy ~$107/mo or $2.14/user at 100% utilisation). The window is policy, not code: - admin/data/pricing-policy.json: free_tier_usd=0.1, free_tier_period="day" - wx11_billing: FREE_TIER_PERIOD, allowance_window(plan) (day for free, month for paid), period_start(window), spend_usd(session, window) β€” one definition used by BOTH the gateway and the billing API (the old month_spend_usd duplicate is what let them drift before) - gateway + /api/billing/subscription now report the same window; plan_allowance gains `period` - dashboard.html, newOS4 module-billing, chatstudio account panel say "today" when period=day - published copy: index.html, pricing.html (card + compare table + how-usage-works), i18n planFree1, admin/docs/ai-gateway.html - verify-public-claims.py asserts the number AND the period per plan: a right number on the wrong window is the same defect as a wrong number newOS4 bundle rebuilt (module-billing), LATEST_VER v4.6.135 -> v4.6.136, main.html cache keys bumped. Proof: - verify-public-claims.py rc=0 (copy == policy == live billing API, including periods) - /api/billing/subscription: limit_usd 0.1, period "day", remaining 0.1 - gateway: gemma4 200, kimi-k2-thinking 502 (gate gone; fake model => upstream 404) - smoke-auth PASS, smoke-gateway PASS (limit=$0.1 in the payload check) - live pages serve "$0.10 of AI usage / day", zero $0.50 left on index/pricing/subscriptions - allowance math: free cap_cost $0.0714/day, pro $7.7779/mo, team $23.35/mo, ent $77.85/mo
* feat(newos4): register `test` / `[` so chains decide locally instead of ↡Agando2026-10-293-1/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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): shell tokenises command lines quote-aware everywhere (v4.6.132)Agando2026-10-291-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | v4.6.131 taught the pipe stages to forward their raw text, but the shell's own wrapper still split each stage with `cmdText.split(/\s+/)` β€” so `grep "alpha beta" /qt.txt | wc -l` handed grep the two args `"alpha` and `beta"` and matched nothing. Measured live: the wrapper (ShellModule.init() replaces terminal.executePipeChain) is the implementation that runs, so the core fix alone is decorative. All six command-line split sites in modules/ext/shell.js now go through one helper, `_tokenize()`, which delegates to `security._parseArgs` β€” the parser the bare-command path already uses β€” so the two cannot drift again: 111 single command in a `&&` / `;` chain -> `grep "a b" f ; echo done` 176 pipe stage (the wrapper that runs) -> `grep "a b" f | wc -l` 358 `<` redirect input -> `cat < /f` 401 pipe chain inside the script runner -> also forwards the stage's raw text now 424 single command in the script runner -> also forwards raw text 778 `[ -f "path" ]` / `test -f "path"` -> quoted paths in test expressions Empty stages are skipped instead of being dispatched as a command named '' (`help | | wc -l`). The guard test grew the assertion that would have caught this: test-pipe-guard.js now checks `shellTokensQuoted` (tokeniser used, no `cmdText.split(`, empty-stage guard present) alongside `shellForwardsRaw`; both are RED against HEAD's shell.js. Verification node modules/test/test-pipe-guard.js -> {"empty":[],"blank":[],"classifyGuarded":true, "pipeGuarded":true,"shellForwardsRaw":true,"shellTokensQuoted":true} node modules/test/test-parse-args.js -> 28/28 Β· daily job -> node tests 21/21 pass newos4-maintain.py -> maintenance result: OK (index 349, help 363/363, changelog v4.6.132) live v4.6.132, read from the terminal DOM (no print monkey-patch): cat /qt.txt -> alpha beta grep "alpha beta" /qt.txt -> 1 match, 1 file grep "alpha beta" /qt.txt | wc -l -> 2 (match line + summary; 1 when the quotes are lost) grep "alpha beta" /qt.txt ; echo done-> match count + done ShellModule._tokenize(c) === security._parseArgs(c) for 5 sample lines -> true _tokenize('') / _tokenize(' ') -> [] / [] Also closes the last open feedback item: RPT-20261027-160751 ("Connection failed on all providers") -> resolve, recorded in decisions.jsonl with evidence (headless probe URL `?probe=ctx`, v4.6.117, single occurrence, no stack; Ollama answers 200 with 37 models now). Feedback queue: awaiting 0.
* fix(newos4): pipe stage quoting, maintain ordering, help cost row, plan ↡Agando2026-10-294-27/+413
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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-293-7/+23
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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): js console handles promise rejections + Node-builtin hint ↡Agando2026-10-291-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | (v4.6.128) evalJS() evaluated the expression bare and printed JSON.stringify(result), so a rejected thenable (e.g. `js import('fs')`) became `{}` on screen while the rejection escaped to window.onunhandledrejection and was auto-filed as a runtime bug report (RPT-20261028-172211-9c7f2adb, cluster 480x). evalJSFile() already had .then/.catch β€” the asymmetry was the defect. - modules/ext/console.js: thenable branch in evalJS() awaits and catches; new _printError() is the single error sink for all four catch paths and appends a hint when a bare specifier fails to resolve ("'fs' is a Node built-in; the browser has no Node β€” use the VFS (ls/cat/run, terminal.fs) instead"). - help.html: js / js-run rows say "browser only β€” no Node built-ins". - module-loader.js + main.html: v4.6.128. Verification (/tmp/test-console-fs.js, 11 asserts, new Function harness over the module): GREEN 11/11 β€” rejection prints one red error + hint, no unhandledRejection; resolved thenable prints its value; sync value/throw unchanged; js-run file + missing file OK. RED 4/11 against .bkp-console-20261029_121243/console.js β€” A1/A2/A3 print `{}`, A4/G2 leak "Failed to resolve module specifier 'fs'". node --check modules/ext/console.js β†’ OK. console.js is loaded separately via extKeys (not in critical-ext-bundle.js), so no bundle rebuild β€” proven by `grep -c JSConsoleModule modules/ext/critical-ext-bundle.js` = 0. Consumes STEER-20261028-195043-8084. Plan: .hermes/plans/2026-10-29_063000-newos4-js-console-fs-specifier.md
* fix(newos4): `if test …` was always false and `for` items never globbedAgando2026-10-271-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | Found by running the shipped runner the way a user would, not by re-reading it. - _evalTest() stripped [ ] but not the `test` keyword, so `if test -f /work/alpha.txt` evaluated the word "test" as an operator, returned false, and every such branch took the else arm β€” measured live: "alpha missing" for a file that exists. Both spellings work now (`test -f x`, `[ -f x ]`), plus `-d`, `-e`, `-z`, `-n`, `!` and the = / != comparisons. - `for f in /work/*.txt` ran ONCE with $f set to the literal pattern, because only command arguments went through _expandGlobs. Loop items are expanded per iteration now, so the body runs once per matched file with the real path. - _expandGlobs() falls back to readDir() when a fs has no ls(), so the helper works against either shape instead of silently returning the literal pattern. Verification, live on v4.6.119: - for+glob: "--- /work/alpha.txt / 3 /work/alpha.txt / --- /work/beta.txt / 1 /work/beta.txt" (6 statements, exit 0) β€” previously one iteration printing the pattern. - conditionals: "alpha exists", "nope missing", grep + $? intact. - end-to-end /work/report.sh (loop + glob + >> append + test -f) wrote a correct log: "start / file: /work/alpha.txt / 3 /work/alpha.txt / file: /work/beta.txt / 1 /work/beta.txt …". - unknown command still reports "command not found" and never calls the AI. - test-shell-script.js 27 -> 35 asserts. Retracted: I reported `echo x > file` as broken. It was my probe monkey-patching terminal.print, which bypassed the capture stack the redirect uses. Both paths write correctly: direct helper -> "direct-call", prompt -> "prompt-call\n".
* fix(newos4): send num_ctx to Ollama, and make one auto-connect failure read ↡Agando2026-10-274-21/+57
| | | | | | | | | | | | | | | | | | | | | | | | | | as one line Two things I had flagged as "worth your attention" and one leftover nit. - num_ctx was never sent. Ollama's server-side default context is 2k-4k depending on the build, so a system prompt + 20 messages + up to 24k chars of tool results could be truncated server-side, which is exactly the "it forgot what it just read" class of report. Every Ollama request now carries options.num_ctx (default 16384, overridable via webx11_ollama_numctx, floor 512); the OpenAI-compatible branch is untouched because num_ctx is not one of its parameters. - Automatic connect prints one outcome instead of one line per endpoint tried (7 -> 3 on a dead endpoint). Interactive `ollamaconnect` keeps the per-endpoint detail. - write_file's failure message no longer echoes a trailing slash. Verification: - test-ollama-numctx.js (7 asserts): generate and chat payloads carry num_ctx=16384, a stored override is used, junk/too-small values fall back, and the /v1/chat/completions branch does not receive it. RED on HEAD: TypeError reading 'num_ctx' of undefined. Suite 20/20. - Live v4.6.117, capturing the real request: {"num_ctx":16384} on /api/generate with model gemma4:31b-cloud, reply "OK". - Live v4.6.118, dead endpoint (127.0.0.1:9, fallback disabled): automatic -> 3 lines ("connecting on first use…", "Connecting to dead…", "Auto-connect failed β€” …"); manual connect() -> 4 lines including "endpoint http://127.0.0.1:9 failed", so the suppression is conditional, not blanket.
* fix(newos4): piped grep searched the working directory instead of its inputAgando2026-10-273-1/+33
| | | | | | | | | | | | | | | | | | | | | | | | The stdin work in 5cf5010 fixed wc/head/tail but left grep alone, and grep's no-operand branch sweeps the cwd. Measured on v4.6.115: `cat /gdir/in.txt | grep zzz` answered `other.txt:1: zzz in a cwd file` β€” a file that was never piped, and `cat in.txt | grep bbb` reported the match twice (`in.txt` from the cwd sweep plus `__pipebuf__` from the injected file). - module-fs.js: when terminal._pipeInput is set and no file operand was given, grep searches the staged text and prints `<line>: <text>` (stdin has no filename, as in GNU). The cwd sweep stays for a bare interactive `grep x`. - shell.js: _isPipeAware now covers only sort/cut/uniq/expand. grep/head/tail/wc read _pipeInput themselves, so injecting the __pipebuf__ file produced duplicate hits and named stdin results. Verification: - test-shell-stdin.js 20 asserts (+4 for grep: no cwd match, non-match reports none, match prints no filename, -c counts stdin). RED on HEAD: G1/G2/G3 fail with `other.txt:1: zzz in a cwd file`. - Suite 18/18 files; maintain --fix "node tests 18/18 pass". - Live v4.6.116 after `cd /gdir` (where other.txt contains zzz): `cat /gdir/in.txt | grep zzz` -> (no matches) Β· `| grep bbb` -> `2: bbb` Β· `| grep -c b` -> 1 Β· `| head -n 2` -> aaa,bbb Β· `| tail -n 1` -> ccc Β· `| wc -l` -> 3. Credit: the false-positive grep was reported by the stdin subagent as an aside; re-derived here against the live terminal before fixing.
* feat(newos4): the script runner runs scripts β€” control flow, variables, ↡Agando2026-10-273-23/+108
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | globs, redirects, stdin MAINT-021 and MAINT-017, executed from .hermes/plans/2026-10-27_145500-newos4-script-runner.md. - shell.js: new block parser (_parseBlocks) and executor (_runInstructions/_runSimple/_runCommand/ _dispatchOne). `for`/`while`/`if` bodies now run, including one-line `if [ -f x ]; then y; fi`. Variables expand through _expandVars ($VAR, ${VAR}, $?, single quotes literal), globs expand through _expandGlobs (unquoted args only, per-command), and `>`/`>>` capture the command's output into the VFS instead of being passed to the command as text. - shell.js: a script never answers an unknown command by calling the AI any more. It prints `script:<path>:<line>: <cmd>: command not found` and sets $? β€” the AI fallback stays for typing. - shell.js: unclosed blocks and bad `for` headers surface as `Script <path> failed: …` (the parse used to sit outside the try, so a bad script crashed the runner). - shell.js: interactive path gets the same treatment β€” prompt globbing and `echo hi > /f` now work. Cause of the asymmetry: the two code paths had separate dispatch. - shell.js: `_isPipeAware` no longer injects the __pipebuf__ file into wc (it reads _pipeInput), and the module registers itself with a versioned URL and calls its own init() β€” registerModule() does not call init(), so the executeCommand wrapper was never installed from the boot bundle. - module-fs.js: wc/head/tail/grep read piped stdin when no file operand is given; `cat` accepts several operands (`cat *.txt`); `ls <file>` names the file instead of printing "(empty)". - main.html: shell.js removed from the extension list β€” it is already inside critical-ext-bundle.js, and loading it twice produced two ShellModule instances that could disagree about their version. Verification: - test-shell-script.js 27 asserts (parser, variables, globs, executor, redirect, command-not-found, one-line if, parse error). RED on HEAD: `TypeError: shell._parseBlocks is not a function`. - test-shell-stdin.js 16 asserts (added by a subagent): wc -l/-c, head -n, tail -n over stdin, and the file-argument path unchanged. - Suite 18/18 files; maintain --fix "node tests 18/18 pass". - Live on v4.6.115, through the real executeCommand: `ls /n*.txt` -> /n1.txt Β· `cat /n*.txt` -> one|two Β· `echo hi > /r.txt` then `cat /r.txt` -> hi Β· `cat /probe.txt | wc -l` -> 3 (bare, no __pipebuf__) Β· `script /run.sh` -> item 1 / item 2 / item 3 / exists / command not found, exit 1, no AI routing.
* fix(newos4): the loop guard reads the config it documentsAgando2026-10-273-13/+49
| | | | | | | | | | | MAINT-004. The guard read warnAt/blockAt/hardStopAt while one config path wrote `threshold`, and `enabled` was never consulted anywhere β€” writing {"enabled": false} into webx11_ai_config changed nothing at all. One resolver, _loopPolicy(), now normalises both spellings, guarantees warnAt <= blockAt < hardStopAt, and both the block and the warning are gated on enabled. Live on v4.6.110 (_loopPolicy called in the page): defaults {warn 3, block 6, hardstop 9}; {threshold: 4} -> {warn 2, block 4, hardstop 7}; {enabled: false, threshold: 3} -> enabled false. test-loop-policy.js: 17 asserts, RED on HEAD ("_loopPolicy is not a function"), suite 16/16.
* fix(newos4): agent registration is absolute-path and honest about what it storedAgando2026-10-275-43/+129
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Three of the four agent-cluster items, each with a test that is red on HEAD. - MAINT-008 (module-agent-registry.js + module-orchestrator.js): registerAgent took args[0] as the agent id and only args[1] as the capability list, while both orchestrator callers passed the command word first β€” so the boot log read "Agent 'register' registered with 1 capabilities" and four capabilities were stored as one. The command word is now stripped, args[1..] are joined and split on commas (both "a,b" and "a b c" work), and the callers pass [id, ...caps]. - MAINT-007 (registry + aiassist): mkdir/rm used relative paths, so a registration made from /docs created /docs/agents/<id> and an unregister from any other cwd removed nothing β€” the leaked directories the report suspected. Every path in the registry (and the four /_sessions writes in module-aiassist.js) is absolute now. - MAINT-009 (listAgents): a directory alone counted as a registered agent, so a hand-made /agents/foo appeared as "foo β€” unknown (0 capabilities)". Only a readable status.json with an id counts now; the rest are reported as unregistered directories. Verification: test-agent-registry.js (11 asserts). RED on HEAD: 9 of 11 fail, including the exact `βœ… Agent 'register' registered with 1 capabilities` line and `hand-made β€” unknown (0 capabilities)`. Suite 15/15 files green; maintain --fix "node tests 15/15 pass". Live on v4.6.109: boot prints "βœ… Agent 'orchestrator-0' registered with 4 capabilities"; `agent-list` warns "1 directory(ies) in /agents have no status.json (not registered): hand-made"; register from / with a comma list stores 3; unregister from /docs removes the directory and leaves no /docs/agents. MAINT-006 stays open with a narrower note: the hard stop was only reachable when every call in a batch was blocked, so a mixed batch (one repeated + one novel call) never ended. The same ceiling now applies on the mixed path, but that branch lives inside agenticLoop and is not covered by a unit test β€” it needs a live agent run that repeats a call.
* fix(newos4): help names the owner of an undocumented command; loop lessons ↡Agando2026-10-274-35/+73
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | leave /skills Two board items plus the context-window close-out. - MAINT-019 (module-system.js): `help <cmd>` answered "No help available for X" for a command that is registered but has no help line, which reads as if the command does not exist. It now says "X β€” registered by <Module>, which publishes no help line for it. Try: help". A command that genuinely does not exist still gets the old message. - MAINT-020 (module-aiassist.js): auto-generated loop lessons were written into /skills with their own index entries, so `skill list` filled up with system noise. Lessons now live in /lessons with /lessons/index.json; the reader still falls back to legacy loop-lesson-* entries in /skills for one release, and the one-time migration deletes them from the skills index. Verification: test-help-and-lessons.js (10 asserts, includes a lint-style guard that no lesson is written into /skills). Suite 14/14 files green; maintain --fix "node tests 14/14 pass", "help coverage 362/362". Live on v4.6.108: `help agent-register` -> "registered by AgentRegistryModule, which publishes no help line for it"; `help a2a-prune` unchanged; `help nope-not-real` still "No help available". MAINT-001 close-out (no code change here): history is capped at 20 messages with a "[context trim]" marker (first trim at turn 11 in a 60-turn probe), token compression took 80 msgs/8080 tok to 17/2625, and since v4.6.107 one turn's tool bodies are clipped to 6 000 chars each and elided past 24 000 with the newest 4 kept verbatim. Residual, recorded on the board: maxContextTokens is never sent to the server as num_ctx, so the real window is whatever the endpoint defaults to. MAINT-018 close-out: not reproducible. Live probes on v4.6.107/108 β€” `sed -n '1,3p' /probe.txt` prints lines 1-3, `sed -n "2,4p"` prints 2-4, `grep 'line4'` matches, and the same commands inside `script /q.sh` print correctly. The "Command not found: for;" symptom belongs to the line-by-line script runner, which is MAINT-021 and stays open.
* fix(newos4): bound one turn's tool context, honest skill-create, quarantine ↡Agando2026-10-274-5/+135
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | a corrupt memory graph The three items the last review and probes left open, each with a test that is red on HEAD. - MAINT-023 (module-aiassist.js): one agentic turn grew without limit β€” every role:'tool' body was pushed verbatim and only this.context was trimmed BETWEEN turns (measured 6 -> 112 705 tokens across 8 model calls against maxContextTokens 12 000). Observations are now clipped to maxToolResultChars (6 000) with a note saying how much was withheld and to re-read with offset/limit, and _boundToolContext() elides the oldest tool bodies once the running total passes toolContextBudgetChars (24 000), always keeping the newest 4 verbatim. The ceiling is turn-independent: the same ~26 k chars at 8 turns and at 20. - MAINT-024 (modules/ext/skill-pack-core.js): the command printed "βœ… Skill created" with fs.writeFile()'s boolean discarded (probe: fresh VFS, success printed, no file), stripped invalid characters instead of rejecting the name (My-Skill -> y-kill, DevOps_Thing -> evpshing), and treated an unreadable /path argument as the skill body. Now: kebab-case is validated (uppercase normalises and the final name is reported; anything else is refused), /skills is created on first use, the write and index results are checked, and an unreadable body path is an error. - MAINT-003 (module-memory-graph.js): a corrupt store looked exactly like a fresh install β€” fsReadJSON returns null on a parse failure, so 85 nodes could vanish with no warning and no copy. The probe confirmed the loss: seed 6 nodes, truncate nodes.json and export.json, reload -> 1 node, no message, nothing in .trash. _quarantineCorrupt() now keeps the unreadable file as <name>.corrupt, warns, and then loads from whatever remains. Tests: test-tool-context-bound.js (11), test-skill-create.js (16), test-memory-graph-recovery.js (8). RED on HEAD: tool-context crashes on _clipToolResult (exit 1), skill-create 16 assertions fail on the missing file/index, memory-graph B1/B2/C1 fail (no warning, no .corrupt copy). Suite now 13/13 files green; maintain --fix reports "node tests 13/13 pass". Live on v4.6.107: skill-create My-Skill -> βœ… "my-skill"; DevOps_Thing -> Invalid skill name; /does/not/exist.md -> Cannot read skill body; only my-skill in the index.
* fix(newos4): name the real Ollama failure, bound auto-connect, fix the ↡Agando2026-10-274-43/+141
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | review findings Second pass over v4.6.105, driven by an independent review that reproduced each item. - ollama-manager.js: chat() now reports the captured cause ("Ollama error: Failed to fetch", "…: HTTP 500") instead of the generic "No provider responded"; _lastChatError is reset per call so an old reason is never replayed. chatWithTools carries the reason in its error string. - ollama-manager.js: _ensureConnected() bounds connect() with a deadline (connectTimeoutMs, default 10 s). connect() uses a bare fetch(), so a blackholed endpoint previously left the shared in-flight promise pending forever and hung every later message β€” the old code failed fast. The attempt is now try/finally, so a throwing terminal.log() can no longer poison _connecting and suppress the retry on the next message. - ollama-manager.js: an automatic connect no longer silently rewrites the user's saved model; the previous choice is restored in memory and in localStorage, and the user is told. - ollama-manager.js: the DeepSeek empty-content branch returned top-level tool_calls with content '', so the caller pushed an assistant message with no tool_calls and then a role:'tool' reply β€” an orphan tool message, which is the HTTP 400 that branch exists to prevent. tool_calls now live inside the message as well. - module-aiassist.js: a no-argument list_files returned the ROOT instead of the cwd (readDir('/') instead of ls()); it now means the current directory again. Error text keeps the path as the caller wrote it, and write_file distinguishes a read-only virtual path (/sys/…) from a missing parent instead of blaming the parent. Tests: test-ollama-error-text.js (new, 14 asserts, RED 9/5 before the edit), test-ai-fs-tools.js 7 β†’ 16 asserts (write onto a directory, /sys write, list a file, '/', no-arg cwd, relative path), test-ollama-autoconnect.js 11 β†’ 19 (blackholed endpoint settles + retries, saved model survives; the old "connecting hint" assertion is now falsifiable β€” the reviewer showed it also matched connect()'s pre-existing line). Suite: 10/10 files green. Live on v4.6.106: healthy auto-connect 42 ms with the hint; blackholed endpoint settles in 1242 ms with connectTimeoutMs=1200, prints "timed out" and "unreachable", _connecting cleared.
* fix(newos4): connect to Ollama on first use, honest write_file, list_files ↡Agando2026-10-274-82/+112
| | | | | | | | | | | | | | | | | | | | | | | | | | | sees /sys Three defects, each re-probed first and covered by a test that is red on HEAD. - ollama-manager.js: nothing connects at boot; only the Ollama tab button called connect(), so a user's first message died with "Ollama not connected. Use ollamaconnect first." even when the endpoint was healthy. New _ensureConnected() runs from chat(), chatStream() and chatWithTools(): one attempt, shared by concurrent callers, a "Connecting…" hint, and only then the error β€” which now says the endpoint is unreachable instead of telling the user to run a command they already expected to work. - module-aiassist.js write_file tool: fs.writeFile()'s false return was dropped and the tool answered "File written: …" anyway, sending the model on to a read that found nothing (MAINT-012). It now returns "Write FAILED: … β€” parent directory missing or target is a directory; nothing was written". - module-aiassist.js list_files tool: the hand-rolled walk over root.content never resolved the virtual mounts, so /sys β€” a directory the UI and `ls` both show β€” answered "Directory not found" (MAINT-011). It now uses fs.readDir(), which resolves mounts and returns null for a genuinely missing path. Tests: modules/test/test-ai-fs-tools.js (7 asserts) and modules/test/test-ollama-autoconnect.js (11 asserts). RED on HEAD: ai tools 4 passed/3 FAILED (/sys β†’ "Directory not found", write_file β†’ "File written: …"), ollama 2 passed/9 FAILED ("Use ollamaconnect first"). GREEN now 7/7 and 11/11; maintain --fix reports "node tests 9/9 pass". Live on v4.6.105: connectedBefore=false, _ensureConnected()=true in 41 ms, 37 models.
* fix(newos4): never overwrite a directory node; ls honours a path after its flagsAgando2026-10-274-9/+51
| | | | | | | | | | | | | | | | | | | | | MAINT-010 and MAINT-016, the two high items the review left open. Both were re-probed before the change and the new test is red on HEAD (5 passed, 7 FAILED). - virtual-fs.js rename()/copy(): they assigned dst.dir.content[dst.name] with no destination type check, so `mv file /docs` replaced the /docs node and every child vanished while the command printed "Moved: …". Refuse when the destination is a real directory node (the virtual /sys entry is exempt). rename() also erased the file on `mv a a`, because assigning and then deleting the same key removes it; that path is now an explicit no-op. - module-fs.js mv/cp: a refusal used to surface as "No such file or directory", which sent the operator looking for a missing source. The destination is now inspected first and the message says "is a directory β€” refusing to overwrite it". - module-fs.js ls: the path was only read from argv[0], so `ls -l /work` listed the cwd. The first non-flag argument is now the target; the path-first form is unchanged. Tests: modules/test/test-fs-mv-cp-guard.js (12 asserts) β€” RED on HEAD (A1-A3, B1, B2, C2, E1), GREEN now; maintain --fix reports "node tests 7/7 pass". Browser on v4.6.104: "mv: /vtest: is a directory β€” refusing to overwrite it", child and source intact, "ls -l /vtest" lists /vtest (keep.txt), not the cwd.
* fix(newos4): rollback keeps scaffolding, checkpoints cannot collide, tests ↡Agando2026-10-274-15/+81
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | gate the build Review of the 08:30 implementer run (v4.6.102, checkpoint name fix). The shipped fix was correct; these are the defects it introduced or left behind, each measured first. - virtual-fs.js rollback(): the snapshot strips _snapshots/_sessions/.trash for bloat. Restoring it verbatim deleted every checkpoint, every agent session and the trash, made rollback irreversible and unrepeatable. Re-attach the live copies after the root swap. - checkpoint(): names are millisecond-resolution, so tight calls overwrote each other (measured 6 calls -> 3 snapshots). Dedupe with a -N suffix, refuse names containing "/" (written but unlistable), and prune by snapshot createdAt instead of filename sort, which let a custom name ("aaa-oldest") evict newer auto-named checkpoints. - module-predictor.js: legacy pre-v2 points have no `usable`, so a 0.079 s aborted run re-entered similarity as evidence and floored every prediction at 1 s. Backfill `usable` from the measurement on load, drop the ones it rejects, add a belt-and-braces guard in _findSimilar and count only usable points as actual executions. - newos4-maintain.py: run every node-runnable modules/test/test-*.js on each maintenance pass. The v4.6.102 regression was written but no job ever executed it. - newos4-steer.py: `done` now requires --commit <sha> that exists in the repo, and gains `reopen`. A written plan was being closed as done at 06:17 while the predictor fix shipped nowhere, so the implementer's pre-flight queue was empty. - steering.json: predictor item reopened, checkpoint item stamped with f7249ff. - maintenance board: MAINT-002/005/015 were fixed in v4.6.81-85 but still listed open/high; closed with their commit ids, and MAINT-010/016 annotated with the probes that keep them open. Tests: test-vfs-checkpoint 23/23 (RED 14/9 before), test-predictor-accuracy 38/38 (T1-T3 RED on HEAD), maintain --fix all PASS, browser round-trip "Checkpoint created: <name>" -> "Rolled back to checkpoint: <name>" on v4.6.103. Also carries the already-deployed 2026-10-26 syntax-gate hunk in newos4-maintain.py (syntax_failures + gated rebuild) that had never been committed.
* fix(newos4): print the checkpoint name actually saved (auto)Agando2026-10-273-8/+6
| | | | | | | | | | | | | | | | | | | | | | | | | | | Task 2 + ship of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md. module-system.js `checkpoint` read the clock a second time for the printed name, so the name in "βœ… Checkpoint created: <name>" differed from the name on disk by >=1ms whenever no explicit name was given. It now prints the value returned by fs.checkpoint(). try/catch and error strings unchanged. Bundles rebuilt (rebuild-bundles.py); LATEST_VER in module-loader.js and all three ?v= tags in main.html (core-bundle, module-loader, critical-ext-bundle) bumped v4.6.101 -> v4.6.102. Verification: $ node --check modules/core/module-system.js SYNTAX OK $ node --check modules/core/core-bundle.js BUNDLE SYNTAX OK $ node modules/test/test-vfs-checkpoint.js (x5) test-vfs-checkpoint: 10 passed, 0 failed $ curl -s https://webx11.duckdns.org/spaces/newos4/main.html | grep -o 'core-bundle.js?v=[^"]*' core-bundle.js?v=v4.6.102 $ python3 scripts/newos4-maintain.py PASS: version v4.6.102 x3 in main.html (expect 3) ... maintenance result: OK $ python3 scripts/newos4-skill-audit.py skill audit: 7/7 pass
* fix(newos4): checkpoint() returns resolved name (auto)Agando2026-10-271-1/+1
| | | | | | | | | | | | | | | | Task 1 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md. virtual-fs.js checkpoint(name) generated an ISO name when none was given but returned bare `true`, so callers could not know which name the snapshot was written under. It now returns the resolved name (string). Prune (max 3) and saveToStorage() behaviour unchanged. Verification: $ node --check modules/core/virtual-fs.js SYNTAX OK $ node modules/test/test-vfs-checkpoint.js test-vfs-checkpoint: 9 passed, 1 FAILED (remaining FAIL was the module-system display path, fixed in the next commit)
* feat(newos4): cut/copy in Files, paste into WebDAV (Issue D2)Agando2026-10-261-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | The two panes kept separate clipboards (filemgr._clipboard vs webdav._selected), so "cut here, paste there" did not exist β€” the operator's last phone-test item. filemgr.js _openOverlay() publishes a read-only view of its clipboard on the terminal object: webx11.fileClipboard() / fileClipboardClear() / fileClipboardSet(paths, mode). webdav.js the bulk bar gains "πŸ“₯ Paste N" (hidden when the clipboard is empty). Handler: for each path readFile -> _webdavRequest('PUT', dest + '/' + name, {body}); only 200/201/204 counts. For mode "cut" the local file is deleted AFTER the server accepted the bytes, never before. A failed PUT keeps the local copy, prints "πŸ“₯ Paste failed: <name> β€” HTTP n", and leaves that path in the clipboard so "πŸ“₯ Paste 1" is a retry. Successes leave it. Summary "πŸ“₯ Pasted N item(s) to <path>" (+ "β€” M failed"), then the pane reopens there. Semantics: cut moves (upload then delete local), copy leaves the local file and keeps the clipboard so the same files can go into several folders. Verified with the transport stubbed in the live page (this browser has no WebDAV credential): button "πŸ“₯ Paste 2" for a 2-path cut PUT ok (201) -> local file null (moved) PUT fail(500) -> local file "BBB" (kept) clipboard after -> {mode:"cut", paths:["/fail-d.txt"]}, button "πŸ“₯ Paste 1" node --check both modules, no-undef clean (55 files), v4.6.101 x3 in main.html, syntax OK, help 362/362.
* fix(newos4): four phone-test defects β€” no preview close, single-file ↡Agando2026-10-261-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | share, dead-end fullscreen, silent WebDAV move All four came from the operator's phone pass on v4.6.95 and each collapsed to a line of code. A "No file selected to download": the preview pane had no close control; the only βœ• was the file manager's own, and it calls _closeOverlay() which wipes _multiSelect/_selectedFile/_selectMode. On a phone the preview covers the list, so closing it before pressing Download always emptied the selection. The toolbar now starts with βœ• -> _clearPreview(), which keeps the selection and leaves fullscreen first; Download with no targets names the fix instead of stating a dead end. B share/copy: πŸ”— existed only in the preview toolbar and took a single path, and after the Android share sheet there was no copy affordance. Added _sharePaths(paths, mode) used by both the toolbar (1 path, πŸ“‹ copy and πŸ”— share) and a new action-bar πŸ”— Share (whole selection), plus _inlineLinkBox() which renders a selectable input/textarea with all URLs when neither the share sheet nor the clipboard is available. No prompt() anywhere on this path. C fullscreen: requestFullscreen targeted .fm-preview-body, which sits INSIDE the pane but excludes the toolbar, so β›Ά and βœ— disappeared and only the OS back gesture got out. Now fullscreens #filemgr-preview β€” toolbar, βœ• and β›Ά stay on screen and β›Ά toggles. D webdav move: bulk Move asked for the destination with prompt(), which returns null in iOS standalone PWAs and most Android WebViews, so it silently did nothing and looked unimplemented. Destination is now an inline row in the bulk bar (44px input + "Move here"/"Cancel", prefilled). Verified on the served code: ext bundle md5 5f440c77b6ab5529729aee8019a6c351 (served == local), webdav.js carries wdav-move-dest x2, and a headless browser probe on the real page shows the toolbar as ["βœ•","β›Ά","πŸ“‹","πŸ”—"], closing the preview prints "Preview closed β€” selection kept" with the selection intact, selecting 3 files and copying yields a textarea with the 3 real WebDAV URLs, and Download with nothing selected explains what to do. 6 PASS on the URL test, no-undef clean, v4.6.99, help 362/362. Not done, deliberately: cut/copy + paste across the VFS/WebDAV border. Recorded as Issue D2 in the plan with the exact insertion points, the upload-then-delete ordering rule and the failure case.
* feat(newos4): mobile download/share UX β€” deferred blob revoke, ↡Agando2026-10-261-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | multi-select, Web Share, 44px targets Executes .hermes/plans/2026-10-24_051600-newos4-mobile-download-share.md (approved via the feedback dashboard; marker autonomy: safe). Front-end only, v4.6.94 -> v4.6.95. T1 modules/ext/filemgr.js β€” URL.revokeObjectURL ran in the same tick as a.click(); iOS Safari and Android Chrome hand the blob: URL to the download manager asynchronously, so the transfer was aborted while the status line still said "Downloaded". Now revoked after 30 s. T2 download honours the multi-selection via _getSelectedPaths() (falling back to _selectedFile), reports "⬇ Downloaded N files" and "❌ Skipped <path>" instead of a silent partial success. T3 share: navigator.share -> clipboard.writeText -> inline read-only <input> (focused, selected). prompt() is suppressed in iOS standalone PWAs and most Android WebViews, which is why tapping πŸ”— did nothing. Unresolvable space now reports "πŸ”— Share unavailable β€” no space resolved". T4 both preview toolbar buttons: min-height/min-width 44px, touch-action manipulation, 1em, gap 10px (was 24px/0.85em/6px β€” the smallest tap targets in the UI). T5 _previewShareUrl falls back _resolveSpaceName -> _detectSpaceName (webdav.js:22) -> page path, and returns null rather than emitting /spaces//<path>. T6 the URL helpers are now module-scope pure functions exported for Node, so modules/test/test-download-url.js tests the shipped code instead of a local copy (4 -> 6 cases). Also fixes a latent bug that made require() impossible: `typeof window.webx11` throws under Node because typeof does not protect a member access on an undefined global. Verified on the served bundle (filemgr.js is concatenated into critical-ext-bundle.js, NOT core-bundle.js β€” the plan's T7 grep pointed at the wrong file): HTTP 200, 129 867 B, 2x navigator.share, 3x fmBuildShareUrl, deferred revoke present, 11x min-height:44px. Live browser probe on /spaces/newos4/main.html: _previewShareUrl('docs/a.md') -> https://webx11.duckdns.org/spaces/newos4/docs/a.md (no double slash); with navigator.share absent and clipboard blocked, clicking πŸ”— produced .fm-share-input containing that URL; button style ends min-height:44px;min-width:44px;touch-action:manipulation; toolbar gap:10px. node modules/test/test-download-url.js -> 6 PASS. lint-no-undef clean (55 files). maintain: PASS version v4.6.95 x3, syntax OK, HTTP 200 /main.html. Also adds admin/docs/session-overview-2026-10-26.md (glm-5.3-compressed session record).
* feat(feedback): review dashboard, public guide, terminal commands (v4.6.94)Agando2026-10-263-3/+317
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Dashboard (/admin/feedback.html, admin-gated) - User-feedback tab over the live :8908 API: list, filter, search, publish reply, archive/reopen. Reply is what the reporter sees in /stear -> Your reports & replies. - Agent-items tab: 3 plans parked at autonomy:review + 19 defect reports, one click each. - Snapshot age line + stale warning (>30 min), and the last apply outcome with failures. - Existing /admin/ tab relabelled "Client error log" (it renders feedback_log.json, not the feedback store) and now links to the review dashboard. Decision path - admin/api/feedback-decide.php: exact-match nginx location + auth_request, Origin check, item_id allowlist, append-only decisions.jsonl with fwrite+fsync verification. - scripts/newos4-feedback-apply.py (darkbot): last decision per item wins, superseded rows recorded, reject reverts safe->review, per-item try/except, ledger saved per mutation, realpath containment, exact standalone-line marker flip, file mode preserved, queue rotation at 1 MB. Steering directives now carry the report title. - scripts/newos4-feedback-collect.py: snapshot for the dashboard, 40-line plan head, steering shape assert (-1 = unknown), recent_apply outcomes. - newos4-feedback-sync.sh: flock -n so overlapping runs cannot double-apply. Guide + terminal - feedback-guide.html: which channel (terminal feedback / /stear / automatic reports), expectations, exact payload per channel, how to write a report that gets fixed. Linked from /stear, index.html footer and help.html. - newOS4: feedback list|read|reply|archive|reopen|stats|dashboard|help (+ feedback-admin alias), admin-token derived per call, argument-validated dispatch so a sentence like "feedback list is broken on mobile" is still a message, "feedback -- <text>" forces it. - help.html: 8 rows, coverage 362/362. Review: glm-5.3 REWORK -> 15 findings fixed, 5 refuted with evidence, 2 doc-only. Guide corrected (invented "60 MB per batch" replaced by the real 512 KB/5-per-10-min limits).
* feat(newos4): context window warns at 80% and says what trimming dropped ↡Agando2026-10-233-7/+53
| | | | | | | | | | | | | | | | | | (v4.6.91) P0-5 asked what to do about a 422k-token session against a 400k threshold. There is no 400k threshold in this tree β€” the budgets are maxContextTokens (12000) and maxContext*2 messages β€” so rather than chase a number that does not exist, this applies the decision that is defensible either way: warn before the cliff, never lose history without saying so. - 80% of the token budget prints one warning per crossing (no per-turn nagging) - the message-count trim unshifts a marker naming how many messages left the window and quoting the earliest one, instead of splicing them away silently - 'max-context-tokens (default: 2000)' now reports the real code default, 12000 Verified: 900/1000 tokens -> warns once, second call silent; 1500/1000 -> compresses to 17 messages with the summary head intact; 9-message window with maxContext=2 -> 4 messages, marker '[context trim: 6 earlier message(s) ... earliest was: "turn 0 ..."]'.
* fix(newos4): the virtual /sys entry leaked into listings, trees and counts ↡Agando2026-10-234-13/+45
| | | | | | | | | | | | | | | | (v4.6.88-90) One phantom entry, three symptoms: - ls() injected 'sys' whenever it was called without a path, so 'cd _agents; ls' advertised a sys directory that does not exist, and list_files('_agents/sys') answered 'Directory not found' β€” the reported false negative. Injection is now gated on dir === root. - tree() pushed the same virtual name into the walk and dereferenced it, throwing mid-output on every root listing. Guarded. - _gatherFileInfo() walked into /sys and counted a synthetic subtree: 146 files / 114 dirs for a 44-file / 15-dir store. Virtual entries are skipped. Verified: root still lists sys; _agents and skills do not; tree('') completes; world model total (44) equals an independent walk of fs.root (44), agree: true.
* fix(newos4): audit names broken refs instead of counting documentation ↡Agando2026-10-231-1/+1
| | | | | | | | | | | | | (v4.6.86-87) The reference checker ran /skill-view \S+/ over every skill's prose, so 'skill-view <name>' and a quoted example in skills-system-guide were counted as missing skills: 'Broken refs: 2' with no names, both phantom. Placeholders and quoted names are now ignored (trailing punctuation stripped first), and the survivors are listed in the audit line and in the suggestion. Verified: clean tree -> Broken refs: 0. A probe skill referencing 'ghost-skill' -> Broken refs: 1 (probe-broken-ref β†’ ghost-skill), while '<name>.' and '"module-management"' in the same file are ignored.
* fix(newos4): the audit stops inventing '999d unused' (v4.6.85)Agando2026-10-233-5/+11
| | | | | | | | | | | | getStaleSkills() returned daysUnused=999 for a skill that had useCount 0 and no lastUsed β€” a placeholder rendered by the audit as '999d unused', indistinguishable from a measurement. It told the operator to archive procedural memory that had simply never been instrumented. Now: daysUnused null + neverUsed true when there is no lastUsed; real day count otherwise; the audit prints '(never used)'. Verified: probe skill with no telemetry -> {daysUnused: null, neverUsed: true}; one last used 2026-08-01 -> 83; no 999 anywhere in getStaleSkills() or the audit output.