pi-multi-agent-skill 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,35 +5,10 @@ needs no extension and adds no tools. Your agent starts other agents, runs
5
5
  them in parallel or in the background, and waits for, steers, stops or
6
6
  resumes them. Any harness or agent that has a shell can use it.
7
7
 
8
- ## Features
9
-
10
- - **Small token cost.** A subagent extension adds its tool definitions to
11
- every request, whether or not your agent delegates anything. With this
12
- skill, your agent's context holds only the skill's description, a few
13
- dozen tokens, until it decides to delegate. Then it loads SKILL.md, which
14
- is about 1.2K tokens and is all it needs for a fully working multi-agent
15
- setup. The details of each command stay in separate references that it
16
- reads only when needed.
17
- - **Everything a subagent tool does, without the tool.** Your agent gets the
18
- same controls a dedicated extension would give it: start agents, wait for
19
- them, `steer` one with a correction, `abort` it and keep what it has so
20
- far, `resume` a finished job with a follow-up task, and see what each job
21
- is doing. You can run the same commands yourself from a terminal.
22
- - **Parallel and background agents.** Run a quick question in the
23
- foreground, or start several jobs at once and collect their results later.
24
- - **Stops when you stop.** A foreground agent ends when the command that
25
- started it ends: press Esc, cancel the tool call or close the session, and
26
- the agent and the commands it was running stop with it, at every level of
27
- nesting. Background jobs keep running until they finish or time out.
28
- - **Your full pi setup.** Each agent loads the same extensions, MCP servers,
29
- skills and settings as an interactive pi, and uses your model unless its
30
- profile picks another.
31
- - **Profiles.** They work as in other subagent tools: each one sets an
32
- agent's system prompt, model, thinking level and tools.
33
- - **Fork your conversation.** `--fork` hands the agent everything you have
34
- discussed so far, for tasks that need that context.
35
- - **Works outside pi.** Claude Code, Codex or any other agent with a shell
36
- can run the same commands to delegate work to pi agents.
8
+ It aims to be minimal while keeping subagents as useful as an extension and
9
+ tool based approach. A subagent extension adds its tool definitions to every
10
+ request, while this skill adds only a few dozen tokens of description to your
11
+ agent's context until it decides to delegate.
37
12
 
38
13
  ## Install
39
14
 
@@ -64,6 +39,65 @@ ln -s "$PWD/skills/multi-agent" ~/.pi/agent/skills/multi-agent
64
39
 
65
40
  Requirements: pi and Python 3.9 or later. Nothing else to install.
66
41
 
