@nledford/omp-coord 0.2.0 → 0.2.1

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.
@@ -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.2.0",
10
+ "version": "0.2.1",
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.2.1] - 2026-10-26
10
+
11
+ ### Added
12
+
13
+ - Releases are published to npmjs.com as `@nledford/omp-coord`, with provenance: `omp plugin install @nledford/omp-coord` needs no token or `.npmrc`.
14
+
15
+ ### Fixed
16
+
17
+ - On Linux, where `/proc` cannot be read (some hardened containers), a timed-out job now falls back to signalling its process group instead of failing.
18
+
9
19
  ## [0.2.0] - 2026-10-26
10
20
 
11
21
  ### Added
@@ -36,6 +46,7 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
36
46
  - An SQLite event log per scope, with retention.
37
47
  - Packaging as an omp plugin, with a marketplace catalog; queued commands run on omp's own Bun.
38
48
 
39
- [Unreleased]: https://github.com/nledford/omp-coord/compare/v0.2.0...HEAD
49
+ [Unreleased]: https://github.com/nledford/omp-coord/compare/v0.2.1...HEAD
50
+ [0.2.1]: https://github.com/nledford/omp-coord/compare/v0.2.0...v0.2.1
40
51
  [0.2.0]: https://github.com/nledford/omp-coord/compare/v0.1.0...v0.2.0
41
52
  [0.1.0]: https://github.com/nledford/omp-coord/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # omp-coord
2
2
 
