@nledford/omp-coord 0.3.2 → 0.4.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/.omp-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +12 -1
- package/docs/configuration.md +1 -1
- package/docs/queues.md +20 -1
- package/examples/omp-coord.json +10 -0
- package/package.json +1 -1
- package/src/domain/commands.ts +12 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "omp-coord",
|
|
8
8
|
"source": "./",
|
|
9
9
|
"description": "Shared subagent caps, build and test queues, claims, pause, a decision log and messages across omp sessions working in one project on one machine.",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.4.0",
|
|
11
11
|
"author": { "name": "Nathaniel Ledford" },
|
|
12
12
|
"homepage": "https://github.com/nledford/omp-coord",
|
|
13
13
|
"repository": "https://github.com/nledford/omp-coord",
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-10-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `examples/omp-coord.json` has two `refuse` rules that protect git's rerere cache, which every worktree of a repository shares: deleting `rr-cache` (`rm`, `find -delete`, `git clean` and the like) and `git rerere clear`, `forget` and `gc` are refused, with a reason that points to `git -c rerere.enabled=false …` for throwaway builds. Reading the cache, `git rerere status`, `diff`, `remaining` and a command that names a throwaway repository under `/tmp` (an `rr-cache` path there, or `git -C`/`GIT_DIR=` for `git rerere`) are allowed; the limits are in the docs. Copy the rules into your `.omp-coord.json` above any `queue` rules to use them; no code or default changed. See [queues](docs/queues.md#example-protecting-gits-rerere-cache).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Breaking:** while any `refuse` rule is configured, a bash command longer than 32,768 characters is refused with a message, instead of being judged by the patterns: a pattern that stalls on a very long line froze the session's `tool_call` check (omp gives it 30 s and fails closed), and one that runs out of backtracking budget answered "no match" and let the command through. A command that used to run and is now refused: write the long script to a file with the write tool and run it from a short command. Configs without a `refuse` rule (queue rules only) are not affected. See [queues](docs/queues.md#agents-commands).
|
|
18
|
+
|
|
9
19
|
## [0.3.2] - 2026-10-28
|
|
10
20
|
|
|
11
21
|
### Changed
|
|
@@ -96,7 +106,8 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
96
106
|
- An SQLite event log per scope, with retention.
|
|
97
107
|
- Packaging as an omp plugin, with a marketplace catalog; queued commands run on omp's own Bun.
|
|
98
108
|
|
|
99
|
-
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.
|
|
109
|
+
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.4.0...HEAD
|
|
110
|
+
[0.4.0]: https://github.com/nledford/omp-coord/compare/v0.3.2...v0.4.0
|
|
100
111
|
[0.3.2]: https://github.com/nledford/omp-coord/compare/v0.3.1...v0.3.2
|
|
101
112
|
[0.3.1]: https://github.com/nledford/omp-coord/compare/v0.3.0...v0.3.1
|
|
102
113
|
[0.3.0]: https://github.com/nledford/omp-coord/compare/v0.2.3...v0.3.0
|
package/docs/configuration.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
| `agents.startTimeoutSeconds` | `120` | How long an approved spawn may take to start before its slot frees. |
|
|
15
15
|
| `agents.queueSpawns` | `true` | While the session is the only one in the scope, keep omp's per-session spawn queue (`task.maxConcurrency`) at what the caps allow it, so spawns past the cap wait and start by themselves instead of being refused ([agent slots](agent-slots.md#spawns-wait-their-turn)); with other sessions, or paused, spawns over the cap are refused as before. A `task.maxConcurrency` you set yourself stays the ceiling. `false` keeps the refusals always. |
|
|
16
16
|
| `messages.perMinute` | `10` | Messages one session may send per minute. It slows two sessions that keep answering each other; it does not stop them. Each delivered message starts a turn in an idle session. |
|
|
17
|
-
| `commands` | `[]` | Rules for agents' bash commands, first match wins: `{ "pattern": "<regex>", "action": "refuse", "reason": "…" }`, or `{ "pattern", "action": "queue", "queue": "<name>", "lane"? }`. The user's own `!` commands are not affected. |
|
|
17
|
+
| `commands` | `[]` | Rules for agents' bash commands, first match wins: `{ "pattern": "<regex>", "action": "refuse", "reason": "…" }`, or `{ "pattern", "action": "queue", "queue": "<name>", "lane"? }`. The user's own `!` commands are not affected. Nothing is refused unless a rule is listed; `examples/omp-coord.json` has ready-made `refuse` rules, including two that protect git's rerere cache ([queues](queues.md#example-protecting-gits-rerere-cache)). |
|
|
18
18
|
| `queues.<name>.slots` | required | How many jobs of this queue run at once, across all processes in the scope. Queue names use letters, digits, `.`, `_`, `-`. |
|
|
19
19
|
| `queues.<name>.order` | `"fifo"` | `"fifo"`, `"fair-share"` or `"priority"` (see [queues](queues.md#order)). |
|
|
20
20
|
| `queues.<name>.lanes` | `["normal"]` | Lane names, first served first; the last is the default. |
|
package/docs/queues.md
CHANGED
|
@@ -51,4 +51,23 @@ When an agent's bash command matches a `queue` rule under `commands`, the extens
|
|
|
51
51
|
|
|
52
52
|
`omp-coord run` records the agent whose command it is on each job, so queue lines read `session A/WaveImport` instead of only `session A`. omp (18.12.4 or later) gives every bash call the id of its agent's conversation (`PI_SESSION_ID`), and the extension records on the board which agent that is, so the command text is never touched: queue rules, omp's `bash.patterns` rules and other extensions' rewrites see exactly what the agent wrote. A script can name the agent itself with `OMP_COORD_AGENT`.
|
|
53
53
|
|
|
54
|
-
A command matching a `refuse` rule is refused with the rule's reason.
|
|
54
|
+
A command matching a `refuse` rule is refused with the rule's reason. While any `refuse` rule is configured, a command longer than 32,768 characters is refused too, with a message that says so (create the file with the write tool and run it from a short command): the check runs inside omp's `tool_call` handler, which omp gives 30 seconds and fails closed, and a regex that runs out of its backtracking budget answers "no match", so a very long line could otherwise stall the session or slip past a rule. Configs without a `refuse` rule are not affected.
|
|
55
|
+
|
|
56
|
+
### Example: protecting git's rerere cache
|
|
57
|
+
|
|
58
|
+
`examples/omp-coord.json` carries two `refuse` rules (copy them into your `.omp-coord.json`; nothing is refused unless a rule is in the file) for a cache that every worktree of a repository shares: `<git-common-dir>/rr-cache`, where `git rerere` keeps recorded conflict resolutions. A command that deletes entries from it takes away resolutions other sessions' agents recorded, and their next rebase hits the same conflicts again. Rules are first-match-wins, so list them before any `queue` rule: a command that matches both (`rm -rf .git/rr-cache && bun test`) is queued, not refused, when the `queue` rule comes first.
|
|
59
|
+
|
|
60
|
+
- **Refused**: `rm`, `rmdir`, `unlink`, `trash`, `shred`, `find … -delete` and `git clean` (also `git --work-tree=<path> clean`) in a command line (up to the next `;`, `&` or newline, a backslash-newline continuing the line, so also `… | xargs rm`) that names a path containing `rr-cache` (`.git/rr-cache`, `"$(git rev-parse --git-common-dir)/rr-cache"`); the same delete as the very next command after `cd` into such a path (`cd .git/rr-cache && rm -rf *`, also `cd -- …`, `pushd`, a delete on the next line, also `cd "$(git rev-parse --git-common-dir)/rr-cache"` and inside `( … )` or `sh -c "…"`); and `git rerere clear`, `forget` and `gc`, with up to 64 `git` global options before `rerere` (values may be quoted, escaped or `$(…)`, up to 4,096 characters each), after `sudo`, `env`, `xargs`, `timeout [-s SIG] [-k D] N` (`sudo`, `env`, `xargs` and `time` with short flags that take no value, so `sudo -u me git …`, `xargs -I{} git …` and `env -u X git …` get through), `nice` (and `nice -n 10`), `!`, `if`, `while`, `until`, `elif`, `do`, `then`, `else`, `{`, a pipe or `;`, and with `git` given by path (`/usr/bin/git`). The reason names the alternative: run the throwaway command with `git -c rerere.enabled=false …`, so its resolutions never reach the shared cache.
|
|
61
|
+
- **Not refused**: reading the cache (`ls`, `cat`, `du`), `git rerere status`, `diff` and `remaining`, and anything aimed at a throwaway repository under `/tmp`, `/private/tmp`, `/var/folders` or `$TMPDIR` (also `${TMPDIR:-/tmp}`): an `rr-cache` path that starts there (and has no `..` after it), or, for `git rerere`, a `GIT_DIR=`, `--git-dir` or `git -C` that points there in the same command (between `;`, `&`, `|` or a newline) before `rerere`.
|
|
62
|
+
|
|
63
|
+
These rules match command text, like every rule here, so they stop mistakes, not a determined dodge. Limits:
|
|
64
|
+
|
|
65
|
+
- A path that does not spell `rr-cache` (`rm -rf .git/rr-*`, a variable holding the path, a script that deletes it) gets through, and so does `mv`.
|
|
66
|
+
- A `cd` in an earlier call, or a delete that is not the next command after the `cd`, is not seen; neither is `cd … && find . -exec rm …` (a `find … -delete` is). A redirection or `|| exit` after the `cd` is allowed for.
|
|
67
|
+
- Text that merely mentions the words is refused too (`git commit -m "…; git rerere gc is refused"`, a heredoc line, `git log -S"git rerere gc"`), as is a verb and `rr-cache` in the same command line even when unrelated: `grep rm .git/rr-cache` and `ls .git/rr-cache | grep rm` are refused. So is `git rerere clear` written as the start of a quoted string (`sh -c 'git rerere clear'` is meant to be caught, and so is `git commit -m 'git rerere clear'`).
|
|
68
|
+
- A throwaway repository is recognized only inside the one command: `cd /tmp/x && git rerere clear` and an earlier `export GIT_DIR=…` are refused (write `git -C /tmp/x rerere clear`), and so is one outside the temporary folders, because text cannot tell it from the project's own.
|
|
69
|
+
- Other ways to run the same thing get through. After a `cd`: `if cd …; then rm …`, `&& { rm …; }`, `&& (rm …)`, `&& nice rm …`, and a loop such as `for d in .git/rr-cache/*; do rm -rf "$d"; done`. For `git rerere`: `eval git rerere clear`, `parallel`, `stdbuf`, `find … -exec git rerere forget {} +`, `git rerere 'clear'`, `xargs` with options that take a value (`xargs -n 1`, `xargs -I{}`), `timeout --signal=KILL`, and a command inside `python -c` or `node -e`. A guard that reads command text cannot close these; it stops the usual spellings.
|
|
70
|
+
- A delete and the `rr-cache` path more than 2,000 characters apart in one command line are not connected; that bound, and the other limits on how far a match reaches, keep matching time small, and the 32,768-character cap above bounds it for every pattern: the tests time adversarial lines of 5 KB to 30 KB, each under half a second on a busy machine, and check that a real offense after each is still refused. A regex that runs out of its engine's backtracking budget answers "no match", which would let a command through; reviewers found such stalling lines of a few hundred bytes (since fixed), and none is known up to the cap (the tests run shapes of up to about 32 KB).
|
|
71
|
+
- A `GIT_DIR=` before an `sh -c "git rerere clear"` is not seen inside the quoted string, and a variable whose name merely ends in `GIT_DIR=` counts as one.
|
|
72
|
+
- Only the first throwaway option counts: `git -C /tmp/x -C /work/repo rerere clear` is exempt, though a later `-C` points at the project. `${TMPDIR:-<anything>}` is exempt whatever the default.
|
|
73
|
+
- A quoted path with a space in it (`"/tmp/my dir/.git/rr-cache"`) is judged by the part after the space.
|
package/examples/omp-coord.json
CHANGED
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
"test": { "slots": 3, "order": "fair-share", "agingMinutes": 10, "timeoutMinutes": 30 }
|
|
23
23
|
},
|
|
24
24
|
"commands": [
|
|
25
|
+
{
|
|
26
|
+
"pattern": "(?:(?<![\\w.-])(?:rm|rmdir|unlink|trash|shred)(?![\\w.-])|\\s-delete\\b|(?=((?<![\\w.-])git\\b[^;&|\\n]{0,200}?\\sclean\\b))\\1)[^;&\\n]{0,2000}(?:\\\\\\n[^;&\\n]{0,2000}){0,4}(?:^|[\\s=(])(?!['\"]?(?:(?:/private)?/(?:tmp|var/folders)/|\\$\\{?TMPDIR(?::-[^}\\s]*)?\\}?['\"]?/)(?![^\\s;&|=(]*\\.\\./))['\"]?[^\\s;&|=(]*rr-cache|(?:^|[\\s=(])(?!['\"]?(?:(?:/private)?/(?:tmp|var/folders)/|\\$\\{?TMPDIR(?::-[^}\\s]*)?\\}?['\"]?/)(?![^\\s;&|=(]*\\.\\./))['\"]?[^\\s;&|=(]*rr-cache[^;&\\n]{0,2000}(?:\\\\\\n[^;&\\n]{0,2000}){0,4}(?:(?<![\\w.-])(?:rm|rmdir|unlink|trash|shred)(?![\\w.-])|\\s-delete\\b|(?<![\\w.-])git\\b[^;&|\\n]{0,200}\\sclean\\b)|(?<![\\w.-])git\\b[^;&|\\n]{0,200}\\s--work-tree[=\\s][^;&|\\n]{0,200}rr-cache[^;&|\\n]{0,200}\\sclean\\b|(?:^|[;&|\\n({\\`]|(?<=\\s)['\"])[^\\S\\n]*(?:cd|pushd)[^\\S\\n]+(?:-[-\\w]*[^\\S\\n]+)?(?!['\"]?(?:(?:/private)?/(?:tmp|var/folders)/|\\$\\{?TMPDIR(?::-[^}\\s]*)?\\}?['\"]?/)(?![^\\s;&|=(]*\\.\\./))(?=[^;&|\\n]{0,2000}(?:\\\\\\n[^;&|\\n]{0,2000}){0,4}rr-cache)(?:\"[^\"\\n]*\"|'[^'\\n]*'|\\$\\([^)\\n]*\\)|[^\\s;&|\"'$<>]|\\$(?!\\())+(?:(?:[^\\S\\n]+\\d+|[^\\S\\n]*)&?[<>]&?[^\\s;&|<>]*){0,4}(?:[^\\S\\n]*\\|\\|[^\\S\\n]*(?:exit|return)(?:[^\\S\\n]+\\d+)?)?[^\\S\\n]*(?:\\\\\\n[^\\S\\n]*)?(?:&&|;|\\n)[\\s\\\\]*(?:(?:sudo|command)\\s+)*(?:/\\S*/)?(?:(?:(?<![\\w.-])(?:rm|rmdir|unlink|trash|shred)(?![\\w.-])|\\s-delete\\b|(?<![\\w.-])git\\b[^;&|\\n]{0,200}\\sclean\\b)|find\\b[^;&|\\n]{0,2000}(?:\\\\\\n[^;&|\\n]{0,2000}){0,4}\\s-delete\\b)",
|
|
27
|
+
"action": "refuse",
|
|
28
|
+
"reason": "git's rerere cache is shared by every worktree and session, and deleting it loses other sessions' recorded resolutions: to keep a throwaway build's resolutions out, run that command with `git -c rerere.enabled=false …`"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"pattern": "(?:^|[;&|\\n({\\`]|(?<=\\s)['\"])(?![^;&|\\n]{0,500}?(?:GIT_DIR=|--git-dir[=\\s]|\\s-C\\s+)['\"]?(?:(?:/private)?/(?:tmp|var/folders)/|\\$\\{?TMPDIR(?::-[^}\\s]*)?\\}?['\"]?/)(?![^\\s;&|=(]*\\.\\./)(?=[^;&|\\n]{0,500}?\\srerere\\b))[^\\S\\n]*(?:(?:\\w+=(?:\"[^\"\\n]*\"|'[^'\\n]*'|\\$\\([^)\\n]*\\)|\\`[^\\`\\n]*\\`|[^\\s\"'$\\`]|\\$(?!\\()){0,512}|sudo(?:\\s+-\\w+)*|command|exec|nice(?:\\s+-\\w+(?:\\s+\\d+)?)?|!|if|while|until|elif|env(?:\\s+-\\w+)*|time(?:\\s+-\\w+)*|nohup|do|then|else|xargs(?:\\s+-\\w+)*|timeout(?:\\s+(?:-[sk]\\s+\\S+|--?[\\w-]+)){0,4}\\s+\\S+)\\s+){0,8}(?:(?:/[\\w.-]+)+/)?git\\b(?:(?:\\s+(?:\\\\\\n\\s*)*|(?:\\\\\\n\\s*)+)(?:-c(?:\\s+(?:\\\\\\n\\s*)*|(?:\\\\\\n\\s*)+)(?!-)(?:[^\\s\"'$\\`\\\\]|\\\\[^\\n]|\\$(?!\\()|\\$\\([^)\\n]*\\)|\\`[^\\`\\n]*\\`|\"[^\"]*\"|'[^']*'){1,4096}|-C(?:\\s+(?:\\\\\\n\\s*)*|(?:\\\\\\n\\s*)+)(?!-)(?:[^\\s\"'$\\`\\\\]|\\\\[^\\n]|\\$(?!\\()|\\$\\([^)\\n]*\\)|\\`[^\\`\\n]*\\`|\"[^\"]*\"|'[^']*'){1,4096}|--(?:git-dir|work-tree|namespace|exec-path)(?:\\s+(?:\\\\\\n\\s*)*|(?:\\\\\\n\\s*)+)(?!-)(?:[^\\s\"'$\\`\\\\]|\\\\[^\\n]|\\$(?!\\()|\\$\\([^)\\n]*\\)|\\`[^\\`\\n]*\\`|\"[^\"]*\"|'[^']*'){1,4096}|--[\\w-]+(?:=(?:[^\\s\"'$\\`\\\\]|\\\\[^\\n]|\\$(?!\\()|\\$\\([^)\\n]*\\)|\\`[^\\`\\n]*\\`|\"[^\"]*\"|'[^']*'){0,4096})?|-\\w+)){0,64}(?:\\s+(?:\\\\\\n\\s*)*|(?:\\\\\\n\\s*)+)rerere(?:(?:\\s+(?:\\\\\\n\\s*)*|(?:\\\\\\n\\s*)+)--?[\\w-]+){0,8}(?:\\s+(?:\\\\\\n\\s*)*|(?:\\\\\\n\\s*)+)(?:clear|forget|gc)\\b",
|
|
32
|
+
"action": "refuse",
|
|
33
|
+
"reason": "git's rerere cache is shared by every worktree and session, and deleting it loses other sessions' recorded resolutions: to keep a throwaway build's resolutions out, run that command with `git -c rerere.enabled=false …`"
|
|
34
|
+
},
|
|
25
35
|
{ "pattern": "(^|[;&|(\\s])(npm|pnpm|yarn|bun)\\s+(run\\s+)?test\\b", "action": "queue", "queue": "test" },
|
|
26
36
|
{ "pattern": "(^|[;&|(\\s])(npm|pnpm|yarn|bun)\\s+run\\s+build\\b", "action": "queue", "queue": "build" },
|
|
27
37
|
{
|
package/package.json
CHANGED
package/src/domain/commands.ts
CHANGED
|
@@ -26,10 +26,22 @@ export interface Runner {
|
|
|
26
26
|
config: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Longest command a `refuse` rule is judged on. omp's `tool_call` check is synchronous here and fails closed after 30 s,
|
|
31
|
+
* and a regex that spends its whole backtracking budget answers "no match", which would let the command through. A
|
|
32
|
+
* longer command is refused instead, so the cost of every pattern is bounded by this length.
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_CHECKED_COMMAND = 32_768;
|
|
35
|
+
|
|
29
36
|
export function decideCommand(rules: CommandRule[], command: string, runner: Runner): CommandDecision {
|
|
30
37
|
// Already queued: a command that runs `omp-coord run`, or one this extension rewrote.
|
|
31
38
|
if (/(^|[;&|(\s'"/])omp-coord'?\s+'?run\b/.test(command) || runner.argv.every(a => command.includes(a)))
|
|
32
39
|
return { kind: "allow" };
|
|
40
|
+
if (command.length > MAX_CHECKED_COMMAND && rules.some(r => r.action === "refuse"))
|
|
41
|
+
return {
|
|
42
|
+
kind: "refuse",
|
|
43
|
+
reason: `the command is ${command.length} characters, over the ${MAX_CHECKED_COMMAND} that omp-coord's refuse rules can check: create the file with the write tool and run it from a short command`,
|
|
44
|
+
};
|
|
33
45
|
const rule = rules.find(r => r.pattern.test(command));
|
|
34
46
|
if (!rule) return { kind: "allow" };
|
|
35
47
|
if (rule.action === "refuse") return { kind: "refuse", reason: rule.reason };
|