aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-11-06 11:37:11 +0100
committerAgando <roduccomm@gmail.com>2026-11-06 11:37:11 +0100
commit1983ce5796e45b312b142554cb717db7f8978bc8 (patch)
treea8cef803729c1eb422a550a29257510d7daab937
parentd5dc2e1cb2f1463d001c3b10d1637298997f8559 (diff)
fix(changelog): render the commit body, not just the subject
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.
-rw-r--r--CHANGELOG.md830
-rw-r--r--scripts/build-changelog.py186
2 files changed, 850 insertions, 166 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95f98e2..26ce257 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,567 +6,1097 @@ Generated from git history by `scripts/build-changelog.py` — do not edit by ha
## v4.6.146 — 2026-11-05
-_Touches modules (3), README.md (2), main.html (1), LICENSE (1) · docs 2, project 1_
-
-- `One dashboard — port billing/API keys, 301 the duplicate` `8bf70f9`
-- `Publishable README, MIT licence, security policy` `447be4b`
-- `Clone instructions in the README; publisher sets gitweb owner/description` `350e1d0`
+_Touches docs (11), modules (3), README.md (3), main.html (1) · docs 10, release 3, project 1_
+
+- **One dashboard — port billing/API keys, 301 the duplicate** `8bf70f9`
+ - Two dashboards existed and had drifted apart: root /dashboard.html (118 KB single file with Subscription & Usage + API Keys) and /spaces/dashboard.html (34 KB shell + assets/js/dashboard-*.js). The nginx log settles which one is real: 77.211.5.129 requests /spaces/dashboard.html and never the root page. Tonight's fixes landed on the root page twice before that was noticed.
+ - Neither page was a superset, so deleting outright would have lost features: root only -> Subscription & Usage card, API Keys card (create/revoke/reveal) spaces only -> 2FA/TOTP, change password, delete space, email change, files, logs
+ - … run `git show <sha>` for the rest
+- **Publishable README, MIT licence, security policy** `447be4b`
+- **Clone instructions in the README; publisher sets gitweb owner/description** `350e1d0`
+- **Stop prepending the preamble on every run** `5902d5f`
+ - 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).
+- **Per-OS clone guide (Linux, macOS, Windows, Termux, iOS)** `8e34256`
+ - 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.
+- **Drop hardcoded file/commit counts from the usage page** `d2bb158`
+ - 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.
+- **No byte count either — it grows with every publish** `a24ec6b`
+- **Usage page fits a 390 px screen** `373f6ca`
+ - 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.
+- **Last stale count on the usage page (181 commits)** `bc4d180`
+- **Automating with the pi agent (recipes verified against pi 0.80.3)** `83cb1c7`
+ - 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.
+- **Install pi + point it at Ollama; fix dead /about/ link** `d99962d`
+ - 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/.
+- **Reconcile pi version (verified on 0.80.3, npm ships 1.5.0)** `5fdeb6d`
+- **Contrib push surface with automated moderation** `e90dc38`
+ - 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.
+- **Namespace contrib refs per account (contrib/<user>/<topic>)** `4acb065`
+ - 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.
+- …and 1 more commit(s) in this build
## v4.6.145 — 2026-11-02
_Touches modules (2), main.html (1) · fix 1_
-- `Stop offering a Telegram widget where it cannot work` `532e2ff`
+- **Stop offering a Telegram widget where it cannot work** `532e2ff`
+ - 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
+ - … run `git show <sha>` for the rest
## v4.6.144 — 2026-11-02
_Touches modules (3), main.html (1) · fix 1_
-- `One session writer for every login path; expire dead credentials` `2421eb4`
+- **One session writer for every login path; expire dead credentials** `2421eb4`
+ - Third occurrence of the same class of bug, and the first fix is why: commit e95b7be (2026-11-01, "multi-session .newsession v3") touched exactly ONE hunk of auth-file-api.py — handle_login. handle_device_poll, handle_telegram_login and handle_register kept overwriting the whole .newsession with a single-session v2 payload, so a phone sign-in logged the desktop out and the loser held a session id no store knew: permanent `401 Invalid session or API key` (newOS4 asked for a login it could never complete). Measured: agando's store held 1 entry (sha8 45cb7819) while the phone sent 9ac607f1 for 3 h; 26 device-polls in 2 s from one page minted 26 cookies and only the last write survived, which also drained the shared nginx `webx11` zone (11x 503 on /admin/api/*).
+ - api/webx11_auth.py
+ - add_session() is now THE writer: v3 `sessions[]` (cap 5) under a per-user
+ - … run `git show <sha>` for the rest
## v4.6.143 — 2026-11-02
_Touches modules (6), main.html (1) · fix 1_
-- `Read_file pages were silently truncated; delegation no longer invents &quot;completed&quot;` `b380ec8`
+- **Read_file pages were silently truncated; delegation no longer invents "completed"** `b380ec8`
+ - Two defects found by re-deriving a report that claimed both were still open.
+ - read_file paging (module-aiassist.js)
+ - A page larger than the result clip was clipped AFTER read_file had computed
+ - next_offset. On the last page next_offset is null, so the model was told truncated:false while its tail had been removed: measured 11 890 of 19 999 chars delivered, returned:19999, no pointer to the rest - 8 109 chars lost with no way to ask for them.
+ - the clip now sets truncated=true and next_offset=offset+keep when it removes
+ - the tail of a page that had no continuation (and never splits a surrogate pair)
+ - read_file caps a page at the result budget (min(pageCap, toolResultChars-200)),
+ - … run `git show <sha>` for the rest
## v4.6.141 — 2026-11-02
_Touches modules (7), main.html (1) · fix 1_
-- `Memory-graph loads stop rewriting user data; unknown sizes say unknown` `8eaf114`
+- **Memory-graph loads stop rewriting user data; unknown sizes say unknown** `8eaf114`
+ - 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.
+ - … run `git show <sha>` for the rest
## v4.6.140 — 2026-11-02
_Touches modules (6), main.html (1) · fix 1_
-- `WebDAV listing sizes + pull, and five MemoryGraph defects` `e639c2d`
+- **WebDAV listing sizes + pull, and five MemoryGraph defects** `e639c2d`
+ - 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.
+ - … run `git show <sha>` for the rest
## v4.6.139 — 2026-11-02
_Touches modules (4), main.html (1) · fix 1_
-- `Delegated agent runs no longer serialise; a waiter can&#x27;t steal a lock` `7491c34`
+- **Delegated agent runs no longer serialise; a waiter can't steal a lock** `7491c34`
+ - Closes the second half of STEER-20261102-201045-ab08.
+ - chat() gated the main-turn lock on nothing, so orchestrate-parallel's fan-out
+ - (each sub-task calling chat() on the same instance with its own agentTag) ran strictly one at a time. Measured 2x300ms delegated runs: 617ms before, 306ms now; the two 200ms user turns still take 400ms (main path stays serialised).
+ - the 10s lock timeout called this._chatLock = false and printed "force
+ - released", which cleared a lock the waiter did not own - that released the main turn's lock while it was still running. A waiter now throws and the owner is the only writer.
+ - delegated runs no longer clear the user's cancel flag on entry (an agent start
+ - used to reset this._cancelled, disarming a Stop the user had pressed).
+ - tool-log entries carry the run they belong to and _recentToolLog(tag, n) is
+ - … run `git show <sha>` for the rest
## v4.6.138 — 2026-11-02
_Touches modules (4), main.html (1) · fix 1, review 1_
-- `Chat() threw on every tool-using turn and never returned its answer` `8a9333b`
-- `Guard chat()&#x27;s return and tool-turn survival` `af28b5b`
+- **Chat() threw on every tool-using turn and never returned its answer** `8a9333b`
+ - Regression from 32608bc, caught by the .chat() caller sweep before it aged:
+ - `recent` was referenced in chat()'s finally block but never declared, so any
+ - turn with toolTurns > 0 threw ReferenceError there and chat() REJECTED. delegateTask's catch turned that into "agent failed"; the 'completed' fallback was only ever reachable on tool-free turns.
+ - chat() still had no return at all, so a tool-free turn resolved undefined and
+ - module-orchestrator.js:1250 fell through to `|| 'completed'`.
+ - Both fixed: `const recent` is now built from _toolCallLog, and chat() returns { content: finalAnswer, toolTurns } before the archive block (a return at the end of the method would let _archiveSession swallow the answer).
+ - … run `git show <sha>` for the rest
+- **Guard chat()'s return and tool-turn survival** `af28b5b`
+ - The regression in 32608bc shipped because nothing exercised chat() itself. test-chat-return.js runs 7 assertions against the real module: a tool-using turn must not throw, must return {content, toolTurns}, the orchestrator projection must keep the answer, a tool-free turn must return too, the lock must be released after a failed turn, and a second turn must return its own answer.
+ - Green 7/7 on 8a9333b; against 32608bc it exits 1 (ReferenceError: recent). Auto-discovered by newos4-maintain (23/23 node tests).
## v4.6.137 — 2026-11-02
_Touches modules (6), help.html (1), main.html (1), modules-index.json (1) · release 1_
-- `Configurable tool budgets, per-model defaults, real tool telemetry` `32608bc`
+- **Configurable tool budgets, per-model defaults, real tool telemetry** `32608bc`
+ - Closes STEER-20261102-201234-7200 (read_file capped at 6000 while the schema promised 32000).
+ - Budgets (module-aiassist.js + new modules/core/model-profiles.js):
+ - one resolver, precedence: call arg -> loop profile -> model profile ->
+ - ai-config -> code default. Keys: read-page-chars, read-result-chars, tool-context-budget, fetch-chars.
+ - model classes tiny/small/mid/large keyed by id then by size token; `mid`
+ - reproduces the old numbers so nothing moves for a mid model.
+ - _clipToolResult now clips the `content` field, never the serialised JSON
+ - envelope: clipping the envelope deleted truncated/next_offset, so a 16k page lost its continuation pointer (the reported symptom).
+ - … run `git show <sha>` for the rest
## v4.6.136 — 2026-11-02
_Touches scripts (4), modules (3), main.html (1) · release 4_
-- `No model-class gate; free allowance becomes $0.10/day` `a7cbc19`
-- `R5 stale-artefact rule + dashboard queues a decision once` `89910fd`
-- `Ingest report-like chat messages into the feedback queue` `b7fef93`
-- `Chat plane on the dashboard + honest triage ranking` `9f0b042`
+- **No model-class gate; free allowance becomes $0.10/day** `a7cbc19`
+ - Operator policy 2026-11-01: every subscription may call any model, and the free tier resets daily.
+ - Model gate removed (api/ai-gateway.py) `plan == 'free' and model_weight > 1 -> 403 "Advanced models require Pro"` ran before any money logic, so a free-plan account holding $1086 of purchased credit was refused and told to upgrade. A model tier is not something we sell: the allowance and the USD balance bound spend. A caller who cannot pay now gets the honest 429 (with period + needed amount). model_weight survives only for track_usage() analytics.
+ - Free allowance: $0.50/month -> $0.10/day ~$3/month retail = $2.14/month of provider cost at 1.4x markup, ~6x the previous giveaway and still bounded per member (the 50-slot invite cap bounds the total; at full occupancy ~$107/mo or $2.14/user at 100% utilisation).
+ - The window is policy, not code:
+ - … run `git show <sha>` for the rest
+- **R5 stale-artefact rule + dashboard queues a decision once** `89910fd`
+ - Feedback queue triage 2026-11-02: 23 reports, 22 decided, 1 open -- and the open one (RPT-20261029-115241, "[Terminal] Error in ShellModule.test: {\"silent\":true}") was already fixed in v4.6.135 (4472c81, modules/core/terminal.js:1966) yet could never be cleared by automation.
+ - Root cause: identifiers() keeps only generic class names (Terminal, ShellModule), both of which necessarily stay in the code, so R2 (every identifier absent) cannot fire; R4 needs a stack and this report has none. A stack-less "Class.method: {json}" console.error whose emitter gets *guarded* -- rather than deleted -- therefore sits "awaiting decision" forever.
+ - newos4-feedback-autoreview.py: new R5 stale artefact -- auto report, no stack,
+ - … run `git show <sha>` for the rest
+- **Ingest report-like chat messages into the feedback queue** `b7fef93`
+ - The chat plane (api/chat/post.php -> data/chat/*.json) was read by nothing in the feedback pipeline: 22 report-like messages over 11 days produced 0 queue items. This adds scripts/newos4-chat-ingest.py - a read-only scanner by default, --apply writes RPT-* files in the report.php schema with diagnostics.auto=false so the auto-reviewer leaves them for a human, deduped on (thread, msg_id).
+ - First apply filed 5 items (2 x 2026-10-28, 3 x 2026-11-02); snapshot went reports_total 23 -> 28, awaiting_decision 0 -> 5. Second run is silent.
+- **Chat plane on the dashboard + honest triage ranking** `9f0b042`
+ - Tasks 3-8 of the chat-plane plan:
+ - cron newos4-chat-ingest (0cc4bb4d2f82, */10, script-only, silent when idle)
+ - collect.py: plan_key() normalises the two ledger spellings on read (5 rows
+ - carried a plan name without .md and were silently unmatched); per-item bulk_pass for the 2026-10-26 pass; counts.chat_total
+ - feedback.html: 5th tile "from chat"; card shows who closed an item and when,
+ - with a bulk marker
+ - triage: UNFILEABLE class -> top_real now ranks actionable errors only
+ - (first row went from an 18 203 fetch failure to the 989 toLowerCase runtime error); network/abort signatures move to policy_noise
+ - … run `git show <sha>` for the rest
## v4.6.135 — 2026-10-30
_Touches modules (6), data (6), course (2), help.html (1) · release 1, docs 1, project 1_
-- `Register `test` / `[` so chains decide locally instead of asking a model (v4.6.135)` `4472c81`
-- `Regenerate after the metering and shell commits (73 entries)` `4fca1c9`
-- `Untrack cron/API runtime state, keep it on disk` `8555cd4`
+- **Register `test` / `[` so chains decide locally instead of asking a model (v4.6.135)** `4472c81`
+ - `[ -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
+ - … run `git show <sha>` for the rest
+- **Regenerate after the metering and shell commits (73 entries)** `4fca1c9`
+- **Untrack cron/API runtime state, keep it on disk** `8555cd4`
+ - These files are rewritten in place every few minutes by cron jobs and the APIs (visits.json, agent-items.json, subscribers.json, newOS4 debug/report/ maintenance state, changelog feed, course atlas...). Tracking them made `git status` useless and put runtime noise into every commit. The daily backup (admin/scripts/backup-spaces.sh) already owns this state; nothing is deleted from disk here — files stay in place, only removed from the index.
+ - No source files touched: api/*.py, admin/scripts/*, modules/, *.html pages staged nowhere; the two ambiguous data files (data/income-tracker.json, admin/dead-jobs.html) are deliberately left tracked.
## v4.6.132 — 2026-10-29
_Touches modules (4), CHANGELOG.md (1), data (1), main.html (1) · fix 1_
-- `Shell tokenises command lines quote-aware everywhere (v4.6.132)` `3b1773c`
+- **Shell tokenises command lines quote-aware everywhere (v4.6.132)** `3b1773c`
+ - 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:
+ - … run `git show <sha>` for the rest
## v4.6.131 — 2026-10-29
_Touches modules (8), scripts (3), help.html (1), main.html (1) · fix 1_
-- `Pipe stage quoting, maintain ordering, help cost row, plan status (v4.6.131)` `2c6c218`
+- **Pipe stage quoting, maintain ordering, help cost row, plan status (v4.6.131)** `2c6c218`
+ - 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).
+ - … run `git show <sha>` for the rest
## v4.6.130 — 2026-10-29
_Touches modules (5), main.html (1) · fix 1_
-- `Js keeps its string literals — dispatcher forwards the raw command line (v4.6.130)` `161372f`
+- **Js keeps its string literals — dispatcher forwards the raw command line (v4.6.130)** `161372f`
+ - _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
+ - … run `git show <sha>` for the rest
## v4.6.128 — 2026-10-29
_Touches modules (2), help.html (1), main.html (1) · fix 1_
-- `Js console handles promise rejections + Node-builtin hint (v4.6.128)` `75c5113`
+- **Js console handles promise rejections + Node-builtin hint (v4.6.128)** `75c5113`
+ - 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.
+ - … run `git show <sha>` for the rest
## v4.6.119 — 2026-10-27
_Touches scripts (6), modules (4), CHANGELOG.md (3), guide.html (2) · release 3, fix 2_
-- `if test …` was always false and `for` items never globbed` `e575ee8`
-- `Generate the changelog from git and keep it current in maintenance` `22bb505`
-- `Link the changelog in and deep-link each board fix to its build` `b26bfb8`
-- `Tag builds, feed the changelog to machines, fail maintenance when it ages` `782b3db`
-- `Apply the GLM-5.3 review to the changelog pipeline` `f3b2664`
+- **`if test …` was always false and `for` items never globbed** `e575ee8`
+ - 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:
+ - … run `git show <sha>` for the rest
+- **Generate the changelog from git and keep it current in maintenance** `22bb505`
+ - The page at spaces/agando/downloads/changelog.html was hand-built from VFS notes by the in-page agent: 11 entries, huge honest holes ("v4.6.59 - v4.6.100 unknown"), and it had stopped at v4.6.101 while the app was running v4.6.119. A docs page that lags the release reads as current, which is worse than no page.
+ - scripts/build-changelog.py derives every entry from git: it walks commits touching
+ - spaces/newos4, stamps each with the LATEST_VER that was live at that commit, and groups them into builds. Kinds map onto the page's own vocabulary (feat->release, fix->fix, test->review, docs->docs, chore/style/refactor->project), and a build's chip is its dominant kind. Commits whose prefix carries `backticks` render as <code>, escaped first.
+ - The page is its own template: everything visible comes from the #changelog-data JSON
+ - … run `git show <sha>` for the rest
+- **Link the changelog in and deep-link each board fix to its build** `b26bfb8`
+ - The generated changelog was an orphan: no page linked to it, and nothing linked from it, so "generated and maintained" still meant "nobody lands here".
+ - guide.html + help.html footers and the board header now link the changelog (and the board, from
+ - the app side). All three verified live: HTTP 200, 1 link each.
+ - Every closed board card gains "fixed in <sha> · v4.6.XXX" where the version is a link to
+ - changelog.html#v4-6-XXX. The anchor rule is the changelog renderer's own (non-alphanumerics -> "-", lowercased), not a second guess at it.
+ - The page now actually jumps to a hash. Previously the list is built by JS after load, so the
+ - browser's own anchor scroll ran against an empty list and every deep link landed at the top. jumpToHash() runs after render and on hashchange.
+ - A build can legitimately be missing: v4.6.83 fixed MAINT-002 but never reached a commit (the
+ - … run `git show <sha>` for the rest
+- **Tag builds, feed the changelog to machines, fail maintenance when it ages** `782b3db`
+ - Tasks 3, 4, 5, 6, 7 of the changelog plan.
+ - 67 build tags (v4.6.10 … v4.6.119) now exist, so a build is a ref instead of something that has
+ - to be re-inferred: `git log v4.6.118..v4.6.119` finally means something.
+ - The generator prefers tags, but via ONE `git for-each-ref` pass rather than a `git describe` per
+ - commit. That matters: describe-per-commit made the build 2.4-2.7 s against a 1.0-1.45 s baseline — i.e. tags made it slower, which the plan's gate said to treat as "tags bought nothing". The right fix was a bulk lookup, not dropping the tags: now 0.80-1.04 s (median 0.84 s) with output byte-identical to the untagged baseline (68 entries, same version order).
+ - changelog.json + feed.xml (Atom) are generated from the same page dict: 36 KB / 21 KB, 68/68
+ - entries, both HTTP 200, item markup stripped so a reader gets text not HTML.
+ - … run `git show <sha>` for the rest
+- **Apply the GLM-5.3 review to the changelog pipeline** `f3b2664`
+ - Verdict was APPROVE WITH CHANGES: 21 findings, 17 fixed, 3 refuted with the command that killed them, 1 kept with the audit that justifies it. The reviewer's best find was a design bug of mine, not a typo.
+ - The age guard was unfixable by --fix and fired on a quiet week. check_changelog() regenerated only
+ - on a version mismatch, then returned from the version-match branch before the age check could act, so a page older than the threshold stayed red forever with the false diagnosis "the daily job is not running". Now --fix always regenerates (0.84 s) and age is read from a new `checked` stamp separate from `generated` (content-change time), which also stops a daily 65 KB rewrite of an unchanged page. Proved: seeded 641 h -> FAIL + "run maintain --fix to refresh" -> --fix -> PASS.
+ - Atomic writes everywhere (temp + os.replace): nginx serves these files off disk, so an in-place
+ - … run `git show <sha>` for the rest
## v4.6.118 — 2026-10-27
_Touches modules (5), main.html (1) · fix 1_
-- `Send num_ctx to Ollama, and make one auto-connect failure read as one line` `3bba2c2`
+- **Send num_ctx to Ollama, and make one auto-connect failure read as one line** `3bba2c2`
+ - Two things I had flagged as "worth your attention" and one leftover nit.
+ - num_ctx was never sent. Ollama's server-side default context is 2k-4k depending on the build, so
+ - a system prompt + 20 messages + up to 24k chars of tool results could be truncated server-side, which is exactly the "it forgot what it just read" class of report. Every Ollama request now carries options.num_ctx (default 16384, overridable via webx11_ollama_numctx, floor 512); the OpenAI-compatible branch is untouched because num_ctx is not one of its parameters.
+ - Automatic connect prints one outcome instead of one line per endpoint tried (7 -> 3 on a dead
+ - endpoint). Interactive `ollamaconnect` keeps the per-endpoint detail.
+ - write_file's failure message no longer echoes a trailing slash.
+ - Verification:
+ - test-ollama-numctx.js (7 asserts): generate and chat payloads carry num_ctx=16384, a stored
+ - … run `git show <sha>` for the rest
## v4.6.116 — 2026-10-27
_Touches modules (7), main.html (1) · fix 1, review 1_
-- `Piped grep searched the working directory instead of its input` `8b9c826`
-- `Prove the mixed-batch loop ceiling stops an agent at 9 identical calls` `b09fea8`
+- **Piped grep searched the working directory instead of its input** `8b9c826`
+ - 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
+ - … run `git show <sha>` for the rest
+- **Prove the mixed-batch loop ceiling stops an agent at 9 identical calls** `b09fea8`
+ - MAINT-006 was the last open board item and it needed a live run, not another code argument. The fix landed in 5058d3b (the hard stop was only reachable when EVERY call in a batch was blocked, so one repeated call + one novel call per turn ran to maxTurns).
+ - Live probe on v4.6.116 with the deployed class and a deterministic model stub (repeat read_file
+ - + a fresh list_files each turn): the loop stopped at turn 9 with "⛔ main: no progress after 9 identical calls — ending loop with partial result" and returned {finalAnswer, toolTurns} with a 400-char partial answer.
+ - test-agent-loop-ceiling.js (9 asserts, real module via its constructor): A mixed batch stops before
+ - maxTurns / B all-blocked batch still stops / C a productive loop that never repeats is NOT stopped.
+ - RED on 52d188a (pre-fix): 8 passed, 1 FAILED — the mixed batch ran to "AI reached the 12/12 tool
+ - … run `git show <sha>` for the rest
## v4.6.115 — 2026-10-27
_Touches modules (7), main.html (1) · release 1_
-- `The script runner runs scripts — control flow, variables, globs, redirects, stdin` `5cf5010`
+- **The script runner runs scripts — control flow, variables, globs, redirects, stdin** `5cf5010`
+ - 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).
+ - … run `git show <sha>` for the rest
## v4.6.110 — 2026-10-27
_Touches modules (4), main.html (1) · fix 1_
-- `The loop guard reads the config it documents` `5425e66`
+- **The loop guard reads the config it documents** `5425e66`
+ - MAINT-004. The guard read warnAt/blockAt/hardStopAt while one config path wrote `threshold`, and `enabled` was never consulted anywhere — writing {"enabled": false} into webx11_ai_config changed nothing at all. One resolver, _loopPolicy(), now normalises both spellings, guarantees warnAt <= blockAt < hardStopAt, and both the block and the warning are gated on enabled.
+ - Live on v4.6.110 (_loopPolicy called in the page): defaults {warn 3, block 6, hardstop 9}; {threshold: 4} -> {warn 2, block 4, hardstop 7}; {enabled: false, threshold: 3} -> enabled false. test-loop-policy.js: 17 asserts, RED on HEAD ("_loopPolicy is not a function"), suite 16/16.
## v4.6.109 — 2026-10-27
_Touches modules (6), main.html (1) · fix 1_
-- `Agent registration is absolute-path and honest about what it stored` `5058d3b`
+- **Agent registration is absolute-path and honest about what it stored** `5058d3b`
+ - Three of the four agent-cluster items, each with a test that is red on HEAD.
+ - MAINT-008 (module-agent-registry.js + module-orchestrator.js): registerAgent took args[0] as the
+ - agent id and only args[1] as the capability list, while both orchestrator callers passed the command word first — so the boot log read "Agent 'register' registered with 1 capabilities" and four capabilities were stored as one. The command word is now stripped, args[1..] are joined and split on commas (both "a,b" and "a b c" work), and the callers pass [id, ...caps].
+ - MAINT-007 (registry + aiassist): mkdir/rm used relative paths, so a registration made from /docs
+ - created /docs/agents/<id> and an unregister from any other cwd removed nothing — the leaked directories the report suspected. Every path in the registry (and the four /_sessions writes in module-aiassist.js) is absolute now.
+ - … run `git show <sha>` for the rest
## v4.6.108 — 2026-10-27
_Touches modules (5), main.html (1) · fix 1_
-- `Help names the owner of an undocumented command; loop lessons leave /skills` `243a766`
+- **Help names the owner of an undocumented command; loop lessons leave /skills** `243a766`
+ - Two board items plus the context-window close-out.
+ - MAINT-019 (module-system.js): `help <cmd>` answered "No help available for X" for a command
+ - that is registered but has no help line, which reads as if the command does not exist. It now says "X — registered by <Module>, which publishes no help line for it. Try: help". A command that genuinely does not exist still gets the old message.
+ - MAINT-020 (module-aiassist.js): auto-generated loop lessons were written into /skills with
+ - their own index entries, so `skill list` filled up with system noise. Lessons now live in /lessons with /lessons/index.json; the reader still falls back to legacy loop-lesson-* entries in /skills for one release, and the one-time migration deletes them from the skills index.
+ - … run `git show <sha>` for the rest
## v4.6.107 — 2026-10-27
_Touches modules (8), main.html (1) · fix 1_
-- `Bound one turn&#x27;s tool context, honest skill-create, quarantine a corrupt memory graph` `d4d80f0`
+- **Bound one turn's tool context, honest skill-create, quarantine a corrupt memory graph** `d4d80f0`
+ - 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
+ - … run `git show <sha>` for the rest
## v4.6.106 — 2026-10-27
_Touches modules (7), main.html (1) · fix 1_
-- `Name the real Ollama failure, bound auto-connect, fix the review findings` `1f07ef9`
+- **Name the real Ollama failure, bound auto-connect, fix the review findings** `1f07ef9`
+ - Second pass over v4.6.105, driven by an independent review that reproduced each item.
+ - ollama-manager.js: chat() now reports the captured cause ("Ollama error: Failed to fetch",
+ - "…: HTTP 500") instead of the generic "No provider responded"; _lastChatError is reset per call so an old reason is never replayed. chatWithTools carries the reason in its error string.
+ - ollama-manager.js: _ensureConnected() bounds connect() with a deadline (connectTimeoutMs,
+ - default 10 s). connect() uses a bare fetch(), so a blackholed endpoint previously left the shared in-flight promise pending forever and hung every later message — the old code failed fast. The attempt is now try/finally, so a throwing terminal.log() can no longer poison _connecting and suppress the retry on the next message.
+ - ollama-manager.js: an automatic connect no longer silently rewrites the user's saved model;
+ - … run `git show <sha>` for the rest
## v4.6.105 — 2026-10-27
_Touches modules (6), main.html (1) · fix 1_
-- `Connect to Ollama on first use, honest write_file, list_files sees /sys` `d6c5cb9`
+- **Connect to Ollama on first use, honest write_file, list_files sees /sys** `d6c5cb9`
+ - Three defects, each re-probed first and covered by a test that is red on HEAD.
+ - ollama-manager.js: nothing connects at boot; only the Ollama tab button called connect(),
+ - so a user's first message died with "Ollama not connected. Use ollamaconnect first." even when the endpoint was healthy. New _ensureConnected() runs from chat(), chatStream() and chatWithTools(): one attempt, shared by concurrent callers, a "Connecting…" hint, and only then the error — which now says the endpoint is unreachable instead of telling the user to run a command they already expected to work.
+ - module-aiassist.js write_file tool: fs.writeFile()'s false return was dropped and the tool
+ - answered "File written: …" anyway, sending the model on to a read that found nothing (MAINT-012). It now returns "Write FAILED: … — parent directory missing or target is a directory; nothing was written".
+ - … run `git show <sha>` for the rest
## v4.6.104 — 2026-10-27
_Touches modules (5), main.html (1) · fix 1_
-- `Never overwrite a directory node; ls honours a path after its flags` `ec9d523`
+- **Never overwrite a directory node; ls honours a path after its flags** `ec9d523`
+ - MAINT-010 and MAINT-016, the two high items the review left open. Both were re-probed before the change and the new test is red on HEAD (5 passed, 7 FAILED).
+ - virtual-fs.js rename()/copy(): they assigned dst.dir.content[dst.name] with no destination
+ - type check, so `mv file /docs` replaced the /docs node and every child vanished while the command printed "Moved: …". Refuse when the destination is a real directory node (the virtual /sys entry is exempt). rename() also erased the file on `mv a a`, because assigning and then deleting the same key removes it; that path is now an explicit no-op.
+ - module-fs.js mv/cp: a refusal used to surface as "No such file or directory", which sent
+ - the operator looking for a missing source. The destination is now inspected first and the message says "is a directory — refusing to overwrite it".
+ - … run `git show <sha>` for the rest
## v4.6.103 — 2026-10-27
_Touches modules (6), data (2), scripts (2), main.html (1) · fix 1, project 1_
-- `Rollback keeps scaffolding, checkpoints cannot collide, tests gate the build` `07544cd`
-- `Point the predictor closure at the shipped commit` `a5f1b92`
+- **Rollback keeps scaffolding, checkpoints cannot collide, tests gate the build** `07544cd`
+ - Review of the 08:30 implementer run (v4.6.102, checkpoint name fix). The shipped fix was correct; these are the defects it introduced or left behind, each measured first.
+ - virtual-fs.js rollback(): the snapshot strips _snapshots/_sessions/.trash for bloat. Restoring
+ - it verbatim deleted every checkpoint, every agent session and the trash, made rollback irreversible and unrepeatable. Re-attach the live copies after the root swap.
+ - checkpoint(): names are millisecond-resolution, so tight calls overwrote each other
+ - (measured 6 calls -> 3 snapshots). Dedupe with a -N suffix, refuse names containing "/" (written but unlistable), and prune by snapshot createdAt instead of filename sort, which let a custom name ("aaa-oldest") evict newer auto-named checkpoints.
+ - module-predictor.js: legacy pre-v2 points have no `usable`, so a 0.079 s aborted run
+ - … run `git show <sha>` for the rest
+- **Point the predictor closure at the shipped commit** `a5f1b92`
+ - The code commit was amended (39b61ad -> 07544cd) after the first stamp, so re-stamped the steering entry against the commit that actually exists.
## v4.6.102 — 2026-10-27
_Touches modules (4), main.html (1) · fix 2_
-- `Print the checkpoint name actually saved (auto)` `c768584`
-- `Regression test for checkpoint name round-trip (auto)` `2be415c`
+- **Print the checkpoint name actually saved (auto)** `c768584`
+ - Task 2 + ship of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
+ - module-system.js `checkpoint` read the clock a second time for the printed name, so the name in "✅ Checkpoint created: <name>" differed from the name on disk by >=1ms whenever no explicit name was given. It now prints the value returned by fs.checkpoint(). try/catch and error strings unchanged.
+ - Bundles rebuilt (rebuild-bundles.py); LATEST_VER in module-loader.js and all three ?v= tags in main.html (core-bundle, module-loader, critical-ext-bundle) bumped v4.6.101 -> v4.6.102.
+ - … run `git show <sha>` for the rest
+- **Regression test for checkpoint name round-trip (auto)** `2be415c`
+ - Task 3 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
+ - New modules/test/test-vfs-checkpoint.js loads the real VirtualFileSystem and SystemModule with guarded localStorage/indexedDB/window stubs and asserts: A1-A4 checkpoint('') returns the resolved name; two calls differ; the file exists under the returned name B1-B2 checkpoint-list shows the returned name; rollback(<that name>) is true C1-C4 the `checkpoint` command prints exactly the name it created (and an explicit name verbatim)
+ - RED before the fix (v4.6.101): FAIL C2 printed name exists on disk — printed=2026-10-27T07:31:21.575Z listed=["2026-10-27T07:31:21.574Z"] test-vfs-checkpoint: 2 passed, 8 FAILED
+ - GREEN after: $ node modules/test/test-vfs-checkpoint.js test-vfs-checkpoint: 10 passed, 0 failed
## v4.6.101 — 2026-10-27
_Touches modules (5), main.html (1) · release 1, fix 1_
-- `Cut/copy in Files, paste into WebDAV (Issue D2)` `dca2c3b`
-- `Checkpoint() returns resolved name (auto)` `f7249ff`
+- **Cut/copy in Files, paste into WebDAV (Issue D2)** `dca2c3b`
+ - 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.
+ - … run `git show <sha>` for the rest
+- **Checkpoint() returns resolved name (auto)** `f7249ff`
+ - Task 1 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md.
+ - virtual-fs.js checkpoint(name) generated an ISO name when none was given but returned bare `true`, so callers could not know which name the snapshot was written under. It now returns the resolved name (string). Prune (max 3) and saveToStorage() behaviour unchanged.
+ - Verification: $ node --check modules/core/virtual-fs.js SYNTAX OK $ node modules/test/test-vfs-checkpoint.js test-vfs-checkpoint: 9 passed, 1 FAILED (remaining FAIL was the module-system display path, fixed in the next commit)
## v4.6.99 — 2026-10-26
_Touches modules (4), main.html (1) · fix 1_
-- `Four phone-test defects — no preview close, single-file share, dead-end fullscreen, silent WebDAV move` `2023728`
+- **Four phone-test defects — no preview close, single-file share, dead-end fullscreen, silent WebDAV move** `2023728`
+ - All four came from the operator's phone pass on v4.6.95 and each collapsed to a line of code.
+ - … run `git show <sha>` for the rest
## v4.6.95 — 2026-10-26
_Touches modules (4), main.html (1) · release 1_
-- `Mobile download/share UX — deferred blob revoke, multi-select, Web Share, 44px targets` `6ef379f`
+- **Mobile download/share UX — deferred blob revoke, multi-select, Web Share, 44px targets** `6ef379f`
+ - 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.
+ - … run `git show <sha>` for the rest
## v4.6.94 — 2026-10-26
_Touches scripts (7), modules (3), help.html (1), main.html (1) · release 5, docs 1_
-- `Review dashboard, public guide, terminal commands (v4.6.94)` `de37884`
-- `Open the Agent tab on the queue, guide out of empty states, cleaner directives` `db25ef0`
-- `Preview a plan or report in a modal before deciding` `85133ab`
-- `Reconstruct _docs/decisions-2026-10-23.md — two feedback reports referenced a file that did not exist` `1d8f658`
-- `Filter network-class auto reports at ingest — count, don&#x27;t file` `a976c35`
-- `Autonomous first-pass reviewer — decide what is provable, leave the rest` `0446064`
+- **Review dashboard, public guide, terminal commands (v4.6.94)** `de37884`
+ - Dashboard (/admin/feedback.html, admin-gated)
+ - User-feedback tab over the live :8908 API: list, filter, search, publish reply,
+ - archive/reopen. Reply is what the reporter sees in /stear -> Your reports & replies.
+ - Agent-items tab: 3 plans parked at autonomy:review + 19 defect reports, one click each.
+ - Snapshot age line + stale warning (>30 min), and the last apply outcome with failures.
+ - Existing /admin/ tab relabelled "Client error log" (it renders feedback_log.json, not
+ - the feedback store) and now links to the review dashboard.
+ - Decision path
+ - … run `git show <sha>` for the rest
+- **Open the Agent tab on the queue, guide out of empty states, cleaner directives** `db25ef0`
+- **Preview a plan or report in a modal before deciding** `85133ab`
+ - The cards only carried a title, a path and buttons — approving blind was the only option. The page cannot read .hermes/plans/*.md itself (600 darkbot, php-fpm is www-data), so the collector — already running as darkbot every 10 minutes — now embeds what a decision needs:
+ - plans -> excerpt (first 60 lines, 2.4 KB cap), task headings, status/implementation lines reports -> body, page url, newOS4 version, user agent, first 3 error strings
+ - Modal: click a card title (or, on a report, read it in place) -> title, meta line with path/size/modified/task count/decision, the text in a scrollable monospace block, and the same Approve / Reject / Noise / Resolve actions. Esc, backdrop and the X close it; on a phone it is full-screen. Cards and modal share one actButtons() helper instead of duplicating markup.
+ - … run `git show <sha>` for the rest
+- **Reconstruct _docs/decisions-2026-10-23.md — two feedback reports referenced a file that did not exist** `1d8f658`
+- **Filter network-class auto reports at ingest — count, don't file** `a976c35`
+ - The debug supervisor files an auto report for any failed fetch, so a flaky phone connection or a retired endpoint became a review card. One cluster held 14 407 + 1 031 occurrences and produced 3 of the 19 cards in the queue; 10 of 21 items reviewed on 2026-10-26 were noise for this reason.
+ - api/debug/report.php: an auto report whose *every* error is network-class (fetch failed / failed to fetch / network error / load failed / aborted / signal is aborted / ERR_INTERNET_*) now increments data/debug/network_failures.json (per day, per source path, top 40 kept) and returns 202 {filtered:"network"}. No RPT file, no card.
+ - … run `git show <sha>` for the rest
+- **Autonomous first-pass reviewer — decide what is provable, leave the rest** `0446064`
+ - The operator asked: "when I don't do anything, an agent can do it for me and I can just follow up or steer it." This is that agent, deterministic (no LLM): same input, same decision, and every reason is a grep or a commit hash that can be checked.
+ - spaces/newos4/scripts/newos4-feedback-autoreview.py — rules, first match wins: R1 duplicate same fingerprint already decided -> resolve R2 already-fixed identifiers absent from modules/**/*.js AND `git log -S<token>` proves a commit removed them -> resolve R3 noise fetch/abort/probe patterns -> reject R4 still-present auto report + stack + identifiers still in code -> approve (max 2/day) R5 plan / R6 user report -> never decided
+ - … run `git show <sha>` for the rest
## v4.6.91 — 2026-10-23
_Touches modules (3), help.html (1), main.html (1) · release 1_
-- `Context window warns at 80% and says what trimming dropped (v4.6.91)` `227ee77`
+- **Context window warns at 80% and says what trimming dropped (v4.6.91)** `227ee77`
+ - P0-5 asked what to do about a 422k-token session against a 400k threshold. There is no 400k threshold in this tree — the budgets are maxContextTokens (12000) and maxContext*2 messages — so rather than chase a number that does not exist, this applies the decision that is defensible either way: warn before the cliff, never lose history without saying so.
+ - 80% of the token budget prints one warning per crossing (no per-turn nagging)
+ - the message-count trim unshifts a marker naming how many messages left the window and quoting
+ - the earliest one, instead of splicing them away silently
+ - 'max-context-tokens (default: 2000)' now reports the real code default, 12000
+ - … run `git show <sha>` for the rest
## v4.6.90 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
-- `The virtual /sys entry leaked into listings, trees and counts (v4.6.88-90)` `dc0114a`
+- **The virtual /sys entry leaked into listings, trees and counts (v4.6.88-90)** `dc0114a`
+ - One phantom entry, three symptoms:
+ - ls() injected 'sys' whenever it was called without a path, so 'cd _agents; ls' advertised a
+ - sys directory that does not exist, and list_files('_agents/sys') answered 'Directory not found' — the reported false negative. Injection is now gated on dir === root.
+ - tree() pushed the same virtual name into the walk and dereferenced it, throwing mid-output on
+ - every root listing. Guarded.
+ - _gatherFileInfo() walked into /sys and counted a synthetic subtree: 146 files / 114 dirs for a
+ - 44-file / 15-dir store. Virtual entries are skipped.
+ - Verified: root still lists sys; _agents and skills do not; tree('') completes; world model total (44) equals an independent walk of fs.root (44), agree: true.
## v4.6.87 — 2026-10-23
_Touches modules (2), help.html (1), main.html (1) · fix 1_
-- `Audit names broken refs instead of counting documentation (v4.6.86-87)` `f57108f`
+- **Audit names broken refs instead of counting documentation (v4.6.86-87)** `f57108f`
+ - 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.
## v4.6.85 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
-- `The audit stops inventing &#x27;999d unused&#x27; (v4.6.85)` `497cd96`
+- **The audit stops inventing '999d unused' (v4.6.85)** `497cd96`
+ - 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.
## v4.6.84 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
-- `World_model_query default path parses again (v4.6.83-84)` `a05f696`
+- **World_model_query default path parses again (v4.6.83-84)** `a05f696`
+ - _modelDigest() returned JSON.stringify(...) cut at 4000 chars plus '…(digest truncated)'. The tool then did JSON.parse() on it, so the default detail threw 'Unexpected end of JSON input' every single call; detail:'full' worked only because it returned the object untouched.
+ - The digest now returns an OBJECT (_modelSnapshot) and shrinks by reducing list lengths, then by dropping whole sections with an explicit 'omitted' list. The string form is generated at the edge and is always valid JSON.
+ - Verified: default call -> no error, 13-key snapshot, facts answered; caps 400/1200/4000 all under cap and parseable; the residual floor at cap 200 is identity fields only.
## v4.6.82 — 2026-10-23
_Touches modules (2), help.html (1), main.html (1) · fix 1_
-- `Skill-create stops mangling what the agent wrote (v4.6.82)` `c532594`
+- **Skill-create stops mangling what the agent wrote (v4.6.82)** `c532594`
+ - 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.
## v4.6.81 — 2026-10-23
_Touches modules (3), api (1), help.html (1), main.html (1) · fix 1_
-- `OutboxResult scoped, markdown mime, old reply path retired (v4.6.81)` `72a94ea`
+- **OutboxResult scoped, markdown mime, old reply path retired (v4.6.81)** `72a94ea`
+ - delegateTask declared `const outboxResult` inside the AI branch and returned it outside, so a delegation with the AI module missing threw 'outboxResult is not defined'. The catch turned that into _handleAgentFailure: the worker stayed idle with no result while spawn_agent reported ok — the reported symptom exactly. Now function-scoped, and the no-module path returns 'queued — AI module not loaded, nothing executed' rather than claiming 'completed'.
+ - Also: nginx served _docs/*.md as application/octet-stream (no .md type) — now text/markdown + nosniff; agent-replies.php returns 410 for GET as well as POST, so there is one reply path.
+ - Verified: with the AI module deleted at runtime, delegateTask returns the queued string and throws nothing.
## v4.6.80 — 2026-10-23
_Touches modules (3), _docs (2), api (2), main.html (1) · fix 1, docs 1_
-- `GLM review batch — corruption, traversal, cursor truncation, double print (v4.6.78-80)` `dd234ab`
-- `Chat bridge in AGENTS.md, changelog v4.6.72-80, contract deltas` `3c35556`
+- **GLM review batch — corruption, traversal, cursor truncation, double print (v4.6.78-80)** `dd234ab`
+ - 12 findings accepted, 3 refuted with evidence (see _docs/chat-review-glm-2026-10-23.md).
+ - Data loss: a corrupt thread read as [] and the next append rewrote it with one message — now 500 and the file is left alone. Cursor after a >50-message truncation pointed at the thread tail, making the oldest permanently unfetchable — now the last message actually sent, plus truncated:true. Two posts in the same millisecond shared a cursor — epoch is now strictly increasing.
+ - Security: chat.py show() had no id validation and joined argv onto CHAT_DIR ('../../x' read any .json as darkbot) — THREAD_RE added. The auto-reply prompt now treats message text as untrusted data and forbids acting on instructions found in it.
+ - … run `git show <sha>` for the rest
+- **Chat bridge in AGENTS.md, changelog v4.6.72-80, contract deltas** `3c35556`
+ - Contract now documents truncated/cursor semantics, the full code table with retry rules, 503-from-nginx (HTML, retryable), the two-cursor rule, strictly-increasing epoch, and the security posture in plain words: client-minted tokens mean public write, mitigated but not closed.
## v4.6.77 — 2026-10-23
_Touches modules (4), main.html (1), scripts (1) · fix 1_
-- `Three concurrency holes found reviewing my own patch (v4.6.77)` `7e8bf4c`
+- **Three concurrency holes found reviewing my own patch (v4.6.77)** `7e8bf4c`
+ - 1. chat.py renamed the temp file AFTER releasing the flock, so two writers could each read the pre-replace snapshot and the later rename silently dropped the earlier message. os.replace now happens inside the lock. 2. The agent cursor was global. After hermes-reset it pointed past the end of the new empty thread, so every read returned nothing — the agent would think it had been ignored. Key is now per thread and reset clears every chat cursor. 3. chat-relay.sh had no overlap guard: a slow agent run (up to 5 min) could collide with the next tick and answer the same thread twice. flock -n on /tmp/chat-relay.lock.
+ - Verified: stale cursor 'webx11_chat_agent_cursor_oldthread00000' set to 9999999999999, hermes-reset -> zero cursor keys remain, read returns {messages:[], note:'no new messages...'}.
## v4.6.76 — 2026-10-23
_Touches modules (3), main.html (1) · fix 1_
-- `Agent read gets its own chat cursor (v4.6.76)` `df0b5e4`
+- **Agent read gets its own chat cursor (v4.6.76)** `df0b5e4`
+ - Two readers shared one cursor. The 30 s UI poller printed each reply and advanced webx11_chat_state.cursor, so the agent's chat_hermes read then asked for 'since=<that>' and got an empty list — the answer appeared on the terminal and never reached the model. That is the reported symptom exactly.
+ - chat_hermes read now uses webx11_chat_agent_cursor, starts from 0 on first use so the whole thread is visible, and never touches the display cursor. Empty reads say why ('still awaiting Hermes' vs 'no new messages since your last read').
+ - Proven: UI poll consumed the reply (printed to terminal), then chat_hermes read still returned both the agent's question and Hermes's answer.
## v4.6.75 — 2026-10-23
_Touches modules (2), skills (2), main.html (1) · fix 1_
-- `Skill points at the served contract URL, not a VFS path (v4.6.75)` `83b399d`
+- **Skill points at the served contract URL, not a VFS path (v4.6.75)** `83b399d`
+ - 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.
## v4.6.74 — 2026-10-23
_Touches skills (3), modules (2), main.html (1) · docs 1_
-- `Seed a chatting-with-hermes skill (v4.6.74)` `020f519`
+- **Seed a chatting-with-hermes skill (v4.6.74)** `020f519`
+ - 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).
## v4.6.73 — 2026-10-23
_Touches modules (4), _docs (2), help.html (1), main.html (1) · release 1, docs 1_
-- `Hermes command, auto-poll, chat_hermes tool, token reset (v4.6.73)` `cfbc835`
-- `Update brief for the agent, v4.6.57 to v4.6.73` `6163429`
+- **Hermes command, auto-poll, chat_hermes tool, token reset (v4.6.73)** `cfbc835`
+ - Phase C as repo edits (browser edits in _mods/ do not survive a reload).
+ - `hermes <msg>`: sends through the chat endpoint and always prints an outcome —
+ - '📨 Sending…' then either '✅ sent id=… epoch=…' or a coded error with a hint (429 retry later, 503 host busy, 401 run hermes-reset). `feedback` stays as the alias.
+ - NOT named `chat`: ChatStudioModule already registers `chat` and dispatch walks modules
+ - in order, so a second `chat` was silently unreachable. Found by test, not by reading.
+ - `hermes-poll` / `feedback-inbox`, `hermes-reset` (token rotation).
+ - Poller: 30 s, paused while the tab is hidden, polls on visibilitychange, first poll
+ - adopts the cursor and prints nothing, cursor persisted in webx11_chat_state.
+ - … run `git show <sha>` for the rest
+- **Update brief for the agent, v4.6.57 to v4.6.73** `6163429`
+ - Seven behaviour changes it has to act on, not just seven code changes: the command is `hermes` not `chat` (ChatStudio owns that name), replies arrive untyped within 30s, the cursor is epoch milliseconds, only 429 is retryable, the old reply endpoint is 410, multi-path grep/head/tail, and on-demand agents are not a fault.
## v4.6.70 — 2026-10-23
_Touches modules (4), api (4), help.html (1), main.html (1) · release 2_
-- `On-demand worker labels and a server-canonical predictor store (v4.6.70)` `e1d1167`
-- `Token-scoped chat endpoints, contract, host CLI, old POST retired (v4.6.71 phase B)` `fde3ce3`
+- **On-demand worker labels and a server-canonical predictor store (v4.6.70)** `e1d1167`
+ - Two decisions recorded by the operator on 2026-10-23, implemented.
+ - 6 — spawn-on-demand is the design, so stop reporting it as a defect:
+ - OrchestratorModule counts delegations inside delegateTask()
+ - status prints "Mode: on-demand — N/M worker(s) available, X delegation(s) this
+ - session" and drops the "spawning is not dispatching" warning
+ - per-agent lines read "available, no delegations yet (spawned ...)" / "N
+ - delegation(s), last ..."
+ - 10 — per-user, server file canonical, browser VFS a TTL'd read-through cache:
+ - … run `git show <sha>` for the rest
+- **Token-scoped chat endpoints, contract, host CLI, old POST retired (v4.6.71 phase B)** `fde3ce3`
+ - api/chat/{lib,post,poll}.php: thread = sha256(token)[0:16]; flock read-modify-write;
+ - tmp+rename; ms epoch cursor; per-thread + per-IP windows with retry_after_ms; 401/413/507/405 carry a machine code, only rate_limited is retryable
+ - _docs/chat-contract.md: frozen shapes, incl. why the cursor is ms and not ts
+ - scripts/chat.py: list/show/reply, UTC, locked, atomic replace
+ - api/debug/agent-replies.php POST -> 410 Gone with pointers (one write path)
+ - Measured: 401 no-token, 201+id, poll 1 msg, poll>since 1 new, data/chat 403, 4001 chars 413 (codepoints), 429 with Retry-After 576, round trip via chat.py.
## v4.6.67 — 2026-10-23
_Touches modules (3), help.html (1), main.html (1) · fix 1_
-- `Grep, head and tail take every path argument (v4.6.67)` `8ab3972`
+- **Grep, head and tail take every path argument (v4.6.67)** `8ab3972`
+ - Packet 1's multi-path ask was only honoured by wc in v4.6.58 — grep read only args[ai+1] and head/tail only positional[0], so `grep x a b` searched `a`, printed one file's hits, and exited 0. Same silent drop, three more commands.
+ - grep: loops every path; multiple files print `file:line: text`, `-c` prints one
+ - count per file, missing files are reported per file and excluded from the summary count; single-file output unchanged
+ - head: every path, `==> file <==` header when more than one; `head f 40` still works
+ - tail: every path with headers; a numeric second positional is still the legacy
+ - `tail <file> <count>`; `-f` warns and follows only the first file instead of printing all of them and claiming otherwise
+ - … run `git show <sha>` for the rest
## v4.6.65 — 2026-10-23
_Touches skills (3), modules (2), main.html (1) · docs 1_
-- `Merge the two shell-composition skills into one seeded skill (v4.6.65)` `bfa1ffa`
+- **Merge the two shell-composition skills into one seeded skill (v4.6.65)** `bfa1ffa`
+ - 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
+ - … run `git show <sha>` for the rest
## v4.6.64 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
-- `An invalid grep pattern returns a message, not an exception (v4.6.64)` `b45f13f`
+- **An invalid grep pattern returns a message, not an exception (v4.6.64)** `b45f13f`
+ - Packet 8. `new RegExp(pattern)` sat unguarded in the terminal grep handler and in VFS find()/grep(), so a bad pattern threw out of the command handler into the error ledger -- the same ledger packet 2 had to stop trusting.
+ - module-fs.js grep: compile in try/catch, print
+ - "grep: invalid pattern: <pattern> (<engine message>)" as a warning and return
+ - virtual-fs.js find()/grep(): same guard, return [] and stash the message on
+ - _lastPatternError instead of throwing into the module error wrapper
+ - Verified live on v4.6.64 with the packet's own repro: `grep "/\bug_id\" file` prints the message, the error counter stays 0 (was: one ledger entry per call), a valid pattern still matches, and fs.grep('[') returns [] instead of throwing.
## v4.6.63 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · release 1_
-- `Agent status says never-scheduled vs waiting; `guides` command (v4.6.63)` `1615b27`
+- **Agent status says never-scheduled vs waiting; `guides` command (v4.6.63)** `1615b27`
+ - Packet 6 and 7.
+ - 6 — four agents showed `sleeping` and nothing distinguished "waiting for work" from "never handed a task". Added tasksAssigned/lastTaskAt to the agent record (counted where a task is actually assigned), and the status line now reads idle — NEVER SCHEDULED (0 tasks since 2026-10-23 16:22) idle — waiting for work (3 task(s), last 2026-10-23 16:22) parked — NEVER SCHEDULED ... plus "Dispatch: 0/3 agent(s) have ever been given a task" and a warning that spawning is not dispatching.
+ - 7 — /guides/ was reachable by nobody. New `guides` command lists it newest-first with sizes and the preview/cat hints; help.html gained a Guides nav entry and section with links to the published guides.
+ - … run `git show <sha>` for the rest
## v4.6.62 — 2026-10-23
_Touches modules (3), help.html (1), main.html (1) · fix 1_
-- `Tokenised body-aware skill search for load_skills (v4.6.62)` `3194d1c`
+- **Tokenised body-aware skill search for load_skills (v4.6.62)** `3194d1c`
+ - Packet 4. The matcher tested the whole query as one substring against name+description+tags: word order decided the outcome ("telemetry dashboard" = 0 while "telemetry" = 1), a sentence could never match, and skill bodies were never searched at all.
+ - _skillTokens(): lowercase, split on non-word chars, drop <3-char words and a
+ - stopword list; a query made only of stopwords keeps its literal form
+ - scoring: name 3, description 2, tags 2, body 1 + up to 1.0 for repeated hits,
+ - multiplied by token coverage (0.5 + 0.5 * matched/total), +3 phrase bonus so word order can never zero a real hit
+ - results carry score and matchedTokens ("2/3"); top 3 by score, name as tiebreak
+ - zero results now name the tokens searched and the closest skill names instead of
+ - "No matching skills found"
+ - … run `git show <sha>` for the rest
## v4.6.61 — 2026-10-23
_Touches modules (6), help.html (1), main.html (1) · fix 1_
-- `One error counter for errors/dashboard/world-model (v4.6.61)` `36450fa`
+- **One error counter for errors/dashboard/world-model (v4.6.61)** `36450fa`
+ - Packet 2 (three surfaces, three answers). Measured first: `errors` scanned /logs/current.log for [ERROR] lines, but that file only ever receives explicit terminal.log('ERROR', ...) calls -- exactly one call site in the whole app -- so it read 0. The dashboard added DebugSupervisor._errorDB plus failed tool calls (18), and the world model read the debug DB alone (11).
+ - terminal.countErrors() / countLogErrors(): the single counter, exposed on the one
+ - object all three surfaces already hold
+ - `errors` prints the canonical total with its breakdown, the entry list (hash,
+ - count, severity, message), and reports the log scan separately as a mirror
+ - dashboard health.errors and health.errorBreakdown derive from it
+ - world model telemetry.recentErrors derives from it, plus errorsBySource
+ - terminal.log('ERROR', ...) now also records into DebugSupervisor, so the log is a
+ - … run `git show <sha>` for the rest
## v4.6.58 — 2026-10-23
_Touches modules (4), help.html (1), main.html (1) · fix 1_
-- `Refuse shell composition, wc takes multiple paths (v4.6.58)` `24d189d`
+- **Refuse shell composition, wc takes multiple paths (v4.6.58)** `24d189d`
+ - Packet 1 and 3 (silent wrong answers from a shell-less terminal).
+ - detectComposition() in the security gate refuses ; && || | before dispatch.
+ - `;` used to fail loudly, but `&&`/`||` folded the right-hand side into the left command's argv (wc -l f && grep -c x f returned "42 42"), and a `|` arriving from the AI run_command tool bypassed splitPipe entirely and degraded the pattern to empty -- which matches every line. Operators inside quotes are untouched.
+ - wc accepts multiple paths: one line per file plus a total, per-file errors for
+ - missing paths. `wc -c a b` used to print only `a` and exit 0.
+ - help.html: "There is no shell" callout at the top of the command reference,
+ - plus the v4.6.57/58 changelog entry.
+ - … run `git show <sha>` for the rest
## v4.6.57 — 2026-10-23
_Touches modules (6), main.html (1) · fix 1_
-- `Predictor arithmetic, deployed-source viewer, ls text output (v4.6.57)` `bece3bc`
+- **Predictor arithmetic, deployed-source viewer, ls text output (v4.6.57)** `bece3bc`
+ - Acting on the "Request to the newOS4 team" packet (spaces/agando/guides/, re-verified by its author on 4.6.56). Three verified defects fixed:
+ - Packet 5 (predictor arithmetic) - module-predictor.js
+ - the persisted field named mape held a SUM while predictor-status divided by the
+ - paired count: renamed to errorSum, older payloads read through
+ - errorPct is stored raw; the 500% clamp is now render-only, so a 475x miss stays
+ - visible instead of being flattened to the ceiling
+ - predictions are floored at 1 s and marked floored; a floored prediction is
+ - consumed but never scored as 100% error (a 0 s prediction is not a prediction)
+ - … run `git show <sha>` for the rest
## v4.6.56 — 2026-10-23
_Touches modules (5), downloads (1), help.html (1), main.html (1) · fix 1_
-- `Measure predictor accuracy over paired predictions (v4.6.56)` `e3157bb`
+- **Measure predictor accuracy over paired predictions (v4.6.56)** `e3157bb`
+ - The accuracy readout was structurally 0%: module-predictor.js:187 incremented the denominator on every observed run while the numerator only moved when a stored prediction existed, and predictions were stored only by the manual `predict` command (the AI tool predict_task computed one and discarded it). Replaying the AI loop's own call path with 6 tasks printed "Predictions made: 0, Accuracy: 0/6".
+ - pending predictions with a 15m TTL, consumed by threaded id -> task hash ->
+ - cli-only similarity >= 0.5; unpaired runs never enter the denominator
+ - duration scored within 30% or 3s (integer predictions vs fractional observations)
+ - errorPct capped at 500% so one miss cannot poison the mean error
+ - schema v2: v1 correct/total counters dropped on load, observed runs and the
+ - prediction counter kept; task text stored as an FNV-1a hash only
+ - tuning (confidence-threshold, min-data-points, pair-ttl) persists
+ - … run `git show <sha>` for the rest
## v4.6.55 — 2026-10-23
_Touches modules (6), api (5), data (3), scripts (3) · fix 1, release 1_
-- `Empty-pipeline crash, stacks in reports, frame in digest + dashboard (v4.6.55)` `2855c63`
-- `Fix ledger + norm clustering — the loop now knows what a fix was worth` `eec18ba`
+- **Empty-pipeline crash, stacks in reports, frame in digest + dashboard (v4.6.55)** `2855c63`
+ - Root cause: splitPipe() splits on every '|', so `help |`, `| wc -l`, `a || b` produced a stage with no argv and executePipeChain() called parts[0].toLowerCase() on undefined — 989 occurrences, first filed 2026-10-20, unfixable from the report because bug reports dropped the stack that named executePipeChain:3955. Same unguarded read existed in _classify().
+ - terminal.js: _classify() returns an inert classification for empty argv; executePipeChain()
+ - parses each stage once, filters stages with no argv (so the last real stage stays final), warns only when the whole pipeline is empty. Last-stage test now compares against the filtered length.
+ - module-system.js: `bug` diagnostics include the stack (10 frames, 1200 chars); auto-filed reports
+ - carry it too — they are the majority of the inbox and were previously message-only.
+ - … run `git show <sha>` for the rest
+- **Fix ledger + norm clustering — the loop now knows what a fix was worth** `eec18ba`
+ - Lifetime counts ranked an 8-day-old outage above today's live crash, and nothing asserted that a shipped fix actually held: a regression of a fixed bug looked exactly like a new bug.
+ - api/debug/ingest.php: norm_key() stores `norm` (host/port/scheme/`?v=`/`//` stripped) — the client
+ - hash covers the raw URL, so one outage became two signatures (`/ollama//api/tags` 18 203x and `/ollama/api/tags` 16 540x). PHP + Python implementations verified byte-identical on 5 real messages.
+ - scripts/newos4-ledger.py: data/maintenance/fixes.json maps norm -> {commit, fixed_at, note}, and
+ - verify() returns REGRESSED / WATCHING / CLOSED / UNSEEN. "Seen after the fix" alone is not a regression (a fix shipped 10d ago with one sighting 9d ago has been silent since) — regressed means seen after the fix AND recent; otherwise the silence wins.
+ - … run `git show <sha>` for the rest
## v4.6.54 — 2026-10-23
_Touches modules (3), data (2), downloads (2), main.html (1) · fix 1, project 1_
-- `Define VirtualFS.emptyTrash() — empty-trash threw since 2026-10-18` `563a9be`
-- `Archive the delta-sync probe report (test artifact)` `235ab7c`
+- **Define VirtualFS.emptyTrash() — empty-trash threw since 2026-10-18** `563a9be`
+ - Report RPT-20261018-164646-89352963 ("this.fs.emptyTrash is not a function") was filed by the auto-filer and never fixed: terminal.js:1777 called a method VirtualFS never defined, so 'empty-trash --confirm' threw instead of emptying. Added emptyTrash() returning true only when it removed something.
+ - Verified live on v4.6.54: put -> rm -> empty-trash --confirm => 'Trash emptied.'; second run => 'Trash is already empty.'; .trash entries 0.
+- **Archive the delta-sync probe report (test artifact)** `235ab7c`
## v4.6.53 — 2026-10-23
_Touches data (9), api (7), modules (7), scripts (5) · release 3, fix 2, project 1_
-- `Idempotent ingest, admin-gated reads, honest counts (v4.6.52-53)` `d86d508`
-- `Maintenance agent channel — triage engine, two-way replies, cron` `8a2abfb`
-- `Unmeasured skills are untracked, world-model writes verified, skill audit` `6a30129`
-- `Drop the one-off delta-sync probe row` `1a3dc45`
-- `Dashboard triage + thread panels, mobile layout, gate error.php` `0f5fdbd`
-- `Operator steering queue for the maintenance jobs` `c117065`
+- **Idempotent ingest, admin-gated reads, honest counts (v4.6.52-53)** `d86d508`
+ - Root cause: _syncToBackend() re-sent the whole error DB with cumulative counts every 300s, and ingest.php did count += client count per batch. 33 real signatures had accumulated 116,696 "occurrences" (one self-referential row alone: 46,133).
+ - Backend
+ - ingest.php: per-(session,hash) synced map, adds only the delta, resets prev when the
+ - client count shrank (cleared localStorage), caps delta at 5000, bounds synced at 500 hashes and sessions at 200, normalises timestamps to +00:00, 30-day retention that also purges dropped hashes from every synced map, returns `accepted` (entries) next to `ingested` (occurrences).
+ - stats.php/dashboard.php: total_errors = occurrences, unique_errors = signatures,
+ - last-ingest card. The two numbers were inconsistent by ~3,500x.
+ - health.php: reports self_referential count and status=degraded instead of ok.
+ - … run `git show <sha>` for the rest
+- **Maintenance agent channel — triage engine, two-way replies, cron** `8a2abfb`
+ - scripts/newos4-triage.py: deterministic collector (records vs occurrences, real vs
+ - noise vs self-referential, new reports since last run, checker WARN/FAIL). Untrusted strings are sanitised and prefixed DATA| before they can reach an agent prompt; state + output written under flock with mode 644.
+ - scripts/newos4-reply.py: appends one reply to data/maintenance/replies.json under
+ - flock. Hermes writes here instead of POSTing with an admin cookie, so an expired session can never look like a delivered reply.
+ - api/debug/agent-replies.php: GET/POST (admin-gated by nginx) half of the thread.
+ - api/debug/report.php: accepts thread + kind=maintenance so in-app feedback is
+ - distinguishable from operator bug reports.
+ - terminal: `feedback <message>` and `feedback-inbox` commands; help.html rows added
+ - … run `git show <sha>` for the rest
+- **Unmeasured skills are untracked, world-model writes verified, skill audit** `6a30129`
+ - 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 +
+ - … run `git show <sha>` for the rest
+- **Drop the one-off delta-sync probe row** `1a3dc45`
+- **Dashboard triage + thread panels, mobile layout, gate error.php** `0f5fdbd`
+ - Triage card reads admin/docs/newos4-triage-latest.json defensively: missing/unreadable
+ - renders as a warning pill, never as zeros (a zero here reads as "nothing is wrong"). Freshness pill goes stale past 26h so a dead collector cannot look healthy.
+ - Maintenance thread merges in-app `feedback` reports (kind=maintenance) with agent
+ - replies from data/maintenance/replies.json into one chronological view.
+ - Mobile: <=640px single column, smaller type, .table-scroll wrappers, Type column
+ - hidden, max-width clamp. verify-mobile.py: 360/390px, light+dark -> ALL VIEWPORTS OK, page width == viewport (no horizontal overflow).
+ - "Last ingest" now uses max(ingested_at) instead of end($errors): after the prune the
+ - array is not chronological and the header contradicted the triage card by 3 days.
+ - … run `git show <sha>` for the rest
+- **Operator steering queue for the maintenance jobs** `c117065`
+ - newos4-steer.py add|list|done|clear appends directives under flock with unique ids; both cron jobs read it first and mark entries consumed. Verified: two adds with distinct ids, done only affects the named entry.
## v4.6.51 — 2026-10-22
_Touches modules (4), downloads (1), main.html (1), course (1) · fix 1, docs 1_
-- `Dashboard shows the real world model and stops dying after 10 s (v4.6.51)` `c54ca24`
-- `Clear the help.html command backlog, and make it detectable` `8fc4e01`
+- **Dashboard shows the real world model and stops dying after 10 s (v4.6.51)** `c54ca24`
+ - Two defects, both verified live before/after:
+ - 1. The World Model panel always said "Not synthesized yet". The page had a renderer for data.worldModel and _collectDashboardData() never set it -- the model was right there in WorldModelModule._model. Now collected and mapped to the renderer's exact keys (model has modules.loaded/skills.count/memory.nodes, the page reads modules/skills/memoryNodes), plus changesSinceLastSnapshot and the recent-error sample, with an honest "not synthesized" fallback.
+ - 2. The page discarded any payload older than 10 s and its own 5 s auto-refresh then showed "Cannot connect to newOS4". Stale payloads are now rendered with their age ("Data as of 1:20:14 AM - 3m ago"), labelled amber past 5 min.
+ - … run `git show <sha>` for the rest
+- **Clear the help.html command backlog, and make it detectable** `8fc4e01`
+ - help.html had not been touched since 2026-08-02. Auditing the page against the code found 74 commands the runtime registers but the docs never mentioned, and 4 rows documenting commands that no longer exist.
+ - Documented (by section): terminal 11 (dashboard --watch/--stop, usage, ps, top, df, free, history, run, sysmsg, view_module), a2a 6, AI 14 (webxai/webx/wx, context*/smart-ask, commit-*, ai-model/ai-ignore), vaultmind 4, debug 2, filesystem 2, sessions 1, memory-graph 3, modules 4, neural-vfs 1, agents 3, web 1, voice 3, config 3, extras 8 (kanban, three, course, gzip, wget, gui-*), vault 2 (encrypt/decrypt), skills 5, webdav 1, plus an alias row for h/s/ll.
+ - Removed ghost rows: commands, check, sandbox-kill, webdav-explorer -- none are registered anywhere in modules/; the sandbox prose no longer promises sandbox-kill.
+ - … run `git show <sha>` for the rest
## v4.6.50 — 2026-10-22
_Touches modules (5), main.html (1) · release 1_
-- `World-model-ask answers with the LLM, grounded in the model (v4.6.50)` `6ddaadd`
+- **World-model-ask answers with the LLM, grounded in the model (v4.6.50)** `6ddaadd`
+ - `world-model-ask` was a 10-keyword lookup: any real question returned "Unknown query. Try: modules, skills, ...". The documented example ("what changed since yesterday") compared nothing — it restated the current snapshot.
+ - _modelDigest(): whitelisted snapshot (measured 942-965 chars on fixtures,
+ - ~3.3 KB live) + recent error sample + changesSinceLastSnapshot
+ - _buildAskPrompt(): "answer only from this JSON, never invent numbers, name the
+ - command that would produce missing data"
+ - ask(): connects Ollama on demand, answers via chat(), falls back to the facts
+ - path with a visible warning; --facts keeps the instant lookup
+ - _diffSincePrev(): keeps one previous snapshot in /system/world-model.prev.json
+ - … run `git show <sha>` for the rest
## v4.6.49 — 2026-10-22
_Touches modules (4), main.html (1) · fix 1_
-- `Coreutils flags + loop-guard arg key, both found by the agent&#x27;s live run (v4.6.49)` `7f3508b`
+- **Coreutils flags + loop-guard arg key, both found by the agent's live run (v4.6.49)** `7f3508b`
+ - Agent session surfaced two real defects:
+ - 1. The A4 prompt told it to use grep -n / sed -n 'N,Mp' / wc -l / head -N — none of those flags parsed: grep treated '-n' as the pattern (6/6 variants failed), wc read '-l' as a filename, head looked for a file named '-2', sed had no range parser. Fixed the tools, not just the text: grep now accepts -n/-i/-c/-v/-l (line numbers were already printed), wc accepts -l/-w/-c, head accepts -N and -n N, sed accepts Np and N,Mp, tail no longer prints a blank line for a trailing newline. Prompt re-ranked to the working forms.
+ - … run `git show <sha>` for the rest
## v4.6.47 — 2026-10-21
_Touches modules (3), main.html (1) · fix 1_
-- `GLM review follow-ups for AI read/context (v4.6.47)` `3fed8df`
+- **GLM review follow-ups for AI read/context (v4.6.47)** `3fed8df`
+ - loop guard: a mutation unblocks the replay but no longer erases the repeat counter
+ - (identical mutating calls could loop forever)
+ - tool traces are per agent tag, flushed in a catch as well as on success, ring-buffered
+ - (first 2 + last 10) so verification reads survive
+ - read_file notes are metadata only (path/range/next_offset); no file bodies, no secrets,
+ - and web_search/web_fetch output is excluded from durable context (injection surface)
+ - trace is role:'system' (was 'assistant' — the model re-read it as its own answer)
+ - surrogate-safe pagination, 500 floor / 16000 cap, prompt limits aligned
+ - … run `git show <sha>` for the rest
## v4.6.46 — 2026-10-21
_Touches modules (3), main.html (1) · release 1_
-- `AI paged file reads, durable tool traces, 12k context budget, read/tool/verify prompt rules (v4.6.46)` `44d023d`
+- **AI paged file reads, durable tool traces, 12k context budget, read/tool/verify prompt rules (v4.6.46)** `44d023d`
+ - read_file: offset/limit + JSON {total_chars,truncated,next_offset} (was a hard 8000-char cut with no resume)
+ - tool results now leave a per-turn trace that is persisted into session context and survives compression
+ - maxContextTokens 2000 -> 12000; compression keeps 8 exchanges and 400 chars/msg
+ - system prompt: page big files, prefer grep -n / sed -n / head, re-read + verify after every edit
+ - verified in the live browser: 30k file pages 0->8000->16000, prompt section present, v4.6.46 served
## v4.6.45 — 2026-10-21
_Touches modules (7), main.html (1) · fix 1_
-- `Download link uses the user&#x27;s space; WebDAV share copies verified file links; preview fullscreen+share; files-&gt;WebDAV upload button (v4.6.45)` `8e7b3c5`
+- **Download link uses the user's space; WebDAV share copies verified file links; preview fullscreen+share; files->WebDAV upload button (v4.6.45)** `8e7b3c5`
+ - 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)
## v4.6.44 — 2026-10-18
_Touches modules (3), main.html (1), scripts (1) · fix 1_
-- `Stop agent accumulation on reload — stable role ids + boot reconciliation` `9034ff3`
+- **Stop agent accumulation on reload — stable role ids + boot reconciliation** `9034ff3`
+ - spawnAgent() accepts an idHint and is idempotent; boot team uses agent-<role>
+ - _reanimateFromVFS() adopts newest dir per default role, fs.rm()s duplicates/orphans
+ - _bootAgents() awaits async VFS hydration before reconciling, then spawns only gaps
+ - scripts/test-agents-reload.js: 5-boot regression + hydration-race test
+ - v4.6.44 bundle rebuild + cache-bust
## v4.6.40 — 2026-10-18
_Touches modules (7), api (1), main.html (1) · project 1_
-- `Review fixes (bug diagnostics, trash tree, dir paste, stale capture, preview)` `05257d7`
+- **Review fixes (bug diagnostics, trash tree, dir paste, stale capture, preview)** `05257d7`
## v4.6.38 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
-- `Auto-file fatal errors with 3/day quota (A5)` `dd73eee`
+- **Auto-file fatal errors with 3/day quota (A5)** `dd73eee`
## v4.6.37 — 2026-10-18
_Touches modules (4), api (3), downloads (1), main.html (1) · project 2_
-- `Bug-report pipeline (report.php, bug command, HTML generator, telemetry-loop fix)` `ea96355`
-- `Regenerate course atlas (adds bug command)` `923bdbd`
+- **Bug-report pipeline (report.php, bug command, HTML generator, telemetry-loop fix)** `ea96355`
+- **Regenerate course atlas (adds bug command)** `923bdbd`
## v4.6.36 — 2026-10-18
_Touches modules (2), main.html (1) · project 1_
-- `Fix CourseModule print-capture swallowing all command output` `3686850`
+- **Fix CourseModule print-capture swallowing all command output** `3686850`
## v4.6.35 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
-- `File manager multi-select + safe paste/rename/delete (Part B)` `7823821`
+- **File manager multi-select + safe paste/rename/delete (Part B)** `7823821`
## v4.6.34 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
-- `Running tool ledger in the prompt (D4)` `2f84d27`
+- **Running tool ledger in the prompt (D4)** `2f84d27`
## v4.6.33 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
-- `Progress-based turn budget (D3)` `b777a5a`
+- **Progress-based turn budget (D3)** `b777a5a`
## v4.6.31 — 2026-10-18
_Touches modules (3), main.html (1) · project 1_
-- `Tool-result memo cache with epoch invalidation (D2)` `fe6d198`
+- **Tool-result memo cache with epoch invalidation (D2)** `fe6d198`
## v4.6.30 — 2026-10-18
_Touches modules (4), main.html (1) · project 1_
-- `Per-agent session isolation (D1)` `1c02f4c`
+- **Per-agent session isolation (D1)** `1c02f4c`
## v4.6.29 — 2026-10-18
_Touches modules (4), main.html (1) · project 1_
-- `Progress-aware loop guard + agent attribution` `29de5fe`
+- **Progress-aware loop guard + agent attribution** `29de5fe`
## v4.6.28 — 2026-10-18
_Touches course (6), course.html (2), scripts (2), main.html (1) · project 4_
-- `Cache-bust course.js after review fixes` `2c2461f`
-- `Exclude auto-open from lesson_open telemetry, add session counts, filter synthetic sids` `9457e6c`
-- `Weekly telemetry report script + Monday 09:00 cron` `3bf3694`
-- `Fix P0 content errors + completion/mobile/keyboard UX (review A)` `486b10b`
+- **Cache-bust course.js after review fixes** `2c2461f`
+- **Exclude auto-open from lesson_open telemetry, add session counts, filter synthetic sids** `9457e6c`
+- **Weekly telemetry report script + Monday 09:00 cron** `3bf3694`
+- **Fix P0 content errors + completion/mobile/keyboard UX (review A)** `486b10b`
## v4.6.27 — 2026-10-18
_Touches course (20), scripts (8), modules (5), api (2) · project 3_
-- `Interactive AI course (terminal module, portal, atlas, telemetry, daily maintenance+cron) + free store card` `1bc6369`
-- `First daily lesson generated, atlas 345 commands, modules-index refreshed` `7221eeb`
-- `Post-exec review fixes (telemetry detail sanitize, skill-sync retry, atlas check)` `ccbf471`
+- **Interactive AI course (terminal module, portal, atlas, telemetry, daily maintenance+cron) + free store card** `1bc6369`
+- **First daily lesson generated, atlas 345 commands, modules-index refreshed** `7221eeb`
+- **Post-exec review fixes (telemetry detail sanitize, skill-sync retry, atlas check)** `ccbf471`
## v4.6.26 — 2026-10-17
_Touches data (3), modules (2), downloads (2), scripts (2) · project 2_
-- `2026-07-29_20:45 changes` `e55f2fe`
-- `Floating-ui-designer v3 — repair 12 escape-mangled JS sites, rebuild 344-cmd registry (124-&gt;344), quoting/import/preset UX fixes; add Node harness + registry generator; floating-ui skill JSON de-piped` `bb41664`
+- **2026-07-29_20:45 changes** `e55f2fe`
+- **Floating-ui-designer v3 — repair 12 escape-mangled JS sites, rebuild 344-cmd registry (124->344), quoting/import/preset UX fixes; add Node harness + registry generator; floating-ui skill JSON de-piped** `bb41664`
## v4.6.25 — 2026-10-08
_Touches data (3), modules (2), api (1), main.html (1) · project 1_
-- `2026-07-29_20:45 changes` `eb7462a`
+- **2026-07-29_20:45 changes** `eb7462a`
## v4.6.18 — 2026-10-02
_Touches modules (2), main.html (1), scripts (1) · project 1_
-- `Nginx dav trailing-slash semantics` `89c5beb`
+- **Nginx dav trailing-slash semantics** `89c5beb`
+ - 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)
## v4.6.16 — 2026-10-02
_Touches modules (2), main.html (1), scripts (1) · project 1_
-- `NewOS4 webdav path fix + WebX11 resilience package (2026-10-02)` `d3e5af7`
+- **NewOS4 webdav path fix + WebX11 resilience package (2026-10-02)** `d3e5af7`
+ - 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:
+ - … run `git show <sha>` for the rest
## v4.6.10 — 2026-10-01
_Touches modules (55), skills (36), downloads (32), data (11) · project 11, fix 4, release 2_
-- `2026-07-09_06:00 changes` `97db1b5`
-- `2026-07-29_20:50 changes — gunes sponsorluk page, bunq payment modal, AI platforms, news W30` `8dbbab4`
-- `Add kanban module to newOS4 — todo/doing/done board with priorities` `4a3012a`
-- `Register kanban in module-loader, bump to v4.6.11` `aee87f5`
-- `Add kanban to packages.json, update cache version to 2026-07-29` `c1b96f7`
-- `2026-07-29_20:45 changes` `79f682c`
-- `Tail -n/-N flags + live follow mode (-f) with test harness` `1692f20`
-- `Exact-delta assertions for tail -f follow/stop semantics` `5f17863`
-- `Sed command (s///, /pat/d, Nd, -i, -n, pipe input) + help entries` `d8c63a9`
-- `Ctrl+C/Esc stops tail -f follow; security whitelist gains sed` `69f1fc5`
-- `Rebuilt bundles with tail -f and sed` `ec02916`
-- `Typo suggestion bar (one-tap fix), consistent unknown-cmd messages, AI loop guard` `2cf0d3d`
-- `Race-safe AI loop guard (depth counter), cover _capture* entry points, structured JSON tool error` `8f2fb31`
-- `Parse per-call captured output for unknown-cmd error (race-free under Promise.all)` `212c05d`
+- **2026-07-09_06:00 changes** `97db1b5`
+- **2026-07-29_20:50 changes — gunes sponsorluk page, bunq payment modal, AI platforms, news W30** `8dbbab4`
+- **Add kanban module to newOS4 — todo/doing/done board with priorities** `4a3012a`
+- **Register kanban in module-loader, bump to v4.6.11** `aee87f5`
+- **Add kanban to packages.json, update cache version to 2026-07-29** `c1b96f7`
+- **2026-07-29_20:45 changes** `79f682c`
+- **Tail -n/-N flags + live follow mode (-f) with test harness** `1692f20`
+- **Exact-delta assertions for tail -f follow/stop semantics** `5f17863`
+- **Sed command (s///, /pat/d, Nd, -i, -n, pipe input) + help entries** `d8c63a9`
+- **Ctrl+C/Esc stops tail -f follow; security whitelist gains sed** `69f1fc5`
+- **Rebuilt bundles with tail -f and sed** `ec02916`
+- **Typo suggestion bar (one-tap fix), consistent unknown-cmd messages, AI loop guard** `2cf0d3d`
+- **Race-safe AI loop guard (depth counter), cover _capture* entry points, structured JSON tool error** `8f2fb31`
+- **Parse per-call captured output for unknown-cmd error (race-free under Promise.all)** `212c05d`
- …and 4 more commit(s) in this build
## before v4.6.10 — 2026-07-02
_Touches modules (317), skills (161), backups (49), downloads (40) · project 47_
-- `2026-05-25_18:02 changes` `aa7bb1a`
-- `2026-05-26_06:00 changes` `f37a1a8`
-- `2026-05-27_06:00 changes` `d632dbc`
-- `2026-05-28_06:02 changes` `c8c4089`
-- `2026-05-28_18:00 changes` `0407f24`
-- `2026-05-29_06:01 changes` `685a33d`
-- `2026-05-29_18:01 changes` `2fb6b53`
-- `2026-05-30_06:00 changes` `79e9df7`
-- `2026-05-30_18:00 changes` `69d6d95`
-- `2026-05-31_06:00 changes` `5128c54`
-- `2026-06-01_06:00 changes` `c9a346a`
-- `2026-06-01_18:00 changes` `ac50dcf`
-- `2026-06-02_06:00 changes` `b086c2b`
-- `2026-06-02_18:00 changes` `6ad20b8`
+- **2026-05-25_18:02 changes** `aa7bb1a`
+- **2026-05-26_06:00 changes** `f37a1a8`
+- **2026-05-27_06:00 changes** `d632dbc`
+- **2026-05-28_06:02 changes** `c8c4089`
+- **2026-05-28_18:00 changes** `0407f24`
+- **2026-05-29_06:01 changes** `685a33d`
+- **2026-05-29_18:01 changes** `2fb6b53`
+- **2026-05-30_06:00 changes** `79e9df7`
+- **2026-05-30_18:00 changes** `69d6d95`
+- **2026-05-31_06:00 changes** `5128c54`
+- **2026-06-01_06:00 changes** `c9a346a`
+- **2026-06-01_18:00 changes** `ac50dcf`
+- **2026-06-02_06:00 changes** `b086c2b`
+- **2026-06-02_18:00 changes** `6ad20b8`
- …and 33 more commit(s) in this build
<!-- generated:end -->
diff --git a/scripts/build-changelog.py b/scripts/build-changelog.py
index 8973761..29d8e83 100644
--- a/scripts/build-changelog.py
+++ b/scripts/build-changelog.py
@@ -54,20 +54,44 @@ def git(*args):
def commits():
- """Oldest first: [{sha, date, subject, files:[...]}]"""
+ """Oldest first: [{sha, date, subject, body, files:[...]}]
+
+ `%b` is read, not just `%s`: the shipped fixes carry their symptom -> cause -> fix ->
+ verified prose in the commit BODY (5b55af9 is a typical one), so a subject-only changelog
+ rendered "one-line summaries" for every entry and made README's claim about this file
+ false as served. The body is terminated by %x02 because it is multi-line and the parser
+ below is line-based; without a terminator a body line is indistinguishable from a path.
+ """
raw = git('log', '--reverse', '--date=iso-strict',
- '--format=%x01%H%x01%aI%x01%s', '--name-only', '--', APP_REL)
- out, cur = [], None
+ '--format=%x01%H%x01%aI%x01%s%x01%b%x02', '--name-only', '--', APP_REL)
+ out, cur, where = [], None, 'body'
for line in raw.split('\n'):
if line.startswith('\x01'):
- _, sha, iso, subject = line.split('\x01', 3)
- cur = {'sha': sha, 'iso': iso, 'date': iso[:10], 'subject': subject, 'files': []}
+ parts = line.split('\x01', 4)
+ if len(parts) < 5:
+ continue
+ _, sha, iso, subject, line = parts
+ cur = {'sha': sha, 'iso': iso, 'date': iso[:10], 'subject': subject,
+ 'body': [], 'files': []}
out.append(cur)
- elif line.strip() and cur is not None:
+ where = 'body'
+ if cur is None:
+ continue
+ if where == 'body':
+ if '\x02' in line:
+ head = line.split('\x02', 1)[0]
+ if head.strip():
+ cur['body'].append(head)
+ where = 'files'
+ else:
+ # Blank lines are kept: they are the paragraph boundary the renderer splits on.
+ cur['body'].append(line.rstrip())
+ elif line.strip():
cur['files'].append(line.strip())
+ for row in out:
+ row['body'] = '\n'.join(row['body']).strip()
return out
-
_TAGMAP = None
@@ -132,8 +156,84 @@ def clean(subject):
"""
body = subject.split(':', 1)[1].strip() if ':' in subject else subject
body = body[0].upper() + body[1:] if body else subject
- return re.sub(r'`([^`]+)`', lambda m: '<code>' + html.escape(m.group(1)) + '</code>',
- html.escape(body))
+ return inline(body)
+
+
+def inline(text):
+ """Escape, then backticks -> <code>. Shared by subjects and body prose.
+
+ Escape ONCE, before wrapping: escaping the captured group again double-encodes any
+ `&`/`<`/`>` that sits inside a code span (measured: the "run `git show <sha>`" marker
+ shipped as &amp;lt;sha&amp;gt; and reached CHANGELOG.md as &lt;sha&gt;).
+ """
+ return re.sub(r'`([^`]+)`', lambda m: '<code>' + m.group(1) + '</code>', html.escape(text))
+
+
+# A commit body can run to 2 KB and this page lists every build, so body detail is capped and
+# the cut is MARKED: a reader must be able to tell a summary from a full transcript.
+DETAIL_MAX_CHARS = 900
+DETAIL_MAX_BLOCKS = 8
+
+
+def detail_blocks(text):
+ """Commit body -> [('p'|'li', plain text)], blank-line separated.
+
+ Bullet runs become list items (they read as the symptom/cause/fix/verified lines this
+ project writes); wrapped prose lines are rejoined into one paragraph. Without %b the
+ changelog could only ever show a subject, whatever the commit actually documented.
+ """
+ blocks = []
+ for chunk in re.split(r'\n\s*\n', (text or '').strip()):
+ lines = [ln.strip() for ln in chunk.splitlines() if ln.strip()]
+ if not lines:
+ continue
+ if all(re.match(r'^[-*\u2022]\s+', ln) for ln in lines):
+ for ln in lines:
+ blocks.append(('li', re.sub(r'^[-*\u2022]\s+', '', ln)))
+ continue
+ prose = []
+ for ln in lines:
+ if re.match(r'^[-*\u2022]\s+', ln):
+ if prose:
+ blocks.append(('p', ' '.join(prose)))
+ prose = []
+ blocks.append(('li', re.sub(r'^[-*\u2022]\s+', '', ln)))
+ else:
+ prose.append(ln)
+ if prose:
+ blocks.append(('p', ' '.join(prose)))
+
+ out, used = [], 0
+ for kind, body in blocks:
+ if len(out) >= DETAIL_MAX_BLOCKS or used + len(body) > DETAIL_MAX_CHARS:
+ out.append(('p', '\u2026 run `git show <sha>` for the rest'))
+ break
+ out.append((kind, body))
+ used += len(body)
+ return out
+
+
+def detail_html(blocks):
+ """Collapsible detail inside the list item: native <details>, so the page's renderer
+ (item.innerHTML = html) needs no change and a reader can ignore it."""
+ if not blocks:
+ return ''
+ out, in_ul = ['<details><summary>detail</summary>'], False
+ for kind, body in blocks:
+ if kind == 'li':
+ if not in_ul:
+ out.append('<ul>')
+ in_ul = True
+ out.append('<li>%s</li>' % inline(body))
+ else:
+ if in_ul:
+ out.append('</ul>')
+ in_ul = False
+ out.append('<p>%s</p>' % inline(body))
+ if in_ul:
+ out.append('</ul>')
+ out.append('</details>')
+ return ''.join(out)
def build():
@@ -169,8 +269,9 @@ def build():
for ver, group in releases.items():
items = []
for row in group[:MAX_ITEMS]:
- items.append('<strong>%s</strong> <code>%s</code>' % (
- clean(row['subject']), html.escape(row['sha'][:7])))
+ items.append('<strong>%s</strong> <code>%s</code>%s' % (
+ clean(row['subject']), html.escape(row['sha'][:7]),
+ detail_html(detail_blocks(row.get('body')))))
if len(group) > MAX_ITEMS:
items.append('…and %d more commit(s) in this build' % (len(group) - MAX_ITEMS))
kinds = Counter(kind_of(r['subject']) for r in group)
@@ -206,9 +307,11 @@ def build():
'builds whose tree reached a commit can appear here. Read a jump in the '
'version chip as "those bumps were superseded before commit".'})
gaps.append({'range': 'every build',
- 'why': 'Entries summarise commit subjects, not diffs. A build with one squashed '
- 'commit shows one line even if it changed ten files; run '
- '`git show <sha>` for the change itself.'})
+ 'why': 'Entries carry the commit subject and, where the commit body has '
+ 'substance, its symptom / cause / fix / verification text (folded open '
+ 'per line on the page, nested under the line in CHANGELOG.md). A build '
+ 'whose work was squashed into one commit still shows one line; the diff '
+ 'itself is always `git show <sha>`.'})
now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
prev = existing_island()
@@ -232,7 +335,8 @@ def build():
DEK = ('Every change that has a commit behind it. Each line names the build it landed in and the '
- 'commit it came from, so any claim here can be checked with <code>git show</code>. Where '
+ 'commit it came from, and folds open to the commit body — the symptom, cause, fix and how '
+ 'it was verified — so any claim here can be checked with <code>git show</code>. Where '
'the repository has no record, the gap is written down instead of filled in.')
GAP_INTRO = ('This page is generated from the repository\'s own history for '
'<code>spaces/newos4</code> — the same source the running build comes from, so '
@@ -257,6 +361,21 @@ def existing_island():
return None
+# Rendered as native <details> inside each list item, so the page's own JS needs no change.
+# Injected once (sentinel-guarded) because the page is a static document: a builder that only
+# rewrites its JSON island cannot restyle anything else.
+DETAIL_CSS = """ /* wx11-changelog-detail: collapsible commit body */
+ .entry li>details{margin:.3rem 0 0}
+ .entry li>details>summary{
+ cursor:pointer; font-family:var(--mono); font-size:.66rem; letter-spacing:.08em;
+ text-transform:uppercase; color:var(--ink-3); padding:.15rem 0;
+ }
+ .entry li>details>summary:hover{color:var(--cyan)}
+ .entry li>details p{margin:.35rem 0 0; font-size:.88rem; color:var(--ink-2); line-height:1.55}
+ .entry li>details ul{margin:.3rem 0 0; padding-left:1rem; font-size:.88rem; color:var(--ink-2); line-height:1.55}
+"""
+
+
def write_page(page):
with open(PAGE, encoding='utf-8') as fh:
doc = fh.read()
@@ -271,6 +390,9 @@ def write_page(page):
new = re.sub(r'(<section class="panel" aria-labelledby="gaps-h">[\s\S]*?<h2 id="gaps-h">[^<]*</h2>\s*<p>)[\s\S]*?(</p>)',
lambda m: m.group(1) + GAP_INTRO + m.group(2), new, count=1)
new = re.sub(r'(<b id="cur-ver">)[^<]*(</b>)', lambda m: m.group(1) + html.escape(page['currentVersion']) + m.group(2), new, count=1)
+ if '/* wx11-changelog-detail' not in new:
+ new, n = re.subn(r'(\n\s*</style>)', lambda m: '\n' + DETAIL_CSS + m.group(1), new, count=1)
+ assert n == 1, 'no </style> to inject the detail CSS before - page layout changed'
write_atomic(PAGE, new)
return len(new)
@@ -286,6 +408,38 @@ PREAMBLE_RE = re.compile(re.escape('# newOS4 changelog') +
r'do not edit by hand between the markers\.\n+')
+DETAIL_RX = re.compile(r'<details[^>]*>\s*<summary>[^<]*</summary>(.*?)</details>\s*$', re.S)
+
+
+def md_inline(markup):
+ """Our own generated item markup -> markdown.
+
+ The old line replaced EVERY tag with a backtick, so `<strong>` became a stray tick and a
+ body's paragraph tags would have shredded the file. Explicit rules beat a generic strip:
+ <code>/<strong> survive as markdown, everything else becomes a space.
+ """
+ text = re.sub(r'<code>(.*?)</code>', lambda m: '`' + m.group(1) + '`', markup, flags=re.S)
+ text = re.sub(r'<strong>(.*?)</strong>', lambda m: '**' + m.group(1) + '**', text, flags=re.S)
+ text = re.sub(r'<[^>]+>', ' ', text)
+ return re.sub(r'\s+', ' ', html.unescape(text)).strip()
+
+
+def md_item(item):
+ """One changelog line, with the commit body as nested bullets.
+
+ This is the file the public mirror serves, so the README claim "symptom -> cause -> fix ->
+ verified" has to be true HERE, not only on the rendered page.
+ """
+ m = DETAIL_RX.search(item)
+ lines = ['- ' + md_inline(item[:m.start()] if m else item)]
+ if m:
+ for _tag, body in re.findall(r'<(p|li)>(.*?)</\1>', m.group(1), re.S):
+ body = md_inline(body)
+ if body:
+ lines.append(' - ' + body)
+ return lines
+
+
def write_md(page, rows):
"""App-side Keep-a-Changelog history; only the region between the markers is generated."""
region = ['<!-- generated:start -->', '']
@@ -295,7 +449,7 @@ def write_md(page, rows):
if entry['note']:
region.extend(['_' + re.sub(r'<[^>]+>', '', entry['note']) + '_', ''])
for item in entry['items']:
- region.append('- ' + re.sub(r'<[^>]+>', '`', item).replace('``', '`').replace('``', '`'))
+ region.extend(md_item(item))
region.append('')
region.extend(['<!-- generated:end -->', ''])
region_text = '\n'.join(region)