CX+AI

CX+AI Tools — forge and the screen

The two programs this project builds and runs itself with

The tools: forge and the screen

Two working programs this project runs on itself. forge builds the CX+AI website. screen runs other programs and says honestly what became of them. Neither was written to be published, which is the only reason they are worth reading. screen ships as source; forge does not, yet. forge takes the layout of this project's own repository as a set of fixed paths compiled into it, so it would arrive as a program that builds exactly one website and no other. It is documented here because the two are used together and the contract below is already true of it — but it is not in the download until it can be pointed at a tree it was not written for.

Everything below was checked against the source that ships beside it. Where a tool refuses to do something, this page says so plainly rather than leaving you to find out.

If you are pointing an AI assistant at these tools, skip to section 7 — it is written to be copied whole into a conversation.


1. What these are, and what they are not

forge.cxscreen.cx + screen.cxi
what it doesassembles a static websitestarts programs, watches them, reports their state
how you run itone command, then it exitsa command or a loopback HTTP daemon
shapeone file, five verbsa door (screen.cx) over a module (screen.cxi)
written forthis project's own siteanything that has to supervise a child process

They are not a framework and they are not general-purpose products. forge in particular reads and writes fixed, repository-relative paths — it knows where this project keeps its pages, its facts file and its version files, and those locations are compiled into it. Pointed at some other tree it will do nothing useful until those paths are changed. That is a deliberate trade: the tool is small because it does not carry a configuration system nobody but this project would use.

screen makes no such assumption. It takes a command line and a working directory, and is usable as it stands.

Why publish any of it. The site claims it is written, checked and served by CX+AI programs, and that claim is worth little while the programs cannot be read. They are also general-purpose proof with no game in sight: file IO, strings, JSON, HTML assembly, a validator, a word-boundary text sweep, sockets, an HTTP server, and process control.


2. Building them

Both are ordinary CX+AI programs. Compile and run in one step:

cx forge.cx --run -- <verb> ...
cx screen.cx --run -- <command> ...

or build once and run the binary many times:

cx forge.cx --build -o forge
./forge <verb> ...

forge is a single file with no includes. screen.cx includes screen.cxi from the same directory, so keep the two together.

Both are native-only, and the compiler says so

Neither program builds for the register VM. Ask for it and you get a clean compile error rather than a broken binary:

$ cx screen.cx --build -P pcode=risc
error: CX-E1013: 'timestamp' is native-only -- not available on the register VM
       (-P pcode=risc); use the native backend, or _C{} for raw C

Measured against the shipped compiler:

programwhat it needs that the register VM does not have
forge.cxnow
screen.cxtimestamp, fappend

The native backend is the default, so this only bites if you asked for the VM on purpose. Nothing here needs a workaround — it needs the default.

One toolchain refuses the screen entirely

screen will not work when built with tcc on Windows. It refuses at the first process call, through the missing-capability rail:

CX-E5037: ... a process model ...

The reason is specific and worth knowing. tcc's bundled Windows import libraries do not carry the Job Object API, so CreateJobObject, AssignProcessToJobObject and TerminateJobObject are undefined at link. The alternative would have been to fall back to TerminateProcess, which kills one process and leaves its children running — a session reported as KILLED with survivors, which is exactly the lie this family exists to end. So it refuses instead. Build the screen with gcc, clang or MSVC.

The same refusal applies to a browser build: a tab has no process model, and an absence is reported as an absence rather than as a failed attempt.


3. forge — the five verbs

Run it with no arguments and it prints its own usage. A mistyped verb or a missing argument is always a refusal with a non-zero exit, never a default — a build step that guessed would be the one place a typo published something.

forge page   fill <src> <out> <repo-root>   |  copy <src> <out>
forge ticker --dispatch|--ship|--check <repo-root>
forge docs   <repo-root> [--out DIR] [--check] [--quiet]
forge assets <repo-root> [--check] [--quiet]
forge site   gen|check|stage <repo-root>    |  restore <repo-root> <stamp>

page — render a page

fill reads an HTML source, replaces every double-brace marker in it from the project's facts file and version files, and writes the result. (This page spells the marker in words rather than showing one: a published page carrying a real marker is the exact shape a site check hunts for, and it found this sentence.) copy carries a page through byte for byte, for pages that have no markers but must still travel the same pipeline — one path in, one path out, one place that can fail.

ticker — the front page's live notice

--dispatch and --ship publish a one-sentence notice; --check validates the published payload and writes nothing.

In your hands this verb will refuse, by design. It runs a word sweep over every line before publishing, and the sweep's word lists are not published — a sweep that refuses internal names is a list of those names. Without that file the verb fails closed: it names the file it could not find and publishes nothing. It never sweeps with an empty list, because an empty list passes everything.

That is the intended behaviour, not a broken build. The other four verbs do not need it.

docs — assemble the documentation door

Builds an index page over the project's generated manuals, plus a sitemap and a robots.txt. --check does all the work and writes nothing, exiting non-zero if what is on disk differs from what would be written — so a freshness test asks the generator rather than keeping a second copy of its logic.

