diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..a2d10723
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+scripts/windows-run-gradle-capped.ps1 text eol=crlf
+scripts/windows-run-slice.ps1 text eol=crlf
diff --git a/.github/workflows/android-verify.yml b/.github/workflows/android-verify.yml
new file mode 100644
index 00000000..b0359d99
--- /dev/null
+++ b/.github/workflows/android-verify.yml
@@ -0,0 +1,136 @@
+# Android embedding verify: assembles the android/ AAR subproject on the same
+# self-hosted linux runner as dex-verify.yml (dsh-jvm-1). Zero local-gradle
+# policy for the feature lanes — CI is the verifier, so this workflow is the
+# gate that keeps android/ compiling against AGP + the pinned deps.
+# Modeled on dex-verify.yml: workflow-level toolchain env, scoped daemon
+# cleanup, non-fatal artifact uploads, and the chmod +x ./gradlew lesson.
+name: android-verify
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [windows-verify, linux-verify]
+
+# Machine-specific toolchain locations for the self-hosted Linux runner.
+# Kept at workflow level so the machine owner edits ONE place here.
+# AGP 8.5.x requires the Gradle daemon to run on JDK 17 — hence temurin-17,
+# while the Kotlin toolchain (jvmTarget 11) is satisfied by the compile.
+env:
+ JAVA_HOME: /home/dingyi/.jdks/temurin-17
+ ANDROID_HOME: /home/dingyi/Android/Sdk
+
+jobs:
+ android-assemble:
+ runs-on: [self-hosted, linux, x64]
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ # Convert the platform android.jar (Java bytecode stubs) to dex so the
+ # sample app can load it through DexClassLoader on-device — the reflection
+ # pipeline then works unchanged, with no desktop JDK required.
+ - name: Convert android.jar to android.dex
+ run: |
+ set -euxo pipefail
+ export PATH="$JAVA_HOME/bin:$PATH"
+ D8=$(ls "$ANDROID_HOME"/build-tools/*/d8 | sort | tail -1)
+ echo "d8: $D8"
+ mkdir -p android/sample/src/main/assets
+ "$D8" --release --min-api 26 --lib "$ANDROID_HOME/platforms/android-35/android.jar" \
+ --output android/sample/src/main/assets \
+ "$ANDROID_HOME/platforms/android-35/android.jar"
+ # d8 names its output classes.dex regardless of input name
+ mv android/sample/src/main/assets/classes.dex android/sample/src/main/assets/android.dex
+ ls -la android/sample/src/main/assets/android.dex
+
+ - name: Show environment
+ run: |
+ export PATH="$JAVA_HOME/bin:$PATH"
+ echo "JAVA_HOME=$JAVA_HOME"
+ java -version
+ echo "ANDROID_HOME=$ANDROID_HOME"
+ git rev-parse --short HEAD
+
+ # ANDROID_HOME is pre-set on the runner; sdkmanager may or may not be on
+ # PATH depending on the cmdline-tools layout — warn, never fail here.
+ # Missing platform/build-tools are auto-installed by AGP iff
+ # SDK licenses are already accepted on this machine.
+ - name: Verify Android SDK
+ run: |
+ echo "ANDROID_HOME=$ANDROID_HOME"
+ # The runner has no cmdline-tools (no sdkmanager), so AGP's own
+ # auto-installer is the only installer. It refuses to run unless the
+ # license files exist — write the standard license hashes directly
+ # (these are the well-known SHA1s of the SDK license texts, same on
+ # every machine; sdkmanager itself just writes these files).
+ mkdir -p "$ANDROID_HOME/licenses"
+ printf "24333f8a63b6825ea9c5514f83c2829b004d1fee\n" > "$ANDROID_HOME/licenses/android-sdk-license"
+ printf "84831b9409646a918e30573bab4c9c91346d8abd\n" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
+ echo "license files written:"
+ ls "$ANDROID_HOME/licenses"
+ if [ -d "$ANDROID_HOME/platforms" ]; then
+ ls "$ANDROID_HOME/platforms" | sed 's/^/platform: /'
+ else
+ echo "WARN: no platforms directory under ANDROID_HOME"
+ fi
+ if [ -d "$ANDROID_HOME/build-tools" ]; then
+ ls "$ANDROID_HOME/build-tools" | sed 's/^/build-tools: /'
+ else
+ echo "WARN: no build-tools directory under ANDROID_HOME"
+ fi
+
+ # assembleRelease needs NO signing config for a library (AARs are never
+ # signed). Builds only :android to keep the run scoped and fast.
+ - name: Assemble android AAR and fat jar
+ run: |
+ export PATH="$JAVA_HOME/bin:$PATH"
+ chmod +x ./gradlew
+ ./gradlew :android:assembleRelease :android:androidReleaseJar --parallel
+
+ # Also builds the sample app (includes sample app APK, android/sample).
+ # Activates when the sample-app lane lands: :android:sample is not yet in
+ # settings.gradle.kts, so this step fails (intentionally, no always-pass
+ # guard) until that lane adds android/sample/build.gradle.kts + the
+ # settings include. Kept plain so it is a real gate, not a no-op.
+ - name: Assemble sample app (includes sample app)
+ run: |
+ export PATH="$JAVA_HOME/bin:$PATH"
+ chmod +x ./gradlew
+ ./gradlew :android:sample:assembleDebug --parallel
+
+ - name: Stop Gradle daemons
+ if: always()
+ # Scoped cleanup: `gradlew --stop` stops only the daemons registered
+ # under this Gradle user home/version. Never a blanket java kill on a
+ # shared self-hosted runner.
+ run: |
+ export PATH="$JAVA_HOME/bin:$PATH"
+ ./gradlew --stop || true
+
+ - name: Upload release AAR
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: android-release-aar
+ path: android/build/outputs/aar/*.aar
+ if-no-files-found: warn
+
+ # Fat classes jar for consumers running their own dex step (the
+ # monaco-lsp-demo classes.dex pipeline). Non-fatal: the AAR above is the
+ # recommended artifact.
+ - name: Upload fat classes jar
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: android-release-fat-jar
+ path: android/build/libs/*.jar
+ if-no-files-found: warn
+
+ - name: Upload sample APK
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: androlua-sample-apk
+ path: android/sample/build/outputs/apk/debug/*.apk
+ if-no-files-found: warn
diff --git a/.github/workflows/dex-verify.yml b/.github/workflows/dex-verify.yml
new file mode 100644
index 00000000..25945cf8
--- /dev/null
+++ b/.github/workflows/dex-verify.yml
@@ -0,0 +1,91 @@
+# Linux self-hosted dex-slice runner: ONLY the dex-relevant jvmTest slices,
+# kept fast (the full suite stays owned by the windows-full-jvmtest workflow).
+# Modeled on windows-jvmtest.yml: self-hosted runner, workflow-level toolchain
+# env, scoped daemon cleanup, non-fatal artifact uploads.
+name: dex-jvmtest
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [windows-verify, linux-verify]
+
+# Machine-specific toolchain locations for the self-hosted Linux runner.
+# Kept at workflow level so the machine owner edits ONE place here.
+env:
+ JAVA_HOME: /home/dingyi/.jdks/temurin-17
+ ANDROID_HOME: /home/dingyi/Android/Sdk
+
+jobs:
+ dex-jvmtest:
+ runs-on: [self-hosted, linux, x64]
+ timeout-minutes: 45
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ # The dex slices include the real-fixture test against the demo corpus
+ # (DexRealFixtureTddTest walks up from user.dir to this exact path); the
+ # corpus is gitignored, so absence is informational (the fixture test
+ # soft-skips via JUnit Assume) — never a workflow failure.
+ - name: Verify dex fixture exists
+ run: |
+ if [ -f tools/monaco-lsp-demo/workspace/libs/classes.dex ]; then
+ echo "fixture-present"
+ else
+ echo "fixture-absent (gitignored demo corpus) — DexRealFixtureTddTest soft-skips"
+ fi
+
+ - name: Show environment
+ run: |
+ export PATH="$JAVA_HOME/bin:$PATH"
+ echo "JAVA_HOME=$JAVA_HOME"
+ java -version
+ echo "ANDROID_HOME=$ANDROID_HOME"
+ git rev-parse --short HEAD
+ echo "mode=dex-slices gradle --parallel"
+
+ # Dex-relevant slices only. Filters verified against real files:
+ # interop.dex.DexParserCommonTddTest
+ # -> src/commonTest/kotlin/interop/dex/DexParserCommonTddTest.kt
+ # interop.jvm.DexMountingTddTest
+ # -> src/jvmTest/kotlin/interop/jvm/DexMountingTddTest.kt
+ # interop.jvm.DexRealFixtureTddTest
+ # -> src/jvmTest/kotlin/interop/jvm/DexRealFixtureTddTest.kt
+ # io.github.dingyi222666.luaparser.interop.jvm.DexClassModelAdapterTest
+ # -> src/commonTest/kotlin/interop/jvm/DexClassModelAdapterTest.kt
+ # lsp.RuntimeJarTempLeakTddTest
+ # -> src/jvmTest/kotlin/lsp/RuntimeJarTempLeakTddTest.kt
+ - name: Run dex test slices
+ run: |
+ export PATH="$JAVA_HOME/bin:$PATH"
+ chmod +x ./gradlew
+ ./gradlew jvmTest --parallel \
+ --tests "interop.dex.*" \
+ --tests "interop.jvm.Dex*" \
+ --tests "io.github.dingyi222666.luaparser.interop.jvm.DexClassModelAdapterTest" \
+ --tests "lsp.RuntimeJarTempLeakTddTest"
+
+ - name: Stop Gradle daemons
+ if: always()
+ # Scoped cleanup: `gradlew --stop` stops only the daemons registered
+ # under this Gradle user home/version. Never a blanket java kill on a
+ # shared self-hosted runner.
+ run: |
+ export PATH="$JAVA_HOME/bin:$PATH"
+ ./gradlew --stop || true
+
+ - name: Upload JUnit XML
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: dex-junit-xml
+ path: build/test-results/jvmTest/**
+ if-no-files-found: warn
+
+ - name: Upload HTML report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: dex-test-report-html
+ path: build/reports/tests/jvmTest/**
+ if-no-files-found: warn
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 00000000..68db3516
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,51 @@
+# MANUAL release publishing to Maven Central.
+#
+# This workflow never runs automatically: it is workflow_dispatch only
+# ("Run workflow" button). Nothing publishes on push or tag.
+#
+# VERSION BUMP GUARD — read before every run:
+# Maven Central forbids re-uploading an existing (group, artifact, version):
+# a failed or partial upload permanently burns that version. Before EVERY
+# publish, bump `publishVersion` in build.gradle.kts (the single source of
+# truth for the published coordinates) and commit the bump. If the version
+# about to be published was already uploaded — even partially — STOP and
+# bump again instead of retrying.
+#
+# SECRETS (placeholders only — no real values live in this file). Create them
+# in Repo Settings > Secrets and variables > Actions:
+# MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD : Central (S01) portal token
+# SIGNING_KEY : ASCII-armored private signing key (keep headers/newlines)
+# SIGNING_PASSWORD : passphrase of the signing key
+# They are exposed to Gradle as project properties via the ORG_GRADLE_PROJECT_
+# prefix, which is how the com.vanniktech.maven.publish plugin (configured in
+# build.gradle.kts: SonatypeHost.S01 + in-memory signing) picks them up.
+name: publish
+
+on:
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 17
+
+ # Caches ~/.gradle (dependencies, wrapper, build cache) between runs.
+ - name: Set up Gradle caching
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Publish to Maven Central
+ env:
+ ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
+ ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
+ ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }}
+ ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
+ run: ./gradlew publishToMavenCentral --no-daemon --stacktrace
diff --git a/.github/workflows/windows-full-jvmtest.yml b/.github/workflows/windows-full-jvmtest.yml
new file mode 100644
index 00000000..5a631950
--- /dev/null
+++ b/.github/workflows/windows-full-jvmtest.yml
@@ -0,0 +1,96 @@
+# Windows self-hosted FULL jvmTest (tip-coherent, TASK-043 evidence).
+# Does NOT use slice progress bar. CPU hard-capped (affinity 0x3F, workers=5).
+# Branch: windows-verify only. Never main.
+name: windows-full-jvmtest
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [windows-verify]
+ paths:
+ - '.github/workflows/windows-full-jvmtest.yml'
+ - 'scripts/windows-run-full-jvmtest.ps1'
+ - 'scripts/windows-run-gradle-capped.ps1'
+ - 'scripts/windows-ensure-android-jar.ps1'
+
+# Machine-specific toolchain locations for the self-hosted Windows runner.
+# Kept at workflow level so the machine owner edits ONE place here instead of
+# hunting through job/step blocks. Same values the job-level env used before.
+env:
+ JAVA_HOME: C:\Users\dingyi\.jdks\temurin-17.0.11
+ ANDROID_HOME: C:\Users\dingyi\AppData\Local\Android\Sdk
+
+jobs:
+ full-jvmtest:
+ runs-on: [self-hosted, Windows, X64]
+ # Full suite can exceed slice timeouts; keep headroom under CPU cap.
+ timeout-minutes: 360
+ env:
+ GRADLE_OPTS: -Dorg.gradle.workers.max=5
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Show environment
+ shell: pwsh
+ run: |
+ $env:Path = "$env:JAVA_HOME\bin;$env:Path"
+ Write-Host "JAVA_HOME=$env:JAVA_HOME"
+ & "$env:JAVA_HOME\bin\java.exe" -version
+ Write-Host "processor_count=$env:NUMBER_OF_PROCESSORS"
+ git rev-parse --short HEAD
+ Write-Host "mode=full-jvmtest cpu_hard_cap=affinity_0x3F workers=5"
+
+ # Arm 174 soft-skips that need reflective android.jar (SDK platforms/android-35).
+ # Idempotent: no-op when jar already present. Never invents G:/.
+ - name: Ensure android.jar (platforms/android-35)
+ shell: pwsh
+ run: |
+ $env:Path = "$env:JAVA_HOME\bin;$env:Path"
+ ./scripts/windows-ensure-android-jar.ps1
+
+ - name: Full jvmTest (CPU hard-capped)
+ shell: pwsh
+ run: |
+ $env:Path = "$env:JAVA_HOME\bin;$env:Path"
+ ./scripts/windows-run-full-jvmtest.ps1
+
+ - name: Stop Gradle daemons
+ if: always()
+ shell: cmd
+ # Cleanup is deliberately scoped: `gradlew --stop` stops only the Gradle
+ # daemons registered under this Gradle user home/version. Do NOT add a
+ # blanket `taskkill /F /IM java.exe` here — on a shared self-hosted runner
+ # that kills every Java process machine-wide (IDEs, other runs' daemons,
+ # Kotlin compile daemons of concurrent jobs). Orphaned Kotlin compile
+ # daemons exit with the Gradle daemon or time out on their own.
+ run: |
+ set "PATH=%JAVA_HOME%\bin;%PATH%"
+ call gradlew.bat --stop
+ exit /b 0
+
+ - name: Upload JUnit XML
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: full-junit-xml
+ path: build/test-results/jvmTest/**
+ if-no-files-found: warn
+
+ - name: Upload full-jvmtest result
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: full-jvmtest-result
+ path: |
+ full-jvmtest-result.json
+ tasks/agent-runs/full-jvmtest/**
+ if-no-files-found: warn
+
+ - name: Upload HTML report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: full-test-report-html
+ path: build/reports/tests/jvmTest/**
+ if-no-files-found: warn
diff --git a/.github/workflows/windows-jvmtest.yml b/.github/workflows/windows-jvmtest.yml
new file mode 100644
index 00000000..1130e4ba
--- /dev/null
+++ b/.github/workflows/windows-jvmtest.yml
@@ -0,0 +1,113 @@
+# Windows self-hosted slice runner (progress-bar mode).
+# One small --tests group per run. Does NOT run full jvmTest.
+# GATE: must-green-to-advance — red slice blocks next until fixed.
+# Progress: tasks/agent-runs/win-slices/progress.json
+name: windows-jvmtest
+
+on:
+ workflow_dispatch:
+ inputs:
+ slice:
+ description: 'Slice id from slices.json (empty = next non-success)'
+ required: false
+ default: ''
+ push:
+ branches: [windows-verify]
+
+# Machine-specific toolchain locations for the self-hosted Windows runner.
+# Kept at workflow level so the machine owner edits ONE place here instead of
+# hunting through job/step blocks. Same values the job-level env used before.
+env:
+ JAVA_HOME: C:\Users\dingyi\.jdks\temurin-17.0.11
+ ANDROID_HOME: C:\Users\dingyi\AppData\Local\Android\Sdk
+
+jobs:
+ jvm-slice:
+ runs-on: [self-hosted, Windows, X64]
+ timeout-minutes: 45
+ env:
+ GRADLE_OPTS: -Dorg.gradle.workers.max=5
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Show environment
+ shell: pwsh
+ run: |
+ $env:Path = "$env:JAVA_HOME\bin;$env:Path"
+ Write-Host "JAVA_HOME=$env:JAVA_HOME"
+ & "$env:JAVA_HOME\bin\java.exe" -version
+ Write-Host "processor_count=$env:NUMBER_OF_PROCESSORS"
+ git rev-parse --short HEAD
+ Write-Host "mode=slice-progress cpu_hard_cap=affinity_0x3F workers=5"
+ if (Test-Path tasks/agent-runs/win-slices/progress.json) {
+ Get-Content tasks/agent-runs/win-slices/progress.json | Select-Object -First 40
+ }
+
+ # Arm reflective Android soft-skips when host SDK platform jar is missing.
+ - name: Ensure android.jar (platforms/android-35)
+ shell: pwsh
+ run: |
+ $env:Path = "$env:JAVA_HOME\bin;$env:Path"
+ ./scripts/windows-ensure-android-jar.ps1
+
+ - name: Compile test sources (CPU hard-capped)
+ shell: pwsh
+ run: |
+ $env:Path = "$env:JAVA_HOME\bin;$env:Path"
+ ./scripts/windows-run-gradle-capped.ps1 compileTestKotlinJvm --parallel --max-workers=5
+
+ - name: Run next test slice (CPU hard-capped)
+ shell: pwsh
+ env:
+ SLICE_INPUT: ${{ github.event.inputs.slice }}
+ run: |
+ $env:Path = "$env:JAVA_HOME\bin;$env:Path"
+ $slice = $env:SLICE_INPUT
+ if ([string]::IsNullOrWhiteSpace($slice)) {
+ ./scripts/windows-run-slice.ps1
+ } else {
+ ./scripts/windows-run-slice.ps1 -Slice $slice
+ }
+
+ - name: Stop Gradle daemons
+ if: always()
+ shell: cmd
+ # Cleanup is deliberately scoped: `gradlew --stop` stops only the Gradle
+ # daemons registered under this Gradle user home/version. Do NOT add a
+ # blanket `taskkill /F /IM java.exe` here — on a shared self-hosted runner
+ # that kills every Java process machine-wide (IDEs, other runs' daemons,
+ # Kotlin compile daemons of concurrent jobs). Orphaned Kotlin compile
+ # daemons exit with the Gradle daemon or time out on their own.
+ run: |
+ set "PATH=%JAVA_HOME%\bin;%PATH%"
+ call gradlew.bat --stop
+ exit /b 0
+
+ - name: Upload JUnit XML
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: junit-xml
+ path: build/test-results/jvmTest/**
+ if-no-files-found: warn
+
+ - name: Upload slice progress
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: slice-progress
+ path: |
+ slice-result.json
+ slice-progress.json
+ tasks/agent-runs/win-slices/progress.json
+ tasks/agent-runs/win-slices/results/**
+ if-no-files-found: warn
+
+ - name: Upload HTML report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-report-html
+ path: build/reports/tests/jvmTest/**
+ if-no-files-found: warn
diff --git a/.gitignore b/.gitignore
index e9b3fac3..2638fec3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,26 @@ src/main/kotlin-antlr
.kotlin
/kotlin-js-store/yarn.lock
.idea/artifacts
+
+locks
+tasks
+
+### Agent session state ###
+.claude/
+scripts/watch-agent-activity.js
+*_HANDOFF*.md
+
+### Node / demo tooling ###
+node_modules/
+tools/monaco-lsp-demo/.lsp-classpath
+
+### Local build shims and crash dumps ###
+gradlew.lf
+gradlew.unix
+gradlew.unix.tmp
+sh.exe.stackdump
+/org/
+tools/monaco-lsp-demo/workspace/libs/
+tools/monaco-lsp-demo/workspace/image/
+tools/monaco-lsp-demo/workspace/.using
+.idea/
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..1bec35e5
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..79ee123c
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 89022a78..45689754 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -5,6 +5,7 @@