procman 0.23.8

A process supervisor with a dependency DAG and a typed .pman language
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# Configuration Reference

Procman reads a single `.pman` file (passed as a positional argument). The file contains
top-level blocks in any order.

## Top-Level Structure

```
config {
  logs = "./my-logs"
}

env {
  RUST_LOG = args.log_level
}

arg port {
  type = string
  default = "3000"
  short = "p"
  description = "Port to listen on"
}

job migrate {
  run "db-migrate up"
}

service web {
  env PORT = args.port
  run "serve --port $PORT"
}

service worker if args.enable_worker {
  run "worker-service start"
}

task test_suite {
  run "pytest tests/"
}

event recovery {
  run "./scripts/recover.sh"
}
```

A `.pman` file may contain:

- **`config { }`** — global settings (logs, log_time)
- **`arg name { }`** — CLI argument declaration
- **`env { }`** / **`env KEY = expr`** — global environment variable bindings
- **`job name { }`** — a one-shot process that runs to completion
- **`job name if expr { }`** — a conditionally evaluated one-shot job
- **`service name { }`** — a long-running daemon process
- **`service name if expr { }`** — a conditionally evaluated service
- **`task name { }`** — an on-demand one-shot process (does not auto-start; triggered via `-t`/`--task`)
- **`task name if expr { }`** — a conditionally evaluated task
- **`event name { }`** — a dormant process, only started via `on_fail spawn`

## Process I/O defaults

- **stdin**: redirected to `/dev/null`. Procman puts every child in its own
  process group, so an inherited TTY would be a background-pgid read and
  raise SIGTTIN. If you need to feed input to a process, plumb it through
  the `run` string itself (e.g. `run "source | program"`).
- **stdout / stderr**: captured (combined into one stream) and written to
  the per-process and combined log files; ANSI escapes are stripped from
  log files only.

## Output formatting

- **Per-process prefix**: every line on stdout is prefixed with the
  originating job/service name, right-aligned and padded for visual scan.
- **Color**: on a TTY, the prefix is colored using a deterministic hash of
  the process name (so the same name always gets the same color across runs).
  Set the `NO_COLOR` environment variable (any non-empty value) to disable.
- **Log files stay plain**: ANSI escape sequences from the child process and
  the prefix coloring are both stripped from the per-process and combined log
  files.
- **Startup messages**: at startup, procman prints to stderr the resolved
  (canonicalized, absolute) log directory and each per-process log file path,
  so log locations are unambiguous.

## Identifiers

Job names, event names, arg names, and variable names are identifiers. Valid
identifiers match `[a-zA-Z_][a-zA-Z0-9_-]*` — they start with a letter or
underscore, followed by letters, digits, underscores, or hyphens.

## Reserved Keywords

The following identifiers are reserved by the language and cannot be used as
job, service, task, event, arg, or `for`/`var` names:

- `module` — used in the `module.dir` built-in.
- `procman` — used in the `procman.dir` built-in.

