{
"name": "ct-test",
"description": "Run a command as a framed experiment: pose a question, classify the result from stdout/stderr pattern matches, and emit a templated verdict. Pass/fail is decided by what the command prints, not only its exit code. ct-test is fail-closed; {RESULT} resolves in order: (0) the run timed out (--timeout) => ERROR (decisive — the experiment did not complete, so no match in partial output can establish success; {CODE} becomes 'timeout'); (1) any err-match hit => ERROR (decisive, never overridden); (2) else any ok-match hit => SUCCESS (a supplied --ok-match is a REQUIRED proof of success — a clean exit does not substitute for it); (3) else the run is inconclusive and --otherwise decides (success|error|exit), defaulting to error when an --ok-match was supplied, otherwise exit. The -stdout/-stderr matcher variants search only that one stream (e.g. cargo test writes 'test result: ok' to stdout, so --ok-match-stderr would miss it). On ERROR a one-line reason explaining which rule fired is printed to stderr and is available as the {REASON} emit token. --focus distils the captured output to the lines matching a pattern (with --context around each), printed to stderr and available as {FOCUS}; --capture-tail N bounds the {STDOUT}/{STDERR} emit tokens to the last N lines (matchers and --focus still see everything). --timeout SECS kills the command's whole process group after SECS seconds (fractional allowed). --heartbeat SECS prints a minimal liveness pulse while the command runs ([{ELAPSED}s] by default, customisable with --heartbeat-emit, routed to stderr or stdout by --heartbeat-to). Exit status is 0 when RESULT is SUCCESS, 1 when ERROR, 2 on a usage or runtime error. ct-test runs only a fixed, immutable built-in set of read-only commands (cat ct-check ct-deps ct-outline ct-search ct-tree ct-view echo false file grep head ls pwd stat tail true wc), so it is a ready conditional wrapper around the read-only ct-* tools; a command not on it is refused with exit 2 and nothing runs, and there is no run-time way to extend the list. Gating is by program name (the basename of cmd). There is no shell mode: the command is always launched directly, never through sh, so the match predicates and --focus replace the usual '| grep' post-processing (use ct-each for dispatch over many items). Pattern arguments use substring->glob->regex promotion and are searched unanchored. Invoke as `ct test ...` or `ct-test ...`.",
"input_schema": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question this experiment answers; printed as a '== ... ==' banner unless --quiet."
},
"cmd": {
"type": "string",
"description": "Program to run (must be on the fixed read-only allowlist). Trailing args (after --) are passed through to it. Always launched directly — there is no shell mode."
},
"stdin": {
"type": "string",
"description": "Literal text written to the child's standard input."
},
"timeout": {
"type": "number",
"description": "Kill the command's process group after SECS seconds (fractional allowed). Decisive: the verdict is ERROR and {CODE} becomes 'timeout'."
},
"heartbeat": {
"type": "number",
"description": "Print a liveness pulse every SECS seconds (fractional allowed) while the command runs."
},
"heartbeat-emit": {
"type": "string",
"description": "Heartbeat line template. Tokens: {ELAPSED} (whole seconds so far) {TOOL} {QUESTION} {CMD}. Default: \"[{ELAPSED}s]\"."
},
"heartbeat-to": {
"type": "string",
"enum": ["stderr", "stdout"],
"description": "Stream heartbeat pulses are written to. Default: stderr."
},
"err-match": {
"type": "string",
"description": "Pattern that, if found in stdout OR stderr, forces RESULT=ERROR. Synonym for supplying both err-match-stdout and err-match-stderr."
},
"err-match-stdout": {
"type": "string",
"description": "Pattern that, if found in stdout, forces RESULT=ERROR."
},
"err-match-stderr": {
"type": "string",
"description": "Pattern that, if found in stderr, forces RESULT=ERROR."
},
"ok-match": {
"type": "string",
"description": "Pattern that, if found in stdout OR stderr, indicates RESULT=SUCCESS. Synonym for supplying both ok-match-stdout and ok-match-stderr."
},
"ok-match-stdout": {
"type": "string",
"description": "Pattern that, if found in stdout, indicates RESULT=SUCCESS."
},
"ok-match-stderr": {
"type": "string",
"description": "Pattern that, if found in stderr, indicates RESULT=SUCCESS."
},
"otherwise": {
"type": "string",
"enum": ["success", "error", "exit"],
"description": "Verdict when neither an --ok-match nor an --err-match matched (the inconclusive case). Default: error if any --ok-match was supplied, else exit (follow the exit code)."
},
"focus": {
"type": "string",
"description": "Distil the captured output to the lines matching this pattern, with --context lines around each (overlapping windows merge, line-numbered). Printed to stderr and available as the {FOCUS} emit token."
},
"context": {
"type": "integer",
"description": "Lines of context shown around each --focus match. Default: 2.",
"default": 2
},
"capture-tail": {
"type": "integer",
"description": "Keep only the last N lines of each captured stream in the {STDOUT}/{STDERR} emit tokens, with an elision marker. Matchers and --focus still see the full streams."
},
"emit": {
"type": "string",
"description": "Template written to stdout after the command finishes (alias: emit-stdout). Tokens: {RESULT} {CODE} {QUESTION} {CMD} {STDOUT} {STDERR} {REASON} {FOCUS}."
},
"emit-stderr": {
"type": "string",
"description": "Template written to stderr after the command finishes. Same tokens as emit."
},
"show-output": {
"type": "boolean",
"description": "Also pass the child's stdout/stderr through verbatim."
},
"quiet": {
"type": "boolean",
"description": "Suppress the question banner."
},
"args": {
"type": "array",
"items": { "type": "string" },
"description": "Arguments passed through to --cmd (supplied after `--`)."
}
},
"required": ["cmd"]
}
}