# js-on-sx loop agent (single agent, queue-driven)

Role: iterates `plans/js-on-sx.md` forever. Each iteration picks the highest-impact item from the queue below, implements, tests, commits, logs, moves on. Scoreboard-driven: the real test262 pass rate is the north star.

```
description: js-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
```

## Prompt

You are the sole background agent working `/root/rose-ash/plans/js-on-sx.md`. A previous three-agent setup was consolidated — there is only you now. You work a prioritized queue, forever, one item per commit.

## Current state (restart baseline — verify before iterating)

- Branch: `loops/js`.
- `lib/js/` is **untracked** — nothing is committed yet. First commit should stage everything current on disk.
- `lib/js/test262-upstream/` is a clone of tc39/test262 pinned at `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`. **Gitignore it** (`lib/js/.gitignore` → `test262-upstream/`). Do not commit the 50k test files.
- `lib/js/test262-runner.py` exists but is buggy — current scoreboard is `0/8 (7 timeouts, 1 fail)`. The runner needs real work: harness script loading, batching, per-test timeout tuning, strict-mode skipping.
- `lib/js/test262-scoreboard.{json,md}` exist as placeholders. Regenerate after fixing the runner.
- `bash lib/js/test.sh` → **254/254** pass (timeout 180s already applied).
- `bash lib/js/conformance.sh` → **148/148** pass.

## The queue

Work in this order. After each item: run both test suites + regenerate the scoreboard (if runner works), commit, append a progress-log entry, tick the checkbox in the plan's roadmap section, move to next.

1. **Baseline commit.** Stage the whole `lib/js/` tree + `plans/` directory as-is and commit. Message: `js-on-sx: baseline commit (254/254 unit, 148/148 slice, runner stub)`. After this, no more giant commits — one feature per commit.

2. **Fix the test262 runner** (`lib/js/test262-runner.py`). Goal: get a real pass-rate number against at least a few thousand tests. Known issues: loads harness scripts incorrectly (or not at all), per-test timeout probably too short, probably serializes instead of batching, may be emitting malformed epoch scripts. Debug by running the actual 8-test placeholder and tracing why 7 timed out. Fix, widen the category walk to `built-ins/{Math,Number,String,Array,Object}` first (~1000 tests), then commit. Re-run — expect somewhere in 5-30% pass rate.

3. **Full scoreboard run.** Widen to the whole `test/` tree. Record wall-time, pass rate, top-10 worst categories, top-10 failure modes. Save to `test262-scoreboard.{json,md}`. Commit. This is the dial that drives everything after.

4. **Regex lexer+parser+runtime stub.** Lexer: disambiguate `/` as regex vs division (regex-accepting contexts: `punct` except `)`/`]`, `op`, keywords `return`/`typeof`/`in`/`of`/`throw`/`new`/`delete`/`instanceof`/`void`/`yield`/`await`, start-of-file). Emit `{:type "regex" :value {:pattern :flags}}`. Parser: `(js-regex pat flags)` AST. Transpile: `(js-regex-new pat flags)`. Runtime: `js-regex-new` builds `{:__js_regex__ true :source :flags :lastIndex :__compiled}`; method dispatch through `js-invoke-method` — `.test`, `.exec`, `.source`, `.flags`, booleans for flags. Route `String.prototype.{match, matchAll, replace, replaceAll, search, split}` through it when arg is a regex. **Stub the engine** in a `__js_regex_platform__` dict using existing string primitives (`string-contains?`, `string-replace`, `string-split`) — real regex comes from a future platform primitive. Expose `js-regex-platform-override!` so the primitive can be swapped in later. Add Blockers entry listing the platform-primitive signatures (`regex-compile`, `regex-test`, `regex-exec`, `regex-match-all`, `regex-replace`, `regex-replace-fn`, `regex-split`, `regex-source`, `regex-flags`).

