一月時,我偶然讀到一篇關於 one-agent-one-browser 的部落格文章,這是一個由單一作者在 Rust 中從頭開始建構的瀏覽器,由單一編碼代理程式在幾天內完成。不過,它不支援 JavaScript。我讀完後心想:「用同樣的方式打造一個 JavaScript 引擎,會有多難?」
六週後,JSSE(JavaScript Simple Engine)成為了我所知第一個通過 test262 非預演測試 100% 的 JavaScript 引擎:涵蓋 language/、built-ins/、annexB/ 和 intl402/ 中的所有 98,426 個場景。不是 V8。不是 SpiderMonkey。不是 JavaScriptCore。這是一個在 Rust 中從頭開始建構的引擎,完全由運行在 YOLO 模式下的 Claude Code 完成。
我沒有寫下一行 Rust 程式碼。一行都沒有。從我的角度來看,這個儲存庫是一個寫入式資料儲存。我甚至沒有手動建立 GitHub 儲存庫;代理程式也完成了這項工作。
一切始於一個關於代理程式編碼和瀏覽器的聊天室對話。1 月 27 日下午 2:42,我提出了這個想法:「好吧,我們要不要從頭開始建構一個 JS 引擎,然後將它整合到那個瀏覽器裡?」十七分鐘後,我找到了單頁規格和關鍵洞察:「最棒的是我們有 test262,所以我們可以建立一個回饋迴路,來建立一個通過所有測試的 JS 引擎。」
下午 3:05,儲存庫就上線了。我設定了初始腳手架(CLAUDE.md、PLAN.md、skills),並在下午 3:59 啟動了一個 Ralph Loop,使用一個龐大的提示,讓代理程式自主地逐項執行引擎任務、運行 test262、審查程式碼並提交。
下午 4:02,第一次提交就完成了。我當時去開會了。一個小時後,下午 5:09,它已經在執行 JavaScript 了:
當天晚上 7:10,它達到了 17.63% 的 test262 通過率。我當晚就休息了。從零到一個功能性的 Rust JS 引擎,大約花了四個小時。
最後這個數字讓許多人感到驚訝。四個小時分散在六週內:這裡 30 秒,那裡兩分鐘。其餘時間都是代理程式自主工作的。
The setup was deliberately vanilla. Claude Code running in YOLO mode (auto-accept all tool use). No fancy orchestration. The plugins I used were:
專案的核心是 PLAN.md,這是 Claude 從 ECMAScript 規格和 test262 子模組自行生成的任務清單。我典型的提示是:「閱讀 PLAN.md 中的第一個未完成項目,規劃並實現它,標記為完成,提交並推送。」就這樣。我從未審查過程式碼,從未審查過計畫,也從未讀過任何差異。CLAUDE.md 包含規則:不允許回歸,始終嘗試通過比之前更多的測試。有時當 Claude 試圖跳過它認為太難的任務時,我會反駁:「你為什麼要跳過這個?我們必須實現所有內容,所以不如現在就處理它。」但這就是我指導的全部內容。
最長的無人值守循環是 16 小時,用於 Temporal API 的實施。我在睡前啟動它,回來時已經完成了 Instant、ZonedDateTime、PlainDateTime、PlainDate、PlainTime、Duration、Temporal.Now 的完整實施,透過 ICU4X 支援 IANA 時區,以及 12 個日曆系統,並通過了 4,482 個 test262 測試。Temporal 是近期 ECMAScript 歷史上最大、最複雜的提案之一,代理程式在一個晚上的時間內就處理了它。
上一節抽象地描述了工作流程:規劃、實施、測試、提交。但實際上每天的樣子是怎樣的?以下是專案中的實際提示,它們本身就講述了一個故事。
最常重複的提示——整個專案的心跳,出現了 20 多次——是這個:
這就是核心循環。我沒有選擇要處理什麼;我要求代理程式分析 test262 的覆蓋範圍差距,提出按影響力排序的選項,然後我會選擇一個(或者只是說「開始」)。我的大部分互動都處於這個層級:策略性的,而不是戰術性的。
當事情出錯時,提示會轉向除錯:
長時間運行的會話有時會在單個測試上停滯不前,或者陷入編譯循環。修復方法通常是終止會話,然後以更小的範圍重新啟動。
隨著專案的成熟,我對並行處理變得更加雄心勃勃:
Claude Code 支援 worktrees——獨立的 git 分支,多個代理程式可以同時在其上工作。這就是最困難的功能是如何完成的:將工作分成獨立的軌道,並行運行,然後合併。Temporal API 的實施大量使用了這種模式。
有些提示只是為馬拉松式會話開綠燈:
最後一個是關於陣列空洞——JavaScript 最棘手的語義角落之一。[1, , 3] 在索引 1 處有一個空洞,其行為與 undefined 不同,具有細微的、規格要求的差異。代理程式處理了它,但這需要一個專門的會話。
最後階段的提示(98% 以上)是最有趣的,因為工作的性質完全改變了。我們不再是實施功能,而是追蹤單個測試失敗:
所有這些的模式是,我從未告訴代理程式如何實施任何東西。我設定目標,選擇優先級,偶爾解除封鎖。代理程式完成了工程工作。
圖表比我更能說明問題。幾個值得一提的里程碑:
1 月 31 日的跳躍是我最喜歡的。對 String.prototype 的連接方式進行的單一錯誤修復,一夜之間解鎖了約 11,000 個測試。這就是擁有完整的 test262 套件作為回饋信號的價值所在;你可以通過其影響範圍來發現錯誤。
為了比較,我針對 Node.js 和 Boa(另一個基於 Rust 的 JS 引擎)運行了相同的 test262 套件。相同的 checkout,相同的 harness,相同的超時,相同的機器(128 核):
一些重要的注意事項:Node 的失敗主要由 Temporal 引起(在 Node 25 中尚未發布,這是其 11,986 次失敗中的 8,980 次)。模組測試(799 個場景)在 Node 中被跳過,因為測試適配器無法通過它運行 ES 模組。Boa 的主要差距在於解析器層級(賦值目標驗證、類解構、RegExp 屬性逸出)。這些是成熟的、生產級的引擎,在它們並非專門優化的指標上進行比較,所以請將此比較視為參考。
JSSE 的場景計數更高(101,234 對比 91,986),因為它包含了完整的預演測試套件。在非預演測試上,JSSE 保持在平坦的 100%。
我也針對 acorn 測試套件運行了 JSSE,作為 test262 以外的真實世界健全性檢查。所有 13,507 個 acorn 測試都通過了。
有一次,我以為 JSSE 也通過了預演測試。事實並非如此。run-test262.py 實際上並沒有運行它們。當我最終明確運行預演套件時,情況變得有趣起來。
非預演套件維護良好且內部一致。預演是提案測試在被推廣之前所在的地方,情況也確實如此。JSSE 目前通過了 2,808 個預演場景中的 2,762 個(98.36%)。其餘失敗分為三類:不穩定的超時、libm 精度差距,以及最有趣的,似乎是測試套件本身的錯誤。
例如,六個預演測試使用了一個共享的 harness 函數,該函數依賴於 eval 聲明的變數洩漏到封閉範圍,而嚴格模式明確禁止了這一點。這些測試在 JSSE 和 Node 上都會失敗。它們需要上游的 noStrict 標誌。另一個例子:兩個預演測試期望 AnnexB 塊作用域函數 arguments() 聲明的提升行為,這直接與主套件測試相矛盾。主套件反映了一個最新的規格變更,目前沒有主流引擎實現。JSSE 遵循規格,因此主套件測試通過,而預演測試失敗。
當你針對的測試套件開始反過來測試你時,這是一個好跡象。
糟糕。故意如此。沒有花費任何精力進行優化。JSSE 是一個純粹的樹狀遍歷解釋器,沒有字節碼編譯。以下是與 Node (V8) 和 Boa 的一些微基準測試:
範圍非常大:從 1.2 倍(JSON,JSSE 實際上擊敗了 Boa)到 703 倍(陣列操作)。fib 基準測試是遞歸斐波那契,它會大量調用函數調用開銷,136 倍正是你對一個在每次調用時重新遍歷 AST 的樹狀遍歷器所期望的。陣列基準測試是最糟糕的情況,很可能觸發了陣列原型實施中的一個病態路徑。
與 Boa 相比更公平,因為兩者都是 Rust 中的解釋器。JSSE 的速度大約是 Boa 的 1.2 倍到 225 倍,具體取決於基準測試。物件操作和 JSON 實際上具有競爭力。
test262 套件本身揭示了痛點所在。所有 30 個最慢的測試都是 RegExp Unicode 屬性轉義測試(\p{...}),每個測試耗時 64-84 秒。最慢的 Script_Extensions_-_Sharada.js,達到 84 秒。這些測試編譯並運行枚舉數千個 Unicode 編碼點的正規表達式,瓶頸在於 Unicode 屬性類別的位元級 WTF-8 正規表達式匹配。在總共 198,258 個測試場景中,有 900 個測試耗時超過 10 秒,1,062 個測試耗時超過 1 秒。其餘的都很快完成。長尾幾乎完全是正規表達式。
這沒關係。正確性是唯一目標。效能是顯而易見的下一步。
Opus 4.6 完成了絕大部分繁重的工作。Haiku 用於背景任務,如子代理程式搜索。總 token 數約為 89 億,但積極的提示緩存使成本可控;僅緩存讀取 token 就有 88 億。
從另一個角度來看:相當於 API 成本 4,619 美元,換來了一個 17 萬行的 Rust 程式碼庫,該程式庫通過了 100% 的 test262 測試。這大約是每行程式碼 0.03 美元,或者每通過 1% 的 test262 合規性約 47 美元。
計畫比程式碼更重要。Claude 從規格和 test262 子模組中自行生成了整個 PLAN.md。我沒有寫它,沒有審查它,只是接受了它。而且它奏效了。但回顧過去,計畫的品質是專案速度的最大槓桿。Claude 選擇了一個合理的特性順序,但它早期做出了一些架構決策(例如如何處理生成器、如何構建 GC、如何處理 WTF-8 字串),導致後來進行了昂貴的返工。如果我前期投入時間研究正確的架構並將其納入計畫,我很有信心這個專案會花費一半的時間。教訓不是「你必須自己編寫計畫」,而是「確保計畫是好的,無論它是如何產生的」。
test262 是一個令人難以置信的回饋信號。擁有一個全面、結構良好的測試套件,代理程式可以自主運行,這使得這類專案成為可能。代理程式不需要深入理解 JavaScript 語義;它只需要讓數字上升,同時不讓它下降。test262 提供了精確的回饋信號。
代理程式會在長上下文的過程中迷失方向。這需要一些解釋。Claude Code 在接近上下文限制時(當時是 20 萬 token)會自動壓縮對話。壓縮會總結較舊的消息以釋放空間,但不可避免地會丟失細節:哪些具體的測試失敗了,正在嘗試什麼方法,錯誤消息說了什麼。在長時間運行的任務中經過幾輪壓縮後,Claude 會明顯退化。它會原地打轉,重複嘗試相同的修復。它會通過一些測試但會回歸其他測試,然後試圖修復回歸並破壞其他東西。我的解決方法是停止會話,將任務分解成更小的部分,然後重新開始。一個帶有聚焦提示(「實施 SharedArrayBuffer」)的新會話,遠比經過多次壓縮的長時間循環效果更好。最佳做法是:每次會話一個功能,規劃它,實施它,運行測試,停止。
重新架構很昂貴,但並非災難性的。有一次,Claude 意識到目前的架構不適合異步函數,不得不回去重新架構。這花費的時間比預期的要長,但最終奏效了。一個理解問題空間的人類架構師可以避免這一點,這又回到了第一點。
Rust 對於代理程式編碼很有幫助。我特意選擇 Rust,因為我相信它目前是代理程式驅動開發的最佳語言。類型系統和編譯器可以在運行時捕獲大量的錯誤,這意味著代理程式花費更少的時間除錯,而將更多時間用於取得進展。嚴格的編譯器基本上是 test262 之外的第二個回饋信號。
效能。該引擎是一個純粹的樹狀遍歷器,最容易實現的改進是字節碼編譯和一個簡單的虛擬機。僅僅通過消除重複的 AST 遍歷,就有可能獲得 10 到 100 倍的提升。除此之外,還有內聯緩存、隱藏類,如果我想認真對待,最終還有 JIT 編譯。
但說實話,JSSE 的目的從來都不是要構建一個生產級的 JavaScript 引擎。它的目的是探索當前代理程式編碼工具的可能性,並在此過程中學習代理程式的工作流程。在這兩方面,我都對結果感到滿意。
這些工具非常棒,模型也令人難以置信,但這僅僅是開始。代理程式編碼已經在改變人們的工作方式,我相信它將徹底改變軟體生產(我已經寫過我的一些想法)。在不久的將來,從頭開始編寫一個 JS 引擎並將其優化到比任何其他引擎都快,將會像散步一樣輕鬆,字面意義上就是散步的時間。我從這個實驗中學到了很多,我會繼續樂在其中。
程式碼位於 github.com/pmatos/jsse。MIT 授權。如果你想自己運行 test262,README 提供了完整的重現說明。歡迎對 JSSE 做出貢獻,但請保持其代理程式特性。