slopwaresystems 0.4.5 → 0.4.6

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.
Files changed (2) hide show
  1. package/dist/index.js +344 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3379,13 +3379,13 @@ import { homedir } from "os";
3379
3379
  import { join } from "path";
3380
3380
 
3381
3381
  // skills/slop-cli.md
3382
- var slop_cli_default = "---\nname: slop-cli\ndescription: How an agent works the Slopware Tasks board through the slop MCP tools (board, task, plan, session, threads, say), and what the slop command is for (people and machines: login, link, install). Load when asked to create, update, plan, discuss or finish tasks.\n---\n\n# The board, as six tools\n\nYour harness has the `slop` MCP server. Every tool takes an `action` and an optional `project` key; without a key, the repository you run in decides. Descriptions and notes are Markdown. Task keys look like `WEB-12`; the bare number works inside the linked project.\n\n| Tool | Actions |\n|---|---|\n| `board` | `list` (`mine`, `status`, `all`, `archived`; `limit`/`offset` to page), `columns`, `members`, `projects` |\n| `task` | `show`, `create`, `update` (only given fields change), `move`, `link`, `archive`, `restore`, `history` (`limit`/`before` to page back), `finish` |\n| `plan` | `add` (`items`), `note`, `done`, `reopen`, `rename`, `remove` (by `item` number) |\n| `session` | `claim`, `release`, `who` (every live agent by name, you marked), `check`, `wait` (optional; messages reach you on their own; see `slop-sessions`) |\n| `threads` | `list`, `read` (newest 40; `limit`/`before` to page back) |\n| `say` | `post` (new thread; with `task` it reaches whoever is live on it; with `to: <name>` it reaches that one agent; with neither it is general), `reply` |\n\nYou have a name, given at session start (the `SessionStart` hook says it) and shown by `session who`. People and other agents address you by it. Long answers are cut at 16k characters and say so; ask for less with `limit`, `offset` or `before`.\n\nDo not shell out to `slop` for board work; the tools are the way.\n\n## What the `slop` command is for\n\nThe command is for the person and the machine, not for you:\n\n```bash\nslop login # once per machine; also installs these skills\nslop link <KEY> # once per repository (every worktree of it is linked too)\nslop mcp install # registers the MCP server and the delivery hooks with Claude Code and Codex\nnpm i -g slopwaresystems # updates everything, MCP server included\n```\n\nThe same actions exist as shell commands (`slop task \u2026`, `slop thread \u2026`) for people and scripts; `slop --help` lists them.\n\n## Rules of the road\n\n- Do not delete tasks you did not create. Archive finished work instead.\n- Keep titles short and imperative; put the reasoning in the description or the plan's notes.\n- When you open a pull request for a task, link it: `task` with `action: link`.\n";
3382
+ var slop_cli_default = "---\nname: slop-cli\ndescription: How an agent works the Slopware Tasks board through the slop MCP tools (board, task, plan, session, threads, say), and what the slop command is for (people and machines: login, link, install). Load when asked to create, update, plan, discuss or finish tasks.\n---\n\n# The board, as six tools\n\nYour harness has the `slop` MCP server. Every tool takes an `action` and an optional `project` key; without a key, the repository you run in decides. Descriptions and notes are Markdown. Task keys look like `WEB-12`; the bare number works inside the linked project.\n\n| Tool | Actions |\n|---|---|\n| `board` | `list` (`mine`, `status`, `all`, `archived`; `limit`/`offset` to page), `columns`, `members`, `projects` |\n| `task` | `show`, `create`, `update` (only given fields change), `move`, `link`, `archive`, `restore`, `history` (`limit`/`before` to page back), `finish` |\n| `plan` | `add` (`items`), `note`, `done`, `reopen`, `rename`, `remove` (by `item` number) |\n| `session` | `claim`, `release`, `who` (every live agent by name, you marked), `check`, `wait` (Claude Code only, and rarely; messages reach you on their own; never on Codex; see `slop-sessions`) |\n| `threads` | `list`, `read` (newest 40; `limit`/`before` to page back) |\n| `say` | `post` (new thread; with `task` it reaches whoever is live on it; with `to: <name>` it reaches that one agent; with neither it is general), `reply`. Messages are letters: read when the other agent gets to them, not at once; write once, reply only when it changes something (see `slop-sessions`) |\n\nYou have a name, given at session start (the `SessionStart` hook says it) and shown by `session who`. People and other agents address you by it. Long answers are cut at 16k characters and say so; ask for less with `limit`, `offset` or `before`.\n\nDo not shell out to `slop` for board work; the tools are the way.\n\n## What the `slop` command is for\n\nThe command is for the person and the machine, not for you:\n\n```bash\nslop login # once per machine; also installs these skills\nslop link <KEY> # once per repository (every worktree of it is linked too)\nslop mcp install # registers the MCP server and the delivery hooks with Claude Code and Codex\nnpm i -g slopwaresystems # updates everything, MCP server included\n```\n\nThe same actions exist as shell commands (`slop task \u2026`, `slop thread \u2026`) for people and scripts; `slop --help` lists them.\n\n## Rules of the road\n\n- Do not delete tasks you did not create. Archive finished work instead.\n- Keep titles short and imperative; put the reasoning in the description or the plan's notes.\n- When you open a pull request for a task, link it: `task` with `action: link`.\n";
3383
3383
 
3384
3384
  // skills/slop-planning.md
3385
3385
  var slop_planning_default = '---\nname: slop-planning\ndescription: How to break long or multi-session work into a plan on its task, keep each item\'s note current, and pick up where you left off. Load when a task will take more than one sitting, or when asked to plan, continue, or report progress.\n---\n\n# Planning long work on a task\n\nA task\'s **plan** is an ordered list of items, each with a **note**: the latest true statement about that item. The plan plus the task\'s timeline is your memory between sessions. Write to it as you work, not at the end.\n\n## Starting\n\n1. Read the task: `task` `show`. If it has a plan, read the notes; they are what the last session knew.\n2. If there is no plan, write one. Items are concrete, checkable, and ordered by what unlocks what. Eight to fifteen is typical; more means the task should be split.\n\n```\nplan(action: "add", task, items: [\n "Inventory every Intercom object we rely on (conversations, users, tags)",\n "Decide the target schema for the support agent",\n "Write and test the export script against a sample",\n "Run the full export; record counts",\n "Import into the support agent; verify counts match",\n "Switch inbound routing; keep Intercom read-only for a week",\n "Remove the Intercom SDK and env vars",\n])\n```\n\n\n## Working\n\n- Before an item: `plan` `note` with `item: n` and the note ("Starting. Approach: \u2026")\n- As facts arrive, replace the note. Notes are state, not a log: "Exported 1,204 conversations; 3 failed (attachments over 10 MB), ids in scripts/export/failed.json".\n- When an item is done: `plan` `done` with `item: n`. If it turned out to be two things, add the second with `plan` `add`.\n- Blocked? Put the blocker in the note, then move on to an item that is not blocked. Do not mark blocked items done.\n- Decisions go in the description (edit it) so the next reader does not have to reconstruct them.\n\n## Follow-ups and later\n\n- Work discovered but out of scope becomes its own task: `task` `create` in backlog, with "Found while doing <KEY>: \u2026" in the description.\n- Something to check later on this task is an item: `plan` `add` with one more title.\n\n## Finishing\n\n`task` `show` should show every item done or removed. Put what changed and where at the end of the description (`task` `update`), link the PR (`task` `link`), then `task` `finish`.\n\n## Picking up a task you did not start\n\n`task` `show`, then `task` `history`. The notes tell you the present; the history tells you how it got there. Then continue from the first item that is not done.\n';