Other reserved tokens (`job`, `service`, `task`, `event`, `config`, `env`,
`arg`, `import`, `as`, `wait`, `watch`, `for`, `if`, `in`, `on_fail`, `run`,
`true`, `false`, `none`) are also unavailable as identifiers, but `module` and
`procman` are called out here because they look like ordinary names yet are
reserved for built-in directory references (see
[Language Design](language-design.md#built-in-directory-references)).

## String Literals

String literals are double-quoted. Supported escape sequences: `\"` (literal
quote), `\\` (literal backslash), `\n` (newline), `\t` (tab). No other
backslash escapes are recognized.

## Duration Literals

Duration literals are a number followed by a unit suffix: `s` (seconds), `ms`
(milliseconds), `m` (minutes). Fractional values are allowed (e.g., `1.5s`).

## The `none` Literal

`none` represents the absence of a value. It is valid only in specific
positions: `timeout = none` (infinite wait), `default = none` (no default).
Using `none` in env value positions or boolean contexts is a parse-time error.

## `config { }` Block

Global settings applied to all jobs.

### `config.logs`

Optional log directory path. Defaults to `logs/procman`. Recreated each run.

```
config {
  logs = "./my-logs"
}
```

## `env { }` Block

Global environment variable bindings applied to all jobs. Overridable per-job.
Declared at the top level.

Block form:
```
env {
  RUST_LOG = args.log_level
}
```

Single-binding form:
```
env RUST_LOG = args.log_level
```

Both forms can appear multiple times and coexist in the same file.

## `arg name { }` Block

CLI arguments parsed after `--`. Declared at the top level. Underscores become
dashes on the CLI (`log_level` → `--log-level`).

```
arg port {
  type = string
  default = "3000"
  short = "p"
  description = "Port to listen on"
}

arg enable_feature {
  type = bool
  default = false
}
```

| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `type` | no | `string` | `string` or `bool` |
| `short` | no || Single character shorthand |
| `description` | no || Help text for `-- --help` |
| `default` | no || Fallback value. Args without a default are required. |

Arg values are referenced in expressions as `args.name`. There is no `env`
field on args — use a top-level `env { }` block to explicitly bind args to
environment variables.

Running `procman myapp.pman -- --help` prints generated usage based on the
arg definitions.

### Env Precedence

Lowest to highest:

| Source | Priority |
|--------|----------|
| System env (inherited) | lowest |
| CLI `-e KEY=VALUE` flags | |
| Top-level `env { }` | |
| Per-job `env` | |
| Per-iteration `for` bindings | highest |

Note: `var` bindings from `contains` conditions are procman expressions, not
direct env injections. They enter the environment only when explicitly assigned
via `env KEY = var_name`.

## `job` and `service` Blocks

Each `job` block defines a one-shot process (runs to completion). Each `service`
block defines a long-running daemon process. Both share the same fields.

### `run` (required)

The command to execute. All commands are passed to `bash -euo pipefail -c`, so
shell features like pipes, redirects, `&&`, variable expansion, and multi-line
scripts all work naturally. `bash` must be on `PATH`.

Inline form:

```
run "echo hello"
```

Multi-line fenced form:

```
run """
  ./run-migrations
  echo "DATABASE_URL=postgres://localhost:5432/mydb" > $PROCMAN_OUTPUT
"""
```

Procman never interpolates inside shell strings. Values flow in exclusively
via environment variables.

An empty or whitespace-only `run` value is rejected at parse time.

### `env` (optional)

Environment variables merged into the job's environment. Single-binding and
block forms can coexist:

```
service api {
  env DB_URL = @migrate.DATABASE_URL
  env {
    API_KEY = "secret"
    LOG_DIR = args.log_dir
  }
  run "start-api --db $DB_URL"
}
```

### `job` vs `service`

A `job` is a one-shot process that runs to completion. Exit code 0 is treated
as success and does **not** trigger supervisor shutdown. A non-zero exit code
still triggers shutdown. Jobs can write key-value output to `$PROCMAN_OUTPUT`,
which other processes reference via `@job.KEY` expressions (see
[Process Output](templates.md)).

A `service` is a long-running daemon process. If a service exits, it triggers
supervisor shutdown.

### `task`

A `task` is a one-shot process that does not auto-start. Tasks must be explicitly
triggered via the `-t`/`--task` CLI flag. Like jobs, tasks run to completion —
exit code 0 is success, non-zero triggers shutdown.

Tasks are useful for operations that should only run on demand: test suites,
database migrations, cleanup scripts, or any one-shot utility work.

```
task test_suite {
  wait {
    after @migrate
  }
  run "pytest tests/"
}
```

Trigger with: `procman myapp.pman -t test_suite`

Tasks share all the same fields as jobs and services (`run`, `env`, `wait`,
`for`, `if`, `watch`).

### `wait` (optional)

A block of conditions that must all be satisfied before `run` executes. See
the [Dependencies](dependencies.md) chapter for the full reference.

```
wait {
  after @migrate
  http "http://localhost:3000/health" {
    status = 200
    timeout = 30s
    poll = 500ms
  }
}
```

### `if` (optional)

An expression on the `job` or `service` line. If falsy, the job/service is not
evaluated at all — no dependency waiting, no env resolution. Skipped jobs still
register as exited so `after` dependents can proceed.

```
service worker if args.enable_worker {
  run "worker-service start"
}
```

### `watch` (optional)

Named runtime health check blocks that monitor a service after it starts. See the
[Dependencies](dependencies.md) chapter for condition syntax.

```
service web {
  run "web-server --port 8080"

  watch health {
    http "http://localhost:8080/health" {
      status = 200
    }
    initial_delay = 5s
    poll = 10s
    threshold = 3
    on_fail shutdown
  }
}
```

### `for` (optional)

Iteration block that wraps `env` and `run`, spawning one instance per element.
See the [Fan-out](fan-out.md) chapter for full details.

```
job nodes {
  for config_path in glob("configs/node-*.yaml") {
    env NODE_CONFIG = config_path
    run "start-node --config $NODE_CONFIG"
  }
}
```

## `event name { }` Block

Event handlers are declared at the top level. They are never auto-started —
they are spawned via `on_fail spawn @name` in a watch block.

```
event recovery {
  run "./scripts/recover.sh"
}
```

`on_fail spawn @name` must reference an `event`, not a `job`.

## Shell Blocks

Procman never interpolates inside shell strings. Values flow into shell
exclusively via environment variables set with `env` bindings.

Inline:
```
run "echo hello"
```

Multi-line fenced:
```
run """
  ./run-migrations
  echo "DATABASE_URL=postgres://localhost:5432/mydb" > $PROCMAN_OUTPUT
"""
```

## Expression Language

Expressions appear in `if` conditions, `env` value positions, and `var`
bindings. They are never evaluated inside shell strings.

### Value References

| Syntax | Description |
|--------|-------------|
| `args.name` | CLI arg value |
| `@job.KEY` | Output from a job's `PROCMAN_OUTPUT` |
| `local_var` | Job-scoped variable (from `for` or `var` binding) |

### Literals

| Type | Examples |
|------|---------|
| String | `"hello"`, `"3000"` |
| Number | `42`, `3.14` |
| Bool | `true`, `false` |
| Duration | `5s`, `500ms`, `2m` |
| None | `none` |

### Operators

| Category | Operators |
|----------|----------|
| Comparison | `==`, `!=`, `>`, `<`, `>=`, `<=` |
| Logical | `&&`, `\|\|`, `!` |
| Grouping | `( )` |

No arithmetic in v1.

### Type Errors

Type errors in expressions cause immediate procman runtime panic and shutdown.
There is no silent coercion. A type error is a bug in the config.

## Parse-Time Validation

Procman validates the configuration at parse time and exits with an error
(with `file:line:col` location) if any of these checks fail:

- **Syntax errors** — malformed blocks, missing fields, invalid tokens
- **Unknown identifiers** — referencing an arg or job that doesn't exist
- **`after @job` targets** — must reference a `job` (not a `service`)
- **`@job.KEY` references** — must point to a `job` (not a `service`) and
  require `after @job` in the process's `wait` block (direct or transitive)
- **Circular dependencies** — cycles in `after` references
- **`on_fail spawn @name`** — must reference an `event`
- **Variable shadowing** — reusing a name already bound by `for`, `var`, or
  args
- **Empty `run` commands** — rejected at parse time