v0.30.0 Phase 2 Owning Command and Status Review
Status: complete with no open finding (2026-09-03). Phase 2 publishes the adopted Command and ExitStatus source contracts, closes all launch allocation before fork, and composes Phase 1 raw target facts into one synchronous create-and-reap operation. It does not add shell execution, PATH lookup, mutable environments, stream redirection, or a discardable child resource.
Reviewed Boundary
The review traced public declarations, private storage, validation and mutation order, argv construction, process-context environment access, pipe ownership, fork branches, exec reporting, wait retry, terminal-status construction, public errno mapping, ARM64 pair-result transport, generated destruction, and complete native execution.
The adversarial pass searched for borrowed command input, a mutation before validation, a missing argv terminator, pointer capture before relocating storage, allocation after fork, child-side destruction, reconstructed environment entries, magic exit-code detection, an exec-failure path that skips wait, an interrupted state escaping retry, ambiguous exit/signal observation, and a target ABI implemented independently by both standard source and the compiler.
Ownership and Mutation
OwnedProcessText owns one allocation containing the exact input bytes followed by one private NUL terminator. Validation rejects an empty path, malformed UTF-8, embedded NUL bytes, and unrepresentable terminator growth before allocating. Command.arg completes construction of this temporary owner before pushing it, so its only recoverable rejection leaves the argument vector unchanged. Allocation follows the ordinary aborting current-context policy fixed by the public contract.
prepare_command consumes the command and creates one null-terminated Vec<usize>. Its first entry points to the owned path, later entries point to arguments in insertion order, and its final entry is zero. Moving OwnedProcessText or the prepared aggregate cannot relocate its separately owned byte allocation. PreparedCommand owns both the command buffers and pointer vector until the parent completes report collection and wait, then is explicitly destroyed on the parent side.
Child and Parent Lifecycle
All allocation, validation, byte copying, and pointer-vector growth precede pipe creation and fork. The child branch calls one noalloc target operation with already prepared addresses. That operation closes the read descriptor, attempts exact-path execve with the immutable inherited environment-vector address, writes the four-byte errno with interrupted and partial-write handling if exec returns, closes the report descriptor, and terminates through raw exit. It cannot return to ordinary Nocter control flow or command destruction.
The parent closes its report writer, reads until clean close, exact rejection, malformed payload, or channel failure, closes the reader, and then waits for the exact pid. WaitAttempt represents a single syscall result; wait_for_child consumes interruptions internally and returns the narrower WaitOutcome. Public policy therefore cannot accidentally treat an interrupted wait as terminal. The wait completes before exec-report selection, so every created child is reaped even when exec or the report channel failed. Ordinary nonzero exit and signal termination remain ExitStatus values; exec rejection remains independent of child exit 127.
ARM64 ABI Finding and Remediation
The first public-command native build found a concrete Phase 1 qualification gap. A three-word SyscallPairResult is an indirect ordinary Nocter return on ARM64, but the original pair selector required a nonexistent three-register direct return. Earlier standard discovery compiled the body, yet did not make the private primitive reachable from a process executable; the claim was therefore not exercised at architecture selection.
The selector now accepts only the canonical indirect-result register and a 24-byte, eight-byte- aligned machine layout. The syscall materializer retains Darwin x0 and x1 plus normalized errno in x2; the selector then stores those three words into caller-owned result storage. Standard process native-test selection now includes both the raw pair-descriptor case and all process module tests, preventing a body-only compilation from being mistaken for executable qualification.
Public Failure Selection
Input validation returns std.process.invalid_input. Pipe or fork failure, malformed reports, and unclassified exec errors return std.process.spawn_failed. Exec errno classification maps absent or non-directory paths to std.process.not_found, denied execution to std.process.permission_denied, and invalid pointers, argument size, executable format, or request shape to std.process.invalid_input. A completed child whose terminal status cannot be observed returns std.process.wait_failed.
Raw target source owns syscall constants, wait-bit decoding, descriptor protocol, and errno facts. The policy source owns public error codes and ExitStatus. The compiler owns process-context and machine ABI transport but contains no Command, argv, wait-status, or public process-error policy.
Qualification
Focused qualification passes:
- authored-standard discovery, exact dependency-graph review, name resolution, and body checking;
- all ARM64 library tests after indirect pair-result selection;
- all native-session tests, including every standard process and Darwin pair test;
- an external public
Commandexecutable whose mutable path and argument sources are cleared after insertion, whose helper verifies exact ordered arguments including an empty argument and an inherited environment entry, whose ordinary exit 7 remains anExitStatus, and whose missing executable returnsstd.process.not_found; - source tests for empty-path rejection, embedded-NUL rejection atomicity, exact argv addresses and terminator, clean and rejected launch reports, fork and wait classification, and mutually exclusive exit/signal observations.
No open Phase 2 remediation remains. Phase 3 broadens native failure and lifecycle coverage rather than changing this ownership model.