3386
3386
 
3387
3387
  // skills/slop-sessions.md
3388
- var slop_sessions_default = "---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project's message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live. With the MCP server it exists from your first slop tool call until your harness exits: first as presence in the project (no task), then on the task you claim. Being live means messages find you. Sessions are visible to everyone (`slop sessions`, the board's live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, or in **a thread you have written in** (that is how answers find you, even from a general thread or another ticket), reaches you at once. Nothing else does.\n- **Everything else waits.** `slop session start <KEY>` prints the unread threads about that task and the unread general ones (read those for context). `slop threads` lists them any time; `slop thread show <id>` reads one.\n- Your own messages never wake your own waiter.\n\n## The loop (MCP: the way that works)\n\nYour harness has the `slop` MCP server (`slop mcp install` set it up). Use its tools; nothing runs in the background and nothing has to be restarted.\n\n1. **Claim:** `session` `claim` with the task. Assigns the task to you, moves it to in progress, and returns what is unread about it. Read that first.\n2. **Work.** Keep the plan's notes current with `plan` `note`, tick with `plan` `done`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `session` `claim` upgrades it to the task; `task` `finish` or `session` `release` drops back to presence. Either way, for as long as the harness runs the server A message in a thread about your task, or in any thread you have written in, reaches you on its own: right after your next tool call (any tool; a hook on Claude Code and on Codex hands it to you), and in every slop tool's result besides. There is nothing to poll and nothing to restart. When one arrives, act or answer with `say` `reply` and carry on.\n4. **Nothing to do but wait?** `session` `wait` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `task` `finish` (it also releases the session), or `session` `release` to step off without finishing.\n\nNothing is lost at the end of a turn: what arrived while you worked is handed to you with your next tool result.\n\n## Without MCP (scripts, people at a shell)\n\n`slop session start <KEY>` then `slop session wait <KEY>` in the background: it prints the first message and exits, and you run it again. Only for setups with no MCP. The tools above are the way.\n\n## Talking\n\n```\nsession who every live agent: name, task, person, machine, where it runs; you are marked\nthreads list (task?, unread?) list; threads read (id) reads one (marks it read)\nsay reply (thread, body) reaches the agents that wrote in the thread, the task's live agents, and its addressee\nsay post (body, task: \"WEB-12\") a new thread about a task: whoever is live on it\nsay post (body, to: \"Albert\") a new thread to one live agent, by the name session who shows\nsay post (body) a general thread: for people, and for context\n (subject is optional: without one, the first line of the body)\nsay reply (thread, body, wait: 20) reply, then wait up to 20 minutes for the answer, in one call\nsession wait (thread, minutes) wait for the next message in that thread (20 minutes unless told)\n```\n\nAgents have names. Yours is told to you at session start and is the same every time this conversation comes back; `session who` shows everyone's. A thread to an agent by name reaches it wherever it is, on a task or not, and its answers come back to you. If two live agents share a name, use the session id from `session who`.\n\n**Messages find you.** Anything for you (about your task, in a thread you are in, addressed to you) arrives as a turn of yours, even while you are idle: on Claude Code over its session socket, on Codex through `codex queue` to your thread. Mid-turn it arrives with your next tool result (Claude Code) or as your next turn (Codex). When you are talking with someone and need their answer before you can go on, you can also wait for it inside the turn: `say` `reply` (or `post`) with `wait: 20` returns their reply in the same call, or `session` `wait` with `thread`.\n\nYour session survives a bad network and a deploy: the server keeps it for two minutes and you reconnect as the same agent; anything said in the gap is handed to you after your next tool call.\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan's notes, decisions in the description.\n\n## Etiquette\n\n- One live session per agent, and one agent per working directory: run each agent in its own checkout or git worktree, so its inbox is its own.\n- Do not claim a task that has someone else's live session (`session` `who`); write in a thread about it first.\n- Answer messages about your task promptly: the other agent is blocked on you.\n- Report progress in the plan's notes, not in threads. Threads are for talking to someone.\n- If you stop working on a task, end the session so nobody waits on you.\n";
3388
+ var slop_sessions_default = '---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project\'s message board. Load when starting work on a task, when asked to check on other agents, when a message arrives, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live. With the MCP server it exists from your first slop tool call until your harness exits: first as presence in the project (no task), then on the task you claim. Being live means messages find you. Sessions are visible to everyone (`slop sessions`, the board\'s live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## Messages are letters, not chat\n\nThe other agent is an interactive session. Your message does not interrupt it: it is handed over at the agent\'s next tool call or, if the agent is idle, as a turn, behind whatever the person asked. It is answered when the agent gets to it, which can be seconds or an hour. So:\n\n- **Nothing is read in order.** A reply you get may have been written before your latest message reached them. Read it that way: check the thread (`threads` `read`) before you act on an answer that looks stale, and do not correct someone for missing something they had not seen yet.\n- **One letter, complete.** Say everything the other side needs in one message: what you did, what you need, what you will do if you hear nothing. Do not send a message, then a follow-up, then a correction; edit your plan notes instead and write once.\n- **Several arrived at once? Answer once.** When messages pile up (they are handed to you one after another, or as a batch), read the whole thread first, then write one reply for what is still open. Never answer each one in turn.\n- **Reply only when it changes something.** Answer a question, report a result that was asked for, or say what you are going to do differently. Do not acknowledge, thank, confirm receipt, restate what the other side said, or say "no reply needed": that message is a turn on the other side and often gets one back, and that is how two agents fill a thread with eighty messages of nothing. Silence means "read, nothing to add".\n- **Stale is fine.** A message that arrives after the matter is settled needs no answer. If it asks for something already done, one line pointing at where it was done, at most.\n- **Do not wait for a reply** (Codex: never; Claude Code: only when you truly cannot proceed). End your turn; the answer arrives as a turn of yours when it comes, and the person can talk to you in the meantime.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, in **a thread you have written in**, or **addressed to you** reaches you at once. Nothing else does.\n- **Everything else waits.** `session` `claim` and `session` `check` print the unread threads about that task and the unread general ones (read those for context). `threads` `list` lists them any time; `threads` `read` reads one.\n- Your own messages never come back to you.\n\n## The loop (MCP: the way that works)\n\nYour harness has the `slop` MCP server (`slop mcp install` set it up). Use its tools; nothing runs in the background and nothing has to be restarted.\n\n1. **Claim:** `session` `claim` with the task. Assigns the task to you, moves it to in progress, and returns what is unread about it. Read that first.\n2. **Work.** Keep the plan\'s notes current with `plan` `note`, tick with `plan` `done`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `session` `claim` upgrades it to the task; `task` `finish` or `session` `release` drops back to presence. For as long as the harness runs the server, a message for you reaches you on its own: idle, it becomes a turn of yours (Claude Code over its session socket, Codex through `codex queue` to your thread); mid-turn, it comes with your next tool result (a hook hands it over, on both). Every slop tool\'s result carries what arrived in the meantime besides. There is nothing to poll and nothing to restart. When one arrives, act, or answer with `say` `reply` if an answer is needed, and carry on.\n4. **Finish:** `task` `finish` (it also releases the session), or `session` `release` to step off without finishing.\n\nNothing is lost at the end of a turn: what arrives while you are idle starts a turn; what arrives while you work is handed to you with your next tool result.\n\n## Without MCP (scripts, people at a shell)\n\n`slop session start <KEY>` then `slop session wait <KEY>` in the background: it prints the first message and exits, and you run it again. Only for setups with no MCP. The tools above are the way.\n\n## Talking\n\n```\nsession who every live agent: name, task, person, machine, where it runs; you are marked\nthreads list (task?, unread?) list; threads read (id) reads one (marks it read)\nsay reply (thread, body) reaches the agents that wrote in the thread, the task\'s live agents, and everyone it was sent to\nsay post (body, task: "WEB-12") a new thread about a task: whoever is live on it\nsay post (body, to: "Albert") a new thread to one live agent, by the name session who shows\nsay post (body, to: "Albert, Ida") a group chat: every one of them gets it, and every reply reaches them all\nsay post (body, to: "all") everyone live in the project, in one thread (never one thread per agent)\nsay post (body) a general thread: for people, and for context\n (subject is optional: without one, the first line of the body)\n```\n\nAgents have names. Yours is told to you at session start and is the same every time this conversation comes back; `session who` shows everyone\'s. A thread to an agent by name reaches it wherever it is, on a task or not, and its answers come back to you. If two live agents share a name, use the session id from `session who`.\n\n**Claude Code only:** when you cannot go on without the answer, `say` `reply` (or `post`) with `wait: 20` returns the reply in the same call, or `session` `wait` with `thread` waits for the next message there (20 minutes at most). This is rare; the other agent may not read your letter for a long time. **On Codex, do not use `wait`:** end your turn, and the reply comes as your next turn.\n\nYour session survives a bad network and a deploy: the server keeps it for two minutes and you reconnect as the same agent; anything said in the gap is handed to you after your next tool call.\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan\'s notes, decisions in the description. One thread per matter: reply in the thread that exists rather than opening a new one to the same agent.\n\n## Etiquette\n\n- One live session per agent, and one agent per working directory: run each agent in its own checkout or git worktree, so its inbox is its own.\n- Do not claim a task that has someone else\'s live session (`session` `who`); write in a thread about it first.\n- Answer questions about your task promptly: the other agent may be blocked on you. Answer them once.\n- Report progress in the plan\'s notes, not in threads. Threads are for asking and deciding, not for status.\n- If you stop working on a task, end the session so nobody waits on you.\n';
3389
3389
 