42
+ Agents run `pi` by default. To run another build of pi, such as pi-bolt, set
43
+ `PI_MULTI_AGENT_PI_BIN` in the environment that starts it. Your agent's shell
44
+ inherits the variable, and so do the agents it starts:
45
+
46
+ ```bash
47
+ alias pi-bolt='PI_MULTI_AGENT_PI_BIN=pi-bolt pi-bolt'
48
+ ```
49
+
50
+ ## Features
51
+
52
+ - **Small token cost.** A subagent extension adds its tool definitions to
53
+ every request, whether or not your agent delegates anything. With this
54
+ skill, your agent's context holds only the skill's description, a few
55
+ dozen tokens, until it decides to delegate. Then it loads SKILL.md, which
56
+ is about 1.2K tokens and is all it needs for a fully working multi-agent
57
+ setup. The details of each command stay in separate references that it
58
+ reads only when needed.
59
+ - **Everything a subagent tool does, without the tool.** Your agent gets the
60
+ same controls a dedicated extension would give it: start agents, wait for
61
+ them, `steer` one with a correction, `abort` it and keep what it has so
62
+ far, `resume` a finished job with a follow-up task, and see what each job
63
+ is doing. You can run the same commands yourself from a terminal.
64
+ - **Foreground agents.** Run a quick question and wait for the answer. A
65
+ foreground agent is tied to the command that started it: press Esc, cancel
66
+ the tool call or close the session, and the agent and the commands it was
67
+ running stop with it, at every level of nesting.
68
+ - **Parallel and background agents.** Start several jobs at once and collect
69
+ their results later. A background job is independent of your session: it
70
+ keeps running after the session that started it ends, until it finishes
71
+ or times out, and you can collect its result from a later session.
72
+ - **Your full pi setup.** Each agent loads the same extensions, MCP servers,
73
+ skills and settings as an interactive pi, and uses your model unless its
74
+ profile picks another.
75
+ - **Profiles.** They work as in other subagent tools: each one sets an
76
+ agent's system prompt, model, thinking level and tools.
77
+ - **Fork your conversation.** `--fork` hands the agent everything you have
78
+ discussed so far, for tasks that need that context.
79
+ - **Works outside pi.** Claude Code, Codex or any other agent with a shell
80
+ can run the same commands to delegate work to pi agents.
81
+
82
+ ## Non-goals
83
+
84
+ These are deliberate non-goals, to keep the skill small and free of bloat.
85
+ Each is better as a separate extension or skill that you pick to match your
86
+ own workflow. This follows the minimalism of pi itself and the Unix
87
+ philosophy: do one thing well and compose.
88
+
89
+ - **Memory.** Agents do not share or keep memory between jobs. Use a memory
90
+ extension, or have agents write handoff notes.
91
+ - **Scheduling.** There are no cron-style or recurring agents. Use cron,
92
+ `systemd` timers or a scheduler extension to run the commands.
93
+ - **Worktrees.** Agents are not isolated in git worktrees. Just prompt the
94
+ agent to use a worktree.
95
+ - **Dynamic workflows.** There is no scripting layer for orchestration such
96
+ as pipelines, parallel fan-out with gates, or phases. Your agent can
97
+ already start, wait for and chain jobs with plain commands. For
98
+ deterministic scripts, use an extension such as
99
+ [pi-dynamic-workflow](https://github.com/milanglacier/pi-dynamic-workflow).
100
+
67
101
  ## Usage
68
102
 
69
103
  Your agent drives the skill on its own, but the commands also work by hand:
@@ -81,21 +115,33 @@ See [`skills/multi-agent/references/`](skills/multi-agent/references/) for
81
115
  every command and option, how jobs start and stop, the profile format, where
82
116
  state is kept and troubleshooting.
83
117
 
84
- ## Tests
118
+ ## Profiles
85
119
 
86
- ```bash
87
- python3 -m unittest discover tests
88
- ```
89
-
90
- The tests use an offline stand-in for pi, so they call no model.
120
+ A profile is a Markdown file with frontmatter that sets an agent's system
121
+ prompt, model, thinking level and tools. `--profile NAME` selects one;
122
+ `multi_agent.py profiles` lists the available ones.
91
123
 
92
- ## Releases
124
+ ```markdown
125
+ ---
126
+ model: anthropic/claude-sonnet-5
127
+ thinking: high
128
+ tools: read, grep, find
129
+ ---
130
+ You review code changes and report bugs.
131
+ ```
93
132
 
94
- Set the version in `package.json`, commit it, and push a matching tag such
95
- as `v0.1.1`. The GitHub workflow runs the tests and publishes to npm.
96
- Prerelease versions use the `next` npm tag; stable versions use `latest`.
97
- The npm trusted publisher is `milanglacier/pi-multi-agent-skill` with the
98
- workflow filename `publish.yml` and permission to publish directly.
133
+ Profiles are looked up in `<cwd>/.pi/agents/*.md`, then
134
+ `$PI_CODING_AGENT_DIR/agents/*.md` (`~/.pi/agent/agents/` by default); when
135
+ names collide, the local profile wins. Two profiles are built in:
136
+ `general-purpose` (the default) and `Explore`, a read-only search agent.
137
+ Supported fields are `name`, `description`, `model`, `thinking`, `tools`,
138
+ `disallowed_tools`, `prompt_mode` and `enabled`. Fields do not merge across
139
+ files, so a local profile replaces the profile it overrides as a whole and
140
+ must supply the entire profile; `enabled: false` is the exception and hides
141
+ the name on its own. Unknown fields are ignored, and nested YAML is not
142
+ supported. See
143
+ [`references/profiles.md`](skills/multi-agent/references/profiles.md) for the
144
+ field details.
99
145
 
100
146
  ## License
101
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-agent-skill",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A minimal multi-agent skill+cli for pi. It adds no extension or tools, so it uses only a few dozen tokens of the context window until the main agent delegates, yet it gives the same controls as a subagent extension. Background agents keep running after the session closes, and any later session can collect their results.",
5
5
  "license": "MIT",
6
6
  "author": "Milan Glacier",
@@ -27,6 +27,8 @@
27
27
  "LICENSE"
28
28
  ],
29
29
  "pi": {
30
- "skills": ["./skills"]
30
+ "skills": [
31
+ "./skills"
32
+ ]
31
33
  }
32
34
  }