assets — derive the site's pictures

Crops and resizes source renders into the sizes the pages use. --check again means "all the work, no write".

site — the site's word table

gen generates the renderer's word table from a specification; check refuses a definition that must not reach a visitor; stage keeps a stamped copy of every definition and restore puts one back.

Exit codes

codemeaning
0did the work
1fatal — the message says what, and names the verb that spoke it
2usage — unknown verb, wrong argument count, or a flag on a mode that does not take it
3--check found drift; nothing was written

Every diagnostic names the verb that produced it, so a message in a build log is attributable without guessing.


4. The screen — five states, and none of them guessed

The screen answers one question about a program it started, at any moment, with exactly one of five answers:

statewhat it means
NEVER-STARTEDthe OS refused to start it — carries the reason it gave
RUNNINGalive — carries the pid and when it started
EXITEDfinished on its own — carries the return code and when
KILLEDstopped by someone — carries by whom, and when
UNKNOWNa session from an earlier run of the daemon; its child is no longer ours

Every one of those comes from parentage, not from inference. The screen holds the process handle and asks the operating system about it — WaitForSingleObject on Windows, waitpid(WNOHANG) on POSIX. Nothing in it reads liveness out of output volume, log size, or a process table. A probe that cannot answer "still running" has its answer fixed before it runs.

UNKNOWN exists because the honest answer to "what happened to a process my predecessor started" is that we do not know. It is a real state, not a tidy default.

Using the module directly

screen.cxi is a module, and the caller owns the screen. There are no module globals, so two screens can exist at once and neither is a hidden singleton. The screen is a JSON document rather than a set of parallel lists because the daemon's answer is the state: there is no conversion step between what the screen knows and what it says, so the two cannot disagree.

The functions a caller uses, with their real signatures:

function scrOpen.i(json S, want.s)open a screen; "" takes the default history root
function scrStart.i(json S, name.s, cmd.s, cwd.s)start a program; returns the session id
function scrPoll.v(json S)drain output and refresh every RUNNING session's state
function scrState.s(json S, id.i)one of the five states, as text
function scrLive.i(json S)how many sessions are still RUNNING
function scrTail.s(json S, id.i, fromSeq.i)output since a sequence number
function scrSend.i(json S, id.i, text.s)write to the child's standard input
function scrKill.i(json S, id.i, who.s)stop it, recording who did
function scrFrames.i(json S, ms.i)arm capture for sessions started after this call
json S;
scrOpen(S, "");                                       // "" -> the default root
id.i = scrStart(S, "build", "cx prog.cx --run", ".");
while (scrLive(S) > 0) { scrPoll(S); delay(50); }
println(scrState(S, id));                             // EXITED

Session history is written under a directory of its own, named by CX_SCREEN_DIR. The default sits under the system temp directory, so nothing the screen writes lands inside your source tree.

screen run — one program, to completion

cx screen.cx --run -- run <program> [args...]

Runs one program under a screen, streams what it says, and reports which of the five states it ended in. It exits with the program's own exit code, so a script can use it as a drop-in runner.

There is a 60-second deadline. Set CX_SCREEN_TIMEOUT_MS to change it. A run that hits the deadline is killed and reported as KILLED — it is not left to hang, and it does not quietly return success.

screen serve — the daemon

cx screen.cx --run -- serve --port N [--root DIR] [--token-file F] [--frames [ms]]

It speaks HTTP and answers JSON:

routemethodwhat it does
/healthGETthe daemon is up
/sessionsGETevery session and its state
/session/<id>GETone session's record
/tail/<id>?from=GEToutput since an offset
/side/<id>GETthe session's structured side-channel records
/frame/<id>[?req=N]GETa captured PNG of what the program drew
/startPOSTstart a program; the body carries the command
/kill/<id>POSTstop it
/send/<id>POSTwrite to its standard input

An unknown route is a 404 naming the problem. An unknown session is a 404 saying so.

The safety, which is all refusals

A daemon that starts programs is a remote shell if you get this wrong, so it is worth stating exactly what stops that:

  1. Loopback only, by construction. It binds through the loopback-only call. The differently-spelled call that reaches other machines does not appear in the source at all — a flag can be passed by accident, a distinct function name cannot be typed by accident.
  2. The token is never in the source. It comes from CX_SCREEN_TOKEN, or from a file named with --token-file, and from nowhere else. A default would be a published password.
  3. No token, no daemon. Not a warning and not an open mode: it refuses to start, and says why.
  4. **A wrong token is refused and logged. Requests carry X-Screen-Token; a mismatch is a 401 and** a line in the log. A silent 401 tells an operator nothing, and the refusals are the interesting lines in a screen's log.

These are exercised by a test rather than described in a comment — "it would refuse" is a claim.

Frames

--frames arms capture for every session the daemon starts, so a caller can see what a CX+AI program actually drew. The default is one frame per second; the floor is 100 ms, and below it the daemon refuses to start. Capture costs real time — about 19 ms at 640×480 — so it is off unless asked for.

