aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
Commit message (Collapse)AuthorAgeFilesLines
* fix(auth-ui): stop offering a Telegram widget where it cannot workAgando3 days1-1/+17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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(newos4): memory-graph loads stop rewriting user data; unknown sizes say ↵Agando3 days1-2/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 defectsAgando3 days1-6/+46
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(newos4): register `test` / `[` so chains decide locally instead of ↵Agando8 days2-4/+84
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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)Agando8 days2-44/+102
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 ↵Agando8 days2-4/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 ↵Agando8 days1-5/+21
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 ↵Agando8 days1-4/+31
| | | | | | | | | | | | | | | | | | | | | | | | | | | | (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 globbedAgando9 days2-12/+44
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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): piped grep searched the working directory instead of its inputAgando9 days2-2/+8
| | | | | | | | | | | | | | | | | | | | | | | | 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, ↵Agando9 days2-124/+600
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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): bound one turn's tool context, honest skill-create, quarantine ↵Agando10 days1-8/+29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(newos4): cut/copy in Files, paste into WebDAV (Issue D2)Agando10 days3-1/+76
| | | | | | | | | | | | | | | | | | | | | | | | 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 ↵Agando10 days3-113/+238
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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, ↵Agando11 days2-73/+209
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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).
* fix(newos4): audit names broken refs instead of counting documentation ↵Agando13 days1-5/+16
| | | | | | | | | | | | | (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)Agando13 days1-1/+3
| | | | | | | | | | | | 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.
* fix(newos4): skill-create stops mangling what the agent wrote (v4.6.82)Agando13 days1-15/+36
| | | | | | | | | | | | | Two defects in one parser, both silent: - content = args.slice(2).join(' ') destroyed every newline, so markdown arrived as one line - the last argument was treated as tags whenever it contained a comma, and stayed in the body: a multi-line skill became a single line with ~13 bogus tags, reported as success Bodies can now be read from a VFS path (the only way markdown survives an argv splitter), tags are opt-in via --tags=, category via --category=. skill-patch takes a path too. Verified: body from file -> 6 newlines preserved, tags [alpha,beta], category testing; inline body containing four commas -> tags [], body intact.
* fix(newos4): skill points at the served contract URL, not a VFS path (v4.6.75)Agando13 days1-1/+1
| | | | | | The OS4 agent reported the skill cites /_docs/chat-contract.md, which does not exist in its VFS — correct: the file lives on the host and is served over HTTP. Browser VFS and disk are two stores, so a path-only pointer is a dead end. Now names the URL.
* docs(newos4): seed a chatting-with-hermes skill (v4.6.74)Agando13 days1-0/+72
| | | | | | | | | | A command that exists but is undocumented might as well not exist: the agent had no way to learn that `hermes` exists, that `chat` is taken, that replies are asynchronous, or that the cursor is milliseconds. Skill covers the commands, the chat_hermes tool, the failure table (401/413/429/503/507), the rules that keep it useful, and how to verify a send. Seeded from skill-pack-core._buildSkills() so every fresh session gets it; mirrored to skills/chatting-with-hermes.{md,json} + index.json (19 entries).
* docs(newos4): merge the two shell-composition skills into one seeded skill ↵Agando13 days1-0/+52
| | | | | | | | | | | | | | | | | | | | | (v4.6.65) Packet 9. `terminal-commands-silently-failing` and `command-composition-rules` described the same bug family from two angles, and since v4.6.58 both were describing behaviour that is now refused outright. - new seeded skill `command-composition-rules` in skill-pack-core's _buildSkills(): the rule, why the refusal exists (with the 42-42 / 34k-a measurement), what works (one command per call, wc multi-path, interactive-only pipes, quoted operators), how to verify a plausible-looking result, and the pitfalls - its History section names both retired skills so a reader arriving from either search finds the surviving one - mirrored onto disk as skills/command-composition-rules.md + .json and added to skills/index.json (18 entries) so the repo copy matches what sessions receive Verified live on v4.6.65: seeding reported 15 skills (was 14), the file and index entry exist in the fresh VFS, and the natural-language query "does the terminal have a shell and what is refused" returns command-composition-rules first (13.78, 3/4).
* fix(newos4): unmeasured skills are untracked, world-model writes verified, ↵Agando14 days1-0/+5
| | | | | | | | | | | | | | | | | | | | | | | skill audit External review packet (os4-external-review-v4.6.51.html) triaged against the code: 2 findings confirmed and fixed, 4 could not be reproduced and were recorded as such. - module-memory-graph.js: getStaleSkills() treated a missing lastUsed as 999 days unused, so skill-audit recommended archiving every skill (22 of 22). Skills with no usage telemetry are now collected by getUntrackedSkills() and never counted stale. Live: "Stale: 0" + "Untracked: 22 (no usage telemetry — NOT stale)". - module-world-model.js: _saveToVFS() ignored fsWriteJSON()'s return value and promoted whatever a truthy read returned. Now write -> read-back -> byte compare -> promote, with every return checked. Regression test modules/test/test-world-model-save.js (red on the old code: promoted=true with errs=[]; green now). - scripts/newos4-skill-audit.py: 5 mechanical probes (version x3, read_file paging + context budget, selection symbols, required scripts, extKeys) + skill presence; exits non-zero on drift. 7/7 pass today. Weekly job cb6bc2a16216 runs it Mondays. Not reproduced (recorded in admin/docs/newos4-external-review-verification-2026-10-23.md): world_model_query empty-payload throw (live call returns JSON), pipes-as-command-names, ls skills empty (23 entries via fs.readDir), ~66 agent dirs (4 live).
* fix(newos4): download link uses the user's space; WebDAV share copies ↵Agando2026-10-213-27/+209
| | | | | | | | | | | | verified file links; preview fullscreen+share; files->WebDAV upload button (v4.6.45) - download: URL built from _resolveSpaceName() (config url = logged-in user), not the page space - WebDAV browser 🔗: shares selected FILES only, URL-encoded, HEAD-verified (200) before claiming success; folders offer a tar fallback instead of a guaranteed-403 link - file manager preview: ⛶ fullscreen + 🔗 copy-link toolbar (was none) - file manager toolbar: ☁ WebDAV publishes selected/current file via webdav-push - modules/test/test-download-url.js: 4 PASS - rebuild bundles, bump v4.6.44 -> v4.6.45 (loader + 3 script tags)
* newOS4 v4.6.40: review fixes (bug diagnostics, trash tree, dir paste, stale ↵Agando2026-10-183-9/+22
| | | | capture, preview)
* newOS4 v4.6.36: fix CourseModule print-capture swallowing all command outputAgando2026-10-181-2/+16
|
* newOS4 v4.6.35: file manager multi-select + safe paste/rename/delete (Part B)Agando2026-10-182-162/+320
|
* newOS4 course: post-exec review fixes (telemetry detail sanitize, skill-sync ↵Agando2026-10-181-0/+1
| | | | retry, atlas check)
* newOS4: interactive AI course (terminal module, portal, atlas, telemetry, ↵Agando2026-10-181-0/+323
| | | | daily maintenance+cron) + free store card
* auto: 2026-07-29_20:45 changesAgando2026-10-151-1/+7
|
* auto: 2026-07-29_20:45 changesAgando2026-10-081-6/+424
|
* newOS4 v4.6.18: nginx dav trailing-slash semanticsAgando2026-10-021-4/+20
| | | | | | | | | | | Discovered by dav-health-check.sh E2E (10/10 pass now): - nginx dav module REQUIRES trailing slash on MKCOL (no-slash → 409) and on directory DELETE (no-slash → 409/404); existing dir + slash MKCOL → 405 (the real 'already exists' signal) - _normalizeRemotePath now PRESERVES trailing slashes (collection ops) - _ensureDir MKCOLs with trailing slash - _deleteFile retries with slash on 409/404 (collection delete) - health-check expectations corrected (405 = existing, slash required)
* newOS4 webdav path fix + WebX11 resilience package (2026-10-02)Agando2026-10-021-26/+85
| | | | | | | | | | | | | | | | | | | | Root cause fixes (mindtech incident): - webdav.js: _normalizeRemotePath strips full URLs/space prefixes, applied to all request entry points + _absoluteUrl for MOVE/COPY Destinations - _ensureDir: __list__ existence fast-path (kills MKCOL 409 storms) - dashboard-files.js: updateFilesJson parses __list__ autoindex (no more '#' entries), ensureSpaceExists only re-inits on genuine 404 - 20-test regression suite: scripts/test-webdav-paths.mjs Resilience: - dav-auth-check.py: server-side WebDAV write auth microservice (nginx auth_request on 8904) — anonymous/wrong-token writes now 401 - fail2ban-telegram.py: ban alerts + auto-unban for known space users - backup-spaces.sh: daily hardlink snapshots, 14-day retention - space-health-check.py: junk dirs / myfiles.json / version drift / session sanity → Telegram alerts - dashboard-files.js: visible 503 error panel with Retry + auto-retry - v4.6.16: main.html + module-loader LATEST_VER aligned (was drifted)
* newOS4 v4.6.15: reentrant print-capture stack, AI tail -f block, read-only ↵Agando2026-09-292-18/+22
| | | | sed in safeRead, security test harness (21 checks)
* auto: 2026-07-29_20:45 changesAgando2026-08-061-310/+718
|
* feat: add kanban module to newOS4 — todo/doing/done board with prioritiesAgando2026-07-291-0/+190
|
* auto: 2026-07-09_06:00 changesAgando2026-07-096-347/+2089
|
* auto: 2026-07-02_06:00 changesAgando2026-07-022-1/+1240
|
* auto: 2026-06-23_06:00 changesAgando2026-06-231-3/+6
|
* auto: 2026-06-22_18:00 changesAgando2026-06-225-10/+923
|
* auto: 2026-06-19_18:01 changesAgando2026-06-191-6/+37
|
* auto: 2026-06-18_18:00 changesAgando2026-06-181-1/+11
|
* auto: 2026-06-18_06:00 changesAgando2026-06-185-13/+62
|
* auto: 2026-06-17_06:01 changesAgando2026-06-172-2/+16
|
* auto: 2026-06-16_06:00 changesAgando2026-06-161-0/+167
|
* auto: 2026-06-15_06:00 changesAgando2026-06-151-44/+156
|
* auto: 2026-06-13_18:00 changesAgando2026-06-131-0/+2363
|
* auto: 2026-06-11_18:00 changesAgando2026-06-111-190/+996
|
* auto: 2026-06-10_18:00 changesAgando2026-06-101-11/+39
|
* auto: 2026-06-10_06:00 changesAgando2026-06-101-0/+76
|
* auto: 2026-06-09_18:00 changesAgando2026-06-091-6/+84
|