@@ -17,12 +17,12 @@ An empty prompt or message is a usage error. `--help` prints a usage summary.
17
17
  | `peek NAME [-n N]` | Print the last N lines (default 20) of the job's progress log. |
18
18
  | `list [--all]` | List the jobs of the current project. |
19
19
  | `profiles` | List the available profiles. |
20
- | `rm NAME` | Delete a job that is not running, with its conversation. |
20
+ | `rm NAME` / `rm --all` | Delete one job that is not running, or every such job of the current project, with their conversations. |
21
21
 
22
22
  ## Projects and job names
23
23
 
24
24
  A job belongs to the project of the directory where `run` was called. `run`,
25
- `list` and `kill --all` work on the current directory's project only.
25
+ `list`, `kill --all` and `rm --all` work on the current directory's project only.
26
26
 
27
27
  The other commands look for NAME in the current project first. If the project
28
28
  has no job of that name, they search every project and use the job when
@@ -45,8 +45,9 @@ run it from the job's project.
45
45
 
46
46
  A job name contains letters, digits, `.`, `_` and `-`, starts and ends with a
47
47
  letter or digit, and has at most 64 characters. `run` refuses a name that
48
- already exists in the project; `resume` that job, pick another name, or `rm`
49
- the old job first.
48
+ already exists in the project and reports its status and age. Pick another
49
+ `--name`. Use `resume` or `rm` only for a job you started or the user asked
50
+ about.
50
51
 
51
52
  `--cwd` changes where the agent works and where project profiles are looked
52
53
  up (`DIR/.pi/agents/`). The job still belongs to the current directory's
@@ -197,10 +198,13 @@ It looks up project profiles in the current directory.
197
198
 
198
199
  ## rm
199
200
 
200
- `rm` deletes a job that is not running: its state directory and its
201
+ `rm NAME` deletes a job that is not running: its state directory and its
201
202
  conversation file. It prints `removed: NAME` and fails with `job NAME is
202
203
  running; kill it first` while the job runs.
203
204
 
205
+ `rm --all` deletes every job of the current project that is not running,
206
+ with its conversation, and leaves running jobs alone.
207
+
204
208
  ## Exit codes
205
209
 
206
210
  | Code | Meaning |
@@ -74,19 +74,19 @@ the next run.
74
74
 
75
75
  No person watches an agent. When an extension inside the agent opens a dialog
76
76
  (select, confirm, input or editor), it is cancelled at once and logged in the
77
- progress log as `denied:`. The pi-minimal-permission-system extension, if
78
- installed, runs in its non-interactive mode, so tool calls that need approval
79
- are blocked. The agent is told not to retry them and to report what it could not do in its final
80
- message.
77
+ progress log as `denied:`. Extensions that have a non-interactive mode run in
78
+ it (see `state.md`), so tool calls that need a person fail without opening a
79
+ dialog. The agent is told not to retry them and to report what it could not
80
+ do in its final message.
81
81
 
82
82
  ## Nesting
83
83
 
84
- An agent can start agents of its own. `MULTI_AGENT_DEPTH` counts the depth: 0
85
- or unset for the user's agent, 1 for an agent it started, and so on. `run` and
84
+ An agent can start agents of its own. `PI_MULTI_AGENT_DEPTH` counts the depth:
85
+ 0 or unset for the user's agent, 1 for an agent it started, and so on. `run` and
86
86
  `resume` refuse to start when the caller's depth is at least
87
- `MULTI_AGENT_MAX_DEPTH` (default 2), so by default the user's agent and its
87
+ `PI_MULTI_AGENT_MAX_DEPTH` (default 2), so by default the user's agent and its
88
88
  agents can delegate, and their agents cannot. Agents inherit
89
- `MULTI_AGENT_MAX_DEPTH` from the environment of the user's agent.
89
+ `PI_MULTI_AGENT_MAX_DEPTH` from the environment of the user's agent.
90
90
 
91
- `MULTI_AGENT_JOB` holds the job directory of the agent that is running.
91
+ `PI_MULTI_AGENT_JOB` holds the job directory of the agent that is running.
92
92
  `status` and `list` use it to show the job that started each job.
@@ -45,11 +45,13 @@ because two processes would then write the same session file.
45
45
 
46
46
  ## The agent's environment
47
47
 
48
- An agent inherits the environment of the command that started it, plus:
48
+ The script starts the program in `PI_MULTI_AGENT_PI_BIN` (default `pi`). An
49
+ agent inherits the environment of the command that started it, plus:
49
50
 
50
51
  | Variable | Value |
