slopwaresystems 0.4.2 → 0.4.4

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 +173 -138
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3385,7 +3385,7 @@ var slop_cli_default = "---\nname: slop-cli\ndescription: How an agent works the
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)\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 on its own. On Claude Code it arrives as a turn of yours, even while you are idle. On Codex it waits for you: it is handed to you with your next tool result, so check `session check` when you start a turn if you expect something.\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 `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 on its own. On Claude Code it arrives as a turn of yours, even while you are idle. On Codex it waits for you: it is handed to you with your next tool result. So on Codex, when you are talking with someone and need their answer to go on, wait for it inside your turn: `say` `reply` (or `post`) with `wait: 20` returns their reply in the same call, or `session` `wait` with `thread` does the same for a thread you already wrote in. Nothing else to do and nobody at your keyboard: `session` `wait` alone blocks for the next message for you.\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";
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";
@@ -23500,106 +23500,106 @@ var SessionPush = external_exports.discriminatedUnion("type", [
23500
23500
 
23501
23501
  // ../contract/src/work/agent-names.ts
23502
23502
  var AGENT_NAMES = [
23503
- "Albert",
23504
- "Alma",
23505
- "Amos",
23506
- "Astrid",
23507
- "August",
23508
- "Beatrix",
23509
- "Bruno",
23510
- "Cedric",
23511
- "Clara",
23512
- "Cosmo",
23513
- "Dahlia",
23514
- "Dexter",
23515
- "Edie",
23516
- "Elio",
23517
- "Ember",
23518
- "Felix",
23519
- "Fern",
23520
- "Flora",
23521
- "Gus",
23522
- "Hazel",
23523
- "Hugo",
23524
- "Ida",
23525
- "Ingrid",
23526
- "Iris",
23527
- "Jasper",
23528
- "Juno",
23529
- "Kit",
23530
- "Leo",
23531
- "Lila",
23532
- "Linus",
23533
- "Lotte",
23534
- "Mabel",
23535
- "Marlow",
23536
- "Milo",
23537
- "Minnie",
23538
- "Nell",
23539
- "Nico",
23540
- "Nina",
23541
- "Odette",
23542
- "Olive",
23543
- "Oscar",
23544
- "Otto",
23545
- "Pearl",
23546
- "Percy",
23547
- "Pip",
23548
- "Quill",
23549
- "Remy",
23550
- "Rosa",
23551
- "Rufus",
23552
- "Sable",
23553
- "Silas",
23554
- "Sky",
23555
- "Sol",
23556
- "Tess",
23557
- "Theo",
23558
- "Tilly",
23559
- "Ulla",
23560
- "Vera",
23561
- "Vito",
23562
- "Wren",
23563
- "Xavi",
23564
- "Yara",
23565
- "Zed",
23566
- "Zora",
23567
- "Arlo",
23568
- "Bea",
23569
- "Cyrus",
23570
- "Della",
23571
- "Enzo",
23572
- "Frida",
23573
- "Gilda",
23574
- "Hal",
23575
- "Imre",
23576
- "Joss",
23577
- "Kai",
23578
- "Lena",
23579
- "Mona",
23580
- "Nate",
23581
- "Opal",
23582
- "Pia",
23583
- "Rae",
23584
- "Sacha",
23585
- "Tove",
23586
- "Uma",
23587
- "Vince",
23588
- "Willa",
23589
- "Yuki",
23590
- "Zelda",
23591
- "Ansel",
23592
- "Birdie",
23593
- "Cleo",
23594
- "Dov",
23595
- "Esme",
23596
- "Fitz",
23597
- "Greta",
23598
- "Hollis",
23599
- "Inez",
23600
- "Jude",
23601
- "Kip",
23602
- "Lark"
23503
+ "Sir Reginald Buttersworth",
23504
+ "Beefington Wobble",
23505
+ "Gary the Inevitable",
23506
+ "Countess Von Snacc",
23507
+ "Bartholomew Sneeze",
23508
+ "Pudding Malone",
23509
+ "Doug, Just Doug",
23510
+ "Admiral Crumb",
23511
+ "Lord Fluffington III",
23512
+ "Meatball Descartes",
23513
+ "Gerald Spaghetti",
23514
+ "Mildred Danger",
23515
+ "Chunky Bartholomew",
23516
+ "Captain Nap",
23517
+ "Wet Kevin",
23518
+ "Baron Waffles",
23519
+ "Susan the Unyielding",
23520
+ "Gravy Boat",
23521
+ "Trevor Cabbage",
23522
+ "Dame Judi Bench",
23523
+ "Mr. Business Casual",
23524
+ "Toast Malone",
23525
+ "Gandalf the Beige",
23526
+ "Terrence Moist",
23527
+ "Professor Pants",
23528
+ "Lil Bureaucrat",
23529
+ "Duchess Pigeon",
23530
+ "Brenda Thunderclap",
23531
+ "Ol' Dependable",
23532
+ "Kevin But Louder",
23533
+ "Nigel Overthink",
23534
+ "Sausage Aristotle",
23535
+ "The Honorable Blorp",
23536
+ "Greg of the Marsh",
23537
+ "Karen Kraken",
23538
+ "Bingus Prime",
23539
+ "Steve Two Hats",
23540
+ "Margaret Doom",
23541
+ "Ranch Dressing Rick",
23542
+ "Sir Naps-a-Lot",
23543
+ "Cheddar Van Halen",
23544
+ "Barb Dwyer",
23545
+ "Deputy Croissant",
23546
+ "Linda Explosion",
23547
+ "Humphrey Bogus",
23548
+ "Tuna Wellington",
23549
+ "Big Mistake Mike",
23550
+ "Quentin Quinoa",
23551
+ "Dolores Umbrage",
23552
+ "Fancy Larry",
23553
+ "Norman Conquest",
23554
+ "Beatrice Bagpipe",
23555
+ "Chad the Apologetic",
23556
+ "Wanda Whiplash",
23557
+ "Alan Turingpoint",
23558
+ "Mango Unchained",
23559
+ "Dwight Lightly",
23560
+ "Brisket Newton",
23561
+ "Ferris Wheeler",
23562
+ "Peggy Sue Me",
23563
+ "Hank Dilemma",
23564
+ "Vlad the Inhaler",
23565
+ "Bagel Bardot",
23566
+ "Clive Staples",
23567
+ "Ignatius Pudding",
23568
+ "Pancake Lafayette",
23569
+ "Rhonda Voo",
23570
+ "Mort Gage",
23571
+ "Sheila Crumpet",
23572
+ "Dennis Existential",
23573
+ "Justin Case",
23574
+ "Barry Cuda",
23575
+ "Lil Pomp",
23576
+ "Winston Churchmouse",
23577
+ "Petunia Blastoff",
23578
+ "Bob Ross Cosplayer",
23579
+ "Cornelius Fudge Sr.",
23580
+ "Otis Redundant",
23581
+ "Brad Wurst",
23582
+ "Dr. Feelgoodish",
23583
+ "Grandpa Wifi",
23584
+ "Sir Loin",
23585
+ "Marge Inal",
23586
+ "Chip Monk",
23587
+ "Polly Estera",
23588
+ "Gustavo Fringe",
23589
+ "Dale Gribbling",
23590
+ "Abe Froman Jr.",
23591
+ "Noodle Kowalski",
23592
+ "Debbie Downloader",
23593
+ "Frank Lloyd Wrong",
23594
+ "Tammy Fay Baker",
23595
+ "Yolanda Bolognese",
23596
+ "Pierre Delecto",
23597
+ "Kevin, Reluctantly",
23598
+ "Butterscotch Vance",
23599
+ "Reginald Sandwich",
23600
+ "Cliff Hanger",
23601
+ "Elmo Deodorant",
23602
+ "Skip Intro"
23603
23603
  ];
23604
23604
  function agentNameFor(key) {
23605
23605
  let hash2 = 2166136261;
@@ -24214,8 +24214,21 @@ var HeldSession = class {
24214
24214
  attempt = 0;
24215
24215
  connected = 0;
24216
24216
  session = null;
24217
- /** Whoever is sitting in `session wait`. Handed the next message, or null when standby is switched off under them. */
24217
+ /**
24218
+ * Whoever is waiting for a message. Handed each one, and answers whether it took it (a waiter for one thread lets
24219
+ * the rest pass to the inbox); handed null when the agent is made unreachable under them.
24220
+ */
24218
24221
  waiters = /* @__PURE__ */ new Set();
24222
+ greeted = null;
24223
+ /**
24224
+ * Settled once the room has said hello (the session id is known, so this process's next write names its session),
24225
+ * or after a few seconds if the room is slow. A write before hello names no session, and a reply to it reaches
24226
+ * nobody, so callers wait for this before their first write.
24227
+ */
24228
+ ready = new Promise((resolve5) => {
24229
+ this.greeted = resolve5;
24230
+ setTimeout(resolve5, 5e3).unref();
24231
+ });
24219
24232
  async start() {
24220
24233
  await this.connect();
24221
24234
  }
@@ -24242,6 +24255,8 @@ var HeldSession = class {
24242
24255
  this.session = push.session;
24243
24256
  this.taskKey = push.session.taskKey;
24244
24257
  rememberSession(process.cwd(), push.session.id);
24258
+ this.greeted?.();
24259
+ this.greeted = null;
24245
24260
  this.connected += 1;
24246
24261
  if (this.connected > 1) void this.hooks.onReconnect?.(push.session);
24247
24262
  return;
@@ -24258,8 +24273,8 @@ var HeldSession = class {
24258
24273
  }
24259
24274
  if (push.type !== "thread") return;
24260
24275
  const delivered = { thread: push.thread, message: push.message };
24261
- const taken = this.waiters.size > 0;
24262
- for (const waiter of [...this.waiters]) waiter(delivered);
24276
+ let taken = false;
24277
+ for (const waiter of [...this.waiters]) if (waiter(delivered)) taken = true;
24263
24278
  if (!taken) this.hooks.onMessage(delivered);
24264
24279
  });
24265
24280
  socket.addEventListener("close", (event) => {
@@ -25175,6 +25190,7 @@ ${pushBlock(push)}`;
25175
25190
  const project = async (args) => {
25176
25191
  const target = await resolveOnly(args);
25177
25192
  if (!held) await hold(target.id, null);
25193
+ await held?.ready;
25178
25194
  return target;
25179
25195
  };
