aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* test(newos4): guard chat()'s return and tool-turn survivalAgando4 days1-0/+97
| | | | | | | | | | | | 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).
* fix(newos4): chat() threw on every tool-using turn and never returned its answerAgando4 days4-6/+20
| | | | | | | | | | | | | | | | | | | | | | | | | Regression from 32608bc, caught by the .chat() caller sweep before it aged: - `recent` was referenced in chat()'s finally block but never declared, so any turn with toolTurns > 0 threw ReferenceError there and chat() REJECTED. delegateTask's catch turned that into "agent failed"; the 'completed' fallback was only ever reachable on tool-free turns. - chat() still had no return at all, so a tool-free turn resolved undefined and module-orchestrator.js:1250 fell through to `|| 'completed'`. Both fixed: `const recent` is now built from _toolCallLog, and chat() returns { content: finalAnswer, toolTurns } before the archive block (a return at the end of the method would let _archiveSession swallow the answer). Verified with /tmp/probe-chat-hotfix.js, 6/6: tool-using turn resolves {content:'WORKER ANSWER', toolTurns:2}, tool-free turn resolves the answer, the orchestrator's own projection returns the answer instead of 'completed', and _chatLock is released. Run against 32608bc the same probe says "recent is not defined" - i.e. it reproduces the shipped defect. Not included: the _chatLock serialisation (orchestrate-parallel still runs sub-tasks one at a time and the 10s force-release can clear the main turn's lock). That stays open in STEER-20261102-201045-ab08.
* feat(newos4): configurable tool budgets, per-model defaults, real tool telemetryAgando4 days10-58/+462
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Closes STEER-20261102-201234-7200 (read_file capped at 6000 while the schema promised 32000). Budgets (module-aiassist.js + new modules/core/model-profiles.js): - one resolver, precedence: call arg -> loop profile -> model profile -> ai-config -> code default. Keys: read-page-chars, read-result-chars, tool-context-budget, fetch-chars. - model classes tiny/small/mid/large keyed by id then by size token; `mid` reproduces the old numbers so nothing moves for a mid model. - _clipToolResult now clips the `content` field, never the serialised JSON envelope: clipping the envelope deleted truncated/next_offset, so a 16k page lost its continuation pointer (the reported symptom). - read_file's schema no longer promises a number the model class may not give. Telemetry: - _recordToolCall keeps sizes, duration, ok and a 120-char arg preview instead of the whole payload; ai-stats prints calls / ok% / avg ms / KB / truncation from localStorage webx11_tool_stats. Server (api/proxy-api.py): - /api/extract honours max_chars (default 12000, cap 100000) and returns content_chars + truncated; the bs4 branch is no longer uncapped while the regex fallback was silently cut at 10 000. BEHAVIOUR CHANGE: num_ctx for a model with no explicit setting now follows the class default (16384 -> 65536 for cloud/large, 8192 for tiny). Cloud models bill on tokens used, so cost is unchanged; local models get a bigger KV cache. Two build traps, both now documented in AGENTS.md: - model-profiles.js must be the FIRST entry in CORE_FILES and must export the MODEL_PROFILES global (a TDZ typeof throws; without the global every budget silently falls back to the code default). - a budget key needs the setter branch AND the constructor loader at module-aiassist.js:49 - persisting without restoring looks fine until reload. Verified: test-tool-context-bound 23/23 (RED before: E1-E4 + TypeError on _toolLimit); live bundle read_page 32000 / result 12000 / budget 48000 / fetch 12000 and _ctxTokens 65536 for gemma4:31b-cloud; /api/extract 28970 -> 500 chars with truncated true; ai-config write+restore probe 6/6.
* feat(feedback): chat plane on the dashboard + honest triage rankingAgando4 days2-2/+31
| | | | | | | | | | | | | | | | | | 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 - AGENTS.md: the chat plane documented as an intake Verified: autoreview --dry-run over the 5 chat reports -> "nothing provable (5 left for the operator)" (R6 refuses auto:false); maintain 16 PASS 0 FAIL; feedback.html inline JS node --check OK; collect -> awaiting 5, chat_total 5.
* feat(newos4): ingest report-like chat messages into the feedback queueAgando4 days1-0/+186
| | | | | | | | | | | 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.
* feat(newos4): R5 stale-artefact rule + dashboard queues a decision onceAgando4 days1-1/+47
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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, every error count <= 1, and nothing newer than STALE_DAYS=5 in data/debug/errors.json -> resolve with the measured quiet_days as evidence. New --errors <path> knob so the rule can be run against fixtures. RED (pristine copy): "nothing provable (1 left for the operator)" GREEN (aged fixture): "resolve ... quiet 5.3d -- reopen if it recurs" NEG (live store 4.3d, and a report with count 21): both still refuse. - admin/feedback.html: markDecided() patches the local snapshot, so a queued decision leaves the default "Awaiting only" list immediately instead of after the 10-minute sync rebuild. Measured cause of 3x approve on one plan in 3 min (10-30 16:03/16:05/16:05) and 6 items with duplicate rows. - AGENTS.md: documents R5 and the fixture knob (also carries the already-live 2026-11-02 cookie-notice paragraph that was pending in the tree).
* feat(billing): no model-class gate; free allowance becomes $0.10/dayAgando5 days4-18/+43
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Operator policy 2026-11-01: every subscription may call any model, and the free tier resets daily. Model gate removed (api/ai-gateway.py) `plan == 'free' and model_weight > 1 -> 403 "Advanced models require Pro"` ran before any money logic, so a free-plan account holding $1086 of purchased credit was refused and told to upgrade. A model tier is not something we sell: the allowance and the USD balance bound spend. A caller who cannot pay now gets the honest 429 (with period + needed amount). model_weight survives only for track_usage() analytics. Free allowance: $0.50/month -> $0.10/day ~$3/month retail = $2.14/month of provider cost at 1.4x markup, ~6x the previous giveaway and still bounded per member (the 50-slot invite cap bounds the total; at full occupancy ~$107/mo or $2.14/user at 100% utilisation). The window is policy, not code: - admin/data/pricing-policy.json: free_tier_usd=0.1, free_tier_period="day" - wx11_billing: FREE_TIER_PERIOD, allowance_window(plan) (day for free, month for paid), period_start(window), spend_usd(session, window) — one definition used by BOTH the gateway and the billing API (the old month_spend_usd duplicate is what let them drift before) - gateway + /api/billing/subscription now report the same window; plan_allowance gains `period` - dashboard.html, newOS4 module-billing, chatstudio account panel say "today" when period=day - published copy: index.html, pricing.html (card + compare table + how-usage-works), i18n planFree1, admin/docs/ai-gateway.html - verify-public-claims.py asserts the number AND the period per plan: a right number on the wrong window is the same defect as a wrong number newOS4 bundle rebuilt (module-billing), LATEST_VER v4.6.135 -> v4.6.136, main.html cache keys bumped. Proof: - verify-public-claims.py rc=0 (copy == policy == live billing API, including periods) - /api/billing/subscription: limit_usd 0.1, period "day", remaining 0.1 - gateway: gemma4 200, kimi-k2-thinking 502 (gate gone; fake model => upstream 404) - smoke-auth PASS, smoke-gateway PASS (limit=$0.1 in the payload check) - live pages serve "$0.10 of AI usage / day", zero $0.50 left on index/pricing/subscriptions - allowance math: free cap_cost $0.0714/day, pro $7.7779/mo, team $23.35/mo, ent $77.85/mo
* chore(git): untrack cron/API runtime state, keep it on diskAgando7 days1-3893/+0
| | | | | | | | | | | | | 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.
* docs(changelog): regenerate after the metering and shell commits (73 entries)Agando8 days1-0/+20
|
* feat(newos4): register `test` / `[` so chains decide locally instead of ↵Agando8 days9-38/+321
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | asking a model (v4.6.135) `[ -f "/qt.txt" ] && echo exists` — the probe that closed the v4.6.132 batch — was not a shell test at all: both commands were unregistered, so the line fell through to the AI fallback and a model was asked whether a file exists. `_evalTest()` has implemented `-f -d -e -z -n ! = !=` for the if/while parser since the script runner was written; only the command was missing. * `test` and `[` registered in ShellModule. Success resolves silently; false rejects with a `silent` error so `&&`/`||` skip it — POSIX prints nothing on exit 1. * `[` requires the closing bracket (`[ : missing ']'`), and that message is NOT silent. * `test` already names the on-demand regression-suite runner ("test auto" / "test list" in help.html). A argv that does not look like a test expression (`-f`/`-d`/`-e`/`-z`/`-n`/`!`, or `a = b` / `a != b`) is delegated to the handler registered before us, so `test auto` still runs the suite and the two meanings cannot collide. * terminal.dispatchCommand() now honours `err.silent` — without it a top-level `test -f /nope` printed `Error in ShellModule.test:` (measured on v4.6.134, fixed in v4.6.135). * help.html documents the bracket form next to the existing `test` row. Three tags were minted minutes apart — v4.6.133 (commands), v4.6.134 (suite delegation), v4.6.135 (silent top-level) — each a real cache-bust for a fix that superseded the previous build; the net change is this commit. Verification node modules/test/test-shell-test-cmd.js -> 24/24 pass (RED against HEAD: "test command registered" fails, "m.commands.test is not a function") node modules/test/test-pipe-guard.js -> all seven flags true node modules/test/test-parse-args.js -> 28/28 · daily job -> node tests 22/22 pass newos4-maintain.py -> atlas 355 commands (was 354), modules-index 349, help 364/364 live v4.6.135, read from the terminal DOM: [ -f /qt.txt ] && echo F135 -> F135 test -f /nope || echo M135 -> M135 [ -d /nope ] && echo BAD || echo N134-> N134 (false branch skipped, nothing printed) test -> no output (suite not loaded; POSIX false) test -f /nope -> no output js "a b".length | wc -c -> 1 (v4.6.132 pipe fix still intact) The AI fallback is no longer reachable for a bracketed file test.
* fix(newos4): shell tokenises command lines quote-aware everywhere (v4.6.132)Agando8 days6-51/+166
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | v4.6.131 taught the pipe stages to forward their raw text, but the shell's own wrapper still split each stage with `cmdText.split(/\s+/)` — so `grep "alpha beta" /qt.txt | wc -l` handed grep the two args `"alpha` and `beta"` and matched nothing. Measured live: the wrapper (ShellModule.init() replaces terminal.executePipeChain) is the implementation that runs, so the core fix alone is decorative. All six command-line split sites in modules/ext/shell.js now go through one helper, `_tokenize()`, which delegates to `security._parseArgs` — the parser the bare-command path already uses — so the two cannot drift again: 111 single command in a `&&` / `;` chain -> `grep "a b" f ; echo done` 176 pipe stage (the wrapper that runs) -> `grep "a b" f | wc -l` 358 `<` redirect input -> `cat < /f` 401 pipe chain inside the script runner -> also forwards the stage's raw text now 424 single command in the script runner -> also forwards raw text 778 `[ -f "path" ]` / `test -f "path"` -> quoted paths in test expressions Empty stages are skipped instead of being dispatched as a command named '' (`help | | wc -l`). The guard test grew the assertion that would have caught this: test-pipe-guard.js now checks `shellTokensQuoted` (tokeniser used, no `cmdText.split(`, empty-stage guard present) alongside `shellForwardsRaw`; both are RED against HEAD's shell.js. Verification node modules/test/test-pipe-guard.js -> {"empty":[],"blank":[],"classifyGuarded":true, "pipeGuarded":true,"shellForwardsRaw":true,"shellTokensQuoted":true} node modules/test/test-parse-args.js -> 28/28 · daily job -> node tests 21/21 pass newos4-maintain.py -> maintenance result: OK (index 349, help 363/363, changelog v4.6.132) live v4.6.132, read from the terminal DOM (no print monkey-patch): cat /qt.txt -> alpha beta grep "alpha beta" /qt.txt -> 1 match, 1 file grep "alpha beta" /qt.txt | wc -l -> 2 (match line + summary; 1 when the quotes are lost) grep "alpha beta" /qt.txt ; echo done-> match count + done ShellModule._tokenize(c) === security._parseArgs(c) for 5 sample lines -> true _tokenize('') / _tokenize(' ') -> [] / [] Also closes the last open feedback item: RPT-20261027-160751 ("Connection failed on all providers") -> resolve, recorded in decisions.jsonl with evidence (headless probe URL `?probe=ctx`, v4.6.117, single occurrence, no stack; Ollama answers 200 with 37 models now). Feedback queue: awaiting 0.
* fix(newos4): pipe stage quoting, maintain ordering, help cost row, plan ↵Agando8 days14-48/+509
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | status (v4.6.131) Four items closed in one batch; three ran as parallel subagents on disjoint files, the bundle was rebuilt once by the parent (rebuild-bundles.py serialises the working tree, which carries other sessions' work — see 161372f). 1. PIPE STAGES KEEP THEIR QUOTES. `js "a b".length | wc -c` reached eval as `a b.length` and printed a ReferenceError. terminal.js executePipeChain() now keeps `stages[i]` aligned 1:1 with `pipeParts[i]` (the `.filter(argv => argv.length > 0)` shifted the index; replaced by a lastStage scan + `if (argv.length === 0) continue`) and forwards the stage's own text to the dispatcher. The core path alone was dead code: ShellModule.init() replaces terminal.executePipeChain (shell.js:144), and that override is what runs in the browser (`executePipeChain.toString()` contained `cmdText.split`, not `lastStage`). Both now pass `cmdText`/`pipeParts[i]`. test-pipe-guard.js was rewritten for the new shape and now also asserts the shell override forwards raw text — the assertion that would have caught the shadowing. RED on HEAD: both flags false; GREEN now. 2. MAINTENANCE --fix CAN CLEAR ITS OWN WARN. check_index() ran before check_atlas(), which is what regenerates course/atlas.json, so --fix rebuilt modules-index.json from the stale atlas and the same WARN reappeared every run. check_atlas() now runs immediately before check_index(). modules-index.json regenerated: cost/usage present, 349 commands, version 2026-10-29-atlas. 3. `cost` DOCUMENTED. modules/core/module-billing.js registers `cost [on|off]` (an alias of `usage`); help.html had no row. Added above the `usage` row. help coverage 362/363 -> 363/363. 4. SHIPPED PLANS STOP ASKING FOR A DECISION. The applier only flips review<->safe, so every executed review-gated plan kept counting as an open decision (5 of 5). newos4-feedback-collect.py now reads the body: a `## Status` / `## Implementation` heading followed by shipped|executed plus a version, commit or date is `status: done`; those stay in `plans` (visible as shipped) but leave plans_review and awaiting_decision. Counts: plans_review 5 -> 0, awaiting_decision 4 -> 1 (the remaining one is a real open report, RPT-20261027-160751). admin/feedback.html read `it.status[0]`, which would render `d` for a string status; it now prefers `status_lines[0]`. Also carried, authored by earlier uncommitted sessions and already live in the bundle (attributed, not silently absorbed): modules/core/module-billing.js (new, BillingModule: cost/usage), its CORE_FILES entry in scripts/rebuild-bundles.py, and the BillingModule registry entry in terminal.js. The bundle was rebuilt from HEAD + this batch with ollama-manager.js taken from HEAD, so another session's in-flight `_httpError` refactor is deliberately NOT in the shipped bundle (grep -c _httpError core-bundle.js = 0). Verification node modules/test/test-pipe-guard.js -> {"empty":[],"blank":[],"classifyGuarded":true,"pipeGuarded":true,"shellForwardsRaw":true} node modules/test/test-parse-args.js -> 28/28 pass maintained job -> node tests 21/21 pass scripts/newos4-maintain.py -> maintenance result: OK (modules-index 349, help 363/363, changelog current v4.6.131) live v4.6.131 (real DOM, no print monkey-patch): js "a b".length | wc -c -> 1 (js printed 3, one byte) js "a b".length -> 3 js "abc".toUpperCase() -> ABC echo "two words" -> two words node -e "import('fs')" -> red error + Node-builtin hint js 6*7 -> 42 unhandledrejection -> 0 livePipeImpl: shell-override, livePipeForwardsRaw: true, costRegistered: true
* fix(newos4): js keeps its string literals — dispatcher forwards the raw ↵Agando8 days6-15/+163
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | command line (v4.6.130) _parseArgs() treats quote characters as delimiters and drops them, so every string literal in a js expression died before eval: `js "abc".toUpperCase()` reached the module as `abc.toUpperCase()` → ReferenceError: abc is not defined. Same for `js import('fs')` → `import(fs)`. Found while verifying the v4.6.128 console fix (plan 2026-10-29_112000-newos4-parseargs-strips-js-string-literals.md). - terminal.js: checkAndRun() records the untokenised line (`_currentRaw`), _dispatch() forwards it, dispatchCommand(cmd, args, rawText) passes it to the module. Security classification still runs on the tokenised form, and pipes/AI/script-runner calls pass nothing, so they behave as before. - console.js: `js` evaluates _expr(args, rawText) — the raw remainder. `node -e` deliberately does NOT: the tokenizer already keeps `'…'` inside `"…"`, and the raw form would hand eval the string literal `"import('fs')"` instead of the import (caught by the live probe, not by review). js-run keeps tokenised paths. - modules/test/test-parse-args.js: tokenizer table (records the deletion as the current contract), dispatcher plumbing asserts, _expr cases, and the user-visible paths. 27 assertions. - module-loader.js + main.html + bundle: v4.6.130 (129 was minted, then console.js changed again — re-minting so no cached client keeps the intermediate). Verification node modules/test/test-parse-args.js -> 27/27 pass same file against HEAD sources (git show HEAD:...) -> TypeError: _expr is not a function (RED) scripts/lint-no-undef.py terminal.js console.js -> clean, 122 cross-file globals core-bundle.js rebuilt from HEAD sources + this patch in /tmp/iso (the working tree carries another session's uncommitted ollama-manager.js `_httpError`); diff vs HEAD bundle = 14 lines, all mine. critical-ext-bundle.js byte-identical. Live page v4.6.130 (real executeCommand, output captured): js "abc".toUpperCase() -> ABC js 'a b'.split(' ').length -> 2 js JSON.stringify({a:1}) -> {"a":1} js 6*7 -> 42 js import('fs') -> red "Failed to resolve module specifier 'fs'" + Node-builtin hint node -e "import('fs')" -> same error + hint (was printing the expression as a string) echo "two words" -> two words js-run /nope.js -> js-run: /nope.js: No such file unhandledrejection count -> 0 The sibling's parked terminal.js hunk (BillingModule registry entry) was stashed for this commit and is restored in the working tree afterwards; it is not part of this change.
* fix(newos4): js console handles promise rejections + Node-builtin hint ↵Agando8 days4-10/+37
| | | | | | | | | | | | | | | | | | | | | | | | | | | | (v4.6.128) evalJS() evaluated the expression bare and printed JSON.stringify(result), so a rejected thenable (e.g. `js import('fs')`) became `{}` on screen while the rejection escaped to window.onunhandledrejection and was auto-filed as a runtime bug report (RPT-20261028-172211-9c7f2adb, cluster 480x). evalJSFile() already had .then/.catch — the asymmetry was the defect. - modules/ext/console.js: thenable branch in evalJS() awaits and catches; new _printError() is the single error sink for all four catch paths and appends a hint when a bare specifier fails to resolve ("'fs' is a Node built-in; the browser has no Node — use the VFS (ls/cat/run, terminal.fs) instead"). - help.html: js / js-run rows say "browser only — no Node built-ins". - module-loader.js + main.html: v4.6.128. Verification (/tmp/test-console-fs.js, 11 asserts, new Function harness over the module): GREEN 11/11 — rejection prints one red error + hint, no unhandledRejection; resolved thenable prints its value; sync value/throw unchanged; js-run file + missing file OK. RED 4/11 against .bkp-console-20261029_121243/console.js — A1/A2/A3 print `{}`, A4/G2 leak "Failed to resolve module specifier 'fs'". node --check modules/ext/console.js → OK. console.js is loaded separately via extKeys (not in critical-ext-bundle.js), so no bundle rebuild — proven by `grep -c JSConsoleModule modules/ext/critical-ext-bundle.js` = 0. Consumes STEER-20261028-195043-8084. Plan: .hermes/plans/2026-10-29_063000-newos4-js-console-fs-specifier.md
* fix(docs): apply the GLM-5.3 review to the changelog pipelineAgando10 days5-65/+186
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 truncate could hand a visitor half a page. - The JSON island now escapes "</" (a commit subject containing </script> would have closed it early and truncated the page). Proved by injecting exactly that subject: island still parses, no stray script tag, payload survives as data. - Sibling drift is checked, not assumed: changelog.json's currentVersion is compared with LATEST_VER. Seeded v4.6.100 -> FAIL "feed drifted", --fix repairs. - Build tags are audited under --deep: 67 tags, each must be reachable from HEAD and name the version its commit tree carries. Proved by tampering (v9.9.9 on the root commit -> FAIL) and by deleting it (-> PASS). - Feed: ids no longer hardcode 2026 or contain spaces, entries carry a <link>, timestamps are real iso-strict commit times instead of a fake T00:00:00Z on a local date, and summaries are truncated before escaping so no "&l" tail. 0 ids with spaces, 0 broken entity tails. - write_md refuses to overwrite a CHANGELOG.md that lost its markers; run() gained a 300 s timeout (a hung git used to block the whole 07:15 job); jumpToHash no longer stacks hint paragraphs; dead fields removed (KIND_ORDER, order, currentSynthesized, sourcesRead — each appeared exactly once in the page, i.e. only in the island the generator wrote). - HTML nits: guide.html footer separator, empty "fixed in" line suppressed, help.html version bumped off v4.5.22. Header stat relabelled "Commits behind it" (it read "Bug records on disk" while counting commits — my own defect, not the reviewer's). - Refuted: merge commits (repo has 0: `git log --merges | wc -l`), first-version board links (the 2 multi-version notes both name the fixing build first), sources disclosure (the page already renders those module paths). maintain --deep: node tests 20/20, changelog current, age 0.0h, 67 tags verified, result OK. Mobile: ALL VIEWPORTS OK.
* feat(docs): tag builds, feed the changelog to machines, fail maintenance ↵Agando10 days3-5/+126
| | | | | | | | | | | | | | | | | | | | | | | | | | when it ages 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. - check_changelog() now also asserts the page was GENERATED recently, not merely that its version matches: a version match survives a dead cron forever. Seeded a 26-day-old timestamp -> "FAIL: changelog: generated 640h ago (>48h) - the daily job is not running", maintenance FAIL; restored -> "PASS: changelog age 0.1h", maintenance OK. - The exact cron pre-script was run end to end, not a stand-in: MAINTENANCE (--fix) block prints "PASS: changelog: changelog current: v4.6.119 (68 entries)" and "PASS: changelog age 0.2h", result OK, no second cron created. - Scale trigger written into the generator's docstring (split by year at ~150 builds), not built. - Mobile re-checked after the page gained the deep-link note: changelog + board both ALL VIEWPORTS OK at 360/390 px, light and dark.
* feat(docs): link the changelog in and deep-link each board fix to its buildAgando10 days2-0/+2
| | | | | | | | | | | | | | | | | | | | | | | | 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 version was superseded before anything was committed), so #v4-6-83 has no entry. Rather than dropping the reader at the top, the page lands on v4.6.84 and says why in a hint line. Verification, live: - 22 cards carry a changelog link; board header shows "Release notes: changelog.html". - #v4-6-107 -> targetExists true, :target highlight true, top 0, scrolled into view, no note. - #v4-6-83 -> scrollY 5235, note: "#v4-6-83 has no commit of its own — the version was superseded before it reached a commit — so this is v4.6.84, the next build that did." - renderer JS and board JS both pass node --check on the extracted <script> bodies.
* feat(docs): generate the changelog from git and keep it current in maintenanceAgando10 days3-1/+783
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 island, so regeneration swaps the island and refreshes the two prose blocks that described the VFS-only source. No second copy of the CSS or the 296-line renderer. - Every unshowable thing is stated, not smoothed: commits before the first version stamp become one "before v4.6.10" bucket, build numbers that were bumped twice before a commit are called out as absent, and the page says entries summarise subjects, not diffs. - CHANGELOG.md is generated from the same data between markers (app-scoped; the root CHANGELOG.md documents the platform's 0.2.x line and is left alone). - newos4-maintain.py gains check_changelog(): FAIL when the page's build differs from LATEST_VER, regenerate under --fix, PASS when current. So the daily 07:15 job keeps the docs in step with the build instead of relying on someone remembering. Verification: - build: 68 entries, 158 commits, v4.6.10 - v4.6.119, page 63 327 bytes, CHANGELOG.md 15 840. - live page renders 68 entries with kind chips, search, per-build anchors ("Copy a direct link to v4.6.119"), "68 of 68". - stale detection proved by bumping LATEST_VER to v4.6.120 without rebuilding: "FAIL: changelog changelog stale: page says v4.6.119, build is v4.6.120"; --fix then "PASS: changelog regenerated: ... v4.6.10 - v4.6.120"; version reverted, page back to 119. - maintain: PASS node tests 20/20, PASS changelog current: v4.6.119 (68 entries), result OK. - mobile: verify-mobile.py at 360/390 px, light+dark -> ALL VIEWPORTS OK, 0 overflow.
* fix(newos4): `if test …` was always false and `for` items never globbedAgando10 days5-17/+75
| | | | | | | | | | | | | | | | | | | | | | | | | | | Found by running the shipped runner the way a user would, not by re-reading it. - _evalTest() stripped [ ] but not the `test` keyword, so `if test -f /work/alpha.txt` evaluated the word "test" as an operator, returned false, and every such branch took the else arm — measured live: "alpha missing" for a file that exists. Both spellings work now (`test -f x`, `[ -f x ]`), plus `-d`, `-e`, `-z`, `-n`, `!` and the = / != comparisons. - `for f in /work/*.txt` ran ONCE with $f set to the literal pattern, because only command arguments went through _expandGlobs. Loop items are expanded per iteration now, so the body runs once per matched file with the real path. - _expandGlobs() falls back to readDir() when a fs has no ls(), so the helper works against either shape instead of silently returning the literal pattern. Verification, live on v4.6.119: - for+glob: "--- /work/alpha.txt / 3 /work/alpha.txt / --- /work/beta.txt / 1 /work/beta.txt" (6 statements, exit 0) — previously one iteration printing the pattern. - conditionals: "alpha exists", "nope missing", grep + $? intact. - end-to-end /work/report.sh (loop + glob + >> append + test -f) wrote a correct log: "start / file: /work/alpha.txt / 3 /work/alpha.txt / file: /work/beta.txt / 1 /work/beta.txt …". - unknown command still reports "command not found" and never calls the AI. - test-shell-script.js 27 -> 35 asserts. Retracted: I reported `echo x > file` as broken. It was my probe monkey-patching terminal.print, which bypassed the capture stack the redirect uses. Both paths write correctly: direct helper -> "direct-call", prompt -> "prompt-call\n".
* fix(newos4): send num_ctx to Ollama, and make one auto-connect failure read ↵Agando10 days6-24/+155
| | | | | | | | | | | | | | | | | | | | | | | | | | as one line Two things I had flagged as "worth your attention" and one leftover nit. - num_ctx was never sent. Ollama's server-side default context is 2k-4k depending on the build, so a system prompt + 20 messages + up to 24k chars of tool results could be truncated server-side, which is exactly the "it forgot what it just read" class of report. Every Ollama request now carries options.num_ctx (default 16384, overridable via webx11_ollama_numctx, floor 512); the OpenAI-compatible branch is untouched because num_ctx is not one of its parameters. - Automatic connect prints one outcome instead of one line per endpoint tried (7 -> 3 on a dead endpoint). Interactive `ollamaconnect` keeps the per-endpoint detail. - write_file's failure message no longer echoes a trailing slash. Verification: - test-ollama-numctx.js (7 asserts): generate and chat payloads carry num_ctx=16384, a stored override is used, junk/too-small values fall back, and the /v1/chat/completions branch does not receive it. RED on HEAD: TypeError reading 'num_ctx' of undefined. Suite 20/20. - Live v4.6.117, capturing the real request: {"num_ctx":16384} on /api/generate with model gemma4:31b-cloud, reply "OK". - Live v4.6.118, dead endpoint (127.0.0.1:9, fallback disabled): automatic -> 3 lines ("connecting on first use…", "Connecting to dead…", "Auto-connect failed — …"); manual connect() -> 4 lines including "endpoint http://127.0.0.1:9 failed", so the suppression is conditional, not blanket.
* test(newos4): prove the mixed-batch loop ceiling stops an agent at 9 ↵Agando10 days1-0/+78
| | | | | | | | | | | | | | | | | | | | identical calls 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 limit" with no ceiling line. Suite in that checkout: 19 files, this one failing. - Suite now 19/19 files; maintain OK. No bundle change: the shipped code already contains the ceiling.
* fix(newos4): piped grep searched the working directory instead of its inputAgando10 days7-6/+71
| | | | | | | | | | | | | | | | | | | | | | | | The stdin work in 5cf5010 fixed wc/head/tail but left grep alone, and grep's no-operand branch sweeps the cwd. Measured on v4.6.115: `cat /gdir/in.txt | grep zzz` answered `other.txt:1: zzz in a cwd file` — a file that was never piped, and `cat in.txt | grep bbb` reported the match twice (`in.txt` from the cwd sweep plus `__pipebuf__` from the injected file). - module-fs.js: when terminal._pipeInput is set and no file operand was given, grep searches the staged text and prints `<line>: <text>` (stdin has no filename, as in GNU). The cwd sweep stays for a bare interactive `grep x`. - shell.js: _isPipeAware now covers only sort/cut/uniq/expand. grep/head/tail/wc read _pipeInput themselves, so injecting the __pipebuf__ file produced duplicate hits and named stdin results. Verification: - test-shell-stdin.js 20 asserts (+4 for grep: no cwd match, non-match reports none, match prints no filename, -c counts stdin). RED on HEAD: G1/G2/G3 fail with `other.txt:1: zzz in a cwd file`. - Suite 18/18 files; maintain --fix "node tests 18/18 pass". - Live v4.6.116 after `cd /gdir` (where other.txt contains zzz): `cat /gdir/in.txt | grep zzz` -> (no matches) · `| grep bbb` -> `2: bbb` · `| grep -c b` -> 1 · `| head -n 2` -> aaa,bbb · `| tail -n 1` -> ccc · `| wc -l` -> 3. Credit: the false-positive grep was reported by the stdin subagent as an aside; re-derived here against the live terminal before fixing.
* feat(newos4): the script runner runs scripts — control flow, variables, ↵Agando10 days8-151/+977
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | globs, redirects, stdin MAINT-021 and MAINT-017, executed from .hermes/plans/2026-10-27_145500-newos4-script-runner.md. - shell.js: new block parser (_parseBlocks) and executor (_runInstructions/_runSimple/_runCommand/ _dispatchOne). `for`/`while`/`if` bodies now run, including one-line `if [ -f x ]; then y; fi`. Variables expand through _expandVars ($VAR, ${VAR}, $?, single quotes literal), globs expand through _expandGlobs (unquoted args only, per-command), and `>`/`>>` capture the command's output into the VFS instead of being passed to the command as text. - shell.js: a script never answers an unknown command by calling the AI any more. It prints `script:<path>:<line>: <cmd>: command not found` and sets $? — the AI fallback stays for typing. - shell.js: unclosed blocks and bad `for` headers surface as `Script <path> failed: …` (the parse used to sit outside the try, so a bad script crashed the runner). - shell.js: interactive path gets the same treatment — prompt globbing and `echo hi > /f` now work. Cause of the asymmetry: the two code paths had separate dispatch. - shell.js: `_isPipeAware` no longer injects the __pipebuf__ file into wc (it reads _pipeInput), and the module registers itself with a versioned URL and calls its own init() — registerModule() does not call init(), so the executeCommand wrapper was never installed from the boot bundle. - module-fs.js: wc/head/tail/grep read piped stdin when no file operand is given; `cat` accepts several operands (`cat *.txt`); `ls <file>` names the file instead of printing "(empty)". - main.html: shell.js removed from the extension list — it is already inside critical-ext-bundle.js, and loading it twice produced two ShellModule instances that could disagree about their version. Verification: - test-shell-script.js 27 asserts (parser, variables, globs, executor, redirect, command-not-found, one-line if, parse error). RED on HEAD: `TypeError: shell._parseBlocks is not a function`. - test-shell-stdin.js 16 asserts (added by a subagent): wc -l/-c, head -n, tail -n over stdin, and the file-argument path unchanged. - Suite 18/18 files; maintain --fix "node tests 18/18 pass". - Live on v4.6.115, through the real executeCommand: `ls /n*.txt` -> /n1.txt · `cat /n*.txt` -> one|two · `echo hi > /r.txt` then `cat /r.txt` -> hi · `cat /probe.txt | wc -l` -> 3 (bare, no __pipebuf__) · `script /run.sh` -> item 1 / item 2 / item 3 / exists / command not found, exit 1, no AI routing.
* fix(newos4): the loop guard reads the config it documentsAgando10 days5-16/+126
| | | | | | | | | | | MAINT-004. The guard read warnAt/blockAt/hardStopAt while one config path wrote `threshold`, and `enabled` was never consulted anywhere — writing {"enabled": false} into webx11_ai_config changed nothing at all. One resolver, _loopPolicy(), now normalises both spellings, guarantees warnAt <= blockAt < hardStopAt, and both the block and the warning are gated on enabled. Live on v4.6.110 (_loopPolicy called in the page): defaults {warn 3, block 6, hardstop 9}; {threshold: 4} -> {warn 2, block 4, hardstop 7}; {enabled: false, threshold: 3} -> enabled false. test-loop-policy.js: 17 asserts, RED on HEAD ("_loopPolicy is not a function"), suite 16/16.
* fix(newos4): agent registration is absolute-path and honest about what it storedAgando10 days7-46/+225
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Three of the four agent-cluster items, each with a test that is red on HEAD. - MAINT-008 (module-agent-registry.js + module-orchestrator.js): registerAgent took args[0] as the agent id and only args[1] as the capability list, while both orchestrator callers passed the command word first — so the boot log read "Agent 'register' registered with 1 capabilities" and four capabilities were stored as one. The command word is now stripped, args[1..] are joined and split on commas (both "a,b" and "a b c" work), and the callers pass [id, ...caps]. - MAINT-007 (registry + aiassist): mkdir/rm used relative paths, so a registration made from /docs created /docs/agents/<id> and an unregister from any other cwd removed nothing — the leaked directories the report suspected. Every path in the registry (and the four /_sessions writes in module-aiassist.js) is absolute now. - MAINT-009 (listAgents): a directory alone counted as a registered agent, so a hand-made /agents/foo appeared as "foo — unknown (0 capabilities)". Only a readable status.json with an id counts now; the rest are reported as unregistered directories. Verification: test-agent-registry.js (11 asserts). RED on HEAD: 9 of 11 fail, including the exact `✅ Agent 'register' registered with 1 capabilities` line and `hand-made — unknown (0 capabilities)`. Suite 15/15 files green; maintain --fix "node tests 15/15 pass". Live on v4.6.109: boot prints "✅ Agent 'orchestrator-0' registered with 4 capabilities"; `agent-list` warns "1 directory(ies) in /agents have no status.json (not registered): hand-made"; register from / with a comma list stores 3; unregister from /docs removes the directory and leaves no /docs/agents. MAINT-006 stays open with a narrower note: the hard stop was only reachable when every call in a batch was blocked, so a mixed batch (one repeated + one novel call) never ended. The same ceiling now applies on the mixed path, but that branch lives inside agenticLoop and is not covered by a unit test — it needs a live agent run that repeats a call.
* fix(newos4): help names the owner of an undocumented command; loop lessons ↵Agando10 days6-38/+151
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | leave /skills Two board items plus the context-window close-out. - MAINT-019 (module-system.js): `help <cmd>` answered "No help available for X" for a command that is registered but has no help line, which reads as if the command does not exist. It now says "X — registered by <Module>, which publishes no help line for it. Try: help". A command that genuinely does not exist still gets the old message. - MAINT-020 (module-aiassist.js): auto-generated loop lessons were written into /skills with their own index entries, so `skill list` filled up with system noise. Lessons now live in /lessons with /lessons/index.json; the reader still falls back to legacy loop-lesson-* entries in /skills for one release, and the one-time migration deletes them from the skills index. Verification: test-help-and-lessons.js (10 asserts, includes a lint-style guard that no lesson is written into /skills). Suite 14/14 files green; maintain --fix "node tests 14/14 pass", "help coverage 362/362". Live on v4.6.108: `help agent-register` -> "registered by AgentRegistryModule, which publishes no help line for it"; `help a2a-prune` unchanged; `help nope-not-real` still "No help available". MAINT-001 close-out (no code change here): history is capped at 20 messages with a "[context trim]" marker (first trim at turn 11 in a 60-turn probe), token compression took 80 msgs/8080 tok to 17/2625, and since v4.6.107 one turn's tool bodies are clipped to 6 000 chars each and elided past 24 000 with the newest 4 kept verbatim. Residual, recorded on the board: maxContextTokens is never sent to the server as num_ctx, so the real window is whatever the endpoint defaults to. MAINT-018 close-out: not reproducible. Live probes on v4.6.107/108 — `sed -n '1,3p' /probe.txt` prints lines 1-3, `sed -n "2,4p"` prints 2-4, `grep 'line4'` matches, and the same commands inside `script /q.sh` print correctly. The "Command not found: for;" symptom belongs to the line-by-line script runner, which is MAINT-021 and stays open.
* fix(newos4): bound one turn's tool context, honest skill-create, quarantine ↵Agando10 days9-16/+450
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | a corrupt memory graph The three items the last review and probes left open, each with a test that is red on HEAD. - MAINT-023 (module-aiassist.js): one agentic turn grew without limit — every role:'tool' body was pushed verbatim and only this.context was trimmed BETWEEN turns (measured 6 -> 112 705 tokens across 8 model calls against maxContextTokens 12 000). Observations are now clipped to maxToolResultChars (6 000) with a note saying how much was withheld and to re-read with offset/limit, and _boundToolContext() elides the oldest tool bodies once the running total passes toolContextBudgetChars (24 000), always keeping the newest 4 verbatim. The ceiling is turn-independent: the same ~26 k chars at 8 turns and at 20. - MAINT-024 (modules/ext/skill-pack-core.js): the command printed "✅ Skill created" with fs.writeFile()'s boolean discarded (probe: fresh VFS, success printed, no file), stripped invalid characters instead of rejecting the name (My-Skill -> y-kill, DevOps_Thing -> evpshing), and treated an unreadable /path argument as the skill body. Now: kebab-case is validated (uppercase normalises and the final name is reported; anything else is refused), /skills is created on first use, the write and index results are checked, and an unreadable body path is an error. - MAINT-003 (module-memory-graph.js): a corrupt store looked exactly like a fresh install — fsReadJSON returns null on a parse failure, so 85 nodes could vanish with no warning and no copy. The probe confirmed the loss: seed 6 nodes, truncate nodes.json and export.json, reload -> 1 node, no message, nothing in .trash. _quarantineCorrupt() now keeps the unreadable file as <name>.corrupt, warns, and then loads from whatever remains. Tests: test-tool-context-bound.js (11), test-skill-create.js (16), test-memory-graph-recovery.js (8). RED on HEAD: tool-context crashes on _clipToolResult (exit 1), skill-create 16 assertions fail on the missing file/index, memory-graph B1/B2/C1 fail (no warning, no .corrupt copy). Suite now 13/13 files green; maintain --fix reports "node tests 13/13 pass". Live on v4.6.107: skill-create My-Skill -> ✅ "my-skill"; DevOps_Thing -> Invalid skill name; /does/not/exist.md -> Cannot read skill body; only my-skill in the index.
* fix(newos4): name the real Ollama failure, bound auto-connect, fix the ↵Agando10 days8-47/+343
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | review findings Second pass over v4.6.105, driven by an independent review that reproduced each item. - ollama-manager.js: chat() now reports the captured cause ("Ollama error: Failed to fetch", "…: HTTP 500") instead of the generic "No provider responded"; _lastChatError is reset per call so an old reason is never replayed. chatWithTools carries the reason in its error string. - ollama-manager.js: _ensureConnected() bounds connect() with a deadline (connectTimeoutMs, default 10 s). connect() uses a bare fetch(), so a blackholed endpoint previously left the shared in-flight promise pending forever and hung every later message — the old code failed fast. The attempt is now try/finally, so a throwing terminal.log() can no longer poison _connecting and suppress the retry on the next message. - ollama-manager.js: an automatic connect no longer silently rewrites the user's saved model; the previous choice is restored in memory and in localStorage, and the user is told. - ollama-manager.js: the DeepSeek empty-content branch returned top-level tool_calls with content '', so the caller pushed an assistant message with no tool_calls and then a role:'tool' reply — an orphan tool message, which is the HTTP 400 that branch exists to prevent. tool_calls now live inside the message as well. - module-aiassist.js: a no-argument list_files returned the ROOT instead of the cwd (readDir('/') instead of ls()); it now means the current directory again. Error text keeps the path as the caller wrote it, and write_file distinguishes a read-only virtual path (/sys/…) from a missing parent instead of blaming the parent. Tests: test-ollama-error-text.js (new, 14 asserts, RED 9/5 before the edit), test-ai-fs-tools.js 7 → 16 asserts (write onto a directory, /sys write, list a file, '/', no-arg cwd, relative path), test-ollama-autoconnect.js 11 → 19 (blackholed endpoint settles + retries, saved model survives; the old "connecting hint" assertion is now falsifiable — the reviewer showed it also matched connect()'s pre-existing line). Suite: 10/10 files green. Live on v4.6.106: healthy auto-connect 42 ms with the hint; blackholed endpoint settles in 1242 ms with connectTimeoutMs=1200, prints "timed out" and "unreachable", _connecting cleared.
* fix(newos4): connect to Ollama on first use, honest write_file, list_files ↵Agando10 days7-85/+287
| | | | | | | | | | | | | | | | | | | | | | | | | | | sees /sys Three defects, each re-probed first and covered by a test that is red on HEAD. - ollama-manager.js: nothing connects at boot; only the Ollama tab button called connect(), so a user's first message died with "Ollama not connected. Use ollamaconnect first." even when the endpoint was healthy. New _ensureConnected() runs from chat(), chatStream() and chatWithTools(): one attempt, shared by concurrent callers, a "Connecting…" hint, and only then the error — which now says the endpoint is unreachable instead of telling the user to run a command they already expected to work. - module-aiassist.js write_file tool: fs.writeFile()'s false return was dropped and the tool answered "File written: …" anyway, sending the model on to a read that found nothing (MAINT-012). It now returns "Write FAILED: … — parent directory missing or target is a directory; nothing was written". - module-aiassist.js list_files tool: the hand-rolled walk over root.content never resolved the virtual mounts, so /sys — a directory the UI and `ls` both show — answered "Directory not found" (MAINT-011). It now uses fs.readDir(), which resolves mounts and returns null for a genuinely missing path. Tests: modules/test/test-ai-fs-tools.js (7 asserts) and modules/test/test-ollama-autoconnect.js (11 asserts). RED on HEAD: ai tools 4 passed/3 FAILED (/sys → "Directory not found", write_file → "File written: …"), ollama 2 passed/9 FAILED ("Use ollamaconnect first"). GREEN now 7/7 and 11/11; maintain --fix reports "node tests 9/9 pass". Live on v4.6.105: connectedBefore=false, _ensureConnected()=true in 41 ms, 37 models.
* fix(newos4): never overwrite a directory node; ls honours a path after its flagsAgando10 days6-12/+163
| | | | | | | | | | | | | | | | | | | | | MAINT-010 and MAINT-016, the two high items the review left open. Both were re-probed before the change and the new test is red on HEAD (5 passed, 7 FAILED). - virtual-fs.js rename()/copy(): they assigned dst.dir.content[dst.name] with no destination type check, so `mv file /docs` replaced the /docs node and every child vanished while the command printed "Moved: …". Refuse when the destination is a real directory node (the virtual /sys entry is exempt). rename() also erased the file on `mv a a`, because assigning and then deleting the same key removes it; that path is now an explicit no-op. - module-fs.js mv/cp: a refusal used to surface as "No such file or directory", which sent the operator looking for a missing source. The destination is now inspected first and the message says "is a directory — refusing to overwrite it". - module-fs.js ls: the path was only read from argv[0], so `ls -l /work` listed the cwd. The first non-flag argument is now the target; the path-first form is unchanged. Tests: modules/test/test-fs-mv-cp-guard.js (12 asserts) — RED on HEAD (A1-A3, B1, B2, C2, E1), GREEN now; maintain --fix reports "node tests 7/7 pass". Browser on v4.6.104: "mv: /vtest: is a directory — refusing to overwrite it", child and source intact, "ls -l /vtest" lists /vtest (keep.txt), not the cwd.
* fix(newos4): rollback keeps scaffolding, checkpoints cannot collide, tests ↵Agando10 days10-32/+256
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | gate the build Review of the 08:30 implementer run (v4.6.102, checkpoint name fix). The shipped fix was correct; these are the defects it introduced or left behind, each measured first. - virtual-fs.js rollback(): the snapshot strips _snapshots/_sessions/.trash for bloat. Restoring it verbatim deleted every checkpoint, every agent session and the trash, made rollback irreversible and unrepeatable. Re-attach the live copies after the root swap. - checkpoint(): names are millisecond-resolution, so tight calls overwrote each other (measured 6 calls -> 3 snapshots). Dedupe with a -N suffix, refuse names containing "/" (written but unlistable), and prune by snapshot createdAt instead of filename sort, which let a custom name ("aaa-oldest") evict newer auto-named checkpoints. - module-predictor.js: legacy pre-v2 points have no `usable`, so a 0.079 s aborted run re-entered similarity as evidence and floored every prediction at 1 s. Backfill `usable` from the measurement on load, drop the ones it rejects, add a belt-and-braces guard in _findSimilar and count only usable points as actual executions. - newos4-maintain.py: run every node-runnable modules/test/test-*.js on each maintenance pass. The v4.6.102 regression was written but no job ever executed it. - newos4-steer.py: `done` now requires --commit <sha> that exists in the repo, and gains `reopen`. A written plan was being closed as done at 06:17 while the predictor fix shipped nowhere, so the implementer's pre-flight queue was empty. - steering.json: predictor item reopened, checkpoint item stamped with f7249ff. - maintenance board: MAINT-002/005/015 were fixed in v4.6.81-85 but still listed open/high; closed with their commit ids, and MAINT-010/016 annotated with the probes that keep them open. Tests: test-vfs-checkpoint 23/23 (RED 14/9 before), test-predictor-accuracy 38/38 (T1-T3 RED on HEAD), maintain --fix all PASS, browser round-trip "Checkpoint created: <name>" -> "Rolled back to checkpoint: <name>" on v4.6.103. Also carries the already-deployed 2026-10-26 syntax-gate hunk in newos4-maintain.py (syntax_failures + gated rebuild) that had never been committed.
* fix(newos4): regression test for checkpoint name round-trip (auto)Agando11 days1-0/+92
| | | | | | | | | | | | | | | | | | | | | 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
* fix(newos4): print the checkpoint name actually saved (auto)Agando11 days4-11/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | Task 2 + ship of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md. module-system.js `checkpoint` read the clock a second time for the printed name, so the name in "✅ Checkpoint created: <name>" differed from the name on disk by >=1ms whenever no explicit name was given. It now prints the value returned by fs.checkpoint(). try/catch and error strings unchanged. Bundles rebuilt (rebuild-bundles.py); LATEST_VER in module-loader.js and all three ?v= tags in main.html (core-bundle, module-loader, critical-ext-bundle) bumped v4.6.101 -> v4.6.102. Verification: $ node --check modules/core/module-system.js SYNTAX OK $ node --check modules/core/core-bundle.js BUNDLE SYNTAX OK $ node modules/test/test-vfs-checkpoint.js (x5) test-vfs-checkpoint: 10 passed, 0 failed $ curl -s https://webx11.duckdns.org/spaces/newos4/main.html | grep -o 'core-bundle.js?v=[^"]*' core-bundle.js?v=v4.6.102 $ python3 scripts/newos4-maintain.py PASS: version v4.6.102 x3 in main.html (expect 3) ... maintenance result: OK $ python3 scripts/newos4-skill-audit.py skill audit: 7/7 pass
* fix(newos4): checkpoint() returns resolved name (auto)Agando11 days1-1/+1
| | | | | | | | | | | | | | | | Task 1 of plan 2026-10-27_061724-newos4-checkpoint-name-mismatch.md. virtual-fs.js checkpoint(name) generated an ISO name when none was given but returned bare `true`, so callers could not know which name the snapshot was written under. It now returns the resolved name (string). Prune (max 3) and saveToStorage() behaviour unchanged. Verification: $ node --check modules/core/virtual-fs.js SYNTAX OK $ node modules/test/test-vfs-checkpoint.js test-vfs-checkpoint: 9 passed, 1 FAILED (remaining FAIL was the module-system display path, fixed in the next commit)
* feat(newos4): cut/copy in Files, paste into WebDAV (Issue D2)Agando11 days5-5/+80
| | | | | | | | | | | | | | | | | | | | | | | | The two panes kept separate clipboards (filemgr._clipboard vs webdav._selected), so "cut here, paste there" did not exist — the operator's last phone-test item. filemgr.js _openOverlay() publishes a read-only view of its clipboard on the terminal object: webx11.fileClipboard() / fileClipboardClear() / fileClipboardSet(paths, mode). webdav.js the bulk bar gains "📥 Paste N" (hidden when the clipboard is empty). Handler: for each path readFile -> _webdavRequest('PUT', dest + '/' + name, {body}); only 200/201/204 counts. For mode "cut" the local file is deleted AFTER the server accepted the bytes, never before. A failed PUT keeps the local copy, prints "📥 Paste failed: <name> — HTTP n", and leaves that path in the clipboard so "📥 Paste 1" is a retry. Successes leave it. Summary "📥 Pasted N item(s) to <path>" (+ "— M failed"), then the pane reopens there. Semantics: cut moves (upload then delete local), copy leaves the local file and keeps the clipboard so the same files can go into several folders. Verified with the transport stubbed in the live page (this browser has no WebDAV credential): button "📥 Paste 2" for a 2-path cut PUT ok (201) -> local file null (moved) PUT fail(500) -> local file "BBB" (kept) clipboard after -> {mode:"cut", paths:["/fail-d.txt"]}, button "📥 Paste 1" node --check both modules, no-undef clean (55 files), v4.6.101 x3 in main.html, syntax OK, help 362/362.
* fix(newos4): four phone-test defects — no preview close, single-file ↵Agando11 days5-117/+242
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | share, dead-end fullscreen, silent WebDAV move All four came from the operator's phone pass on v4.6.95 and each collapsed to a line of code. A "No file selected to download": the preview pane had no close control; the only ✕ was the file manager's own, and it calls _closeOverlay() which wipes _multiSelect/_selectedFile/_selectMode. On a phone the preview covers the list, so closing it before pressing Download always emptied the selection. The toolbar now starts with ✕ -> _clearPreview(), which keeps the selection and leaves fullscreen first; Download with no targets names the fix instead of stating a dead end. B share/copy: 🔗 existed only in the preview toolbar and took a single path, and after the Android share sheet there was no copy affordance. Added _sharePaths(paths, mode) used by both the toolbar (1 path, 📋 copy and 🔗 share) and a new action-bar 🔗 Share (whole selection), plus _inlineLinkBox() which renders a selectable input/textarea with all URLs when neither the share sheet nor the clipboard is available. No prompt() anywhere on this path. C fullscreen: requestFullscreen targeted .fm-preview-body, which sits INSIDE the pane but excludes the toolbar, so ⛶ and ✗ disappeared and only the OS back gesture got out. Now fullscreens #filemgr-preview — toolbar, ✕ and ⛶ stay on screen and ⛶ toggles. D webdav move: bulk Move asked for the destination with prompt(), which returns null in iOS standalone PWAs and most Android WebViews, so it silently did nothing and looked unimplemented. Destination is now an inline row in the bulk bar (44px input + "Move here"/"Cancel", prefilled). Verified on the served code: ext bundle md5 5f440c77b6ab5529729aee8019a6c351 (served == local), webdav.js carries wdav-move-dest x2, and a headless browser probe on the real page shows the toolbar as ["✕","⛶","📋","🔗"], closing the preview prints "Preview closed — selection kept" with the selection intact, selecting 3 files and copying yields a textarea with the 3 real WebDAV URLs, and Download with nothing selected explains what to do. 6 PASS on the URL test, no-undef clean, v4.6.99, help 362/362. Not done, deliberately: cut/copy + paste across the VFS/WebDAV border. Recorded as Issue D2 in the plan with the exact insertion points, the upload-then-delete ordering rule and the failure case.
* feat(newos4): mobile download/share UX — deferred blob revoke, ↵Agando11 days5-94/+251
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | multi-select, Web Share, 44px targets Executes .hermes/plans/2026-10-24_051600-newos4-mobile-download-share.md (approved via the feedback dashboard; marker autonomy: safe). Front-end only, v4.6.94 -> v4.6.95. T1 modules/ext/filemgr.js — URL.revokeObjectURL ran in the same tick as a.click(); iOS Safari and Android Chrome hand the blob: URL to the download manager asynchronously, so the transfer was aborted while the status line still said "Downloaded". Now revoked after 30 s. T2 download honours the multi-selection via _getSelectedPaths() (falling back to _selectedFile), reports "⬇ Downloaded N files" and "❌ Skipped <path>" instead of a silent partial success. T3 share: navigator.share -> clipboard.writeText -> inline read-only <input> (focused, selected). prompt() is suppressed in iOS standalone PWAs and most Android WebViews, which is why tapping 🔗 did nothing. Unresolvable space now reports "🔗 Share unavailable — no space resolved". T4 both preview toolbar buttons: min-height/min-width 44px, touch-action manipulation, 1em, gap 10px (was 24px/0.85em/6px — the smallest tap targets in the UI). T5 _previewShareUrl falls back _resolveSpaceName -> _detectSpaceName (webdav.js:22) -> page path, and returns null rather than emitting /spaces//<path>. T6 the URL helpers are now module-scope pure functions exported for Node, so modules/test/test-download-url.js tests the shipped code instead of a local copy (4 -> 6 cases). Also fixes a latent bug that made require() impossible: `typeof window.webx11` throws under Node because typeof does not protect a member access on an undefined global. Verified on the served bundle (filemgr.js is concatenated into critical-ext-bundle.js, NOT core-bundle.js — the plan's T7 grep pointed at the wrong file): HTTP 200, 129 867 B, 2x navigator.share, 3x fmBuildShareUrl, deferred revoke present, 11x min-height:44px. Live browser probe on /spaces/newos4/main.html: _previewShareUrl('docs/a.md') -> https://webx11.duckdns.org/spaces/newos4/docs/a.md (no double slash); with navigator.share absent and clipboard blocked, clicking 🔗 produced .fm-share-input containing that URL; button style ends min-height:44px;min-width:44px;touch-action:manipulation; toolbar gap:10px. node modules/test/test-download-url.js -> 6 PASS. lint-no-undef clean (55 files). maintain: PASS version v4.6.95 x3, syntax OK, HTTP 200 /main.html. Also adds admin/docs/session-overview-2026-10-26.md (glm-5.3-compressed session record).
* feat(feedback): autonomous first-pass reviewer — decide what is provable, ↵Agando11 days2-1/+244
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | leave the rest 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 Writes to the same append-only decisions.jsonl with actor=auto-reviewer, note + evidence; the applier's last-wins rule means any operator click overrides an agent decision. Kill switch: data/feedback/autonomy.json {"mode":"off"} (safe default, aggressive = cap 5). Cron job e31ac58a559e, 07:40 and 19:40, no_agent, silent unless it decided something. Fixture proof (6 cases, /tmp corpus, git repo with a real removal commit): RPT-A duplicate -> resolve (duplicate of already-decided RPT-B) RPT-C already-fixed -> resolve (goneFromCode removed from modules/ in fc4eaf3) RPT-D noise -> reject RPT-E still-present -> approve RPT-F user report -> SKIP RPT-G never existed -> SKIP <- caught the first draft, which called it "already fixed" Live dry-run on the real corpus: "nothing provable (0 left for the operator, mode=safe)" and `git log -S emptyTrash -- spaces/newos4/modules` resolves to 563a9be, i.e. the report I wrongly approved by hand tonight would have been auto-resolved correctly. Dashboard: agent-decided cards carry a 🤖 badge + the reason + "Any button below overrides it", and the tab shows "🤖 feedback agent: N decided on its own in its last run (safe mode, M left for you)".
* feat(newos4): filter network-class auto reports at ingest — count, don't fileAgando11 days2-0/+65
| | | | | | | | | | | | | | | | | | | | | | 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. Never filtered: any report with auto=false (user content), and any auto report carrying a stack/TypeError, i.e. a real defect. Verified on a throwaway tree and live through nginx: auto network poll.php -> 202 filtered, no file auto network /ollama/ -> 202 filtered, no file auto TypeError + stack -> 201 filed (RPT-20261026-134455-95f10612) user report w/ "fetch" -> 201 filed (RPT-20261026-134455-ab0f2cd4) Live: counter total=1 by_source {'/spaces/newos4/api/chat/poll.php': 1}, reports stayed at 19. The dashboard now shows the counter instead of silence: "🌐 1 network failure counted at ingest today, not filed as cards — /spaces/newos4/api/chat/poll.php ×1"
* docs(newos4): reconstruct _docs/decisions-2026-10-23.md — two feedback ↵Agando11 days1-0/+30
| | | | reports referenced a file that did not exist
* feat(feedback): preview a plan or report in a modal before decidingAgando11 days1-1/+18
| | | | | | | | | | | | | | | | | | | | | | | | | 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. Also fixes a bug I introduced earlier: recent_apply() referenced APPLIED, which was never defined — the NameError was swallowed by the function's own except, so the "last decision applied" line could never render. It now shows, e.g. "last decision applied 2026-10-26 12:47:03 UTC (plan:…mobile-download-share.md → approve)". Verified: node --check on the inline script; browser harness against the real snapshot (plan modal shows Goal + 9 tasks + status, report modal shows message/errors/page/ua, Esc closes); verify-mobile 390/360 x light/dark ALL VIEWPORTS OK with the modal open; collector py_compile + run (recent_apply 8 rows).
* feat(feedback): open the Agent tab on the queue, guide out of empty states, ↵Agando11 days1-3/+5
| | | | cleaner directives
* feat(feedback): review dashboard, public guide, terminal commands (v4.6.94)Agando11 days7-7/+681
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Dashboard (/admin/feedback.html, admin-gated) - User-feedback tab over the live :8908 API: list, filter, search, publish reply, archive/reopen. Reply is what the reporter sees in /stear -> Your reports & replies. - Agent-items tab: 3 plans parked at autonomy:review + 19 defect reports, one click each. - Snapshot age line + stale warning (>30 min), and the last apply outcome with failures. - Existing /admin/ tab relabelled "Client error log" (it renders feedback_log.json, not the feedback store) and now links to the review dashboard. Decision path - admin/api/feedback-decide.php: exact-match nginx location + auth_request, Origin check, item_id allowlist, append-only decisions.jsonl with fwrite+fsync verification. - scripts/newos4-feedback-apply.py (darkbot): last decision per item wins, superseded rows recorded, reject reverts safe->review, per-item try/except, ledger saved per mutation, realpath containment, exact standalone-line marker flip, file mode preserved, queue rotation at 1 MB. Steering directives now carry the report title. - scripts/newos4-feedback-collect.py: snapshot for the dashboard, 40-line plan head, steering shape assert (-1 = unknown), recent_apply outcomes. - newos4-feedback-sync.sh: flock -n so overlapping runs cannot double-apply. Guide + terminal - feedback-guide.html: which channel (terminal feedback / /stear / automatic reports), expectations, exact payload per channel, how to write a report that gets fixed. Linked from /stear, index.html footer and help.html. - newOS4: feedback list|read|reply|archive|reopen|stats|dashboard|help (+ feedback-admin alias), admin-token derived per call, argument-validated dispatch so a sentence like "feedback list is broken on mobile" is still a message, "feedback -- <text>" forces it. - help.html: 8 rows, coverage 362/362. Review: glm-5.3 REWORK -> 15 findings fixed, 5 refuted with evidence, 2 doc-only. Guide corrected (invented "60 MB per batch" replaced by the real 512 KB/5-per-10-min limits).
* feat(newos4): context window warns at 80% and says what trimming dropped ↵Agando14 days5-10/+75
| | | | | | | | | | | | | | | | | | (v4.6.91) P0-5 asked what to do about a 422k-token session against a 400k threshold. There is no 400k threshold in this tree — the budgets are maxContextTokens (12000) and maxContext*2 messages — so rather than chase a number that does not exist, this applies the decision that is defensible either way: warn before the cliff, never lose history without saying so. - 80% of the token budget prints one warning per crossing (no per-turn nagging) - the message-count trim unshifts a marker naming how many messages left the window and quoting the earliest one, instead of splicing them away silently - 'max-context-tokens (default: 2000)' now reports the real code default, 12000 Verified: 900/1000 tokens -> warns once, second call silent; 1500/1000 -> compresses to 17 messages with the summary head intact; 9-message window with maxContext=2 -> 4 messages, marker '[context trim: 6 earlier message(s) ... earliest was: "turn 0 ..."]'.
* fix(newos4): the virtual /sys entry leaked into listings, trees and counts ↵Agando2026-10-236-16/+68
| | | | | | | | | | | | | | | | (v4.6.88-90) One phantom entry, three symptoms: - ls() injected 'sys' whenever it was called without a path, so 'cd _agents; ls' advertised a sys directory that does not exist, and list_files('_agents/sys') answered 'Directory not found' — the reported false negative. Injection is now gated on dir === root. - tree() pushed the same virtual name into the walk and dereferenced it, throwing mid-output on every root listing. Guarded. - _gatherFileInfo() walked into /sys and counted a synthetic subtree: 146 files / 114 dirs for a 44-file / 15-dir store. Virtual entries are skipped. Verified: root still lists sys; _agents and skills do not; tree('') completes; world model total (44) equals an independent walk of fs.root (44), agree: true.
* fix(newos4): audit names broken refs instead of counting documentation ↵Agando2026-10-234-9/+37
| | | | | | | | | | | | | (v4.6.86-87) The reference checker ran /skill-view \S+/ over every skill's prose, so 'skill-view <name>' and a quoted example in skills-system-guide were counted as missing skills: 'Broken refs: 2' with no names, both phantom. Placeholders and quoted names are now ignored (trailing punctuation stripped first), and the survivors are listed in the audit line and in the suggestion. Verified: clean tree -> Broken refs: 0. A probe skill referencing 'ghost-skill' -> Broken refs: 1 (probe-broken-ref → ghost-skill), while '<name>.' and '"module-management"' in the same file are ignored.
* fix(newos4): the audit stops inventing '999d unused' (v4.6.85)Agando2026-10-236-9/+32
| | | | | | | | | | | | getStaleSkills() returned daysUnused=999 for a skill that had useCount 0 and no lastUsed — a placeholder rendered by the audit as '999d unused', indistinguishable from a measurement. It told the operator to archive procedural memory that had simply never been instrumented. Now: daysUnused null + neverUsed true when there is no lastUsed; real day count otherwise; the audit prints '(never used)'. Verified: probe skill with no telemetry -> {daysUnused: null, neverUsed: true}; one last used 2026-08-01 -> 83; no 999 anywhere in getStaleSkills() or the audit output.
* fix(newos4): world_model_query default path parses again (v4.6.83-84)Agando2026-10-236-24/+93
| | | | | | | | | | | | | _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.
* fix(newos4): skill-create stops mangling what the agent wrote (v4.6.82)Agando2026-10-234-19/+58
| | | | | | | | | | | | | Two defects in one parser, both silent: - content = args.slice(2).join(' ') destroyed every newline, so markdown arrived as one line - the last argument was treated as tags whenever it contained a comma, and stayed in the body: a multi-line skill became a single line with ~13 bogus tags, reported as success Bodies can now be read from a VFS path (the only way markdown survives an argv splitter), tags are opt-in via --tags=, category via --category=. skill-patch takes a path too. Verified: body from file -> 6 newlines preserved, tags [alpha,beta], category testing; inline body containing four commas -> tags [], body intact.
* fix(newos4): outboxResult scoped, markdown mime, old reply path retired ↵Agando2026-10-236-48/+55
| | | | | | | | | | | | | | | | (v4.6.81) 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.