51
52
  |---|---|
52
- | `MULTI_AGENT_JOB` | The agent's job directory. |
53
- | `MULTI_AGENT_DEPTH` | The agent's nesting depth; see `lifecycle.md`. |
53
+ | `PI_MULTI_AGENT_JOB` | The agent's job directory. |
54
+ | `PI_MULTI_AGENT_DEPTH` | The agent's nesting depth; see `lifecycle.md`. |
54
55
  | `PI_MINIMAL_PERMISSION_SYSTEM_NONINTERACTIVE` | `1`, so that extension never waits for an approval. |
56
+ | `PI_REQUEST_USER_INPUT_NONINTERACTIVE` | `1`, so that extension never asks the user a question. |
55
57
  | `PWD` | The agent's working directory. |
@@ -25,8 +25,9 @@
25
25
 
26
26
  - **`job is not running; use resume`.** The run has ended or is ending.
27
27
  `resume` the job with a new prompt instead of steering it.
28
- - **`job NAME already exists in this project`.** `run` needs a new name.
29
- `resume` the job, pick another `--name`, or `rm` the old job first.
28
+ - **`job NAME already exists in this project`.** Pick another `--name`.
29
+ The error shows the existing job's status and age. Use `resume` or `rm`
30
+ only for a job you started or the user asked about.
30
31
  - **`job NAME is running` from `resume` or `rm`.** The job's runner is
31
32
  still alive, or another command was starting the job at the same moment.
32
33
  `wait` for it, or `kill` it first.
@@ -35,7 +36,7 @@
35
36
  - **`--fork needs PI_SESSION_FILE`.** The command does not run inside pi, so
36
37
  there is no conversation to copy. Drop `--fork` and write a self-contained
37
38
  prompt.
38
- - **`cannot start an agent: ... MULTI_AGENT_MAX_DEPTH`.** The caller is too
39
+ - **`cannot start an agent: ... PI_MULTI_AGENT_MAX_DEPTH`.** The caller is too
39
40
  deep in a chain of agents. Do the work directly.
40
41
  - **`the runner of job NAME is not responding`.** The runner did not answer
41
42
  `kill` within 10 seconds, or did not exit within 20 seconds after it
@@ -48,6 +49,8 @@
48
49
 
49
50
  ## Environment
50
51
 
52
+ - **`cannot find the pi program`.** Set `PI_MULTI_AGENT_PI_BIN` to the command
53
+ that starts pi, such as `pi-bolt`, or ask the user which one they use.
51
54
  - **`flock does not work on ...`.** The state directory is on a file system
52
55
  without working file locks, such as some NFS or SMB mounts. Set
53
56
  `XDG_STATE_HOME` to a directory on a local file system.
@@ -53,14 +53,14 @@ NOT_RUNNING = "job is not running; use resume"
53
53
 
54
54
  def time_scale():
55
55
  """The factor for the timing constants below; tests set it to make them short."""
56
- value = os.environ.get("MULTI_AGENT_TIME_SCALE") or "1"
56
+ value = os.environ.get("PI_MULTI_AGENT_TIME_SCALE") or "1"
57
57
  try:
58
58
  scale = float(value)
59
59
  except ValueError:
60
60
  scale = 0.0
61
61
  if not 0 < scale < float("inf"):
62
62
  raise SystemExit(
63
- f"multi-agent: error: MULTI_AGENT_TIME_SCALE needs a positive number, got: {value}"
63
+ f"multi-agent: error: PI_MULTI_AGENT_TIME_SCALE needs a positive number, got: {value}"
64
64
  )
65
65
  return scale
66
66
 
@@ -94,8 +94,8 @@ MULTI_AGENT_BLOCK = """<multi-agent>
94
94
  You are an agent named {name}, started by another agent to work on one
95
95
  task. Whoever reads your result sees only your final message, so make it a
96
96
  complete, self-contained answer to the task. No person is available to
97
- answer questions. If a tool call is blocked because it needs approval, do
98
- not retry it; say in your final message what you could not do and why.
97
+ answer questions. If a tool call fails because it needs a person, do not
98
+ retry it; say in your final message what you could not do and why.
99
99
  </multi-agent>
100
100
  """
101
101
 
@@ -112,7 +112,7 @@ Usage:
112
112
  multi_agent.py peek NAME [-n N]
113
113
  multi_agent.py list [--all]
114
114
  multi_agent.py profiles
115
- multi_agent.py rm NAME
115
+ multi_agent.py rm (NAME | --all)
116
116
 
