Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
4fd2a1e
Add PTY characterization harness for stale controlling-TTY topology
gorandodig Aug 15, 2026
b52da2c
Add characterization tests for execute_script
gorandodig Aug 15, 2026
b8c8902
Redirect script stdin to /dev/null
gorandodig Aug 15, 2026
c73034e
Run tests on macOS and Windows and type-check per platform
gorandodig Aug 15, 2026
0955af0
Run tests on macOS and Windows and type-check per platform
gorandodig Aug 15, 2026
5c4053e
Add PTY launcher with framed handshake
gorandodig Aug 15, 2026
b7a1bc7
Add TerminalProcess with POSIX PTY backend
gorandodig Aug 15, 2026
2442a55
Cover TerminalProcess lifecycle and failure paths
gorandodig Aug 15, 2026
5d597ec
Add VT output normalizer backed by pyte
gorandodig Aug 15, 2026
ebb611c
Answer terminal queries live from the reader
gorandodig Aug 15, 2026
c5619f3
Fix channel setup rollback and input-queue edge cases
gorandodig Aug 15, 2026
5d9f98e
Bound normalizer parser state and harden query-failure tracking
gorandodig Aug 15, 2026
283a836
Add legacy pipe backend behind TerminalProcess
gorandodig Aug 15, 2026
15ba58e
Route execute_script through TerminalProcess
gorandodig Aug 15, 2026
c01c980
Add CODEPLAIN_NO_PTY escape hatch
gorandodig Aug 15, 2026
f354470
Validate terminal semantics through execute_script
gorandodig Aug 15, 2026
36fedb8
Add macOS e2e job running Node through the installed wheel
gorandodig Aug 15, 2026
fe89b0d
Fix outcome arbitration and artifact lifecycle in execute_script
gorandodig Aug 15, 2026
6b9120c
Harden validation-suite process hygiene and coverage
gorandodig Aug 15, 2026
9d4545a
Keep console log output verbatim under color terminals
gorandodig Aug 15, 2026
f6c9571
Detach Windows children from the renderer console
gorandodig Aug 15, 2026
681cc9a
Wait for script teardown before TUI shutdown completes
gorandodig Aug 15, 2026
02926c5
Remove migration leftovers
gorandodig Aug 15, 2026
d4f8020
Ignore .idea directory
gorandodig Aug 15, 2026
2ba7e88
Fix test collection and harness teardown on CI runners
gorandodig Aug 15, 2026
1ba1196
Stop collecting the POSIX backend suite on Windows
gorandodig Aug 15, 2026
80dbe40
Make the test suite green on Windows and deflake the drain bound
gorandodig Aug 15, 2026
ee04eae
Close GitPython repos instead of skipping tests on Windows
gorandodig Aug 15, 2026
affcde8
Add the platform-neutral half of the ConPTY backend
gorandodig Aug 16, 2026
d7ac38c
Add the ConPTY backend and make it the Windows default
gorandodig Aug 16, 2026
eda6023
Report every backend pump failure through one execution-loop check
gorandodig Aug 16, 2026
fa72d18
Test the ConPTY lifecycle and backend selection on Windows
gorandodig Aug 16, 2026
824e196
Run the ConPTY lifecycle tests on windows-2022
gorandodig Aug 16, 2026
390aed3
Publish the preemption generation with the item it belongs to
gorandodig Aug 16, 2026
eb551e0
Keep the terminal session owned and its native failures explicit
gorandodig Aug 16, 2026
a9e8d9b
Assert the ConPTY invariants the earlier tests only implied
gorandodig Aug 16, 2026
5ccaae8
Close the job before anything that can block on release
gorandodig Aug 16, 2026
2d63d59
Cover the abandoned session and the post-launch native seams
gorandodig Aug 16, 2026
728c7f7
Give the ConPTY child the terminal instead of the renderer's handles
gorandodig Aug 16, 2026
9b218bd
Measure the ConPTY backend's own handles and attribute its leaks
gorandodig Aug 16, 2026
5f12ecb
Release every allocation through the owner that makes it
gorandodig Aug 16, 2026
774ac63
Make the terminal probe report its own failure
gorandodig Aug 16, 2026
22e29f0
Embed the terminal probe so it cannot arrive malformed
gorandodig Aug 16, 2026
c874ead
Share the backend surface the three terminals implemented separately
gorandodig Aug 16, 2026
90c9ab1
Remove dead code the reviews flagged
gorandodig Aug 16, 2026
40a5bf3
Let the backends own their teardown budget and no-input story
gorandodig Aug 16, 2026
acc4cba
Drive the acknowledgment window from a hook instead of a parameter
gorandodig Aug 16, 2026
78ae633
Pin the grace probe in the tick-driven teardown tests
gorandodig Aug 16, 2026
10051ed
Fix legacy pipe teardown deadlock and group escalation
gorandodig Aug 16, 2026
a2036ed
Keep transcript output visible past broken control strings
gorandodig Aug 16, 2026
ca07837
Report missing build commits before a broken tests repo can mask them
gorandodig Aug 16, 2026
4f42360
Let an observed exit win over a coincident deadline or reply failure
gorandodig Aug 16, 2026
9f182a5
Scale the render shutdown wait to a live script
gorandodig Aug 16, 2026
7c83f53
State the absent-input note per backend semantics
gorandodig Aug 16, 2026
2b2dfc2
Merge origin/main into node-subprocess-handling
gorandodig Aug 17, 2026
bc46087
fix: emulate ONLCR in the pipe backend's normalizer
gorandodig Aug 17, 2026
2bb65b4
feat: platform-test runtime capability model and REST plumbing (codep…
gorandodig Aug 17, 2026
e93f784
feat: codeplain-tty broker, helper CLI, and typed input-driver contra…
gorandodig Aug 17, 2026
71a1959
feat: scope the platform-test runtime to conformance execution (codep…
gorandodig Aug 17, 2026
7be2136
feat: final portability audit before publishing a build (codeplain-tt…
gorandodig Aug 17, 2026
c52fb97
fix: strip end-of-line whitespace from wait-for needles (codeplain-tty)
gorandodig Aug 17, 2026
d6777d1
feat: typed client handling for ConformanceTestsFixExhausted
gorandodig Aug 17, 2026
4ed6ae6
fix: expect-style sequencing for wait-for (codeplain-tty)
gorandodig Aug 17, 2026
d3c15ab
fix: report why a render failed instead of a raw payload
gorandodig Aug 18, 2026
a6fb7ba
fix: type the unit-test exhaustion outcome, and interpolate the refac…
gorandodig Aug 18, 2026
462be01
feat: instrument the fix loops — count attempts, detect a stuck loop
gorandodig Aug 18, 2026
801d892
fix: end every render's log file with what the render did
gorandodig Aug 18, 2026
3127595
fix: stop the portability audit from failing renders over their own t…
gorandodig Aug 18, 2026
b025eee
style: apply black to test_fix_loop_metrics
gorandodig Aug 18, 2026
97dfca8
fix: keep the renderer's own log out of the portability audit
gorandodig Aug 18, 2026
b4d8d7d
refactor: name the helper module plain2code_tty
gorandodig Aug 18, 2026
ac3f37e
refactor: drop the dead pre-rename marker from the audit
gorandodig Aug 18, 2026
2d08d6d
fix: complete the plain2code_tty rename
gorandodig Aug 18, 2026
7558e08
Narrate headless renders, and split the fix-loop streak per loop
gorandodig Aug 18, 2026
ff1f2fc
Regenerate the conformance test once the fix loop stops making progress
gorandodig Aug 19, 2026
598b4f5
Bound the broker's accept() so shutdown does not leak a thread per ex…
gorandodig Aug 19, 2026
f9414ce
Give up on patching when a fix loop stops making progress, in either …
gorandodig Aug 19, 2026
b198543
Re-deliver end-of-file to a driverless target that has gone quiet
gorandodig Aug 19, 2026
f27465f
Ask the fix loop differently once, before discarding its test
gorandodig Aug 19, 2026
c817b2b
Judge only the conformance loop on failures that are merely consecutive
gorandodig Aug 19, 2026
59e840e
Let a wedged functionality regenerate its conformance test more than …
gorandodig Aug 19, 2026
fdcd2f6
Give a regenerated conformance test a chance before judging it stuck
gorandodig Aug 19, 2026
ecaa0a8
End the input of a conformance target no test is driving
gorandodig Aug 19, 2026
0267db7
Remove the terminal-driving helper for generated tests
gorandodig Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: '3.11'

- run: pip install pytest

Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: '3.11'

- run: pip install pytest

Expand All @@ -61,22 +61,57 @@ jobs:
CODEPLAIN_API_KEY: ${{ secrets.CODEPLAIN_API_KEY }}
run: pytest tests/e2e/ -v --tb=short

# The installer jobs above test the published package; this one tests the wheel built
# from the checkout. It runs neither the pytest e2e collection (its POSIX fixture needs
# a Docker daemon and its other case is Windows-only) nor anything that needs the API
# key: the point is that a real toolchain runs through the terminal backend on macOS.
e2e-macos:
runs-on: macos-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # hatch-vcs derives the version from the git tags

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Build and install this checkout's wheel
run: |
python -m pip install --upgrade pip build
python -m build --wheel
pip install dist/*.whl

- name: Report the Node under test
run: node --version # macos-latest ships Node preinstalled

# Under `nohup` with stdin from /dev/null: the detached shape a renderer is started
# in, where nothing upstream of the script has a terminal to lend it.
- name: Run Node through the installed execute_script()
run: nohup python tests/e2e/macos_node_smoke.py < /dev/null

notify-on-failure:
name: Notify Slack on failure
needs: [e2e-linux, e2e-windows]
needs: [e2e-linux, e2e-windows, e2e-macos]
if: failure()
runs-on: ubuntu-latest
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_E2E_WEBHOOK_URL }}
LINUX_RESULT: ${{ needs.e2e-linux.result }}
WINDOWS_RESULT: ${{ needs.e2e-windows.result }}
MACOS_RESULT: ${{ needs.e2e-macos.result }}
steps:
- name: Determine failed platforms
id: platforms
run: |
failed=""
[ "${{ needs.e2e-linux.result }}" = "failure" ] && failed="Linux"
if [ "${{ needs.e2e-windows.result }}" = "failure" ]; then
[ -n "$failed" ] && failed="$failed and Windows" || failed="Windows"
fi
for entry in "Linux:$LINUX_RESULT" "Windows:$WINDOWS_RESULT" "macOS:$MACOS_RESULT"; do
if [ "${entry#*:}" = "failure" ]; then
name="${entry%%:*}"
if [ -n "$failed" ]; then failed="$failed and $name"; else failed="$name"; fi
fi
done
echo "failed=$failed" >> "$GITHUB_OUTPUT"

- name: Send failure notification to Slack
Expand Down
85 changes: 61 additions & 24 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,15 @@ jobs:
run: flake8 .

mypy:
name: MyPy Type Checking
name: MyPy Type Checking (${{ matrix.platform }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# --platform is a static narrowing setting, so every target is checked from
# one runner: code is verified under the platform where it runs and proven
# guarded under the platforms where it does not.
platform: [linux, darwin, win32]
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand All @@ -90,37 +97,67 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Type check with mypy
run: mypy . --check-untyped-defs
run: mypy . --check-untyped-defs --platform ${{ matrix.platform }}

tests:
name: Run Tests
runs-on: ubuntu-latest
conpty-lifecycle:
# windows-latest is Windows Server 2025 (build 26100), the build that made
# ClosePseudoConsole() non-blocking, so the teardown-ordering assertions pass there
# whether or not the ordering is correct. Windows Server 2022 is build 20348, inside the
# affected range, which is the only place those assertions prove anything.
name: ConPTY Lifecycle (windows-2022)
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Configure git for tests
run: |
git config --global user.email "test@example.com"
git config --global user.name "Test Runner"
git config --global init.defaultBranch main
- name: Cache pip
uses: actions/cache@v4
cache: pip
cache-dependency-path: requirements.txt
- name: Upgrade pip
run: python -m pip install --upgrade pip
- name: Install requirements
run: pip install -r requirements.txt
- name: Run the ConPTY lifecycle tests
run: python -m pytest tests/test_conpty.py -v

tests:
name: Run Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install coverage
python-version: ${{ env.PYTHON_VERSION }}
cache: pip
cache-dependency-path: requirements.txt
# Every step below runs a single command. Windows runners default to
# PowerShell, where a failing native command does not abort the remaining
# commands of a multi-command run block; one command per step makes a
# failure fail the job on every OS without shell-specific workarounds.
- name: Configure git user email
run: git config --global user.email "test@example.com"
- name: Configure git user name
run: git config --global user.name "Test Runner"
- name: Configure git default branch
run: git config --global init.defaultBranch main
- name: Upgrade pip
run: python -m pip install --upgrade pip
- name: Install requirements
run: pip install -r requirements.txt
- name: Install coverage
run: pip install coverage
- name: Run tests with coverage
run: |
export $(cat .env.dev.example | xargs)
coverage run -m pytest tests/ -v
coverage xml
coverage report
run: coverage run -m pytest tests/ -v
- name: Generate coverage XML
run: coverage xml
- name: Show coverage report
run: coverage report
- name: Upload coverage reports
if: matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ dist

.coverage
logging_config.yaml
/.idea/
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ git push origin subtree/standard-template-library
```

### Windows Support
Windows users must use WSL (Windows Subsystem for Linux). The codebase has some platform-specific script handling (`.ps1` for Windows, `.sh` for Unix).
Native Windows is supported and tested in CI. The codebase has platform-specific script handling (`.ps1` for Windows, `.sh` for Unix), and scripts run on a ConPTY-backed terminal with a Job Object containing their process tree (`render_machine/_conpty.py`), which needs Windows 10 build 17763 (1809) or newer. WSL works too, and is then an ordinary Linux host.

### CRITICAL: No User-Specific Paths in Version Control
**Never commit files containing user-specific absolute paths** (e.g., `/Users/username/...`, `/home/username/...`, `C:\Users\...`) to version-controlled files like:
Expand Down
53 changes: 52 additions & 1 deletion cli_output/render_summary.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
"""Render completion summary display."""

import logging
from typing import Optional

import plain2code_logger
from plain2code_console import console
from plain2code_state import RunState
from usage_summary import format_usage_summary

logger = logging.getLogger(plain2code_logger.LOGGER_NAME)

# Marks the last line of a render's log file. Greppable on purpose: benchmark runs and
# support artifacts are read by tooling before they are read by a person.
RENDER_TRAILER_PREFIX = "[render-trailer]"


def print_exit_summary(
run_state: RunState,
Expand All @@ -27,6 +35,49 @@ def print_exit_summary(
msg += format_usage_summary(run_state.rendered_functionalities, run_state.render_time_accumulated) + "\n"
console.print(msg)

if not run_state.render_succeeded and error_message:
# Reported whenever there is one. A render can finish its functionalities and still
# raise on the way out — publishing the build, for instance — and that combination
# used to print the success banner and swallow the reason entirely, leaving a caller
# with a tick mark and a non-zero exit code.
if error_message:
console.error(error_message)
console.quiet = True

log_render_trailer(run_state, spec_filename, error_message)


def log_render_trailer(
run_state: RunState,
spec_filename: str,
error_message: Optional[str] = None,
) -> None:
"""Writes the render's outcome to the log file, as its last line.

The summary above reaches the terminal through Rich, which never touches logging, so
a captured `codeplain.log` used to stop at whatever happened to be logged last —
indistinguishable from a process that died silently. This ends every log with what
the render did, and because it is written on every exit path a log *without* a
trailer is itself evidence that the file was truncated.
"""
if run_state.render_succeeded:
outcome = "completed"
elif run_state.render_cancelled:
outcome = "cancelled"
else:
outcome = "failed"

logger.info(
f"{RENDER_TRAILER_PREFIX} outcome={outcome} "
f"render_id={run_state.render_id} "
f"functionalities={run_state.rendered_functionalities} "
f"render_time_s={run_state.render_time_accumulated} "
f"generated_code={run_state.render_generated_code_path or '-'} "
f"spec={spec_filename}"
)
if error_message:
logger.error(f"{RENDER_TRAILER_PREFIX} error={error_message}")

# The process may exit immediately after this; an unflushed trailer would defeat the
# purpose of writing one.
for handler in logger.handlers:
handler.flush()
7 changes: 7 additions & 0 deletions codeplain_REST_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ERROR_CODE_EXCEPTIONS = {
"FunctionalRequirementTooComplex": plain2code_exceptions.FunctionalRequirementTooComplex,
"ConflictingRequirements": plain2code_exceptions.ConflictingRequirements,
"ConformanceTestsFixExhausted": plain2code_exceptions.ConformanceTestsFixExhausted,
"RenderingCreditBalanceTooLow": plain2code_exceptions.RenderingCreditBalanceTooLow,
"LLMInternalError": plain2code_exceptions.LLMInternalError,
"MissingResource": plain2code_exceptions.MissingResource,
Expand Down Expand Up @@ -382,6 +383,7 @@ def fix_conformance_tests_issue(
current_testing_frid_high_level_implementation_plan: Optional[str],
conflicting_requirements_count: int,
run_state: RunState,
stalled_reason: Optional[str] = None,
):
endpoint_url = f"{self.api_url}/fix_conformance_tests_issue"
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
Expand All @@ -408,6 +410,11 @@ def fix_conformance_tests_issue(
if acceptance_tests is not None:
payload["acceptance_tests"] = acceptance_tests

# Sent only once the loop has stopped moving. Omitted otherwise, so an
# ordinary fix request is byte-identical to what it was.
if stalled_reason is not None:
payload["stalled_reason"] = stalled_reason

return self.post_request(endpoint_url, headers, payload, run_state)

def render_acceptance_tests(
Expand Down
Loading
Loading