Overview
Make ax.Execute install SIGTERM/SIGINT handling so that a killed process
emits one final structured error envelope on stderr and returns a documented,
deterministic exit code — instead of a bare 130 with truncated logs. The
"stderr always carries a structured error" invariant should hold even in death.
User Story
As an agent host that routinely kills hung or over-budget child processes, I
want a terminated ax-go CLI to leave one final machine-readable error envelope
and a deterministic exit code, So that my parser contract has no hole exactly
when things go wrong.
Problem Statement
Agent hosts kill processes routinely (over budget, hung, cancelled turn). Today
ax-go installs no signal handling: ax.Execute runs root.ExecuteContext(ctx)
with no signal.NotifyContext, so SIGINT/SIGTERM produce Go's default behavior
— a bare exit (130 for SIGINT) and whatever logs happened to flush. The central
ax-go invariant that stderr always carries a structured error is violated
precisely at termination, the moment an agent most needs a parseable signal.
Proposed Solution
Wrap the execution context in signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) inside ax.Execute. When a signal cancels the context,
root.ExecuteContext returns a cancellation error; normalize it to a structured
ax.Error (e.g. error_code: "interrupted"/"terminated") written via
WriteError to stderr, and return a documented deterministic exit code. The OTel
flush-on-exit path already deferred in Execute still runs so telemetry is not
truncated.
Technical Approach
- In
Execute (execute.go), derive a signal-aware context with
signal.NotifyContext and defer its stop, before root.ExecuteContext.
- Distinguish signal-induced cancellation from ordinary errors in
normalizeExecuteError and produce the signal envelope with a fixed
error_code and exit code.
- Decide the deterministic mapping in the spec. Two candidates: (a) reuse the
existing context.Canceled → ExitInternal (1) mapping in
contract/error.go for the structured path, or (b) adopt a documented
128+signum convention (130 for SIGINT, 143 for SIGTERM). The AX contract
favors the fixed 0-4 codes, so (a) with a clearly documented envelope is the
likely default; the spec pins it.
- Ensure the deferred OTel shutdown and any Loki flush still complete within the
shutdown budget so the final envelope and telemetry are not truncated.
Files Likely Affected
execute.go - install signal.NotifyContext; detect signal cancellation; emit the structured death envelope; map the exit code
contract/error.go - signal error_code construction and the deterministic exit mapping (reuse context.Canceled → ExitInternal or a documented convention)
contract/exit.go - documentation of the signal-death exit mapping if a new constant/convention is chosen
README.md, docs/ - document signal behavior and the exit mapping
examples/integration/ - optionally demonstrate/verify graceful termination
Acceptance Criteria
Out of Scope
- In-command checkpoint/resume on interrupt (resume-token convention, F15) — F10 only guarantees a structured final envelope, not resumability.
- Per-command custom signal handlers; F10 provides the library-level invariant.
Technical Notes
Additive behavior change: adds a structured envelope + documented exit on a path
that currently produces none; no existing output field is removed or retyped
(Constitution XI). The signal error_code and exit mapping belong in the
import-isolated contract package (source of truth), never the runtime. Concurrency
is central — signal delivery races with normal completion and with the OTel/Loki
flush goroutines, so -race coverage is required (AGENTS.md mandates the race
detector). The final envelope must have no non-deterministic content beyond the
documented fields (trace_id). Routes through Spec Kit (issue → spec → plan →
tasks).
Testing Strategy
Related
Overview
Make
ax.Executeinstall SIGTERM/SIGINT handling so that a killed processemits one final structured error envelope on stderr and returns a documented,
deterministic exit code — instead of a bare 130 with truncated logs. The
"stderr always carries a structured error" invariant should hold even in death.
User Story
As an agent host that routinely kills hung or over-budget child processes, I
want a terminated ax-go CLI to leave one final machine-readable error envelope
and a deterministic exit code, So that my parser contract has no hole exactly
when things go wrong.
Problem Statement
Agent hosts kill processes routinely (over budget, hung, cancelled turn). Today
ax-go installs no signal handling:
ax.Executerunsroot.ExecuteContext(ctx)with no
signal.NotifyContext, so SIGINT/SIGTERM produce Go's default behavior— a bare exit (130 for SIGINT) and whatever logs happened to flush. The central
ax-go invariant that stderr always carries a structured error is violated
precisely at termination, the moment an agent most needs a parseable signal.
Proposed Solution
Wrap the execution context in
signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)insideax.Execute. When a signal cancels the context,root.ExecuteContextreturns a cancellation error; normalize it to a structuredax.Error(e.g.error_code: "interrupted"/"terminated") written viaWriteErrorto stderr, and return a documented deterministic exit code. The OTelflush-on-exit path already deferred in
Executestill runs so telemetry is nottruncated.
Technical Approach
Execute(execute.go), derive a signal-aware context withsignal.NotifyContextand defer its stop, beforeroot.ExecuteContext.normalizeExecuteErrorand produce the signal envelope with a fixederror_codeand exit code.existing
context.Canceled → ExitInternal (1)mapping incontract/error.gofor the structured path, or (b) adopt a documented128+signum convention (130 for SIGINT, 143 for SIGTERM). The AX contract
favors the fixed 0-4 codes, so (a) with a clearly documented envelope is the
likely default; the spec pins it.
shutdown budget so the final envelope and telemetry are not truncated.
Files Likely Affected
execute.go- installsignal.NotifyContext; detect signal cancellation; emit the structured death envelope; map the exit codecontract/error.go- signalerror_codeconstruction and the deterministic exit mapping (reusecontext.Canceled → ExitInternalor a documented convention)contract/exit.go- documentation of the signal-death exit mapping if a new constant/convention is chosenREADME.md,docs/- document signal behavior and the exit mappingexamples/integration/- optionally demonstrate/verify graceful terminationAcceptance Criteria
ax.Executeto write exactly one structuredax.Errorenvelope to stderr-raceOut of Scope
Technical Notes
Additive behavior change: adds a structured envelope + documented exit on a path
that currently produces none; no existing output field is removed or retyped
(Constitution XI). The signal
error_codeand exit mapping belong in theimport-isolated
contractpackage (source of truth), never the runtime. Concurrencyis central — signal delivery races with normal completion and with the OTel/Loki
flush goroutines, so
-racecoverage is required (AGENTS.md mandates the racedetector). The final envelope must have no non-deterministic content beyond the
documented fields (
trace_id). Routes through Spec Kit (issue → spec → plan →tasks).
Testing Strategy
Executeand asserts a single structured envelope + deterministic exit-racetest exercising signal-vs-completion and signal-vs-flush orderingRelated