3
+ [![CI](https://github.com/nledford/omp-coord/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/nledford/omp-coord/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@nledford/omp-coord)](https://www.npmjs.com/package/@nledford/omp-coord)
5
+ [![omp](https://img.shields.io/badge/omp-%E2%89%A5%2018.12-blue)](https://github.com/can1357/oh-my-pi)
6
+ [![License: MIT](https://img.shields.io/github/license/nledford/omp-coord)](LICENSE)
7
+
3
8
  An [omp](https://github.com/can1357/oh-my-pi) plugin, plus a small command-line runner, that lets several omp sessions on one machine, working in the same project, share one set of agent caps and build/test queues, and talk to each other.
4
9
 
5
10
  - **Shared agent caps.** Subagents count against pools (for example 8 `work` and 2 `short` slots) shared by every session in the project. A spawn over the cap is refused through omp's spawn check, so the cap does not depend on agents remembering to take and free slots.
@@ -16,7 +21,7 @@ Each session gets a letter and shows `coord B · work 5/8 · short 1/2` in omp's
16
21
  ## Quick start
17
22
 
18
23
  ```sh
19
- omp plugin install "git+ssh://git@github.com/nledford/omp-coord.git"
24
+ omp plugin install @nledford/omp-coord
20
25
  ```
21
26
 
22
27
  Then put a `.omp-coord.json` in the project's top folder:
@@ -36,7 +41,7 @@ Every omp session started in that folder or below now shares 6 agent slots, and
36
41
 
37
42
  | | |
38
43
  |---|---|
39
- | [Install](docs/install.md) | Plugin install over SSH or the marketplace, updating, the `omp-coord` command. |
44
+ | [Install](docs/install.md) | From npm, GitHub, the marketplace or GitHub Packages; updating; the `omp-coord` command. |
40
45
  | [Configuration](docs/configuration.md) | Every `.omp-coord.json` field. |
41
46
  | [Agent slots](docs/agent-slots.md) | How caps, the line, holds and nested agents work. |
42
47
  | [Queues](docs/queues.md) | `omp-coord run`, order, lanes, time limits, agents' commands. |
package/docs/install.md CHANGED
@@ -2,43 +2,50 @@
2
2
 
3
3
  Requires omp 18.12 or later. No dependencies at runtime: the extension and the runner use the Bun built into omp.
4
4
 
5
- ## From GitHub Packages
5
+ ## From npm
6
+
7
+ ```sh
8
+ omp plugin install @nledford/omp-coord
9
+ omp plugin install @nledford/omp-coord@0.2.0 # a given release
10
+ ```
11
+
12
+ Releases are published to npmjs.com from the release workflow, with [provenance](https://docs.npmjs.com/generating-provenance-statements) that ties each package to the commit and workflow that built it (from 0.2.1 on). A machine set up for GitHub Packages (below) maps the `@nledford` scope to GitHub, so this command installs from there instead.
6
13
 
7
- Each release is published as the npm package `@nledford/omp-coord` to GitHub Packages. It is private like the repository, and GitHub Packages asks for a token even for public packages, so each machine needs one-time setup: a token that can read packages, and the `@nledford` scope pointed at GitHub's registry in `~/.npmrc`.
14
+ ## From GitHub
8
15
 
9
16
  ```sh
10
- gh auth refresh --scopes read:packages # once: let the GitHub CLI's token read packages
11
- cat >> ~/.npmrc <<EOF
12
- @nledford:registry=https://npm.pkg.github.com
13
- //npm.pkg.github.com/:_authToken=$(gh auth token)
14
- EOF
17
+ omp plugin install github:nledford/omp-coord
15
18
  ```
16
19
 
17
- Then install a release, or the newest one:
20
+ Or through omp's marketplace, which this repository also serves:
18
21
 
19
22
  ```sh
20
- omp plugin install @nledford/omp-coord@0.2.0
21
- omp plugin install @nledford/omp-coord
23
+ omp plugin marketplace add nledford/omp-coord
24
+ omp plugin install omp-coord@omp-coord
22
25
  ```
23
26
 
24
- ## From GitHub
27
+ ## From GitHub Packages
25
28
 
26
- Over SSH. The repository is private for now, and the `github:` shorthand and HTTPS URLs fail without credentials; `github:nledford/omp-coord` works once it is public:
29
+ Each release is also published to GitHub Packages. GitHub Packages asks for a token to install any npm package, public ones included, so each machine needs one-time setup: a token that can read packages, and the `@nledford` scope pointed at GitHub's registry in `~/.npmrc`.
27
30
 
28
31
  ```sh
29
- omp plugin install "git+ssh://git@github.com/nledford/omp-coord.git"
32
+ gh auth refresh --scopes read:packages # once: let the GitHub CLI's token read packages
33
+ cat >> ~/.npmrc <<EOF
34
+ @nledford:registry=https://npm.pkg.github.com
35
+ //npm.pkg.github.com/:_authToken=$(gh auth token)
36
+ EOF
30
37
  ```
31
38
 
32
- Or through omp's marketplace, which this repository also serves:
39
+ Then install a release, or the newest one:
33
40
 
34
41
  ```sh
35
- omp plugin marketplace add git@github.com:nledford/omp-coord.git
36
- omp plugin install omp-coord@omp-coord
42
+ omp plugin install @nledford/omp-coord@0.2.0
43
+ omp plugin install @nledford/omp-coord
37
44
  ```
38
45
 
39
46
  ## Updating
40
47
 
41
- `omp plugin upgrade @nledford/omp-coord` moves a package or git URL install to the newest release or commit; `omp plugin upgrade omp-coord@omp-coord` moves a marketplace install once a release has raised the catalog's version. Bun remembers the commit a git URL resolved to, so installing a git URL again can bring back an older commit; to pin a release over git, add its tag: `…/omp-coord.git#v0.2.0`. [CHANGELOG.md](../CHANGELOG.md) lists what each release changed.
48
+ `omp plugin upgrade @nledford/omp-coord` moves a package or git install to the newest release or commit; `omp plugin upgrade omp-coord@omp-coord` moves a marketplace install once a release has raised the catalog's version. Bun remembers the commit a git install resolved to, so installing from git again can bring back an older commit; to pin a release, add its tag: `github:nledford/omp-coord#v0.2.0`. [CHANGELOG.md](../CHANGELOG.md) lists what each release changed.
42
49
 
43
50
  ## From a checkout
44
51
 
package/package.json CHANGED
@@ -1,9 +1,6 @@
1
1
  {
2
2
  "name": "@nledford/omp-coord",
3
- "version": "0.2.0",
4
- "publishConfig": {
5
- "registry": "https://npm.pkg.github.com"
6
- },
3
+ "version": "0.2.1",
7
4
  "files": [
8
5
  "src",
9
6
  "docs",
@@ -16,7 +13,7 @@
16
13
  "author": "Nathaniel Ledford",
17
14
  "repository": {
18
15
  "type": "git",
19
- "url": "https://github.com/nledford/omp-coord.git"
16
+ "url": "git+https://github.com/nledford/omp-coord.git"
20
17
  },
21
18
  "keywords": [
22
19
  "omp",
@@ -14,7 +14,7 @@
14
14
  * 2 a usage or configuration error.
15
15
  */
16
16
  import { randomBytes } from "node:crypto";
17
- import { mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
17
+ import { mkdirSync, readdirSync, statSync } from "node:fs";
18
18
  import { constants, userInfo } from "node:os";
19
19
  import { join } from "node:path";
20
20
  import { parseArgs } from "node:util";
@@ -24,6 +24,7 @@ import { EXIT_TIMED_OUT, runJob } from "../../app/job-runner.ts";
24
24
  import { type Config, ConfigError, findConfigFile, loadConfig } from "../../config.ts";
25
25
  import { MAX_PRIORITY } from "../../domain/queues.ts";
26
26
  import { osLoad } from "../os/load.ts";
27
+ import { groupRunning } from "../os/process-group.ts";
27
28
  import { BoardStore, stateDir } from "../sqlite/store.ts";
28
29
  import { formatFindings, formatInterrupted, formatNotes, formatStats, formatStatus } from "../text/report.ts";
29
30
  import { startBeacon, unixLiveness } from "../unix/beacon.ts";
@@ -309,32 +310,6 @@ function signalNumber(signal: NodeJS.Signals): number {
309
310
  return constants.signals[signal] ?? 1;
310
311
  }
311
312
 
312
- /**
313
- * Whether process group `pgid` still has a member that has not exited. A signal to the group succeeds while it holds
314
- * only zombies, which is what an exited command's children become in a container whose first process never reaps
315
- * orphans; on Linux, /proc tells them apart. Elsewhere (macOS has no /proc; launchd reaps orphans) the signal decides.
316
- */
317
- function groupRunning(pgid: number): boolean {
318
- try {
319
- process.kill(-pgid, 0);
320
- } catch {
321
- return false;
322
- }
323
- if (process.platform !== "linux") return true;
324
- for (const entry of readdirSync("/proc")) {
325
- if (!/^\d+$/.test(entry)) continue;
326
- try {
327
- // /proc/<pid>/stat: "pid (comm) state ppid pgrp …"; comm may contain spaces and parentheses.
328
- const stat = readFileSync(`/proc/${entry}/stat`, "utf8");
329
- const [state, , pgrp] = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
330
- if (Number(pgrp) === pgid && state !== "Z") return true;
331
- } catch {
332
- // The process ended while being read.
333
- }
334
- }
335
- return false;
336
- }
337
-
338
313
  if (import.meta.main) {
339
314
  try {
340
315
  process.exitCode = await main(Bun.argv.slice(2));
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Whether a process group still has a member that has not exited, for stopping a timed-out job's whole group.
3
+ *
4
+ * A signal to the group succeeds while it holds only zombies, which is what an exited command's children become in a
5
+ * container whose first process never reaps orphans. On Linux, /proc tells them apart. Elsewhere (macOS has no /proc;
6
+ * launchd reaps orphans), or when /proc cannot be read, the signal decides.
7
+ */
8
+ import { readdirSync, readFileSync } from "node:fs";
9
+
10
+ /** The state and process group of a process, from the text of `/proc/<pid>/stat`. */
11
+ export function parseProcStat(stat: string): { state: string; pgrp: number } | undefined {
12
+ // "pid (comm) state ppid pgrp …": comm may contain spaces and parentheses, so fields start after the last ")".
13
+ const close = stat.lastIndexOf(")");
14
+ if (close === -1) return undefined;
15
+ const [state, , pgrp] = stat.slice(close + 2).split(" ");
16
+ const group = Number(pgrp);
17
+ return state && Number.isInteger(group) ? { state, pgrp: group } : undefined;
18
+ }
19
+
20
+ export function groupRunning(pgid: number): boolean {
21
+ try {
22
+ process.kill(-pgid, 0);
23
+ } catch {
24
+ return false;
25
+ }
26
+ if (process.platform !== "linux") return true;
27
+ let pids: string[];
28
+ try {
29
+ pids = readdirSync("/proc").filter(entry => /^\d+$/.test(entry));
30
+ } catch {
31
+ return true;
32
+ }
33
+ for (const pid of pids) {
34
+ let stat: string;
35
+ try {
36
+ stat = readFileSync(`/proc/${pid}/stat`, "utf8");
37
+ } catch {
38
+ continue; // ended while being read, or hidden
39
+ }
40
+ const p = parseProcStat(stat);
41
+ if (p?.pgrp === pgid && p.state !== "Z") return true;
42
+ }
43
+ return false;
44
+ }