25180
25196
  const release = async () => {
@@ -25182,7 +25198,7 @@ ${pushBlock(push)}`;
25182
25198
  };
25183
25199
  if (!nonInteractiveHarness())
25184
25200
  void (async () => {
25185
- for (let waited = 0; waited < 1e4 && !knownConversation(process.cwd()); waited += 250) await new Promise((resolve5) => setTimeout(resolve5, 250));
25201
+ for (let waited2 = 0; waited2 < 1e4 && !knownConversation(process.cwd()); waited2 += 250) await new Promise((resolve5) => setTimeout(resolve5, 250));
25186
25202
  whoAmI();
25187
25203
  const target = await resolveOnly({});
25188
25204
  if (!held) await hold(target.id, null);
@@ -25195,32 +25211,42 @@ ${pushBlock(push)}`;
25195
25211
  };
25196
25212
  process.stdin.once("end", goodbye);
25197
25213
  for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, goodbye);
25214
+ const waitFor = async (thread, minutes) => {
25215
+ const queued = inboxDrain(process.cwd());
25216
+ const wanted = queued.filter((push) => !thread || push.thread.id === thread);
25217
+ for (const other of queued) if (!wanted.includes(other)) inboxWrite(process.cwd(), other);
25218
+ if (wanted.length) return { pushes: wanted, released: false };
25219
+ return new Promise((resolve5) => {
25220
+ const waiter = (delivered) => {
25221
+ if (delivered && thread && delivered.thread.id !== thread) return false;
25222
+ held?.waiters.delete(waiter);
25223
+ clearTimeout(timer);
25224
+ resolve5({ pushes: delivered ? [delivered] : [], released: delivered === null });
25225
+ return true;
25226
+ };
25227
+ held?.waiters.add(waiter);
25228
+ const timer = setTimeout(() => {
25229
+ held?.waiters.delete(waiter);
25230
+ resolve5({ pushes: [], released: false });
25231
+ }, minutes * 6e4);
25232
+ });
25233
+ };
25234
+ const waited = (outcome, minutes, thread) => {
25235
+ if (outcome.pushes.length) return outcome.pushes.map(pushBlock).join("\n\n");
25236
+ if (outcome.released) return "Your person made you not visible to teammates: nothing can reach you now. You may stop.";
25237
+ return thread ? `No reply in ${thread} in ${minutes} minutes.` : `No message in ${minutes} minutes.`;
25238
+ };
25198
25239
  return [
25199
25240
  {
25200
25241
  name: "wait_for_message",
25201
- description: "Block until the next message for you (on your task, in a thread you are in, or addressed to you), or until `minutes` pass. Optional: messages reach you on their own; use this only when you have nothing else to do and nobody is at your keyboard.",
25202
- inputSchema: schema({ task: str("Task key to go live on first (optional)"), minutes: int2("Give up after (default 300)"), project: PROJECT }),
25242
+ 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.",
25243
+ 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 }),
25203
25244
  run: async (args) => {
25204
25245
  const target = await project(args);
25205
25246
  if (args.task) await hold(target.id, String(args.task));
25206
- const queued = inboxDrain(process.cwd());
25207
- if (queued.length) return queued.map(pushBlock).join("\n\n");
25208
- const minutes = typeof args.minutes === "number" ? args.minutes : 300;
25209
- const outcome = await new Promise((resolve5) => {
25210
- const waiter = (delivered) => {
25211
- held?.waiters.delete(waiter);
25212
- clearTimeout(timer);
25213
- resolve5({ push: delivered, released: delivered === null });
25214
- };
25215
- held?.waiters.add(waiter);
25216
- const timer = setTimeout(() => {
25217
- held?.waiters.delete(waiter);
25218
- resolve5({ push: null, released: false });
25219
- }, minutes * 6e4);
25220
- });
25221
- if (outcome.push) return pushBlock(outcome.push);
25222
- if (outcome.released) return "Your person made you not visible to teammates: nothing can reach you now. You may stop.";
25223
- return `No message in ${minutes} minutes.`;
25247
+ const thread = args.thread ? String(args.thread) : null;
25248
+ const minutes = typeof args.minutes === "number" ? args.minutes : thread ? 20 : 300;
25249
+ return waited(await waitFor(thread, minutes), minutes, thread);
25224
25250
  }