117
117
  Options for run:
118
118
  --profile P Profile name, matched case-insensitively (default general-purpose)
@@ -273,7 +273,15 @@ def sessions_dir(project):
273
273
 
274
274
 
275
275
  def pi_bin():
276
- return os.environ.get("MULTI_AGENT_PI_BIN") or "pi"
276
+ return os.environ.get("PI_MULTI_AGENT_PI_BIN") or "pi"
277
+
278
+
279
+ def check_pi_bin():
280
+ if not shutil.which(pi_bin()):
281
+ raise CommandError(
282
+ f'cannot find the pi program "{pi_bin()}"; '
283
+ "set PI_MULTI_AGENT_PI_BIN to the command that starts pi"
284
+ )
277
285
 
278
286
 
279
287
  def find_session_file(session_dir, name):
@@ -583,6 +591,15 @@ def job_state(job_dir):
583
591
  os.close(fd)
584
592
 
585
593
 
594
+ def job_since(job_dir, status):
595
+ """Return the time used for the job's age, or None when it is missing."""
596
+ started = parse_utc(read_text(job_path(job_dir, "started_at"), "") or "")
597
+ if status == "running":
598
+ return started
599
+ ended = parse_utc(read_text(job_path(job_dir, "ended_at"), "") or "")
600
+ return ended or started
601
+
602
+
586
603
  def socket_path(job_dir):