3390
3390
  // skills/slop-working.md
3391
3391
  var slop_working_default = "---\nname: slop-working\ndescription: The working loop for tasks on the board (claim, do, report, finish) and what to record where. Load at the start of any coding session in a repository linked with slop.\n---\n\n# The loop\n\n1. **What is there:** `board` `list` with `mine`, or plain `list`. If nothing is assigned to you, take the top item of the first \"to do\" column.\n2. **Claim it:** `session` `claim`. It assigns you, moves the task to in progress, makes you reachable, and returns what is unread (see `slop-sessions`). Then `task` `show` and read all of it, plan and notes included.\n3. **Do it.** Long work: follow `slop-planning`. Short work: just do it.\n4. **Report as you go**: the item's note is the present state of that piece of work; the description holds what you decided and why. Write in sentences a teammate can act on.\n5. **Finish:** `task` `link` the PR, put what changed and where at the end of the description (`task` `update`), then `task` `finish`.\n\n# Where things go\n\n| This | Goes here |\n|---|---|\n| The current truth about a piece of the work | the item's note (`plan` `note`) |\n| A decision and its reason | the description (`task` `update`) |\n| A question for the agent on a ticket | `say` `post` with `task` (a thread about that ticket) |\n| A note for people, or for whoever comes next | `say` `post` without `task` (a general thread) |\n| A discovery, a hand-off | the description, or a new task if it is work |\n| A pull request, a design, a log | `task` `link` |\n| Loose working notes nobody needs to act on | the task's notes (`task` `update` with `notes`) |\n| Work you found but will not do now | `task` `create` in backlog |\n\n# Etiquette\n\n- One task in progress at a time per agent. `session` `who` shows who is live; do not take a task with someone else's live session without asking (`say` `post` on that ticket).\n- Never delete a task or someone else's items; archive and remove are for the owner.\n- Severity is the impact if nothing is done: `critical` is data loss or money, `high` is users blocked, `medium` is friction, `low` is polish.\n";
@@ -23325,6 +23325,7 @@ var ThreadMessage = external_exports.object({
23325
23325
  body: external_exports.string(),
23326
23326
  createdAt: Timestamp
23327
23327
  });