The child captures its own window. There is no portable way for a parent to capture another process's window, and every non-portable way is an OS window-capture API this project keeps out of the language.


5. Environment

variabletoolwhat it does
CX_SCREEN_TOKENscreenthe daemon's token. No token, no daemon.
CX_SCREEN_DIRscreenwhere session history lives. Defaults under temp.
CX_SCREEN_TIMEOUT_MSscreenthe run deadline, in milliseconds. Defaults to 60000.

forge reads no environment variables. Everything it needs is an argument, or a compiled-in repository-relative path.


6. Honest limits, collected


7. A prompt for an AI driving these tools

Copy the block below into a conversation with an AI assistant that is going to run these tools. It is deliberately complete, and deliberately negative in places: most of the ways an assistant gets this wrong are assumptions it brought with it, not facts it was missing.

You are driving two CX+AI command-line programs: `forge` and `screen`. Read
these constraints before running anything.

BUILDING
- Both are CX+AI source. Run with `cx <file>.cx --run -- <args>`, or build first
  with `cx <file>.cx --build -o <name>`.
- Both are NATIVE-ONLY. Do not pass `-P pcode=risc`; it is a compile error, not
  a slow path. forge needs `now`; screen needs `timestamp` and `fappend`.
- screen.cx includes screen.cxi from the same directory. Keep them together.
- Do not build screen with tcc on Windows. It refuses at the first process call
  with CX-E5037, because tcc's Windows import libraries lack the Job Object API.

FORGE -- five verbs, all exiting non-zero on a mistake
  forge page   fill <src> <out> <repo-root>   |  copy <src> <out>
  forge ticker --dispatch|--ship|--check <repo-root>
  forge docs   <repo-root> [--out DIR] [--check] [--quiet]
  forge assets <repo-root> [--check] [--quiet]
  forge site   gen|check|stage <repo-root>    |  restore <repo-root> <stamp>

- Exit codes: 0 did the work, 1 fatal, 2 usage, 3 --check found drift.
- `--check` means "do all the work and write nothing". Prefer it when you are
  verifying rather than building. A 3 is a real answer, not an error.
- forge's paths are hardcoded and repository-relative. It is not a general
  static-site generator. Do not point it at an arbitrary directory and expect
  output, and do not invent flags to relocate its inputs.
- `forge ticker` needs a word list that is not published. Without it the verb
  fails closed, names the missing file, and writes nothing. That is correct
  behaviour. Do NOT work around it, do NOT substitute an empty list, and do NOT
  report it as a bug.
- Every diagnostic names the verb that produced it. Quote the whole line when
  reporting a failure; the verb name is the useful half.

SCREEN -- a runner and a daemon
  screen run   <program> [args...]
  screen serve --port N [--root DIR] [--token-file F] [--frames [ms]]

- `run` exits with the child's own exit code. Use it as a runner.
- `run` has a 60-second deadline. Raise it with CX_SCREEN_TIMEOUT_MS (in
  milliseconds) if the program legitimately takes longer. A program stopped at
  the deadline is reported KILLED, not EXITED.
- A session is always in exactly one of five states, and they are not
  interchangeable:
    NEVER-STARTED  the OS refused to start it; the reason is in the record
    RUNNING        alive; pid and start time are in the record
    EXITED         finished by itself; return code and end time are in the record
    KILLED         stopped by someone; by whom and when are in the record
    UNKNOWN        a session from an earlier daemon run; its child is not ours
- Do NOT infer state from output. Empty output does not mean dead, and a full
  log does not mean alive. Ask the screen: it holds the process handle and asks
  the operating system.
- UNKNOWN is a real answer. Report it as UNKNOWN. Do not resolve it to EXITED.

SERVE -- the daemon
- Routes, all answering JSON: GET /health, /sessions, /session/<id>,
  /tail/<id>?from=, /side/<id>, /frame/<id>[?req=N];
  POST /start, /kill/<id>, /send/<id>.
- LOOPBACK ONLY. It binds the loopback interface and cannot be made to bind
  anything else. Do not look for a flag to expose it; there is none, on purpose.
- A TOKEN IS REQUIRED. Set CX_SCREEN_TOKEN or pass --token-file. With no token
  the daemon refuses to start. Never put a token in a file that will be
  committed, and never suggest a default one.
- Send the token as the `X-Screen-Token` header. A mismatch is a 401, and is
  logged.
- /frame needs --frames at startup. Without it there are no captures, and asking
  for one tells you so rather than returning an empty image.

WHAT YOU MUST NOT ASSUME
- There is no shell. `screen` spawns a program directly, so pipes, redirection,
  globs and `&&` inside a command string are literal text, not shell syntax. Run
  a shell explicitly if you need one.
- There is no configuration file for either tool.
- There is no way to make the daemon multi-user or network-visible.
- A refusal from either tool is usually the designed answer. Read the message
  before treating it as a defect: these programs are built to fail loudly rather
  than to do something approximate.