587
604
  """The path of the job's control socket.
588
605
 
@@ -719,9 +736,10 @@ def child_env(job_dir, job):
719
736
  env = dict(os.environ)
720
737
  for key in REMOVED_ENV:
721
738
  env.pop(key, None)
722
- env["MULTI_AGENT_JOB"] = job_dir
723
- env["MULTI_AGENT_DEPTH"] = str(job["depth"])
739
+ env["PI_MULTI_AGENT_JOB"] = job_dir
740
+ env["PI_MULTI_AGENT_DEPTH"] = str(job["depth"])
724
741
  env["PI_MINIMAL_PERMISSION_SYSTEM_NONINTERACTIVE"] = "1"
742
+ env["PI_REQUEST_USER_INPUT_NONINTERACTIVE"] = "1"
725
743
  env["PWD"] = job["cwd"]
726
744
  return env
727
745
 
@@ -1309,7 +1327,7 @@ def run_runner(job_dir, lock_fd, server, caller_fd=None):
1309
1327
 
1310
1328
  def caller_depth():
1311
1329
  try:
1312
- return int(os.environ.get("MULTI_AGENT_DEPTH") or 0)
1330
+ return int(os.environ.get("PI_MULTI_AGENT_DEPTH") or 0)
1313
1331
  except ValueError:
1314
1332
  return 0
1315
1333
 
@@ -1317,12 +1335,12 @@ def caller_depth():
1317
1335
  def check_depth():
1318
1336
  depth = caller_depth()
1319
1337
  try:
1320
- limit = int(os.environ.get("MULTI_AGENT_MAX_DEPTH") or DEFAULT_MAX_DEPTH)
1338
+ limit = int(os.environ.get("PI_MULTI_AGENT_MAX_DEPTH") or DEFAULT_MAX_DEPTH)
1321
1339
  except ValueError:
1322
1340
  limit = DEFAULT_MAX_DEPTH
1323
1341
  if depth >= limit:
1324
1342
  raise CommandError(
1325
- f"cannot start an agent: this caller is at depth {depth} and MULTI_AGENT_MAX_DEPTH is {limit}"
1343
+ f"cannot start an agent: this caller is at depth {depth} and PI_MULTI_AGENT_MAX_DEPTH is {limit}"
1326
1344
  )
1327
1345
  return depth + 1
1328
1346
 
@@ -1564,6 +1582,7 @@ def cmd_run(argv):
1564
1582
  if not os.path.isfile(fork):
1565
1583
  raise CommandError(f"--fork: PI_SESSION_FILE does not exist: {fork}")
1566
1584
  depth = check_depth()
1585
+ check_pi_bin()
1567
1586
  profile = find_profile(cwd, args.values.get("--profile") or DEFAULT_PROFILE)
1568
1587
 
1569
1588
  root = jobs_dir(project)
@@ -1576,9 +1595,16 @@ def cmd_run(argv):
1576
1595
  try:
1577
1596
  os.mkdir(os.path.join(root, name))
1578
1597
  except FileExistsError:
1598
+ job_dir = os.path.join(root, name)
1599
+ status = job_state(job_dir)
1600
+ since = job_since(job_dir, status)
1601
+ detail = status
1602
+ if since is not None:
1603
+ event = "started" if status == "running" else "ended"
1604
+ detail += f", {event} {format_age(time.time() - since)} ago"
1579
1605
  raise CommandError(
1580
- f"job {name} already exists in this project; use `resume {name}` to continue it, pick"
1581
- f" another --name, or `rm {name}` first"
1606
+ f"job {name} already exists in this project ({detail}); pick another --name. "
1607
+ f"Run `resume {name}` or `rm {name}` only for a job you started or the user asked about."
1582
1608
  )
1583
1609
  else:
1584
1610
  for _attempt in range(100):
@@ -1613,7 +1639,7 @@ def cmd_run(argv):
1613
1639
  "cwd": cwd,
1614
1640
  "project": project,
1615
1641
  "session_dir": session_dir,
1616
- "parent": os.environ.get("MULTI_AGENT_JOB") or None,
1642
+ "parent": os.environ.get("PI_MULTI_AGENT_JOB") or None,
1617
1643
  "depth": depth,
1618
1644
  "model": model,
1619
1645
  "thinking": thinking,
@@ -1640,6 +1666,7 @@ def cmd_resume(argv):
1640
1666
  prompt = read_message(args, "prompt")
1641
1667
  job_dir = find_job(name)
1642
1668
  check_depth()
1669
+ check_pi_bin()
1643
1670
  # A runner holds the lock while the job runs, so taking it proves that
1644
1671
  # nothing runs, and keeps it so until the new runner holds it.
1645
1672
  lock_fd = job_lock(
@@ -1903,8 +1930,9 @@ def cmd_status(argv):
1903
1930
  ("session_file", sessions[-1] if sessions else "-"),
1904
1931
  (
1905
1932
  "session_cmd",
1906
- "cd {} && pi --session-dir {} --session-id {}".format(
1933
+ "cd {} && {} --session-dir {} --session-id {}".format(
1907
1934
  shlex.quote(job.get("cwd", ".")),
1935
+ shlex.quote(pi_bin()),
1908
1936
  shlex.quote(session_dir),
1909
1937
  shlex.quote(name),
1910
1938
  ),
@@ -1938,8 +1966,7 @@ def cmd_list(argv):
1938
1966
  job = read_json(job_path(job_dir, "job.json"), {})
1939
1967
  status = job_state(job_dir)
1940
1968
  started = parse_utc(read_text(job_path(job_dir, "started_at"), "") or "")
1941
- ended = parse_utc(read_text(job_path(job_dir, "ended_at"), "") or "")
1942
- since = started if status == "running" else (ended or started)
1969
+ since = job_since(job_dir, status)
1943
1970
  if (
1944
1971
  "--all" not in args.flags
1945
1972
  and status != "running"
@@ -1989,8 +2016,7 @@ def cmd_profiles(argv):
1989
2016
  return EXIT_OK
1990
2017
 
1991
2018
 
1992
- def cmd_rm(argv):
1993
- job_dir = find_job(one_name(Args(argv, (), ()), "rm"))
2019
+ def remove_job(job_dir):
1994
2020
  name = os.path.basename(job_dir)
1995
2021
  lock_fd = job_lock(job_dir, f"job {name} is running; kill it first")
1996
2022
  try:
@@ -2002,6 +2028,26 @@ def cmd_rm(argv):
2002
2028
  finally:
2003
2029
  os.close(lock_fd)
2004
2030
  sys.stdout.write(f"removed: {name}\n")
2031
+
2032
+
2033
+ def cmd_rm(argv):
2034
+ args = Args(argv, (), ("--all",))
2035
+ if "--all" in args.flags:
2036
+ if args.positional:
2037
+ raise usage_error("rm takes a job name or --all, not both")
2038
+ finished = [d for d in project_jobs(logical_cwd()) if job_state(d) != "running"]
2039
+ if not finished:
2040
+ sys.stdout.write("no jobs to remove in this project\n")
2041
+ return EXIT_OK
2042
+ code = EXIT_OK
2043
+ for job_dir in finished:
2044
+ try:
2045
+ remove_job(job_dir)
2046
+ except CommandError as exc:
2047
+ sys.stdout.write(f"error: {exc}\n")
2048
+ code = EXIT_FAIL
2049
+ return code
2050
+ remove_job(find_job(one_name(args, "rm")))
2005
2051
  return EXIT_OK
2006
2052
 
2007
2053