{
"name": "ct-each",
"description": "Run one command template once per item and judge the sweep with an aggregate verdict — the declarative replacement for a bash for-loop. Items come from --items, the shared walker (--base/--name/--ext/--hidden/--follow: matched file paths become items, in walk order — the natural source for per-file sweeps), and/or --stdin (one per line), in that order; everything after `--` is the command template, and {ITEM} (the current item) and {INDEX} (its 1-based position) expand inside every argv element. Each expansion is launched directly — there is no shell anywhere, so item values can never re-shape the command. Each run is classified by the suite's exit contract (exit 0 => that item SUCCESS, anything else => ERROR), and the count of per-item successes is judged against --expect: all (every item, the default) | any | none | N (at least) | =N (exactly) | +N (more than) | -N (fewer than). Dispatch targets are gated by program name against a fixed, immutable set: the read-only allowlist (cat ct-check ct-deps ct-outline ct-search ct-tree ct-view echo false file grep head ls pwd stat tail true wc) plus ct-test by default; with --mutating also ct-edit and ct-patch (each still enforces its own --expect/--dry-run gates). Nothing else is ever runnable and there is no run-time way to extend the set; every item's expanded command is gated before the first one runs, so a refusal (exit 2, nothing run) cannot strike mid-sweep. --dry-run prints each expanded command and runs nothing. --timeout SECS bounds each item's run (process-group kill; that item is ERROR with {CODE}=timeout). --heartbeat SECS prints a minimal liveness pulse ([{ELAPSED}s] by default, customisable with --heartbeat-emit, routed by --heartbeat-to). On a per-item ERROR a one-line reason goes to stderr; --show-output passes children's streams through verbatim. Exit status: 0 when the aggregate verdict is SUCCESS, 1 when ERROR, 2 on usage/runtime error or a refused target. Invoke as `ct each ...` or `ct-each ...`. --items accepts payload schemes: file:PATH expands to the file's non-empty lines, text:VALUE is one literal item.",
"input_schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
},
"description": "Items to dispatch over, in order; one run per item. May be combined with stdin. A file:PATH item expands to the file's non-empty lines; text:VALUE is one literal item."
},
"mode": {
"type": "string",
"enum": [
"literal",
"glob",
"regex"
],
"description": "Pin how --name/--ext walker patterns are interpreted; promotion off. Use literal for names whose characters would otherwise promote."
},
"base": {
"type": "string",
"description": "Walker item source: files under this root become items (paths). A file yields itself; a directory is descended."
},
"name": {
"type": "string",
"description": "Walker filter: limit to files whose name matches; '|'-separated alternatives, promoted and anchored. Implies --base . when --base is absent."
},
"ext": {
"type": "array",
"items": {
"type": "string"
},
"description": "Walker filter: restrict to these extensions (no dots); combined with --name as alternatives. Implies --base . when --base is absent."
},
"hidden": {
"type": "boolean",
"description": "Walker: include dot-entries; default skips them."
},
"follow": {
"type": "boolean",
"description": "Walker: follow symlinks."
},
"stdin": {
"type": "boolean",
"description": "Also read items from standard input, one per line (blank lines skipped), after any --items."
},
"question": {
"type": "string",
"description": "The question this sweep answers; printed as a '== ... ==' banner unless --quiet."
},
"expect": {
"type": "string",
"description": "Aggregate expectation over the per-item SUCCESS count: all|any|none|N|=N|+N|-N. Default: all (every item must succeed)."
},
"fail-fast": {
"type": "boolean",
"description": "Stop after the first per-item ERROR; remaining items are reported as skipped (and still count against --expect all)."
},
"mutating": {
"type": "boolean",
"description": "Permit the suite's mutating tools (ct-edit, ct-patch) as the dispatch target. Arbitrary external commands are never runnable."
},
"dry-run": {
"type": "boolean",
"description": "Print each fully-expanded command ('would run: ...') and run nothing."
},
"timeout": {
"type": "number",
"description": "Per item: kill that run's process group after SECS seconds (fractional allowed); the item is classified ERROR and its {CODE} becomes 'timeout'."
},
"heartbeat": {
"type": "number",
"description": "Print a liveness pulse every SECS seconds (fractional allowed) while the sweep is in progress."
},
"heartbeat-emit": {
"type": "string",
"description": "Heartbeat line template. Tokens: {ELAPSED} (whole seconds so far) {TOOL} {QUESTION} {ITEM} {INDEX} {DONE} {TOTAL}. Default: \"[{ELAPSED}s]\"."
},
"heartbeat-to": {
"type": "string",
"enum": [
"stderr",
"stdout"
],
"description": "Stream heartbeat pulses are written to. Default: stderr."
},
"emit-each": {
"type": "string",
"description": "Per-item template written to stdout. Tokens: {RESULT} {ITEM} {INDEX} {CODE} {CMD} {STDOUT} {STDERR}. Default (unless --quiet): \"{RESULT} {ITEM}\"."
},
"emit": {
"type": "string",
"description": "Summary template written to stdout after the sweep (alias: emit-stdout). Tokens: {RESULT} {OK} {ERRORS} {SKIPPED} {TOTAL} {QUESTION} {EXPECT} {REASON}."
},
"emit-stderr": {
"type": "string",
"description": "Summary template written to stderr. Same tokens as emit."
},
"show-output": {
"type": "boolean",
"description": "Also pass each child's stdout/stderr through verbatim."
},
"quiet": {
"type": "boolean",
"description": "Suppress the question banner, the default per-item lines, and the default summary."
},
"json": {
"type": "boolean",
"description": "Emit one structured JSON result (tool, verdict, expect, ok, errors, skipped, total, items[{index,item,cmd,code,result}]) instead of text; overrides the emit templates."
},
"command": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command and arguments run per item (supplied after `--`); {ITEM} and {INDEX} expand in every element. The program must be on the fixed dispatch gate."
}
},
"required": [
"command"
]
}
}