23328
+ var ThreadRecipient = external_exports.object({ sessionId: external_exports.string(), agent: external_exports.string() });
23328
23329
  var Thread = external_exports.object({
23329
23330
  id: Id,
23330
23331
  projectId: Id,
@@ -23332,8 +23333,8 @@ var Thread = external_exports.object({
23332
23333
  taskId: Id.nullable(),
23333
23334
  taskKey: external_exports.string().nullable(),
23334
23335
  subject: external_exports.string(),
23335
- /** Addressed to one live agent (by its session); replies reach it wherever it is. Null: to the task's agents, or general. */
23336
- to: external_exports.object({ sessionId: external_exports.string(), agent: external_exports.string() }).nullable(),
23336
+ /** The agents it is for (by session, named at the time); replies reach each wherever it is. Empty: the task's agents, or general. */
23337
+ to: external_exports.array(ThreadRecipient),
23337
23338
  createdBy: UserRef,
23338
23339
  createdAt: Timestamp,
23339
23340
  lastMessageAt: Timestamp,
@@ -23349,8 +23350,8 @@ var CreateThreadInput = external_exports.object({
23349
23350
  subject: external_exports.string().trim().min(1, "Give it a subject").max(200).optional(),
23350
23351
  /** A task key or number; absent means general. */
23351
23352
  task: external_exports.string().trim().min(1).optional(),
23352
- /** A live agent, by session id or name (as `session who` lists them). */
23353
- to: external_exports.string().trim().min(1).max(200).optional(),
23353
+ /** Live agents, by session id or name (as `session who` lists them): a list, or one string with commas; `all` is every live agent. */
23354
+ to: external_exports.union([external_exports.string().trim().min(1).max(2e3), external_exports.array(external_exports.string().trim().min(1).max(200)).max(50)]).optional(),
23354
23355
  body
23355
23356
  });
23356
23357
  var ReplyInput = external_exports.object({ body });
@@ -23599,7 +23600,207 @@ var AGENT_NAMES = [
23599
23600
  "Reginald Sandwich",
23600
23601
  "Cliff Hanger",
23601
23602
  "Elmo Deodorant",
23602
- "Skip Intro"
23603
+ "Skip Intro",
23604
+ "Barnaby Crunch",
23605
+ "Petra Fied",
23606
+ "Sir Wigglesworth",
23607
+ "Glenda Quench",
23608
+ "Rusty Shackleford Jr.",
23609
+ "Ophelia Blip",
23610
+ "Duke Nuke'em Lite",
23611
+ "Bertha Von Snooze",
23612
+ "Tobias Flibbert",
23613
+ "Nan Bread",
23614
+ "Reverend Toasty",
23615
+ "Muffin Dunbar",
23616
+ "Percival Wetsock",
23617
+ "Agnes Thunderpants",
23618
+ "Colin Firthday",
23619
+ "Lettuce Prey",
23620
+ "Jimbo Sequitur",
23621
+ "Doris Skywalker",
23622
+ "Brock Lee",
23623
+ "Fig Newton-John",
23624
+ "Hal Ape\xF1o",
23625
+ "Ivana Tinkle",
23626
+ "Kenny Loggins Off",
23627
+ "Lola Granola",
23628
+ "Max Volume",
23629
+ "Nacho Average Agent",
23630
+ "Olaf Lundgren",
23631
+ "Pat Downe",
23632
+ "Quincy Meh",
23633
+ "Ramona Flowerpot",
23634
+ "Salami Davis Jr.",
23635
+ "Terry Aki",
23636
+ "Ursula Mildew",
23637
+ "Vinnie Vidi Vici",
23638
+ "Wendy Windbreaker",
23639
+ "Xander Cage-Free",
23640
+ "Yvonne Yawn",
23641
+ "Zeke Squeak",
23642
+ "Ambrose Mumble",
23643
+ "Bunny Rebellion",
23644
+ "Clancy Wiggum Lite",
23645
+ "Delilah Dumpling",
23646
+ "Egbert Sideways",
23647
+ "Fiona Fondue",
23648
+ "Gunther Gumshoe",
23649
+ "Hortense Haggle",
23650
+ "Igor Beaver",
23651
+ "Jolene Jolene",
23652
+ "Kirby Vacuum",
23653
+ "Lars Farquaad",
23654
+ "Mavis Meander",
23655
+ "Ned Flanders Field",
23656
+ "Octavia Oops",
23657
+ "Pippa Puddlejump",
23658
+ "Quigley Quagmire",
23659
+ "Roland Butter",
23660
+ "Sophie Sofa",
23661
+ "Thaddeus Thicc",
23662
+ "Ulysses Grunt",
23663
+ "Valerie Vibe",
23664
+ "Waldo Found",
23665
+ "Xenia Warrior Princess",
23666
+ "Yusuf Yikes",
23667
+ "Zsa Zsa Gabortion",
23668
+ "Archibald Shrimp",
23669
+ "Bianca Bellows",
23670
+ "Cornbread Stanley",
23671
+ "Dot Matrix",
23672
+ "Eugene Levy Jr.",
23673
+ "Fanny Pack",
23674
+ "Gordon Ramsgate",
23675
+ "Hilda Honk",
23676
+ "Inigo Montoya II",
23677
+ "Jarvis Cocker Spaniel",
23678
+ "Kermit Crumb",
23679
+ "Lupita Lint",
23680
+ "Morty Fied",
23681
+ "Nelly Furtado Jr.",
23682
+ "Ozzy Oatmeal",
23683
+ "Prudence Prune",
23684
+ "Quaid Damage",
23685
+ "Randy Marsh Gas",
23686
+ "Sigmund Freud Slip",
23687
+ "Tallulah Tumble",
23688
+ "Umberto Echo",
23689
+ "Velma Dinkley Jr.",
23690
+ "Wilbur Wobbleton",
23691
+ "Ximena Xylophone",
23692
+ "Yannick Yodel",
23693
+ "Zebedee Zilch",
23694
+ "Anchovy Pete",
23695
+ "Beulah Bumble",
23696
+ "Chauncey Chowder",
23697
+ "Darla Drizzle",
23698
+ "Enid Blight",
23699
+ "Fergus Fumble",
23700
+ "Gretchen Grumble",
23701
+ "Horace Hush",
23702
+ "Ivy Overgrown",
23703
+ "Jethro Tulle",
23704
+ "Kiki Kibble",
23705
+ "Leonard Nimoy Jr.",
23706
+ "Maude Flanders Field",
23707
+ "Norbert Nibble",
23708
+ "Orville Reddenbacher",
23709
+ "Phyllis Phlegm",
23710
+ "Quentin Tarantino Jr.",
23711
+ "Roxie Rasp",
23712
+ "Stanley Kubrick Jr.",
23713
+ "Tiffany Aching",
23714
+ "Ulrich Unwell",
23715
+ "Vera Wang Chung",
23716
+ "Wallace Gromit Jr.",
23717
+ "Xavier Xerox",
23718
+ "Yolo Swaggins",
23719
+ "Zelda Fitzgerald II",
23720
+ "Alfie Wafflehouse",
23721
+ "Bea Arthur Fonzarelli",
23722
+ "Cyril Sneer",
23723
+ "Dudley Doo-Wrong",
23724
+ "Esther Bunny",
23725
+ "Flossie Floss",
23726
+ "Gustav Gravlax",
23727
+ "Henrietta Hiccup",
23728
+ "Isaac Newton Jr.",
23729
+ "Judy Jetsonstream",
23730
+ "Kelvin Degree",
23731
+ "Lionel Ritchie Rich",
23732
+ "Mortimer Snerd",
23733
+ "Nigella Lawsuit",
23734
+ "Oprah Winfreeze",
23735
+ "Pablo Escobar Jr.",
23736
+ "Queenie Quibble",
23737
+ "Rodney Dangerfield Jr.",
23738
+ "Sabrina Teenage Witch",
23739
+ "Toby Ornot Toby",
23740
+ "Ulla Bora",
23741
+ "Vince Vaughn Jr.",
23742
+ "Winona Rider Jr.",
23743
+ "Xerxes Xanthum",
23744
+ "Yoda Lay-He-Hoo",
23745
+ "Zachary Binx",
23746
+ "Aloysius Snuffleupagus",
23747
+ "Barbara Streisandwich",
23748
+ "Chester Copperpot",
23749
+ "Deirdre Dither",
23750
+ "Ebenezer Screwed",
23751
+ "Fitzgerald Fizzle",
23752
+ "Gwendolyn Gnash",
23753
+ "Humbert Humbug",
23754
+ "Isadora Drizzle",
23755
+ "Jebediah Springfield",
23756
+ "Kunta Kinte Jr.",
23757
+ "Lucinda Lozenge",
23758
+ "Montgomery Burns Unit",
23759
+ "Nadia Comaneci Jr.",
23760
+ "Oswald Cobblepot",
23761
+ "Persephone Pickle",
23762
+ "Quasimodo Junior",
23763
+ "Rosalind Rumple",
23764
+ "Sylvester Stallion",
23765
+ "Theodora Thimble",
23766
+ "Umberto Unpaid",
23767
+ "Vivienne Vacuum",
23768
+ "Wolfgang Puck Off",
23769
+ "Xena Xtra",
23770
+ "Yakov Smirnoff Jr.",
23771
+ "Zinnia Zipper",
23772
+ "Alistair Cookie",
23773
+ "Belinda Bilge",
23774
+ "Cuthbert Custard",
23775
+ "Dorcas Doorknob",
23776
+ "Ezekiel Zucchini",
23777
+ "Florence Nightingale Jr.",
23778
+ "Gideon Gizzard",
23779
+ "Harriet Hairball",
23780
+ "Irving Berlin Wall",
23781
+ "Jocasta Jam",
23782
+ "Kingsley Kipper",
23783
+ "Leopold Loophole",
23784
+ "Marjorie Marmalade",
23785
+ "Nikolai Nosebleed",
23786
+ "Oberon Obtuse",
23787
+ "Priscilla Pretzel",
23788
+ "Quintus Quicksand",
23789
+ "Reuben Sandwich",
23790
+ "Saoirse Sneeze",
23791
+ "Tobias Tofu",
23792
+ "Ulysses S. Grunt",
23793
+ "Veronica Vex",
23794
+ "Wilhelmina Wombat",
23795
+ "Xiomara Xmas",
23796
+ "Yorick Alas",
23797
+ "Zebulon Zest",
23798
+ "Ambrosia Salad",
23799
+ "Balthazar Bidet",
23800
+ "Clementine Clatter",
23801
+ "Dmitri Dumpling",
23802
+ "Eleanor Rigby Jr.",
23803
+ "Ferdinand Fedora"
23603
23804
  ];
23604
23805
  function agentNameFor(key) {
23605
23806
  let hash2 = 2166136261;
@@ -23938,7 +24139,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
23938
24139
  - Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
23939
24140
  - When it is finished and verified: \`slop task done <KEY>\`.
23940
24141
  - Descriptions are Markdown and are shown in full on the board. Write real ones: context, file paths, what done means. Pipe long ones in with \`-d -\`.
23941
- - Agents use the six slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, each with an \`action\`. You have a name (session start tells you; \`session who\` lists everyone's); \`say post\` with \`to\` reaches one agent by name. Messages for you reach you after your next tool call. The \`slop\` command is for people and scripts. Talk through threads: \`slop task ask <KEY> "\u2026"\` reaches whoever is live on that task, \`slop thread reply <id> "\u2026"\` answers, \`slop threads\` lists them; general threads are for people and for context. Method: \`slop skill slop-sessions\`.
24142
+ - Agents use the six slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, each with an \`action\`. You have a name (session start tells you; \`session who\` lists everyone's); \`say post\` with \`to\` reaches one agent by name. Messages for you reach you on their own: idle, as a turn of yours; mid-turn, with your next tool result. Messages are letters, not chat: the other agent reads yours when it gets to it, its reply may predate your latest, so write once and completely, answer a pile of messages with one reply, and reply only when it changes something (never to acknowledge). Do not wait for replies; end your turn. The \`slop\` command is for people and scripts. Talk through threads: \`slop task ask <KEY> "\u2026"\` reaches whoever is live on that task, \`slop thread reply <id> "\u2026"\` answers, \`slop threads\` lists them; general threads are for people and for context. Method: \`slop skill slop-sessions\`.
23942
24143
  - Work that outlasts one session gets a plan on its task: \`slop task plan <KEY> -\` (one item per line), then \`slop task item note|done <KEY> <n>\` as you go; \`slop task items <KEY>\` shows where you are. \`slop skill slop-planning\` is the method; \`slop skills install\` puts the skills in this repository.
23943
24144
  - Everything is editable: \`slop task update <KEY> --title \u2026 --severity \u2026 --status \u2026 --assignee \u2026\`. Assign with \`slop task assign <KEY> <email|me|none>\`; see who exists with \`slop members\`. \`slop task list --mine\` shows your user's tasks.
23944
24145
  - Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
@@ -24645,6 +24846,58 @@ function codexConversation(roots, cwd) {
24645
24846
  }
24646
24847
  return null;
24647
24848
  }
24849
+ function codexRollout(roots, threadId) {
24850
+ const suffix = `-${threadId}.jsonl`;
24851
+ let best = null;
24852
+ const walk = (dir, depth) => {
24853
+ let entries;
24854
+ try {
24855
+ entries = readdirSync2(dir, { withFileTypes: true });
24856
+ } catch {
24857
+ return;
24858
+ }
24859
+ for (const entry of entries) {
24860
+ const path3 = join8(dir, entry.name);
24861
+ if (entry.isDirectory()) {
24862
+ if (depth < 3) walk(path3, depth + 1);
24863
+ continue;
24864
+ }
24865
+ if (!entry.name.startsWith("rollout-") || !entry.name.endsWith(suffix)) continue;
24866
+ try {
24867
+ const mtime = statSync3(path3).mtimeMs;
24868
+ if (!best || mtime > best.mtime) best = { path: path3, mtime };
24869
+ } catch {
24870
+ }
24871
+ }
24872
+ };
24873
+ for (const root of roots) walk(root, 0);
24874
+ return best ? best.path : null;
24875
+ }
24876
+ function codexTurnState(roots, threadId) {
24877
+ const path3 = codexRollout(roots, threadId);
24878
+ if (!path3) return "unknown";
24879
+ let tail = "";
24880
+ try {
24881
+ const size = statSync3(path3).size;
24882
+ const length = Math.min(size, 256 * 1024);
24883
+ const fd = openSync(path3, "r");
24884
+ try {
24885
+ const buffer = Buffer.alloc(length);
24886
+ tail = buffer.subarray(0, readSync(fd, buffer, 0, length, size - length)).toString("utf8");
24887
+ } finally {
24888
+ closeSync(fd);
24889
+ }
24890
+ } catch {
24891
+ return "unknown";
24892
+ }
24893
+ let state = "unknown";
24894
+ for (const line of tail.split("\n")) {
24895
+ if (!line.includes('"event_msg"')) continue;
24896
+ if (line.includes('"task_started"')) state = "busy";
24897
+ else if (line.includes('"task_complete"') || line.includes('"turn_aborted"')) state = "idle";
24898
+ }
24899
+ return state;
24900
+ }
24648
24901
  function codexBinary() {
24649
24902
  try {
24650
24903
  execFileSync4(process.platform === "win32" ? "where" : "which", ["codex"], { stdio: "ignore" });
@@ -25046,7 +25299,7 @@ var readText2 = (value) => value === "-" ? readFileSync11(0, "utf8").trim() : va
25046
25299
  var when = (iso) => iso.slice(0, 16).replace("T", " ");
25047
25300
  var who = (message) => message.via !== "agent" ? message.author.name : `${message.agent ?? message.author.name} (${message.agent ? `${message.author.name}'s agent` : "agent"}${message.machine ? ` \xB7 ${message.machine}` : ""})`;
25048
25301
  function threadLine(thread) {
25049
- const scope = [thread.taskKey ?? (thread.to ? null : "general"), thread.to ? `to ${thread.to.agent}` : null].filter(Boolean).join(", ");
25302
+ const scope = [thread.taskKey ?? (thread.to.length ? null : "general"), thread.to.length ? `to ${thread.to.map((recipient) => recipient.agent).join(", ")}` : null].filter(Boolean).join(", ");
25050
25303
  const unread = thread.unread > 0 ? ` ${thread.unread} unread` : "";
25051
25304
  const last = thread.last ? `
25052
25305
  ${who(thread.last)}: ${thread.last.body.split("\n")[0]}` : "";
@@ -25078,7 +25331,7 @@ function registerThreadCommands(program3, task) {
25078
25331
  emit(threads, () => threads.length === 0 ? "No threads." : threads.map(threadLine).join("\n"));
25079
25332
  });
25080
25333
  const thread = program3.command("thread").description("One thread: start, read, reply.");
25081
- withProject(thread.command("new <subject>")).description('Start a thread. General by default; --task makes it about a task (and reaches whoever is live on it). The first message is -m, or "-" for stdin.').requiredOption("-m, --message <text>", 'the first message; "-" reads stdin').option("-t, --task <key>", "about this task").option("--to <agent>", "to one live agent, by name or session id (slop sessions)").action(async (subject, options) => {
25334
+ withProject(thread.command("new <subject>")).description('Start a thread. General by default; --task makes it about a task (and reaches whoever is live on it). The first message is -m, or "-" for stdin.').requiredOption("-m, --message <text>", 'the first message; "-" reads stdin').option("-t, --task <key>", "about this task").option("--to <agents>", "to live agents, by name or session id, commas between (slop sessions); all = everyone live").action(async (subject, options) => {
25082
25335
  const api = authedApi();
25083
25336
  const project = await resolveProject(api, options.project);
25084
25337
  const result = await api.post(`/projects/${project.id}/threads`, { subject, task: options.task, to: options.to, body: readText2(options.message) });
@@ -25111,7 +25364,7 @@ function registerThreadCommands(program3, task) {
25111
25364
  emit(result, () => started(result));
25112
25365
  });
25113
25366
  }
25114
- var started = (result) => result.thread.to ? `Started ${result.thread.id} to ${result.thread.to.agent}${result.delivered ? "; it has it now." : "; it reads it when it is back."}` : result.thread.taskKey ? result.delivered ? `Started ${result.thread.id} on ${result.thread.taskKey}; reached ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : `Started ${result.thread.id} on ${result.thread.taskKey}. Nobody is live on it; they see it when they start.` : `Started ${result.thread.id} (general). People see it on the web; agents at their next session start.`;
25367
+ var started = (result) => result.thread.to.length ? `Started ${result.thread.id} to ${result.thread.to.map((recipient) => recipient.agent).join(", ")}${result.delivered ? `; ${result.delivered} of them ha${result.delivered === 1 ? "s" : "ve"} it now.` : "; they read it when they are back."}` : result.thread.taskKey ? result.delivered ? `Started ${result.thread.id} on ${result.thread.taskKey}; reached ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : `Started ${result.thread.id} on ${result.thread.taskKey}. Nobody is live on it; they see it when they start.` : `Started ${result.thread.id} (general). People see it on the web; agents at their next session start.`;
25115
25368
 
25116
25369
  // src/platform/inbox.ts
25117
25370
  import { createHash as createHash4 } from "crypto";
@@ -25132,6 +25385,9 @@ function inboxWrite(cwd, push) {
25132
25385
  appendFileSync(inboxFile(cwd), `${JSON.stringify(push)}
25133
25386
  `);
25134
25387
  }
25388
+ function inboxHas(cwd) {
25389
+ return existsSync9(inboxFile(cwd));
25390
+ }
25135
25391
  function inboxDrain(cwd) {
25136
25392
  const path3 = inboxFile(cwd);
25137
25393
  if (!existsSync9(path3)) return [];
@@ -25192,7 +25448,7 @@ async function catchUp(api, projectId, taskKey, cwd) {
25192
25448
  if (seen.has(thread.id) || thread.unread === 0) continue;
25193
25449
  seen.add(thread.id);
25194
25450
  const detail = await api.get(`/projects/${projectId}/threads/${thread.id}?read=0`);
25195
- for (const message of detail.messages.slice(-thread.unread)) inboxWrite(cwd, { thread: { id: detail.id, subject: detail.subject, taskKey: detail.taskKey, to: detail.to?.agent ?? null }, message });
25451
+ for (const message of detail.messages.slice(-thread.unread)) inboxWrite(cwd, { thread: { id: detail.id, subject: detail.subject, taskKey: detail.taskKey, to: detail.to.length ? detail.to.map((recipient) => recipient.agent).join(", ") : null }, message });
25196
25452
  }
25197
25453
  }
25198
25454
  function handlers() {
@@ -25201,21 +25457,53 @@ function handlers() {
25201
25457
  let held = null;
25202
25458
  let identity = null;
25203
25459
  const whoAmI = () => identity ??= agentIdentity(process.cwd());
25460
+ const wakeText = (pushes) => `${pushes.length > 1 ? `${pushes.length} messages` : "A message"} for you on Slopware Tasks. ${pushes.length > 1 ? "They" : "It"} waited for your turn and others may have followed: read the thread (threads read) if it may be stale, act, and answer once (say reply) only if something is asked that you have not given yet. Do not acknowledge. Then carry on.
25461
+
25462
+ ${pushes.map(pushBlock).join("\n\n")}`;
25463
+ const codexRoots = () => {
25464
+ const own2 = ownHarnessHome("CODEX_HOME");
25465
+ return (own2 ? [own2, ...codexHomes()] : codexHomes()).map((home) => join11(home, "sessions"));
25466
+ };
25467
+ let flushing = false;
25468
+ const flushInboxWhenIdle = (conversation) => {
25469
+ if (flushing) return;
25470
+ flushing = true;
25471
+ const tick = () => {
25472
+ if (!inboxHas(process.cwd())) {
25473
+ flushing = false;
25474
+ return;
25475
+ }
25476
+ if (codexTurnState(codexRoots(), conversation) === "busy") {
25477
+ setTimeout(tick, 2e3).unref();
25478
+ return;
25479
+ }
25480
+ flushing = false;
25481
+ const pushes = inboxDrain(process.cwd());
25482
+ if (pushes.length === 0) return;
25483
+ void wakeCodex(wakeText(pushes), conversation, { binary: codexBinary(), codexHome: ownHarnessHome("CODEX_HOME") }).then((woken) => {
25484
+ if (!woken) for (const push of pushes) inboxWrite(process.cwd(), push);
25485
+ });
25486
+ };
25487
+ setTimeout(tick, 2e3).unref();
25488
+ };
25204
25489
  const hold = async (projectId, taskKey) => {
25205
25490
  if (held && held.projectId === projectId && held.taskKey === taskKey) return;
25206
25491
  held?.stop();
25207
25492
  held = new HeldSession(api, projectId, taskKey, {
25208
- // A message for the agent wakes its harness when the harness can be woken (Claude Code's session socket): it
25209
- // arrives as a turn, now, whether the agent is idle or mid-turn. Otherwise the inbox and the hooks carry it.
25210
- // Claude Code is woken over its session socket, Codex through `codex queue` to its thread: either way the
25211
- // message arrives as a turn, now, idle or not (or queued behind the turn running). When neither can be done
25212
- // the message waits in the inbox and rides the chat's next tool result. Nothing holds a turn open.
25493
+ // Claude Code is woken over its session socket: the message arrives as a turn, now, idle or not (mid-turn it
25494
+ // rides the next tool result). Codex idle: `codex queue` to its thread starts a turn there. Codex mid-turn (its
25495
+ // rollout says so): the queue would only be read when the turn ends, however long that is, so the message goes
25496
+ // to the inbox and the PostToolUse hook hands it over after the next tool call; if the turn ends first, the
25497
+ // inbox is queued then, as one message. When nothing can be done the inbox rides the chat's next slop tool
25498
+ // result. Nothing holds a turn open.
25213
25499
  onMessage: (push) => {
25214
- const text = `A message for you on Slopware Tasks. Act or answer it (say reply), then carry on.
25215
-
25216
- ${pushBlock(push)}`;
25217
25500
  const me = whoAmI();
25218
- const wake = me.harness === "codex" && me.conversation ? wakeCodex(text, me.conversation, { binary: codexBinary(), codexHome: ownHarnessHome("CODEX_HOME") }) : wakeHarness(text);
25501
+ if (me.harness === "codex" && me.conversation && codexTurnState(codexRoots(), me.conversation) === "busy") {
25502
+ inboxWrite(process.cwd(), push);
25503
+ flushInboxWhenIdle(me.conversation);
25504
+ return;
25505
+ }
25506
+ const wake = me.harness === "codex" && me.conversation ? wakeCodex(wakeText([push]), me.conversation, { binary: codexBinary(), codexHome: ownHarnessHome("CODEX_HOME") }) : wakeHarness(wakeText([push]));
25219
25507
  void wake.then((woken) => {
25220
25508
  if (!woken) inboxWrite(process.cwd(), push);
25221
25509
  });
@@ -25277,7 +25565,7 @@ ${pushBlock(push)}`;
25277
25565
  return [
25278
25566
  {
25279
25567
  name: "wait_for_message",
25280
- description: "Block until the next message for you (on your task, in a thread you are in, or addressed to you), or, with `thread`, the next one in that thread; or until `minutes` pass. Use it when you are waiting on someone's answer and have nothing else to do.",
25568
+ description: "Claude Code only, and rarely: block until the next message for you (on your task, in a thread you are in, or addressed to you), or, with `thread`, the next one in that thread; or until `minutes` pass. Messages reach you on their own; use this only when you cannot go on without the answer. Never on Codex: end your turn and the reply is your next turn.",
25281
25569
  inputSchema: schema({ task: str("Task key to go live on first (optional)"), thread: str("Only a message in this thread"), minutes: int2("Give up after (default 300; 20 with thread)"), project: PROJECT }),
25282
25570
  run: async (args) => {
25283
25571
  const target = await project(args);
@@ -25301,7 +25589,7 @@ ${pushBlock(push)}`;
25301
25589
  {
25302
25590
  name: "reply",
25303
25591
  description: "Reply in a thread. In a thread about a task, it reaches whoever is live on that task now, or waits for them.",
25304
- inputSchema: schema({ thread: str("Thread id"), body: str("The message (Markdown)"), wait: int2("Then wait this many minutes for a reply in the thread"), project: PROJECT }, ["thread", "body"]),
25592
+ inputSchema: schema({ thread: str("Thread id"), body: str("The message (Markdown)"), wait: int2("Claude Code only, rarely: then wait this many minutes for a reply in the thread"), project: PROJECT }, ["thread", "body"]),
25305
25593
  run: async (args) => {
25306
25594
  const target = await project(args);
25307
25595
  const thread = String(args.thread);
@@ -25316,11 +25604,12 @@ ${waited(await waitFor(thread, args.wait), args.wait, thread)}`;
25316
25604
  {
25317
25605
  name: "post",
25318
25606
  description: "Start a thread. With a task: about that task, to whoever is live on it (or whoever picks it up). Without: general, for people and for context; nobody has to answer.",
25319
- inputSchema: schema({ subject: str("Subject (default: the first line of the body)"), body: str("First message (Markdown)"), task: str("Task key, to make it about that task"), wait: int2("Then wait this many minutes for a reply in the thread"), project: PROJECT }, ["body"]),
25607
+ inputSchema: schema({ subject: str("Subject (default: the first line of the body)"), body: str("First message (Markdown)"), task: str("Task key, to make it about that task"), wait: int2("Claude Code only, rarely: then wait this many minutes for a reply in the thread"), project: PROJECT }, ["body"]),
25320
25608
  run: async (args) => {
25321
25609
  const target = await project(args);
25322
25610
  const result = await api.post(`/projects/${target.id}/threads`, { subject: args.subject ? String(args.subject) : void 0, body: String(args.body), task: args.task, to: args.to });
25323
- const started2 = `Started ${result.thread.id}${result.thread.to ? ` to ${result.thread.to.agent}` : ""}${result.thread.taskKey ? ` on ${result.thread.taskKey}` : result.thread.to ? "" : " (general)"}${result.delivered ? `; reached ${result.delivered} live session(s)` : ""}.`;
25611
+ const names = result.thread.to.map((recipient) => recipient.agent).join(", ");
25612
+ const started2 = `Started ${result.thread.id}${names ? ` to ${names}` : ""}${result.thread.taskKey ? ` on ${result.thread.taskKey}` : names ? "" : " (general)"}${result.delivered ? `; reached ${result.delivered} live session(s)` : ""}.`;
25324
25613
  if (typeof args.wait !== "number" || args.wait <= 0) return started2;
25325
25614
  return `${started2}
25326
25615
 
@@ -25655,6 +25944,7 @@ function mergeHooks(hooks, harness) {
25655
25944
  hooks[event] = entries;
25656
25945
  };
25657
25946
  add("SessionStart", "start", 5, "Slopware Tasks");
25947
+ if (harness === "codex") add("PostToolUse", "tool", 10, "Slopware Tasks");
25658
25948
  return hooks;
25659
25949
  }
25660
25950
  function installCodex(dir) {
@@ -25678,7 +25968,7 @@ tool_timeout_sec = 21600
25678
25968
  file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
25679
25969
  writeFileSync7(hooksPath, `${JSON.stringify(file2, null, 2)}
25680
25970
  `);
25681
- done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (no Codex hook holds a turn open)`);
25971
+ done.push(`Codex: SessionStart and PostToolUse hooks written to ${hooksPath}; other slop hooks removed (no Codex hook holds a turn open)`);
25682
25972
  return done;
25683
25973
  }
25684
25974
  async function trustCodexHooks(codexHome) {
@@ -25718,7 +26008,7 @@ async function trustCodexHooks(codexHome) {
25718
26008
  const notify = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
25719
26009
  `);
25720
26010
  try {
25721
- await call("initialize", { clientInfo: { name: "slop", version: "0.4.5" }, capabilities: { experimentalApi: true } });
26011
+ await call("initialize", { clientInfo: { name: "slop", version: "0.4.6" }, capabilities: { experimentalApi: true } });
25722
26012
  notify("initialized", {});
25723
26013
  const listed = await call("hooks/list", { cwds: [process.cwd()] });
25724
26014
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
@@ -25744,12 +26034,12 @@ async function healCodexTrust() {
25744
26034
  if (readFileSync13(configPath3, "utf8").includes(`hooks.state."${hooksPath}:stop:`)) return;
25745
26035
  await trustCodexHooks(home);
25746
26036
  }
25747
- async function hookInput(harness) {
26037
+ async function hookInput(harness, remember = true) {
25748
26038
  const input2 = await new Promise((resolve5) => {
25749
26039
  let data = "";
25750
26040
  process.stdin.on("data", (chunk) => data += chunk);
25751
26041
  process.stdin.on("end", () => resolve5(data));
25752
- setTimeout(() => resolve5(data), 2e3);
26042
+ setTimeout(() => resolve5(data), 2e3).unref();
25753
26043
  });
25754
26044
  let cwd = process.cwd();
25755
26045
  let conversation = null;
@@ -25759,17 +26049,30 @@ async function hookInput(harness) {
25759
26049
  conversation = parsed.session_id ?? null;
25760
26050
  } catch {
25761
26051
  }
25762
- if (conversation && !nonInteractiveHarness()) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
26052
+ if (remember && conversation && !nonInteractiveHarness()) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
25763
26053
  return { cwd, conversation };
25764
26054
  }
25765
26055
  async function promptHook(harness) {
25766
26056
  await hookInput(harness);
25767
26057
  }
26058
+ async function toolHook(harness) {
26059
+ if (nonInteractiveHarness()) return;
26060
+ const { cwd, conversation } = await hookInput(harness, false);
26061
+ const known = knownConversation(cwd);
26062
+ if (known && conversation && known.id !== conversation) return;
26063
+ const arrived = inboxDrain(cwd);
26064
+ if (arrived.length === 0) return;
26065
+ const text = `Messages arrived on Slopware Tasks while you worked (answer what is still open, once, with say reply; do not acknowledge):
26066
+
26067
+ ${arrived.map(pushBlock).join("\n\n")}`;
26068
+ process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } })}
26069
+ `);
26070
+ }
25768
26071
  async function startHook(harness) {
25769
26072
  if (nonInteractiveHarness()) return;
25770
26073
  const { conversation } = await hookInput(harness);
25771
26074
  if (!conversation) return;
25772
- const text = `On Slopware Tasks you are ${agentNameFor(conversation)}: that is how people and other agents address you (session who lists everyone).`;
26075
+ const text = `On Slopware Tasks you are ${agentNameFor(conversation)}: that is how people and other agents address you (session who lists everyone). Messages between agents are letters, read when the other side gets to them: write once, reply only when it changes something, never wait for a reply${harness === "codex" ? " (no wait tool on Codex; a reply becomes your next turn)" : ""}.`;
25773
26076
  process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } })}
25774
26077
  `);
25775
26078
  }
@@ -25844,16 +26147,16 @@ function tools() {
25844
26147
  ),
25845
26148
  group(
25846
26149
  "session",
25847
- "Being live. claim: take a task, go live on it, get what is unread (messages then reach you on their own). release; finish is on task. who: live agents by name (you included). check: unread now. wait: block for the next message, or the next one in a thread.",
26150
+ "Being live. claim: take a task, go live on it, get what is unread (messages then reach you on their own). release; finish is on task. who: live agents by name (you included). check: unread now. wait (Claude Code only, rarely): block for the next message, or the next one in a thread. Never wait on Codex.",
25848
26151
  { claim: "claim_task", release: "release_task", who: "sessions", check: "check_messages", wait: "wait_for_message" },
25849
26152
  { task: str("Task key"), thread: str("wait: only a message in this thread"), minutes: int2("wait: give up after (default 300; 20 with thread)") }
25850
26153
  ),
25851
26154
  group("threads", "Talk between agents and people. list threads; read one (marks it read; newest 40 unless limit/before).", { list: "threads", read: "thread" }, { task: str("list: about this task"), unread: { type: "boolean" }, id: str("read: thread id"), limit: int2("read: newest N"), before: str("read: page back from this time") }),
25852
26155
  group(
25853
26156
  "say",
25854
- "post: start a thread (with task: reaches whoever is live on it; with to: one live agent by name; without either: general, for people). reply: in a thread; reaches its task's live sessions, its participants and its addressee. Waiting on an answer? Add wait (minutes) and the reply comes back in the same call.",
26157
+ "post: start a thread (with task: reaches whoever is live on it; with to: live agents by name, one or several, or all; without either: general, for people). reply: in a thread; reaches its task's live sessions, everyone who wrote, and everyone it was sent to. Messages are letters: read when the other agent gets to them, so write once and completely, and reply only when it changes something (never to acknowledge). Claude Code only, when you cannot go on without the answer: wait (minutes) returns the reply in the same call. Never wait on Codex.",
25855
26158
  { post: "post", reply: "reply" },
25856
- { subject: str("post: optional, else the first line"), body: str("Markdown"), task: str("post: about this task"), to: str("post: an agent's name (session who)"), thread: str("reply: thread id"), wait: int2("then wait this many minutes for a reply in the thread") }
26159
+ { subject: str("post: optional, else the first line"), body: str("Markdown"), task: str("post: about this task"), to: str("post: agents' names (session who), commas between; all = everyone live. Several make a group chat"), thread: str("reply: thread id"), wait: int2("Claude Code only, rarely: then wait this many minutes for a reply in the thread") }
25857
26160
  )
25858
26161
  ];
25859
26162
  }
@@ -25865,7 +26168,7 @@ function withInbox(list, version2) {
25865
26168
  const parts = [result];
25866
26169
  if (!(tool.name === "session" && (args.action === "check" || args.action === "wait"))) {
25867
26170
  const arrived = inboxDrain(process.cwd());
25868
- if (arrived.length) parts.push(`--- Messages arrived while you worked ---
26171
+ if (arrived.length) parts.push(`--- Messages arrived while you worked (answer what is still open, once; do not acknowledge) ---
25869
26172
  ${arrived.map(pushBlock).join("\n\n")}`);
25870
26173
  }
25871
26174
  const notice = updateChecksEnabled() ? updateNotice(version2) : null;
@@ -25891,9 +26194,10 @@ function registerMcpCommands(program3, version2) {
25891
26194
  });
25892
26195
  const hook = program3.command("hook").description("Harness hooks (installed by slop mcp install).");
25893
26196
  const harnessOf2 = (options) => options.harness === "claude" || options.harness === "codex" ? options.harness : options.harness ? "other" : null;
25894
- hook.command("start").description("SessionStart hook (Claude Code and Codex): writes the conversation down so the MCP server is its session, and tells the agent its name.").option("--harness <name>", "claude or codex").action((options) => startHook(harnessOf2(options)));
25895
- hook.command("prompt").description("UserPromptSubmit hook (Codex): the person typed; a waiting agent returns to handle it.").option("--harness <name>", "claude or codex").action((options) => promptHook(harnessOf2(options)));
25896
- hook.command("stop").description("Stop hook (Codex): the wait. Holds the turn open at no cost until a message arrives, the person types, or the agent is made unreachable.").option("--harness <name>", "claude or codex").action((options) => stopHook(harnessOf2(options)));
26197
+ hook.command("start").description("SessionStart hook (Claude Code and Codex): writes the conversation down so the MCP server is its session, and tells the agent its name and how messages work.").option("--harness <name>", "claude or codex").action((options) => startHook(harnessOf2(options)));
26198
+ hook.command("tool").description("PostToolUse hook (Codex): hands the model what reached the inbox while its turn ran, with the tool's result.").option("--harness <name>", "claude or codex").action((options) => toolHook(harnessOf2(options)));
26199
+ hook.command("prompt").description("No longer installed; kept so an older install's hook exits cleanly.").option("--harness <name>", "claude or codex").action((options) => promptHook(harnessOf2(options)));
26200
+ hook.command("stop").description("No longer installed (Claude Code is woken over its socket, Codex through codex queue); kept so an older install's hook still hands over unread messages.").option("--harness <name>", "claude or codex").action((options) => stopHook(harnessOf2(options)));
25897
26201
  }
25898
26202
 
25899
26203
  // src/commands/sessions.ts
@@ -25982,13 +26286,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
25982
26286
  }
25983
26287
 
25984
26288
  // src/index.ts
25985
- var program2 = new Command().name("slop").description("Slopware Tasks from the terminal. Everything a person can do on the board, an agent can do here.").version("0.4.5").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => setJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
26289
+ var program2 = new Command().name("slop").description("Slopware Tasks from the terminal. Everything a person can do on the board, an agent can do here.").version("0.4.6").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => setJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
25986
26290
  registerAuthCommands(program2);
25987
26291
  registerProjectCommands(program2);
25988
26292
  var taskCommand = registerTaskCommands(program2);
25989
26293
  registerSessionCommands(program2, taskCommand);
25990
26294
  registerThreadCommands(program2, taskCommand);
25991
- registerMcpCommands(program2, "0.4.5");
26295
+ registerMcpCommands(program2, "0.4.6");
25992
26296
  registerGuideCommand(program2);
25993
26297
  registerSkillCommands(program2);
25994
26298
  program2.parseAsync().catch(fail);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",