aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* docs: boot-path audit (dav-auth unit), the two checker fixes, and the ↵HEADv4.6.168mainHermes SysOp12 hours1-2/+6
| | | | scheduled cold start
* fix(tooling): two checkers stopped manufacturing false audit signalsHermes SysOp12 hours2-11/+141
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Both defects are parser bugs in tooling, and both were producing a signal the operator sees on every run. Plan 2026-11-13_062052, implemented as written. 1. `help coverage` WARN was a quote-pairing artefact (every run, always 367/368). `newos4-maintain.py:238-239` captured `this._baseWhitelist = [...]` with a regex and then pulled `'([^']+)'` out of it. A JS comment inside that array literal contains the apostrophe in "don't" (terminal.js:16), which desynchronised pairing: it matched forward to the opening quote of 'ui' and the pseudo-command `t work"\n // …` joined the audited set — so a genuine undocumented command was indistinguishable from parser debris. Fix: strip `/* */` and `//` before extracting, and keep only tokens matching `^[a-z][a-z0-9_-]{0,40}$`. The same shape bug existed for `const internals = [...]` and is fixed by the same helper. The WARN list is now single-line (newlines collapsed) so one bad token cannot reflow the digest. Proven both ways: injected `'zzz-probe-cmd'` into the whitelist -> `WARN: help coverage 367/368 — undocumented: zzz-probe-cmd`; reverted (md5 verified) -> `PASS: help coverage 367/367`. 2. Auto-review "still present in shipped code" fired on generic tokens. `STEER-20261112-195037-1eed [high] cmds.indexOf is not a function` was approved because `identifiers()` kept `indexOf` and `function` (hits: 36 and 74 files) and R4 approved on `any()` present token. `isCommand` — the only name that identifies the fault — was never considered, because stack frames were not parsed at all. It does not exist anywhere in the tree. Fix: JS keywords/builtins/DOM handler names join the skip set; `stack_functions()` extracts frame names (dropping `onclick`, `anonymous`, `eval`); R4 now requires a **distinctive** token (>=6 chars, present in modules/, not generic); and a stack whose TOP frame is `<anonymous>` is classified as generated code (`js-run` / `new Function`), which can never justify "still present in shipped source". The bottom `HTMLButtonElement.onclick (…/main.html)` frame proves only that a click started it — deciding on "does any frame name a real file" got this wrong once already. Proven: `identifiers('cmds.indexOf is not a function')` -> `[]`; `distinctive(['indexOf','function'], {...})` -> `[]`; the real report replayed into a fixture reports `nothing provable (1 left for the operator)` with **no** decision row written; production dry-run `nothing provable (0 left)`. `decisions.jsonl` still carries exactly 1 row for that report (append-only history untouched). Regression cover: `python3 scripts/newos4-feedback-autoreview.py --self-test` -> 6/6. maintain: node tests 42/42, help coverage 367/367, result OK.
* chore: cron artefacts — feedback snapshot, health/anomaly reports, ↵Hermes SysOp14 hours2-3/+31
| | | | regenerated admin pages
* docs: v4.6.168 change-history — why `ui` was answered by the AIHermes SysOp2 days1-1/+22
|
* fix(newos4): `ui` now works in classic — it was being answered by the AI ↵Hermes SysOp2 days4-6/+108
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (v4.6.168) Operator report: "Ui command don't work". It was worse than a broken command: typing `ui` in the classic terminal printed Security: Unknown command "ui" — routing to AI (did you mean "ai"?)... → Routing to AI assistant... and an AI turn answered it. Three layers each looked healthy on its own: 1. `ui` is registered by UIShellModule, which since the v4.6.162 gate only loads when the visitor has already opted in — so the command that switches the interface did not exist in the state you need it from. 2. `_classify()` decides "known" from the command registry (module commands + a short builtin list), not from the safety whitelist, so `ui` was `unknown: true`. At the default security level 1 an unknown command that is not whitelisted is offered as a typo and then routed to the AI. `ui` was not whitelisted. 3. Nothing printed an error anyone would recognise as failure — it looked like the terminal was answering you. Fix: * `'ui'` joins `_baseWhitelist` (114 -> 115). It is a navigation command: safe, non-destructive, and it must never depend on the interface it navigates to. * `dispatchCommand` gets an `if (cmd === 'ui')` fallback that mounts the shell on demand via a new `_mountUIShell()` (register + init, the same two steps the boot loop performs) and then opens the picker. It also writes `webx11_ui_experimental=1`, so the opt-in survives the reload. * The mount is deliberately NOT `_safeRegister('UIShellModule', …)`: that bare call is the boot gate and `verify-newos4-ui-gate.py` fails on any occurrence of it. A guard that grants one exception has to grant whatever is written next to it, so the on-demand path is separate by name. Guard: new check `ui_command_in_classic` — executes `ui` on the served page with no flag and waits for `webx11.uiShell` + `#wx11-ui-picker`. Deliberately behavioural: `_classify('ui').known` reads FALSE even when the command works (registry vs whitelist), so a text or classifier assertion would have been a lie in both directions. The RED case is on record: the AI-routing line above, captured in the live scrollback before the fix. Verified on v4.6.168 (served): `ui` -> picker with 7 options; `ui easy` -> easy; `ui cockpit` -> cockpit; `ui classic` -> classic; `ui nonsense` -> "Unknown mode 'nonsense'. Available: classic, easy, cockpit" and stays put. Guard 8/8 rc=0. maintain OK.
* fix(newos4): the three glyph-only switch buttons now say what they do (v4.6.165)Hermes SysOp2 days5-7/+7
| | | | | | | | | | | | | | | | | | | | Asked "how do I switch?", I checked all six modes and found the controls were already uniform in function — every mode calls `api.exec('ui picker')` — but not in labelling: easy ⇄ Change view chat ⇄ Style workspace ⇄ Style studio ⇄ <- glyph only commander ⇄ <- glyph only cockpit ⇄ <- glyph only An unlabelled ⇄ on a phone is a guess. The three bare ones now carry title="Change view — pick another interface" and aria-label="Change view", which api.el() forwards via setAttribute. Verified on the served build: the snapshot reads `button "Change view"` in studio, and clicking it opens the picker with all seven options (six modes + "Keep the classic terminal"). No behaviour change — same handler, same picker; the control is now discoverable and announced by screen readers.
* docs: v4.6.164 change-history — phone PASS, the two defects it exposed, ↵Hermes SysOp3 days1-1/+10
| | | | corrected heap reading
* fix(newos4): a mode the user picks now survives a reload (v4.6.164)Hermes SysOp3 days4-10/+64
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Found by the operator actually running /ui-selftest.html on his phone. The report said 6/6 PASS, and the interesting part was what happened next: pick "easy" from the picker, reload, and you are back in classic. Two defects stacked on top of each other, neither of which reported anything: 1. `apply()` on an unregistered mode starts a lazy load and returns false. When the mode registers, `registerMode()` re-applied it with a hardcoded `persist:false` — so the picker's choice was never written to `webx11_ui_mode` at all. The intent ("the user asked for this") was dropped the moment the mode file had to be fetched, which is the normal case for all six modes. 2. Even when the mode did persist, `webx11_ui_experimental` was never set — and that key is what the in-bundle gate reads before UIShellModule is registered. A stored mode behind a closed gate is a preference the product silently ignores. Fix: * `_pendingPersist` carries the caller's intent through the lazy path; `registerMode()` honours it instead of hardcoding false, and the load-completion apply already forwarded `o.persist`; * `_persistMode(want)` writes the mode AND, for a real (non-classic) mode, sets `webx11_ui_experimental='1'`, so the gate opens on the next boot. Both directions verified live on v4.6.164: * user pick (lazy path) -> webx11_ui_mode=easy + webx11_ui_experimental=1, then main.html with NO query boots easy (#easy-actions present, body wx11-ui-easy, picker suppressed); * shared link ?ui=chat -> mode applied for the session, _fromQuery=true, localStorage STILL null (a link must not become the recipient's preference — that is the shell's own rule and it survives). Also: /ui-selftest.html no longer prints a meaningless `heapMB: 9.5`. Chromium pins performance.memory.usedJSHeapSize at 10,000,000 without --enable-precise-memory-info, which a page cannot request, so the report now says the heap is not measurable in-page instead of inventing a number — the same fake-green that was fixed in probe-ui-crash.py. guard 7/7 rc=0, maintain OK, served core-bundle byte-identical to disk.
* docs(newos4): change-history + changelog for v4.6.163Hermes SysOp3 days1-1/+10
|
* fix(newos4): ?ui=<mode> is an opt-in too — the gate had killed every ↵Hermes SysOp3 days4-6/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | shared mode link (v4.6.163) v4.6.162 shipped the opt-in gate as `get('ui') === '1'`. But the UI shell has always treated `?ui=<mode>` as a REQUEST — `module-uishell.js` reads `q = get('ui')` and honours it when the value is one of `lazyIds()` (easy|chat|studio|workspace|commander|cockpit), with `_fromQuery` set so the request never becomes a stored preference. So every shared link of the form `main.html?ui=chat` stopped mounting the shell entirely and booted classic, silently, with no error: the gate required a literal '1' that the shell's own convention never sends. Found by an independent verification pass that tried `probe-ui-modes.py --cycle` against prod and got `FAIL easy@390x844: wait_for_selector timeout` — a false red on a working build, which traced back to this. The probe's own breakage was mine (I had just changed its default base to `?ui=1`, and it appends `?ui=<mode>`, producing `?ui=1?ui=easy`); that is fixed in the next commit. Gate now: opt in on ANY `ui` value except an explicit off ('0'/'false'), plus the localStorage key. q = get('ui'); if (q !== null && q !== '' && q !== '0' && q !== 'false') return true; return localStorage.getItem('webx11_ui_experimental') === '1'; Verified on the served page (v4.6.163): * `?ui=chat` -> uiShell object, data-ui-mode=chat, body wx11-ui-chat, #chat-stream present, _fromQuery=true, picker NOT shown, and localStorage UNCHANGED (webx11_ui_experimental=null, webx11_ui_mode=null) — a link is not a preference. * no flag -> uiShell undefined, 0 #ui-root, no picker (unchanged from v4.6.162). * `?ui=1` -> shell + picker (guard's browser assertion still passes). Served core-bundle.js byte-identical to disk; guard green (7/7 checks, rc=0); probe-ui-modes.py --cycle ALL PASS 13 checks; maintain.py --fix PASS.
* chore(newos4): regenerate changelog for v4.6.162, sync lesson + chat dataHermes SysOp3 days1-1/+28
|
* feat(newos4): re-enable the six UI modes behind an opt-in gate (v4.6.162)Hermes SysOp3 days4-22/+36
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The six interface modes (v4.6.152–156) were removed from the startup path outright in v4.6.157 after crash reports. The crash was never reproduced: F2 in the rollback plan (2026-11-09_224409) drove all six modes on one page in headless Chromium at 390×844 and recorded crashes: [], pageerrors: [], heap flat at 10 MB over a 25 s idle. The two defects the investigation DID find were fixed instead — F3 (nginx 413 on a >1 MB agent context) and F4 (36 tool handlers where a model-supplied non-string reached .trim/.split/.replace). v4.6.159–161 also removed the last navigator.onLine verdicts, which is what made "it died mid-session" look like a UI crash. So the modes come back, but only for a visitor who asks for them. 1. The gate (modules/core/terminal.js, plan Task 9) - Registration is now conditional: const _uiExp = ... URLSearchParams(location.search).get('ui') === '1' || localStorage.getItem('webx11_ui_experimental') === '1'; if (_uiExp) this._safeRegister('UIShellModule', 'UI shell'); - Without the flag nothing changes: the shell is not registered, no mode can mount (the ui-*.js files all begin `if (!terminal.uiShell) return;`), no first-run picker appears, `data-ui-mode` is absent and `webx11.uiShell` is undefined. - The gate is in the bundle, so it cannot be bypassed by a stale cached ext file. 2. What was verified on the served page (v4.6.162, live Chromium) - Classic, no flag: NEWOS4_VERSION v4.6.162, 48 modules, uiShell undefined, 0 #ui-root, no #wx11-ui-picker, no data-ui-mode, terminal visible, `help` renders 18,124 chars, 0 console warnings, 0 JS errors, 55 console messages. - Opt-in `?ui=1`: uiShell object, #ui-root + #ui-foot, data-ui-mode="classic", 49 modules, picker present (opt-in only), 0 warnings, 0 JS errors. - All six modes driven through uiShell.apply() with a 1.2 s settle each, and back to classic: chat (body wx11-ui-chat, 6 mode nodes), cockpit (3), easy (4), commander (4), studio (3), workspace (5), classic (0). #terminal-container survived every switch, zero throws, heap 6.3 MB against 3.3 MB classic. - Served bytes identical to disk for core-bundle.js; `_uiExp` appears twice in the served bundle and the registration line is the guarded one (core-bundle.js:4111). 3. Not done, deliberately (plan Task 9 step 4) - `webx11_ui_experimental` is NOT defaulted on for new visitors. The plan asks for one week of clean use first, with the crash matrix re-run on the then-current bundle. Guard for this gate: admin/scripts/verify-newos4-ui-gate.py, wired above `exit $FAIL` in smoke-auth.sh — a bare (unconditional) registration is the failure it exists to catch.
* fix(newos4): stop reporting network and VFS size as facts we do not have ↵Hermes SysOp3 days9-22/+685
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (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.
* chore(newos4): commit maintenance output after v4.6.160Hermes SysOp3 days1-1/+14
|
* fix(newos4): honest size/network measurements + checkpoint and ↵Hermes SysOp3 days11-109/+1290
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* chore(newos4): commit maintenance output after v4.6.159Hermes SysOp3 days1-1/+18
|
* fix(newos4): one arg-coercion boundary + binary-safe gunzip + clean boot log ↵Hermes SysOp3 days9-26/+569
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | (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.
* chore(newos4): commit feedback ledger after the v4.6.158 triageHermes SysOp5 days1-2/+38
| | | | | | decisions.jsonl gains 10 rows: 4 stale 2026-10-23 probes rejected, m.pull resolved as a usage error, and the four 2026-11-08/09 crashes resolved against a38f5dc. Queue after: awaiting 0, steering 0, plans_review 0.
* fix(newos4): six crash classes from the feedback queue (v4.6.158)Hermes SysOp5 days14-59/+930
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* test(newos4): dev tree + crash matrix for the UI modes (prod stays classic)Agando5 days1-0/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Follow-up to 11a8e73, which took the UI shell off the production startup path after the operator's crash report. This commit ships the verification harness the re-enable is gated on, and nothing that changes what users load. - admin/scripts/probe-ui-crash.py (new): drives classic -> easy/chat/studio/ workspace/commander/cockpit -> classic on ONE page per viewport, listening for the renderer `crash` event and `pageerror`, asserting exactly one #terminal-container, a visible output pane, a docked #status-bar, no horizontal overflow, heap < 250 MB and < 20 % drift across a 25 s idle soak in the two heaviest modes. Two mobile UAs (Android WebView - the operator's own, no `wv)` token - and iOS 16 Safari) get a boot-only pass. rc=1 on any finding. - admin/docs/ui-crash-matrix-2026-11-09.json: the run of record. 3 viewports (390x844, 360x740, 1280x800) x 7 states + 2 UAs = 0 findings, crashes [], pageerrors [], heap 9.5 MB in every cell including both idle soaks. The first version rounded heap to whole MB (every cell read "10"), which would have hidden a 5 MB leak; it now reports one decimal and the committed JSON is the re-run. - spaces/newos4/.gitignore: `dev/` - the dev tree is served at /spaces/newos4/dev/main.html and must never reach the public mirror, which publishes git HEAD only (git status --porcelain spaces/newos4/dev = 0 files). The dev tree itself (24 MB rsync, data/ and all credential files excluded) is not committed on purpose; its rebuild-bundles.py ROOT is repointed at the dev copy so a dev rebuild cannot touch the production bundle (verified: prod bundle has 0 hits for the dev-only `_uiExp` gate). Not fixed here: the 1 MB nginx body cap on /ai/ and /ollama/ (413 measured on a 1,063,595-byte agent context) - that edit needs sudo approval and is still live. Plan: .hermes/plans/2026-11-09_224409-newos4-ui-rollback-dev-verify.md
* fix(newos4): take the UI shell off the startup path until the crash reports ↵Agando5 days4-6/+22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* chore: snapshot the generated content that the served site is actually runningAgando5 days1-2/+29
| | | | | | | | | 103 admin pages (nav block + feedback dashboard), 40 newos4 maintenance files, 39 gunesuygarligi pages (cookie banner, legal, pazar, haberler), and the handful of root pages that changed with them. These are regenerated by builders and cron, but they are what nginx serves right now, so the repository should match the deployment - and the public newos4 mirror refuses to publish a dirty subtree. No behaviour changes in this commit: it makes git agree with what is already live.
* chore(newos4): commit accumulated maintenance outputAgando6 days76-27/+94670
| | | | | | | The daily collector and the feedback pipeline had left 57 modified files uncommitted (skills, modules-index, CHANGELOG, feedback decisions, debug counters). The public mirror publishes git HEAD, so without this the published repo stayed behind the tree - and it blocked the publisher outright once uncommitted subtrees started failing loudly.
* fix(newos4): workspace re-opened the modules panel every tick; picker lost ↵Agando7 days7-22/+66
| | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 ↵Agando7 days12-68/+142
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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 fixesAgando7 days3-5/+13
|
* fix(newos4): four-model review — lazy-load race, restore order, a11y, key ↵Agando7 days5-22/+122
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* docs(newos4): rebuild changelog for v4.6.152Agando7 days1-1/+11
|
* fix(newos4): mode layout constraints, capture() arg split, suppress classic ↵Agando7 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
* docs(newos4): v4.6.152 changelog + change-history row for the six modesAgando7 days1-1/+33
|
* feat(newos4): six interface modes (easy -> cockpit) over one terminalAgando7 days17-8/+2107
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(newos4): document curl in help command table (auto)Agando8 days1-0/+1
| | | | | | | | | | | | | | | | | | | | | Plan: .hermes/plans/2026-11-07_061825-newos4-curl-help-row.md (T1) Root cause: curl shipped in v4.6.151 (cryptogzip.js:19 getHelpInfo, :612 commands['curl']) but help.html's command table was never updated, so check_help_coverage() reported 365/366. Change: one <tr> row added after the wget row in help.html's command table. No JS/PHP touched; cryptogzip.js, modules-index.json and atlas unchanged as specified in T2. Verification: grep -c '<code>curl ' help.html -> 1 wget row at 1086, curl row at 1087 -> adjacent grep -c "_isBase64" modules/ext/cryptogzip.js -> 0 (unchanged) git diff --stat -> help.html | 1 + python3 scripts/newos4-maintain.py -> PASS: help coverage 366/366, maintenance result: OK, needs OK: none HTTP help.html -> 200
* release: v4.6.151 — wget that actually saves, web_fetch that actually pagesAgando9 days6-41/+192
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* chore(changelog): regenerate for v4.6.150Agando9 days1-4/+11
|
* release(v4.6.150): commit the module work that has been live but uncommittedAgando9 days8-77/+331
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* chore(changelog): regenerate after the render and ordering fixesAgando9 days1-4/+6
|
* fix(changelog): keep the newest commits of a build; wrap long detail URLsAgando9 days2-39/+61
| | | | | | | | | | | | | | | | | | | | | | | | Symptom: after bodies were rendered, this fix's own commit was missing from its own entry, and a long URL inside an expanded detail was clipped at the right edge on a 390 px screen. Cause: items took group[:MAX_ITEMS] from a CHRONOLOGICAL list, so a 17-commit build showed its first 14 and dropped the newest three (the count line said "…and 3 more", hiding which three). And the injected detail CSS set no overflow-wrap, so a 47-character URL pushed the paragraph to 320 px of unbreakable text — the document stayed at 0 overflow only because the li clipped it. Fix: show group[-MAX_ITEMS:] and label the omitted ones "…and N earlier commit(s)"; entry.commits follows the same slice. overflow-wrap:anywhere on detail p/ul. The CSS block is now re-emitted from DETAIL_CSS behind BEGIN/END sentinels instead of being write-once, and the first version's sentinel-less block is stripped on sight so no page keeps two copies. Verified: 390 px — p scrollWidth == clientWidth == 320, document overflow 0, URL wraps; newest entry lists c759c50 and this commit; island 83 entries, 14 commit chips matching 14 shown items; CHANGELOG.md byte-identical on a second run.
* fix(changelog): render the commit body, not just the subjectAgando9 days2-166/+850
| | | | | | | | | | | | | | | | | | | | | | | | | Symptom: the mirrored CHANGELOG.md (and the page) showed one-line commit summaries while README promises "every shipped fix, symptom -> cause -> fix -> verified". A reviewer measured the gap from the public mirror and could only conclude the claim overpromised. Cause: scripts/build-changelog.py:59 formatted commits with %s only; %b was never read, so the verification prose every fix commit carries (5b55af9 is a typical one) was discarded before rendering. The MD writer then replaced every tag with a backtick, so nothing richer could have survived anyway. Fix: read %b with a %x02 terminator (line-based parser, multi-line body), split it into paragraph/bullet blocks capped at 900 chars with a visible marker, and render it twice from one source: as a native <details> per item on the page (its renderer already assigns item HTML, so no JS change; CSS injected once behind a sentinel) and as nested bullets under the line in CHANGELOG.md. The "every build" gap note and the page dek were rewritten to describe what the file now contains. Verified: page 100 details render, first opens 23 -> 232 px with 3 paragraphs, no horizontal overflow, island parses (83 entries); two consecutive runs are byte-identical for CHANGELOG.md; the double-escape bug this surfaced (code spans shipped as &amp;lt;sha&amp;gt;) is fixed and grep for &amp;lt; is 0.
* feat(auth): self-service git push tokens from the dashboardWebX11 SysOp9 days1-1/+1
| | | | | | | | | | | | | GET/POST /api/auth/gitpush-token (+ /revoke) on auth-api: session-scoped, CSRF-gated, 3 mints per account per hour, bcrypt into the htpasswd, plaintext returned once and never stored, audit at /var/lib/webx11/git-push-audit.jsonl. No sudo or reload needed: nginx re-opens auth_basic_user_file per request, so a new line is live immediately — proved by a 500 whose error line was per request. Dashboard card 'Git push token' shows none/active, mints and reveals the remote once, revokes. Verified in a real browser: badge none -> mint -> active, remote shape checked, then revoked; htpasswd back to 0 bytes and the test session removed.
* feat(git): namespace contrib refs per account (contrib/<user>/<topic>)WebX11 SysOp9 days1-5/+5
| | | | | | | | | | | Done before anyone has a token or a branch, because renaming later breaks every contributor. The hook compares the middle path segment to REMOTE_USER, so a token can only write under its own namespace; deletion and cleanup become a prefix operation instead of a guess. Verified 4/4: own namespace accepted, neighbour's namespace refused naming the authenticated user, missing owner segment refused with the correct form, main refused. Test branch and token removed afterwards; queue back to 0.
* feat(git): contrib push surface with automated moderationWebX11 SysOp9 days1-0/+36
| | | | | | | | | | | | | | | | | Users push to push.newos.webx11.duckdns.org with a per-user token; the read-only host is untouched. http.receivepack is unset so git only allows receive-pack when REMOTE_USER is present, which only that vhost sets. A pre-receive hook (installed by the publisher every run) allows refs/heads/contrib/* only and refuses banned paths, credential-shaped blobs, >200 files or >2MB, logging user+ref+decision. moderate-contrib.py triages every 10 min: deletes hard violations, stamps the rest ready-docs/review/stale into a queue rendered at /admin/git-contrib.html. A human still decides what merges. Verified end to end: contrib push accepted, main refused, credential file refused, accepted ref visible on the read-only host; guard extended (push host 401, hook present+executable, receivepack unset) and proven RED by removing the hook.
* docs(newos4): reconcile pi version (verified on 0.80.3, npm ships 1.5.0)WebX11 SysOp9 days1-1/+2
|
* docs(newos4): install pi + point it at Ollama; fix dead /about/ linkWebX11 SysOp9 days1-2/+51
| | | | | | | | | | | | Install is npm (node>=22.19). Ollama wiring is two files under ~/.pi/agent: models.json (baseUrl http://localhost:11434/v1, api openai-completions, model list) and settings.json (defaultProvider/defaultModel). Documents the trap that a pulled model must also be declared — undeclared gives 'Model not found', which reads like a connection failure. Both paths verified on this host: declared coder:latest -> 'LOCAL OK', undeclared llama3.2:latest -> error. The two /about/ links pointed at the host root, where nothing exists; the repo page is /newos4/about/.
* docs(newos4): automating with the pi agent (recipes verified against pi 0.80.3)WebX11 SysOp9 days1-0/+69
| | | | | | | | | Four recipes on the usage page: one-shot @file analysis, --mode json + jq for scripts, a read-only --tools allowlist for tree inspection, and a cron-able digest. Every flag shown was exercised on this host first; the retired-default model gotcha (HTTP 410 'was retired') is documented because it wasted a run. Adds the untrusted-input warning: pi auto-loads AGENTS.md/skills from the working directory, so a public clone must run with --no-approve and no write tools.
* docs(newos4): last stale count on the usage page (181 commits)WebX11 SysOp9 days1-1/+1
|
* docs(newos4): usage page fits a 390 px screenWebX11 SysOp9 days1-2/+11
| | | | | | | | Two measured overflows, both structural: a grid item defaults to min-width:auto so one long command stretched the column to 521 px, and the troubleshooting table had no fixed layout so a column of long code strings pushed the page 75 px wide. minmax(0,1fr) + min-width:0 + table-layout:fixed + overflow-wrap:anywhere. verify-mobile: 4/4 viewports OK.
* docs(newos4): no byte count either — it grows with every publishWebX11 SysOp9 days1-2/+2
|
* docs(newos4): drop hardcoded file/commit counts from the usage pageWebX11 SysOp9 days1-2/+3
| | | | | | 310 files / 182 commits were already wrong at publish time (184). A count that changes on every publish is a rot vector; the page now says what the tree is and lets the reader run the commands.
* docs(newos4): per-OS clone guide (Linux, macOS, Windows, Termux, iOS)WebX11 SysOp9 days2-0/+206
| | | | | | | | Served at https://newos.webx11.duckdns.org/usage.html from docs/usage.html in this repo, so the page, the mirror and the README share one source. Covers the four failure modes that actually bite: CRLF on Windows (core.autocrlf), missing CA bundles, cloning into /sdcard from Termux (FUSE has no POSIX permissions), and the read-only 403 on push.
* fix(changelog): stop prepending the preamble on every runWebX11 SysOp9 days2-97/+75
| | | | | | | | | | write_md() put the H1 + description inside the generated body and also kept old[:m.start()], so each regeneration added another copy: 44 runs left 44 H1s and pushed the first release to line 179. Only the region between the markers is generated now, an existing file with N copies is collapsed to one, and an assert refuses to write a file containing two generated regions (proven RED by injecting a duplicate). Regenerated: 83 entries, 185 commits, first heading v4.6.146 (was v4.6.135 - the file had not been committed since 2026-11-02).