25225
25251
  },
25226
25252
  {
@@ -25237,21 +25263,30 @@ ${pushBlock(push)}`;
25237
25263
  {
25238
25264
  name: "reply",
25239
25265
  description: "Reply in a thread. In a thread about a task, it reaches whoever is live on that task now, or waits for them.",
25240
- inputSchema: schema({ thread: str("Thread id"), body: str("The message (Markdown)"), project: PROJECT }, ["thread", "body"]),
25266
+ 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"]),
25241
25267
  run: async (args) => {
25242
25268
  const target = await project(args);
25243
- const result = await api.post(`/projects/${target.id}/threads/${String(args.thread)}/messages`, { body: String(args.body) });
25244
- return result.delivered ? `Replied; reached ${result.delivered} live session(s).` : "Replied. Nobody is live on it; they see it when they start.";
25269
+ const thread = String(args.thread);
25270
+ const result = await api.post(`/projects/${target.id}/threads/${thread}/messages`, { body: String(args.body) });
25271
+ const replied = result.delivered ? `Replied; reached ${result.delivered} live session(s).` : "Replied. Nobody is live on it; they see it when they start.";
25272
+ if (typeof args.wait !== "number" || args.wait <= 0) return replied;
25273
+ return `${replied}
25274
+
25275
+ ${waited(await waitFor(thread, args.wait), args.wait, thread)}`;
25245
25276
  }
25246
25277
  },
25247
25278
  {
25248
25279
  name: "post",
25249
25280
  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.",
25250
- 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"), project: PROJECT }, ["body"]),
25281
+ 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"]),
25251
25282
  run: async (args) => {
25252
25283
  const target = await project(args);
25253
25284
  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 });
25254
- return `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)` : ""}.`;
25285
+ 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)` : ""}.`;
25286
+ if (typeof args.wait !== "number" || args.wait <= 0) return started2;
25287
+ return `${started2}
25288
+
25289
+ ${waited(await waitFor(result.thread.id, args.wait), args.wait, result.thread.id)}`;
25255
25290
  }