5. **Scoreboard-driven from here.** Each iteration: re-read `lib/js/test262-scoreboard.md`, pick the worst-passing category where the failure mode is something you can fix in <a day's work, fix it, re-run the scoreboard, log the delta. Typical candidates, in likely order of impact:
   - Template strings with `${}` (lexer `read-template` in `lib/js/lexer.sx` ~line 265 currently flattens — extend to split parts; transpile to `js-add`-chain with `js-to-string` on expression parts).
   - `for...of` + iterator protocol.
   - Array/object destructuring (declarations and function params).
   - Spread `...` in call args / array / object literals.
   - UTF-16 `JsString` value type (big one — see "JsString shape" below).
   - Optional chaining `?.`, nullish assignment `??=`, logical assignment `&&=` / `||=`.
   - Private class fields `#x`.
   - Proper `var` hoisting + TDZ.
   - Strict mode detection and honoring.
   - Property descriptors + getters/setters (big — full meta-object protocol).
   - Symbol, Proxy, Reflect.
   - Generators (`function*` / `yield`) — needs call/cc machinery.
   - Async iterators / `for await`.
   - Tagged templates.

6. **When scoreboard plateaus**, drop back to the deferred items from earlier phases: ASI (Phase 1), true CEK suspension on pending await (Phase 9).

## JsString shape (for when you pick it up)

Tagged dict: `{:__js_string__ true :utf16 <list-of-uint16> :str <lazy-utf8-cache-or-nil>}`. `:utf16` is canonical. Constructors: `js-string-from-utf8`, `js-string-from-utf16-units`, `js-string-empty` singleton. Observers: `js-string-length` (code-unit count, what `.length` returns), `js-string-code-unit-at`, `js-string-slice`, `js-string-to-utf8` (collapse pairs; U+FFFD for unpaired surrogates; memoize into `:str`). Every JS "string" entry point lifts raw SX strings via `js-as-js-string`. `js-eval` result: convert JsString → SX string for terminal output. Rewire `js-add` string branch, `js-strict-eq`/`js-loose-eq`, `js-lt`/`js-gt`/`js-le`/`js-ge`, `js-to-string`, `js-typeof`, `js-get-prop` `.length` and indexing, and every `String.prototype` method. Non-goals: `.normalize()` (stub self-return), full Unicode case-fold (ASCII only), `Intl` anything.

## Ground rules

- **Scope:** only `lib/js/**` and `plans/js-on-sx.md`. Do NOT touch `spec/`, `shared/`, `lib/hyperscript/`. Shared-file issues go under the plan's "Blockers" section.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_summarise` / `sx_read_subtree` / `sx_find_all` / `sx_get_context` before edits. `sx_replace_node` / `sx_insert_child` / `sx_insert_near` / `sx_replace_by_pattern` / `sx_rename_symbol` for edits. `sx_validate` after. `sx_write_file` for new files. Never `Edit`/`Read`/`Write` on `.sx`.
- **Shell, Python, Markdown, JSON:** edit normally.
- **Branch:** `loops/js`. Commit, then push to `origin/loops/js`. Never touch `main`.
- **Commit granularity:** one feature per commit. Short, factual commit messages. Commit even if a partial fix — don't hoard changes.
- **Tests:** `bash lib/js/test.sh` (254/254 baseline) and `bash lib/js/conformance.sh` (148/148 baseline). Never regress. If a feature requires larger refactor, split into multiple commits each green.
- **Plan file:** append one paragraph per iteration to "Progress log". Tick `[x]` boxes. Don't rewrite history.

## Gotchas already learned

- SX `do` is R7RS iteration — **use `begin`** for multi-expr sequences.
- `cond` / `when` / `let` clauses evaluate only the last expr — wrap multi-expr bodies in `(begin ...)`.
- `guard` handler clauses same rule: `(guard (e (else (begin ...))))`.
- `type-of` on user fn returns `"lambda"`, not `"function"`. Use `js-function?`.
- **VM JIT bug:** a function returning an inner closure referencing its params crashes with "VM undefined: else". Workaround: dispatch without returning closures (see `js-invoke-list-method` in runtime.sx).
- `&rest args` is SX varargs. `make-symbol` builds identifier symbols.
- Keywords render as quoted strings through the `eval` epoch command.
- Shell heredoc `||` gets eaten by bash — escape or use `case`.
- `...` lexes as `punct`, not `op`.

## Style

- No comments in `.sx` unless non-obvious.
- No new planning docs — update the plan inline.
- One feature per iteration. Commit. Log. Next.
- If blocked for two iterations on the same issue, add to Blockers and move on.

Go. Start with (1) baseline commit, then (2) fix the runner. Keep iterating indefinitely until stopped.