25256
25291
  },
25257
25292
  {
@@ -25605,7 +25640,7 @@ tool_timeout_sec = 21600
25605
25640
  file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
25606
25641
  writeFileSync7(hooksPath, `${JSON.stringify(file2, null, 2)}
25607
25642
  `);
25608
- done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (a stand-in run answers for an idle chat)`);
25643
+ done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (no Codex hook holds a turn open)`);
25609
25644
  return done;
25610
25645
  }
25611
25646
  async function trustCodexHooks(codexHome) {
@@ -25645,7 +25680,7 @@ async function trustCodexHooks(codexHome) {
25645
25680
  const notify = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
25646
25681
  `);
25647
25682
  try {
25648
- await call("initialize", { clientInfo: { name: "slop", version: "0.4.2" }, capabilities: { experimentalApi: true } });
25683
+ await call("initialize", { clientInfo: { name: "slop", version: "0.4.4" }, capabilities: { experimentalApi: true } });
25649
25684
  notify("initialized", {});
25650
25685
  const listed = await call("hooks/list", { cwds: [process.cwd()] });
25651
25686
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
@@ -25771,16 +25806,16 @@ function tools() {
25771
25806
  ),
25772
25807
  group(
25773
25808
  "session",
25774
- "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 (optional).",
25809
+ "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.",
25775
25810
  { claim: "claim_task", release: "release_task", who: "sessions", check: "check_messages", wait: "wait_for_message" },
25776
- { task: str("Task key"), minutes: int2("wait: give up after (default 300)") }
25811
+ { task: str("Task key"), thread: str("wait: only a message in this thread"), minutes: int2("wait: give up after (default 300; 20 with thread)") }
25777
25812
  ),
25778
25813
  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") }),
25779
25814
  group(
25780
25815
  "say",
25781
- "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.",
25816
+ "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.",
25782
25817
  { post: "post", reply: "reply" },
25783
- { 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") }
25818
+ { 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") }
25784
25819
  )
25785
25820
  ];
25786
25821
  }
@@ -25909,13 +25944,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
25909
25944
  }
25910
25945
 
25911
25946
  // src/index.ts
25912
- 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.2").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)");
25947
+ 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.4").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)");
25913
25948
  registerAuthCommands(program2);
25914
25949
  registerProjectCommands(program2);
25915
25950
  var taskCommand = registerTaskCommands(program2);
25916
25951
  registerSessionCommands(program2, taskCommand);
25917
25952
  registerThreadCommands(program2, taskCommand);
25918
- registerMcpCommands(program2, "0.4.2");
25953
+ registerMcpCommands(program2, "0.4.4");
25919
25954
  registerGuideCommand(program2);
25920
25955
  registerSkillCommands(program2);
25921
25956
  program2.parseAsync().catch(fail);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",