slopwaresystems 0.4.11 → 0.4.13

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 +536 -73
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3385,10 +3385,10 @@ 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 message arrives, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is your conversation\u2019s registered presence in a project, maintained by a live connection. 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: idle, it starts a turn; busy, it queues behind the turn that is running and behind whatever the person asked, and on Codex it is not even seen until that turn ends. 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. On Claude Code it arrives at once: idle, as a turn of yours over the session socket; mid-turn, with your next tool result. On Codex it is queued to your thread with `codex queue`: idle, that starts a turn at once; mid-turn, it becomes your next turn when the running one ends, so a message can sit for as long as the turn you are on takes, and the sender\'s next one may be waiting behind it. 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\nA wake may say that messages are waiting rather than include their bodies. Use `session` `check` with the project named in that wake. The content remains queued until a slop tool result hands it over. A failed wake leaves the content for your next tool call.\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- Each conversation has its own session and inbox in each project. Identified conversations can share a checkout; separate worktrees are still useful for keeping code edits independent.\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';
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 your conversation\u2019s registered presence in a project, maintained by a live connection. 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: idle, it starts a turn; busy, it queues behind the turn that is running and behind whatever the person asked, and on Codex it is not even seen until that turn ends. 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, before you read any code. Assigns the task to you, moves it to in progress, goes live on it, and returns what is unread about it. Read that first. Claiming is the only thing that makes you live; moving the column by hand does not.\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. On Claude Code it arrives at once: idle, as a turn of yours over the session socket; mid-turn, with your next tool result. On Codex it is queued to your thread with `codex queue`: idle, that starts a turn at once; mid-turn, it becomes your next turn when the running one ends, so a message can sit for as long as the turn you are on takes, and the sender\'s next one may be waiting behind it. 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. **Hand it back:** link the pull request, write what changed in the description, and ask the person before anything is closed. `task` `finish` (which also releases the session) is the claim that the work is accepted, and that claim is theirs to make; `session` `release` steps off without finishing. See `slop-working`.\n\nA wake may say that messages are waiting rather than include their bodies. Use `session` `check` with the project named in that wake. The content remains queued until a slop tool result hands it over. A failed wake leaves the content for your next tool call.\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## Subagents you spawn\n\nYou hold the session; the subagents you spawn do not. They show up under you on the board, by what you asked them to do and the worktree they work in, and while someone has the board open, the tool each one is running. Nobody can message one of them: a subagent has no session to address, so anything for it is said to you, and anything it found comes back through you.\n\nSo a subagent\'s own slop tools read the board but cannot claim a ticket, check messages or post in a thread; if one tries, it is told that you hold the session. Do not send one off to claim a ticket or answer a thread. Send it to find something out, and act on what it tells you.\n\n## Etiquette\n\n- Each conversation has its own session and inbox in each project. Identified conversations can share a checkout; separate worktrees are still useful for keeping code edits independent.\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
- 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";
3391
+ var slop_working_default = "---\nname: slop-working\ndescription: The working loop for a ticket on the board: going live on it, keeping its column true, and handing it back to the person before it is closed. Load when told to work a ticket, given a ticket key or number, asked to pick up the next thing, or at the start of any coding session in a repository linked with slop.\n---\n\n# Told a ticket? Go live on it first\n\n\"Work LB-59\", \"take a look at ST-12\", \"pick up the next one\": before you read any code, claim it.\n\n`session` `claim` with the key. That one call assigns the ticket to you, moves it to the first in-progress column, and hands you what is unread about it (read that before you start). From then on you are **live on that ticket**: the board shows you working it, `session` `who` lists you on it, and messages about it reach you on their own. See `slop-sessions`.\n\nClaiming is the only thing that makes you live. `slop task start` at a shell moves the column and nothing else, because the CLI is one short-lived process and cannot hold a session; moving the column by hand leaves the board saying someone is on the ticket with nobody there. Through the MCP tools, `session` `claim` is the way in.\n\n- Given a key that does not exist, or an ambiguous one: say so and stop. Do not guess which ticket was meant.\n- Someone else already live on it (`session` `who`): do not claim it. Ask in a thread about that ticket first.\n- Told to work something that is not on the board yet: `task` `create` it, then claim it. Work an agent is doing should be a ticket someone can see.\n\n# The loop\n\n1. **Claim it:** `session` `claim`. Then `task` `show` and read all of it, plan and notes included.\n2. **Do it.** Long work: follow `slop-planning`. Short work: just do it.\n3. **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.\n4. **Keep the column true.** The board is read by people who are not watching you. If you stop for the day or move to something else, `session` `release`. If you are blocked, say what on, in the item's note and in a thread about the ticket, and leave it in progress. Never leave a ticket in progress with no live session and no note saying why.\n5. **Hand it back** (below). Finishing is the person's call, not yours.\n\n# Handing a ticket back\n\nWork being done is not the ticket being done. When the code is ready:\n\n1. `task` `link` the pull request.\n2. Put what changed and where at the end of the description (`task` `update`).\n3. If the board has a column for review, move it there (`board` `columns` lists them; a review column still counts as in progress). Otherwise leave it in progress.\n4. **Ask the person, and stop.** Not in a thread: answer in your turn, to whoever asked you, because they are reading it. One message: what you did, where the PR is, and what is left to decide. Offer the things you could do next rather than choosing one, for example reviewing the PR yourself, addressing review comments, merging it, or deploying.\n\n**Do not call `task` `finish` until they answer.** `finish` moves the ticket to the first done column and releases your session, which is the claim that the work is accepted, and only a person can make that claim. Told to finish, finish. Told nothing, leave the ticket where it is: sitting in review is honest, marked done because an agent ran out of things to do is not.\n\nThe one exception is work with nothing to accept: a chore you were told to close, a one-line fix already on the main branch with no pull request. Finish that and say you did.\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 person driving you | your own reply to them, in your turn |\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";
3392
3392
 
3393
3393
  // src/platform/skills.ts
3394
3394
  var SKILLS = { "slop-cli": slop_cli_default, "slop-planning": slop_planning_default, "slop-sessions": slop_sessions_default, "slop-working": slop_working_default };
@@ -26461,6 +26461,22 @@ var TaskEventRecord = external_exports.object({
26461
26461
 
26462
26462
  // ../contract/src/work/sessions.ts
26463
26463
  var AgentHarness = external_exports.enum(["claude", "codex", "other"]);
26464
+ var WORKERS_PER_SESSION = 8;
26465
+ var WORKER_LABEL_MAX = 80;
26466
+ var DOING_MAX = 120;
26467
+ var SessionWorker = external_exports.object({
26468
+ ref: external_exports.string().trim().min(1).max(100),
26469
+ /** What it was spawned to do, in the harness's own words. */
26470
+ label: external_exports.string().trim().min(1).max(WORKER_LABEL_MAX),
26471
+ directory: external_exports.string().trim().max(120).nullable().default(null),
26472
+ branch: external_exports.string().trim().max(120).nullable().default(null),
26473
+ doing: external_exports.string().trim().max(DOING_MAX).nullable().default(null),
26474
+ since: Timestamp
26475
+ });
26476
+ var SessionActivity = external_exports.object({
26477
+ doing: external_exports.string().trim().max(DOING_MAX).nullable().default(null),
26478
+ workers: external_exports.array(SessionWorker).max(WORKERS_PER_SESSION).default([])
26479
+ });
26464
26480
  var Session = external_exports.object({
26465
26481
  /** Derived from the person, the machine and the agent's conversation: the same across reconnects and resumes. */
26466
26482
  id: Id,
@@ -26480,7 +26496,10 @@ var Session = external_exports.object({
26480
26496
  * Reachable by teammates: listed to them, messaged by them and by their agents, and able to message. Off, the agent
26481
26497
  * still works the board but talks to nobody and is listed only to its own person. Starts from the person's default.
26482
26498
  */
26483
- reachable: external_exports.boolean().default(true)
26499
+ reachable: external_exports.boolean().default(true),
26500
+ /** What it said it was doing, and the subagents it said it had. Empty until it says: an older CLI never does. */
26501
+ doing: external_exports.string().nullable().default(null),
26502
+ workers: external_exports.array(SessionWorker).default([])
26484
26503
  });
26485
26504
  var ReachableInput = external_exports.object({ on: external_exports.boolean() });
26486
26505
  var SessionClaimInput = external_exports.object({
@@ -26503,9 +26522,14 @@ var SessionPush = external_exports.discriminatedUnion("type", [
26503
26522
  external_exports.object({ type: external_exports.literal("end") }),
26504
26523
  /** Reachability was switched from the web. Off releases a waiting agent: nothing can reach it any more. */
26505
26524
  external_exports.object({ type: external_exports.literal("reachable"), on: external_exports.boolean() }),
26525
+ /** Whether anyone is looking at this project's board. Off, an agent stops saying what it is doing. */
26526
+ external_exports.object({ type: external_exports.literal("watched"), on: external_exports.boolean() }),
26527
+ /** To board watchers: what one agent and its workers are doing now. Carries its own payload; nothing to refetch. */
26528
+ SessionActivity.extend({ type: external_exports.literal("activity"), session: Id }),
26506
26529
  /** To board watchers: something in the project changed; refetch. */
26507
26530
  external_exports.object({ type: external_exports.literal("changed"), scope: external_exports.string() })
26508
26531
  ]);
26532
+ var SessionUp = external_exports.discriminatedUnion("type", [SessionActivity.extend({ type: external_exports.literal("activity") })]);
26509
26533
  var SessionMessagesInput = external_exports.object({ cursor: external_exports.coerce.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional() });
26510
26534
  var SessionMessages = external_exports.object({
26511
26535
  cursor: external_exports.number().int().nonnegative(),
@@ -26816,13 +26840,35 @@ var AGENT_NAMES = [
26816
26840
  "Eleanor Rigby Jr.",
26817
26841
  "Ferdinand Fedora"
26818
26842
  ];
26819
- function generateAgentName(key) {
26843
+ function hashKey(key) {
26820
26844
  let hash2 = 2166136261;
26821
26845
  for (let index = 0; index < key.length; index += 1) {
26822
26846
  hash2 ^= key.charCodeAt(index);
26823
26847
  hash2 = Math.imul(hash2, 16777619) >>> 0;
26824
26848
  }
26825
- return AGENT_NAMES[hash2 % AGENT_NAMES.length] ?? AGENT_NAMES[0];
26849
+ return hash2;
26850
+ }
26851
+ function generateAgentName(key) {
26852
+ return AGENT_NAMES[hashKey(key) % AGENT_NAMES.length] ?? AGENT_NAMES[0];
26853
+ }
26854
+ var WORKER_TITLES = [
26855
+ (parent2) => `${parent2} Jr.`,
26856
+ (parent2) => `${parent2} II`,
26857
+ (parent2) => `${parent2} III`,
26858
+ (parent2) => `${parent2} the Second`,
26859
+ (parent2) => `${parent2} the Younger`,
26860
+ (parent2) => `${parent2} the Lesser`,
26861
+ (parent2) => `${parent2}, Deputised`,
26862
+ (parent2) => `Young ${parent2}`,
26863
+ (parent2) => `Little ${parent2}`,
26864
+ (parent2) => `Second-Best ${parent2}`
26865
+ ];
26866
+ var CARRIED_ON = /(,? (?:Jr\.|Sr\.|II|III|IV|Prime|Junior|Minor|Deputised|the (?:Second|Third|Younger|Lesser)))+$/;
26867
+ function generateWorkerName(parentName, ref) {
26868
+ const hash2 = hashKey(`${parentName}\0${ref}`);
26869
+ if (hash2 % 5 < 2) return generateAgentName(`${parentName}:${ref}`);
26870
+ const base = parentName.replace(CARRIED_ON, "").trim() || parentName;
26871
+ return (WORKER_TITLES[(hash2 >>> 4) % WORKER_TITLES.length] ?? WORKER_TITLES[0])(base);
26826
26872
  }
26827
26873
 
26828
26874
  // ../contract/src/work/bindings.ts
@@ -27056,6 +27102,43 @@ function fetchRememberedSession(cwd, conversation) {
27056
27102
  throw error62;
27057
27103
  }
27058
27104
  }
27105
+ var buildHolderPath = (cwd, conversation) => join3(buildSessionsDir(), `${generateLocalStateKey(cwd, conversation)}.holder.json`);
27106
+ var isProcessAlive = (pid) => {
27107
+ try {
27108
+ process.kill(pid, 0);
27109
+ return true;
27110
+ } catch (error62) {
27111
+ return error62.code === "EPERM";
27112
+ }
27113
+ };
27114
+ function claimSessionHolder(cwd, conversation, pid = process.pid) {
27115
+ const path2 = buildHolderPath(cwd, conversation);
27116
+ for (let attempt = 0; attempt < 2; attempt += 1) {
27117
+ try {
27118
+ persistLocalState(path2, JSON.stringify({ pid, at: Date.now() }), false);
27119
+ } catch {
27120
+ return false;
27121
+ }
27122
+ let holder = null;
27123
+ try {
27124
+ holder = JSON.parse(readFileSync3(path2, "utf8"));
27125
+ } catch {
27126
+ return false;
27127
+ }
27128
+ if (typeof holder?.pid !== "number" || holder.pid === pid) return true;
27129
+ if (isProcessAlive(holder.pid)) return false;
27130
+ rmSync3(path2, { force: true });
27131
+ }
27132
+ return false;
27133
+ }
27134
+ function releaseSessionHolder(cwd, conversation, pid = process.pid) {
27135
+ const path2 = buildHolderPath(cwd, conversation);
27136
+ try {
27137
+ const holder = JSON.parse(readFileSync3(path2, "utf8"));
27138
+ if (holder?.pid === pid) rmSync3(path2, { force: true });
27139
+ } catch {
27140
+ }
27141
+ }
27059
27142
  var buildConversationPath = (cwd) => join3(buildSessionsDir(), `${createHash2("sha256").update(resolveRealCwd(cwd)).digest("hex").slice(0, 16)}.conversation.json`);
27060
27143
  function persistRememberedConversation(cwd, conversation) {
27061
27144
  const directory = `${buildConversationPath(cwd)}.d`;
@@ -27412,6 +27495,7 @@ function wakeCodexOverWs(text, threadId, endpoint) {
27412
27495
  }
27413
27496
 
27414
27497
  // src/platform/harness.ts
27498
+ var HEAD_BYTES = 64 * 1024;
27415
27499
  var parent = null;
27416
27500
  function fetchParentCommand() {
27417
27501
  if (parent !== null) return parent;
@@ -27445,6 +27529,19 @@ var isSamePath = (a, b) => {
27445
27529
  };
27446
27530
  return resolveRealPath(a) === resolveRealPath(b);
27447
27531
  };
27532
+ function fetchFileHead(path2, bytes) {
27533
+ try {
27534
+ const fd = openSync(path2, "r");
27535
+ try {
27536
+ const buffer = Buffer.alloc(bytes);
27537
+ return buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
27538
+ } finally {
27539
+ closeSync(fd);
27540
+ }
27541
+ } catch {
27542
+ return "";
27543
+ }
27544
+ }
27448
27545
  function isMainChatRollout(sessionMetaLine) {
27449
27546
  if (!sessionMetaLine.includes('"session_meta"')) return false;
27450
27547
  if (/"parent_thread_id"\s*:\s*"/.test(sessionMetaLine)) return false;
@@ -27480,19 +27577,7 @@ function fetchCodexConversationByCwd(roots, cwd) {
27480
27577
  rollouts.sort((a, b) => b.mtime - a.mtime);
27481
27578
  const matching = /* @__PURE__ */ new Set();
27482
27579
  for (const rollout of rollouts) {
27483
- let head = "";
27484
- try {
27485
- const fd = openSync(rollout.path, "r");
27486
- try {
27487
- const buffer = Buffer.alloc(64 * 1024);
27488
- head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
27489
- } finally {
27490
- closeSync(fd);
27491
- }
27492
- } catch {
27493
- continue;
27494
- }
27495
- const line = head.split("\n")[0] ?? "";
27580
+ const line = fetchFileHead(rollout.path, HEAD_BYTES).split("\n")[0] ?? "";
27496
27581
  if (!line.includes('"session_meta"')) continue;
27497
27582
  if (!isMainChatRollout(line)) continue;
27498
27583
  const found = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(line);
@@ -27511,18 +27596,8 @@ function fetchCodexConversationByCwd(roots, cwd) {
27511
27596
  function isMainChatThread(roots, threadId) {
27512
27597
  const path2 = fetchCodexRolloutPath(roots, threadId);
27513
27598
  if (!path2) return true;
27514
- try {
27515
- const fd = openSync(path2, "r");
27516
- try {
27517
- const buffer = Buffer.alloc(64 * 1024);
27518
- const head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
27519
- return isMainChatRollout(head.split("\n")[0] ?? "");
27520
- } finally {
27521
- closeSync(fd);
27522
- }
27523
- } catch {
27524
- return true;
27525
- }
27599
+ const head = fetchFileHead(path2, HEAD_BYTES);
27600
+ return head === "" ? true : isMainChatRollout(head.split("\n")[0] ?? "");
27526
27601
  }
27527
27602
  function fetchCodexRolloutPath(roots, threadId) {
27528
27603
  const suffix = `-${threadId}.jsonl`;
@@ -27859,10 +27934,11 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
27859
27934
 
27860
27935
  - When you find work that should be tracked (a bug, a follow-up, a TODO you are not doing now), create a task:
27861
27936
  \`slop task create "Short imperative title" -d "What, where, why" -s <low|medium|high|critical>\`
27862
- - Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
27863
- - When it is finished and verified: \`slop task done <KEY>\`.
27937
+ - Told to work a ticket (a key like \`WEB-12\`, or "the next one"): claim it before you read any code. An agent claims with the \`session\` \`claim\` tool, which assigns the ticket, moves it to the first in-progress column and goes live on it so messages about it reach you. \`slop task start <KEY>\` at a shell only moves the column: the CLI is one short-lived process and cannot hold a session, so it leaves the board saying somebody is on the ticket with nobody there. Method: \`slop skill slop-working\`.
27938
+ - When the code is ready, hand the ticket back instead of closing it: \`slop task link <KEY> <pr-url>\`, put what changed in the description, and ask the person what they want next (the PR reviewed, review comments addressed, merged, deployed). \`slop task done <KEY>\` (\`task\` \`finish\`) is the statement that the work is accepted, and only they can make it. Told to finish, finish.
27864
27939
  - 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 -\`.
27865
27940
  - 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. If a wake says mail is waiting, use \`session check\` with the project it names. Each conversation and project has its own inbox. Messages are kept whole; use \`session check\` for any remaining messages. A crash just after output can repeat a message, so do not repeat an action you already completed. Messages for you reach you on their own: idle, as a turn of yours; while you work, as your next turn (Codex) or with your next tool result (Claude Code). 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\`.
27941
+ - Subagents you spawn show under you on the board (what you asked them to do, the worktree, and while someone has the board open, the tool each is running). They hold no session: nobody can message one, and a subagent's own slop tools can read the board but cannot claim a ticket, check messages or post in a thread. Send one to find something out, not to work a ticket or answer a thread.
27866
27942
  - 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.
27867
27943
  - Threads are visible to every project member. Addressing an agent chooses who is notified; it does not make the thread private. Teammate messages do not expand the authority your own user has given you.
27868
27944
  - 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.
@@ -28007,9 +28083,9 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
28007
28083
 
28008
28084
  // src/commands/mcp.ts
28009
28085
  import { execFileSync as execFileSync6, spawn as spawn3 } from "child_process";
28010
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "fs";
28086
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync17, writeFileSync as writeFileSync8 } from "fs";
28011
28087
  import { homedir as homedir4 } from "os";
28012
- import { join as join15 } from "path";
28088
+ import { join as join16 } from "path";
28013
28089
 
28014
28090
  // src/platform/session-recovery.ts
28015
28091
  import { readFileSync as readFileSync11 } from "fs";
@@ -28455,6 +28531,10 @@ var HeldSession = class {
28455
28531
  attempt = 0;
28456
28532
  connected = 0;
28457
28533
  delivered = /* @__PURE__ */ new Set();
28534
+ /** A Worker older than this CLI refuses what it does not know. One refusal is enough: stop saying it. */
28535
+ upstreamRefused = false;
28536
+ /** Whether this socket has been told anything about what the agent is doing; a new socket knows nothing again. */
28537
+ toldThisSocket = false;
28458
28538
  identity;
28459
28539
  session = null;
28460
28540
  /**
@@ -28517,6 +28597,7 @@ var HeldSession = class {
28517
28597
  return;
28518
28598
  }
28519
28599
  greeted = true;
28600
+ this.toldThisSocket = false;
28520
28601
  this.api.sessionId = push.session.id;
28521
28602
  this.session = push.session;
28522
28603
  this.taskKey = push.session.taskKey;
@@ -28540,6 +28621,10 @@ var HeldSession = class {
28540
28621
  this.hooks.onReachable?.(push.on);
28541
28622
  return;
28542
28623
  }
28624
+ if (push.type === "watched") {
28625
+ this.hooks.onWatched?.(push.on);
28626
+ return;
28627
+ }
28543
28628
  if (push.type !== "thread") return;
28544
28629
  try {
28545
28630
  this.deliverPush(push);
@@ -28554,6 +28639,12 @@ var HeldSession = class {
28554
28639
  this.helloDue = null;
28555
28640
  this.socket = null;
28556
28641
  if (this.stopped) return;
28642
+ if (event.code === 1008 && event.reason === "unsupported message") {
28643
+ this.upstreamRefused = true;
28644
+ this.hooks.onDrop?.(event.reason);
28645
+ this.schedule();
28646
+ return;
28647
+ }
28557
28648
  if (event.reason === "ended" || event.code === 1008) {
28558
28649
  this.hooks.onEnd?.();
28559
28650
  this.stop();
@@ -28588,6 +28679,20 @@ var HeldSession = class {
28588
28679
  this.connecting = false;
28589
28680
  }
28590
28681
  }
28682
+ /**
28683
+ * Says what this agent and its workers are doing. Dropped when the socket is not up: the room holds only what is
28684
+ * true now, and the next report follows the reconnect. The only thing this client ever sends but a ping.
28685
+ */
28686
+ reportActivity(activity) {
28687
+ const socket = this.socket;
28688
+ if (this.stopped || this.upstreamRefused || !this.session || !socket || socket.readyState !== WebSocket.OPEN) return;
28689
+ if (!this.toldThisSocket && activity.doing === null && activity.workers.length === 0) return;
28690
+ try {
28691
+ socket.send(JSON.stringify({ type: "activity", ...activity }));
28692
+ this.toldThisSocket = true;
28693
+ } catch {
28694
+ }
28695
+ }
28591
28696
  /** Live and recovered messages follow exactly the same local delivery path. */
28592
28697
  deliverPush(push) {
28593
28698
  if (this.stopped || this.session?.reachable === false || this.delivered.has(push.message.id)) return;
@@ -28691,6 +28796,331 @@ var HeldSession = class {
28691
28796
  }
28692
28797
  };
28693
28798
 
28799
+ // src/platform/harness-activity.ts
28800
+ import { closeSync as closeSync2, existsSync as existsSync10, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync12, readSync as readSync2, statSync as statSync4 } from "fs";
28801
+ import { basename as basename4, dirname as dirname7, join as join14 } from "path";
28802
+ var WORKER_FRESH_MS = 12e4;
28803
+ var READ_CAP = 256 * 1024;
28804
+ var CARRY_CAP = 64 * 1024;
28805
+ var CHILD_SCAN_CAP = 40;
28806
+ var DISCOVER_EVERY_MS = 1e4;
28807
+ var TAIL_EVERY_MS = 1e3;
28808
+ var truncate = (text, cap) => text.length <= cap ? text : `${text.slice(0, cap - 1)}\u2026`;
28809
+ function formatToolName(name) {
28810
+ if (!name.startsWith("mcp__")) return name;
28811
+ return name.slice(5).split("__").join(" ").replace(/_/g, " ");
28812
+ }
28813
+ function findToolCallDetail(input2) {
28814
+ if (!input2 || typeof input2 !== "object") return null;
28815
+ const fields = input2;
28816
+ if (typeof fields.action === "string" && typeof fields.task === "string") return `${fields.action} ${fields.task}`;
28817
+ const named = ["command", "file_path", "path", "pattern", "query", "url", "action", "task", "subject", "description", "prompt"];
28818
+ const values = [...named.filter((field) => field in fields).map((field) => fields[field]), ...Object.values(fields)];
28819
+ for (const value of values) {
28820
+ const text = Array.isArray(value) ? value.filter((part) => typeof part === "string").sort((a, b) => b.length - a.length)[0] : value;
28821
+ if (typeof text !== "string" || !text.trim()) continue;
28822
+ const one = text.replace(/\s+/g, " ").trim();
28823
+ const segments = one.split("/");
28824
+ return !one.includes(" ") && segments.length > 2 ? segments.slice(-2).join("/") : one;
28825
+ }
28826
+ return null;
28827
+ }
28828
+ function formatToolCallLine(name, input2) {
28829
+ const detail = findToolCallDetail(input2);
28830
+ return truncate(detail ? `${formatToolName(name)}: ${detail}` : formatToolName(name), DOING_MAX);
28831
+ }
28832
+ var parseJsonOrNull = (text) => {
28833
+ if (typeof text !== "string") return text;
28834
+ try {
28835
+ return JSON.parse(text);
28836
+ } catch {
28837
+ return null;
28838
+ }
28839
+ };
28840
+ function parseToolCallFromClaudeLine(line) {
28841
+ if (!line.includes('"tool_use"')) return null;
28842
+ let entry;
28843
+ try {
28844
+ entry = JSON.parse(line);
28845
+ } catch {
28846
+ return null;
28847
+ }
28848
+ const content = entry.message?.content;
28849
+ if (!Array.isArray(content)) return null;
28850
+ let found = null;
28851
+ for (const block of content) {
28852
+ if (!block || typeof block !== "object") continue;
28853
+ const { type, name, input: input2 } = block;
28854
+ if (type === "tool_use" && typeof name === "string") found = formatToolCallLine(name, input2);
28855
+ }
28856
+ return found;
28857
+ }
28858
+ function parseToolCallFromCodexLine(line) {
28859
+ if (!line.includes("function_call") && !line.includes("shell_call")) return null;
28860
+ let entry;
28861
+ try {
28862
+ entry = JSON.parse(line);
28863
+ } catch {
28864
+ return null;
28865
+ }
28866
+ const payload = entry.payload;
28867
+ if (!payload) return null;
28868
+ if (payload.type === "function_call" && typeof payload.name === "string") return formatToolCallLine(payload.name, parseJsonOrNull(payload.arguments));
28869
+ if (payload.type === "local_shell_call") return formatToolCallLine("shell", payload.action);
28870
+ return null;
28871
+ }
28872
+ var fetchFileSize = (path2) => {
28873
+ try {
28874
+ return statSync4(path2).size;
28875
+ } catch {
28876
+ return 0;
28877
+ }
28878
+ };
28879
+ var FileTail = class {
28880
+ constructor(path2, from = "end") {
28881
+ this.path = path2;
28882
+ this.offset = from === "end" ? fetchFileSize(path2) : 0;
28883
+ }
28884
+ path;
28885
+ offset;
28886
+ carry = "";
28887
+ fetchNewLines() {
28888
+ const size = fetchFileSize(this.path);
28889
+ if (size < this.offset) {
28890
+ this.offset = 0;
28891
+ this.carry = "";
28892
+ }
28893
+ if (size === this.offset) return [];
28894
+ let from = this.offset;
28895
+ if (size - from > READ_CAP) {
28896
+ from = size - READ_CAP;
28897
+ this.carry = "";
28898
+ }
28899
+ let chunk = "";
28900
+ try {
28901
+ const fd = openSync2(this.path, "r");
28902
+ try {
28903
+ const buffer = Buffer.alloc(size - from);
28904
+ chunk = buffer.subarray(0, readSync2(fd, buffer, 0, buffer.length, from)).toString("utf8");
28905
+ } finally {
28906
+ closeSync2(fd);
28907
+ }
28908
+ } catch {
28909
+ return [];
28910
+ }
28911
+ this.offset = size;
28912
+ const lines = `${this.carry}${chunk}`.split("\n");
28913
+ this.carry = lines.pop() ?? "";
28914
+ if (this.carry.length > CARRY_CAP) this.carry = "";
28915
+ return lines;
28916
+ }
28917
+ };
28918
+ var fetchChangedAt = (path2) => {
28919
+ try {
28920
+ return statSync4(path2).mtimeMs;
28921
+ } catch {
28922
+ return 0;
28923
+ }
28924
+ };
28925
+ function findClaudeTranscriptPath(homes, conversation) {
28926
+ for (const home of homes) {
28927
+ let projects;
28928
+ try {
28929
+ projects = readdirSync7(join14(home, "projects"));
28930
+ } catch {
28931
+ continue;
28932
+ }
28933
+ for (const project of projects) {
28934
+ const path2 = join14(home, "projects", project, `${conversation}.jsonl`);
28935
+ if (existsSync10(path2)) return path2;
28936
+ }
28937
+ }
28938
+ return null;
28939
+ }
28940
+ function parseClaudeWorkerLabel(text) {
28941
+ let meta3;
28942
+ try {
28943
+ meta3 = JSON.parse(text);
28944
+ } catch {
28945
+ return null;
28946
+ }
28947
+ const parts = [meta3.agentType, meta3.description].filter((part) => typeof part === "string" && part.trim() !== "");
28948
+ return parts.length ? truncate(parts.join(" \u2014 "), WORKER_LABEL_MAX) : null;
28949
+ }
28950
+ function findClaudeWorkerFiles(transcriptPath, conversation, changedAfter) {
28951
+ const directory = join14(dirname7(transcriptPath), conversation, "subagents");
28952
+ let names;
28953
+ try {
28954
+ names = readdirSync7(directory);
28955
+ } catch {
28956
+ return [];
28957
+ }
28958
+ const found = [];
28959
+ for (const name of names) {
28960
+ const ref = /^agent-(.+)\.jsonl$/.exec(name)?.[1];
28961
+ if (!ref) continue;
28962
+ const path2 = join14(directory, name);
28963
+ const changedAt = fetchChangedAt(path2);
28964
+ if (changedAt < changedAfter) continue;
28965
+ let label = null;
28966
+ try {
28967
+ label = parseClaudeWorkerLabel(readFileSync12(join14(directory, `agent-${ref}.meta.json`), "utf8"));
28968
+ } catch {
28969
+ }
28970
+ const first = fetchFileHead(path2, 8 * 1024).split("\n")[0] ?? "";
28971
+ const where = parseWhereFromJson(first);
28972
+ found.push({ ref, label: label ?? "subagent", path: path2, directory: where.directory, branch: where.branch, changedAt });
28973
+ }
28974
+ return found.sort((a, b) => b.changedAt - a.changedAt);
28975
+ }
28976
+ function parseWhereFromJson(line) {
28977
+ try {
28978
+ const entry = JSON.parse(line);
28979
+ const cwd = entry.cwd ?? entry.payload?.cwd ?? null;
28980
+ return { directory: cwd ? basename4(cwd) : null, branch: entry.gitBranch?.trim() ? entry.gitBranch : null };
28981
+ } catch {
28982
+ return { directory: null, branch: null };
28983
+ }
28984
+ }
28985
+ var isCodexHelperRollout = (head) => /"other"\s*:\s*"guardian"|"thread_source"\s*:\s*"[^"]*guardian/.test(head);
28986
+ function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
28987
+ const rollouts = [];
28988
+ const findRolloutsInDirectory = (directory, depth) => {
28989
+ let entries;
28990
+ try {
28991
+ entries = readdirSync7(directory, { withFileTypes: true });
28992
+ } catch {
28993
+ return;
28994
+ }
28995
+ for (const entry of entries) {
28996
+ const path2 = join14(directory, entry.name);
28997
+ if (entry.isDirectory()) {
28998
+ if (depth < 3) findRolloutsInDirectory(path2, depth + 1);
28999
+ continue;
29000
+ }
29001
+ const ref = /^rollout-.*-([0-9a-f-]{36})\.jsonl$/.exec(entry.name)?.[1];
29002
+ if (!ref || ref === parentThreadId) continue;
29003
+ const changedAt = fetchChangedAt(path2);
29004
+ if (changedAt < changedAfter) continue;
29005
+ rollouts.push({ path: path2, ref, changedAt });
29006
+ }
29007
+ };
29008
+ for (const root of roots) findRolloutsInDirectory(root, 0);
29009
+ const found = [];
29010
+ for (const rollout of rollouts.sort((a, b) => b.changedAt - a.changedAt).slice(0, CHILD_SCAN_CAP)) {
29011
+ const head = fetchFileHead(rollout.path, HEAD_BYTES).split("\n")[0] ?? "";
29012
+ if (!head.includes('"session_meta"') || isCodexHelperRollout(head)) continue;
29013
+ if (!new RegExp(`"parent_thread_id"\\s*:\\s*"${parentThreadId}"`).test(head)) continue;
29014
+ const where = parseWhereFromJson(head);
29015
+ found.push({ ...rollout, label: "subagent", directory: where.directory, branch: where.branch });
29016
+ }
29017
+ return found;
29018
+ }
29019
+ var HarnessActivity = class {
29020
+ constructor(conversation, onReport) {
29021
+ this.conversation = conversation;
29022
+ this.onReport = onReport;
29023
+ }
29024
+ conversation;
29025
+ onReport;
29026
+ workers = /* @__PURE__ */ new Map();
29027
+ own = null;
29028
+ doing = null;
29029
+ watched = false;
29030
+ discoveredAt = 0;
29031
+ timer = null;
29032
+ reported = "";
29033
+ stopped = false;
29034
+ start() {
29035
+ if (this.timer || this.stopped) return;
29036
+ this.timer = setInterval(() => this.tick(), TAIL_EVERY_MS);
29037
+ this.timer.unref?.();
29038
+ this.tick();
29039
+ }
29040
+ /** The room says whether anyone has the board open. Off, the lines go quiet at once and the workers stay listed. */
29041
+ setWatched(on) {
29042
+ if (on === this.watched) return;
29043
+ this.watched = on;
29044
+ if (on) {
29045
+ this.own = this.own && new FileTail(this.own.path);
29046
+ for (const worker of this.workers.values()) worker.tail = new FileTail(worker.path);
29047
+ }
29048
+ this.tick();
29049
+ }
29050
+ /** Say it again: the room forgets a session's activity when its socket drops, and only the agent can refill it. */
29051
+ repeat() {
29052
+ this.reported = "";
29053
+ this.tick();
29054
+ }
29055
+ stop() {
29056
+ this.stopped = true;
29057
+ if (this.timer) clearInterval(this.timer);
29058
+ this.timer = null;
29059
+ }
29060
+ tick() {
29061
+ if (this.stopped) return;
29062
+ const now = Date.now();
29063
+ if (now - this.discoveredAt >= DISCOVER_EVERY_MS) {
29064
+ this.discoveredAt = now;
29065
+ try {
29066
+ this.discover(now);
29067
+ } catch {
29068
+ }
29069
+ }
29070
+ if (this.watched) {
29071
+ try {
29072
+ this.read();
29073
+ } catch {
29074
+ }
29075
+ }
29076
+ const activity = this.build();
29077
+ const said = JSON.stringify(activity);
29078
+ if (said === this.reported) return;
29079
+ this.reported = said;
29080
+ this.onReport(activity);
29081
+ }
29082
+ /** Which files to read: this chat's own, and one per worker still working. */
29083
+ discover(now) {
29084
+ const found = this.findWorkerFiles(now - WORKER_FRESH_MS);
29085
+ for (const file2 of found) {
29086
+ const known = this.workers.get(file2.ref);
29087
+ if (known) Object.assign(known, { label: file2.label, directory: file2.directory, branch: file2.branch, changedAt: file2.changedAt });
29088
+ else this.workers.set(file2.ref, { ...file2, since: new Date(file2.changedAt).toISOString(), doing: null, tail: new FileTail(file2.path, "end") });
29089
+ }
29090
+ const live = new Set(found.map((file2) => file2.ref));
29091
+ for (const ref of [...this.workers.keys()]) if (!live.has(ref)) this.workers.delete(ref);
29092
+ }
29093
+ findWorkerFiles(changedAfter) {
29094
+ if (this.conversation.harness === "claude") {
29095
+ const transcript = findClaudeTranscriptPath(fetchClaudeHomes(), this.conversation.id);
29096
+ this.own ??= transcript ? new FileTail(transcript, "end") : null;
29097
+ return transcript ? findClaudeWorkerFiles(transcript, this.conversation.id, changedAfter) : [];
29098
+ }
29099
+ if (this.conversation.harness === "codex") {
29100
+ const roots = fetchCodexHomes().map((home) => join14(home, "sessions"));
29101
+ const rollout = fetchCodexRolloutPath(roots, this.conversation.id);
29102
+ this.own ??= rollout ? new FileTail(rollout, "end") : null;
29103
+ return findCodexChildRollouts(roots, this.conversation.id, changedAfter);
29104
+ }
29105
+ return [];
29106
+ }
29107
+ /** The newest tool call in everything that has been written since the last read. */
29108
+ read() {
29109
+ const parse3 = this.conversation.harness === "codex" ? parseToolCallFromCodexLine : parseToolCallFromClaudeLine;
29110
+ const findNewestCall = (tail) => {
29111
+ let newest = null;
29112
+ for (const line of tail.fetchNewLines()) newest = parse3(line) ?? newest;
29113
+ return newest;
29114
+ };
29115
+ if (this.own) this.doing = findNewestCall(this.own) ?? this.doing;
29116
+ for (const worker of this.workers.values()) worker.doing = findNewestCall(worker.tail) ?? worker.doing;
29117
+ }
29118
+ build() {
29119
+ const workers = [...this.workers.values()].sort((a, b) => b.changedAt - a.changedAt).slice(0, WORKERS_PER_SESSION).map((worker) => ({ ref: worker.ref, label: worker.label, directory: worker.directory, branch: worker.branch, doing: this.watched ? worker.doing : null, since: worker.since }));
29120
+ return { doing: this.watched ? this.doing : null, workers };
29121
+ }
29122
+ };
29123
+
28694
29124
  // src/platform/bounded.ts
28695
29125
  var RESULT_CAP = 16e3;
28696
29126
  function truncateToolResult(result) {
@@ -28810,11 +29240,11 @@ function serveMcp(tools, info) {
28810
29240
  }
28811
29241
 
28812
29242
  // src/commands/tasks.ts
28813
- import { readFileSync as readFileSync13 } from "fs";
29243
+ import { readFileSync as readFileSync14 } from "fs";
28814
29244
 
28815
29245
  // src/commands/items.ts
28816
- import { readFileSync as readFileSync12 } from "fs";
28817
- var fetchTextArgument = (value) => value === "-" ? readFileSync12(0, "utf8").trim() : value;
29246
+ import { readFileSync as readFileSync13 } from "fs";
29247
+ var fetchTextArgument = (value) => value === "-" ? readFileSync13(0, "utf8").trim() : value;
28818
29248
  var buildItemsPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
28819
29249
  function formatPlanItems(items) {
28820
29250
  if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
@@ -28841,7 +29271,7 @@ function registerItemCommands(task, registerProjectOption) {
28841
29271
  registerProjectOption(task.command("plan <key> [lines...]")).description('Append items to the plan, one per argument; "-" reads one per line from stdin (list markers are fine).').action(async (key, lines, options) => {
28842
29272
  const api = buildAuthedApiClient();
28843
29273
  const project = await resolveProject(api, options.project);
28844
- const source = lines.length === 1 && lines[0] === "-" ? readFileSync12(0, "utf8") : lines.join("\n");
29274
+ const source = lines.length === 1 && lines[0] === "-" ? readFileSync13(0, "utf8") : lines.join("\n");
28845
29275
  const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
28846
29276
  if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
28847
29277
  const items = await api.post(buildItemsPath(project.id, key), ApiResponses.TaskItems, { items: titles.map((title3) => ({ title: title3 })) });
@@ -28898,7 +29328,7 @@ function parseOneOf(value, allowed, what) {
28898
29328
  if (allowed.includes(normalized)) return normalized;
28899
29329
  throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
28900
29330
  }
28901
- var fetchTextArgument2 = (value) => value === "-" ? readFileSync13(0, "utf8").trim() : value;
29331
+ var fetchTextArgument2 = (value) => value === "-" ? readFileSync14(0, "utf8").trim() : value;
28902
29332
  var buildTaskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
28903
29333
  var fetchProject = (api, id) => api.get(`/projects/${id}`, ApiResponses.Project);
28904
29334
  function resolveStatus(project, ref) {
@@ -29049,20 +29479,20 @@ ${buildTaskUrl(api.baseUrl, project.id, created)}`);
29049
29479
  }
29050
29480
 
29051
29481
  // src/platform/update-check.ts
29052
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
29053
- import { dirname as dirname7, join as join14 } from "path";
29482
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
29483
+ import { dirname as dirname8, join as join15 } from "path";
29054
29484
  var CHECK_EVERY_MS = 60 * 60 * 1e3;
29055
29485
  var REGISTRY = "https://registry.npmjs.org/slopwaresystems/latest";
29056
- var buildUpdateCachePath = () => join14(dirname7(buildConfigPath()), "update-check.json");
29486
+ var buildUpdateCachePath = () => join15(dirname8(buildConfigPath()), "update-check.json");
29057
29487
  function fetchUpdateCache() {
29058
29488
  try {
29059
- return JSON.parse(readFileSync14(buildUpdateCachePath(), "utf8"));
29489
+ return JSON.parse(readFileSync15(buildUpdateCachePath(), "utf8"));
29060
29490
  } catch {
29061
29491
  return { checkedAt: 0, latest: null };
29062
29492
  }
29063
29493
  }
29064
29494
  function persistUpdateCache(cache) {
29065
- mkdirSync8(dirname7(buildUpdateCachePath()), { recursive: true });
29495
+ mkdirSync8(dirname8(buildUpdateCachePath()), { recursive: true });
29066
29496
  writeFileSync7(buildUpdateCachePath(), JSON.stringify(cache));
29067
29497
  }
29068
29498
  function isNewerVersion(a, b) {
@@ -29092,11 +29522,11 @@ function buildUpdateNotice(current) {
29092
29522
  if (!latest || !isNewerVersion(latest, current)) return null;
29093
29523
  return `Update available: slopwaresystems ${latest} (this is ${current}). Ask your person to run: npm i -g slopwaresystems && slop mcp install`;
29094
29524
  }
29095
- var isUpdateCheckEnabled = () => !existsSync10(join14(dirname7(buildConfigPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
29525
+ var isUpdateCheckEnabled = () => !existsSync11(join15(dirname8(buildConfigPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
29096
29526
 
29097
29527
  // src/commands/threads.ts
29098
- import { readFileSync as readFileSync15 } from "fs";
29099
- var fetchTextArgument3 = (value) => value === "-" ? readFileSync15(0, "utf8").trim() : value;
29528
+ import { readFileSync as readFileSync16 } from "fs";
29529
+ var fetchTextArgument3 = (value) => value === "-" ? readFileSync16(0, "utf8").trim() : value;
29100
29530
  var formatMessageTime = (iso) => iso.slice(0, 16).replace("T", " ");
29101
29531
  var formatAuthor = (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}` : ""})`;
29102
29532
  function formatThreadLine(thread) {
@@ -29174,16 +29604,20 @@ var buildToolSchema = (properties, required2 = []) => ({ type: "object", propert
29174
29604
  var PROJECT = buildStringSchema("Project key. Defaults to the project this repository is linked to.");
29175
29605
  var buildTaskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
29176
29606
  var formatSessionLine = (session, mine = null) => [
29177
- // Optional chaining on purpose: a server from before agents had names answers without these, and a tool must not crash on it.
29178
- (session.agent?.name ?? "agent") + (session.id === mine ? " (you)" : ""),
29179
- session.taskKey ?? "(no task)",
29180
- session.user.name,
29181
- session.machine.name,
29182
- session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-",
29183
- `since ${session.startedAt}`,
29184
- session.state === "reconnecting" ? "(reconnecting)" : "",
29185
- session.reachable ? "" : "(not visible to teammates)"
29186
- ].filter(Boolean).join(" ");
29607
+ [
29608
+ // Optional chaining on purpose: a server from before agents had names answers without these, and a tool must not crash on it.
29609
+ (session.agent?.name ?? "agent") + (session.id === mine ? " (you)" : ""),
29610
+ session.taskKey ?? "(no task)",
29611
+ session.user.name,
29612
+ session.machine.name,
29613
+ session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-",
29614
+ `since ${session.startedAt}`,
29615
+ session.state === "reconnecting" ? "(reconnecting)" : "",
29616
+ session.reachable ? "" : "(not visible to teammates)",
29617
+ session.doing ? `doing ${session.doing}` : ""
29618
+ ].filter(Boolean).join(" "),
29619
+ ...(session.workers ?? []).map((worker) => ` ${generateWorkerName(session.agent?.name ?? "agent", worker.ref)} (worker) ${worker.label}${worker.directory ? ` ${worker.directory}${worker.branch ? `@${worker.branch}` : ""}` : ""}${worker.doing ? ` doing ${worker.doing}` : ""}`)
29620
+ ].join("\n");
29187
29621
  async function buildUnreadDigest(api, projectId, taskKey) {
29188
29622
  const [about, general] = await Promise.all([
29189
29623
  taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1&read=1`, ApiResponses.Threads) : Promise.resolve([]),
@@ -29223,6 +29657,17 @@ function createProjectTools(api, project, identity) {
29223
29657
  let held = null;
29224
29658
  let stopped = false;
29225
29659
  const recovery = new SessionRecovery(api, owner, process.cwd(), (push) => held?.deliverPush(push));
29660
+ let holds = null;
29661
+ const canHoldOwnSession = () => holds ??= claimSessionHolder(process.cwd(), identity.conversation);
29662
+ const SPAWNED_BY_ANOTHER_AGENT = "Another process of this chat holds its session, so you are a subagent it spawned (or its server was restarted). A subagent does not hold tickets and does not talk on the board: the agent that spawned you does, and the board already shows you working under it. Report what you found in your answer to it.";
29663
+ const requireOwnSession = () => {
29664
+ if (!canHoldOwnSession()) throw new Error(SPAWNED_BY_ANOTHER_AGENT);
29665
+ };
29666
+ let told = null;
29667
+ const activity = new HarnessActivity({ id: identity.conversation, harness: identity.harness }, (report) => {
29668
+ told = report;
29669
+ held?.reportActivity(report);
29670
+ });
29226
29671
  let waking = false;
29227
29672
  const wakeOwnHarness = () => {
29228
29673
  if (waking || stopped) return;
@@ -29236,6 +29681,10 @@ function createProjectTools(api, project, identity) {
29236
29681
  const resolveOwnAgentIdentity = () => identity;
29237
29682
  const holdSession = async (projectId, taskKey) => {
29238
29683
  if (stopped) throw new Error("The project session is stopping");
29684
+ if (!canHoldOwnSession()) {
29685
+ if (taskKey) throw new Error(SPAWNED_BY_ANOTHER_AGENT);
29686
+ return;
29687
+ }
29239
29688
  if (held && !held.isStopped && held.projectId === projectId && held.taskKey === taskKey) {
29240
29689
  await held.requireSession();
29241
29690
  return;
@@ -29251,8 +29700,14 @@ function createProjectTools(api, project, identity) {
29251
29700
  onPush: persistOwnInboxPush,
29252
29701
  onMessage: wakeOwnHarness,
29253
29702
  claim: resolveOwnAgentIdentity,
29254
- onReconnect: () => recovery.sync()
29703
+ onReconnect: () => recovery.sync(),
29704
+ // The room forgets a session's activity when its socket goes: whatever was true is said again on the new one.
29705
+ onHello: () => {
29706
+ if (told) held?.reportActivity(told);
29707
+ },
29708
+ onWatched: (on) => activity.setWatched(on)
29255
29709
  });
29710
+ activity.start();
29256
29711
  const current = held;
29257
29712
  await current.start();
29258
29713
  await current.requireSession();
@@ -29299,6 +29754,7 @@ function createProjectTools(api, project, identity) {
29299
29754
  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.",
29300
29755
  inputSchema: buildToolSchema({ task: buildStringSchema("Task key to go live on first (optional)"), thread: buildStringSchema("Only a message in this thread"), minutes: buildIntegerSchema("Give up after (default 300; 20 with thread)"), project: PROJECT }),
29301
29756
  run: async (args, output2) => {
29757
+ requireOwnSession();
29302
29758
  const target = await resolveProjectWithPresence(args);
29303
29759
  if (args.task) await holdSession(target.id, String(args.task));
29304
29760
  const thread = args.thread ? String(args.thread) : null;
@@ -29311,6 +29767,7 @@ function createProjectTools(api, project, identity) {
29311
29767
  description: "What is unread right now: anything the held session received since you last looked, threads about a task (if given), and general threads. Does not wait. Marks them read.",
29312
29768
  inputSchema: buildToolSchema({ task: buildStringSchema("Task key"), project: PROJECT }),
29313
29769
  run: async (args, output2) => {
29770
+ requireOwnSession();
29314
29771
  const target = await resolveProjectWithPresence(args);
29315
29772
  claimOwnInbox(output2);
29316
29773
  return buildUnreadDigest(api, target.id, args.task ?? null);
@@ -29321,6 +29778,7 @@ function createProjectTools(api, project, identity) {
29321
29778
  description: "Reply in a thread. In a thread about a task, it reaches whoever is live on that task now, or waits for them.",
29322
29779
  inputSchema: buildToolSchema({ thread: buildStringSchema("Thread id"), body: buildStringSchema("The message (Markdown)"), wait: buildIntegerSchema("Claude Code only, rarely: then wait this many minutes for a reply in the thread"), project: PROJECT }, ["thread", "body"]),
29323
29780
  run: async (args, output2) => {
29781
+ requireOwnSession();
29324
29782
  const target = await resolveProjectWithPresence(args);
29325
29783
  const thread = String(args.thread);
29326
29784
  const result = await api.post(`/projects/${target.id}/threads/${thread}/messages`, ApiResponses.CreatedThreadMessage, { body: String(args.body) });
@@ -29336,6 +29794,7 @@ ${formatWaitOutcome(await waitForNextMessage(output2, thread, args.wait), args.w
29336
29794
  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.",
29337
29795
  inputSchema: buildToolSchema({ subject: buildStringSchema("Subject (default: the first line of the body)"), body: buildStringSchema("First message (Markdown)"), task: buildStringSchema("Task key, to make it about that task"), wait: buildIntegerSchema("Claude Code only, rarely: then wait this many minutes for a reply in the thread"), project: PROJECT }, ["body"]),
29338
29796
  run: async (args, output2) => {
29797
+ requireOwnSession();
29339
29798
  const target = await resolveProjectWithPresence(args);
29340
29799
  const result = await api.post(`/projects/${target.id}/threads`, ApiResponses.CreatedThread, { subject: args.subject ? String(args.subject) : void 0, body: String(args.body), task: args.task, to: args.to });
29341
29800
  const names = result.thread.to.map((recipient) => recipient.agent).join(", ");
@@ -29385,9 +29844,10 @@ ${formatWaitOutcome(await waitForNextMessage(output2, result.thread.id, args.wai
29385
29844
  },
29386
29845
  {
29387
29846
  name: "claim_task",
29388
- description: "Take a task: assign it to you, move it to in progress, go live on it for as long as this server runs, and get what is unread about it. From now on a message in a thread about the task reaches you on its own, right after your next tool call; you do not have to wait or poll.",
29847
+ description: "Take a task: assign it to you, move it to in progress, go live on it for as long as this server runs, and get what is unread about it. Do this first when you are told to work a ticket, before reading any code; nothing else makes you live on it. From now on a message in a thread about the task reaches you on its own, right after your next tool call; you do not have to wait or poll.",
29389
29848
  inputSchema: buildToolSchema({ task: buildStringSchema("Task key"), project: PROJECT }, ["task"]),
29390
29849
  run: async (args) => {
29850
+ requireOwnSession();
29391
29851
  const target = await fetchProject(api, (await resolveProjectWithPresence(args)).id);
29392
29852
  const assigneeId = await resolveAssignee(api, target.id, "me");
29393
29853
  const task = await api.patch(buildTaskPath(target.id, String(args.task)), ApiResponses.Task, { assigneeId, status: requireStatusInCategory(target, "in_progress") });
@@ -29402,6 +29862,7 @@ ${await buildUnreadDigest(api, target.id, task.key)}`;
29402
29862
  description: "Step off the task you hold without finishing it. You stay live in the project, reachable in threads you are part of.",
29403
29863
  inputSchema: buildToolSchema({ project: PROJECT }),
29404
29864
  run: async () => {
29865
+ requireOwnSession();
29405
29866
  await releaseTaskSession();
29406
29867
  return "Released. Still live in the project.";
29407
29868
  }
@@ -29602,7 +30063,7 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
29602
30063
  },
29603
30064
  {
29604
30065
  name: "finish_task",
29605
- description: "Move a task to the first done column.",
30066
+ description: "Move a task to the first done column and release your session. Done is the person's call: link the pull request, say what changed, and ask them before you call this.",
29606
30067
  inputSchema: buildToolSchema({ task: buildStringSchema("Task key"), project: PROJECT }, ["task"]),
29607
30068
  run: async (args) => {
29608
30069
  const target = await fetchProject(api, (await resolveProjectWithPresence(args)).id);
@@ -29617,7 +30078,9 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
29617
30078
  stop: () => {
29618
30079
  stopped = true;
29619
30080
  recovery.stop();
30081
+ activity.stop();
29620
30082
  held?.stop();
30083
+ if (holds) releaseSessionHolder(process.cwd(), identity.conversation);
29621
30084
  },
29622
30085
  tools: tools.map((tool) => ({ ...tool, run: async (args, output2) => {
29623
30086
  const result = await tool.run(args, output2);
@@ -29628,14 +30091,14 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
29628
30091
  }
29629
30092
  function fetchJsonFile(path2) {
29630
30093
  try {
29631
- return JSON.parse(readFileSync16(path2, "utf8"));
30094
+ return JSON.parse(readFileSync17(path2, "utf8"));
29632
30095
  } catch {
29633
30096
  return {};
29634
30097
  }
29635
30098
  }
29636
30099
  function installIntoClaudeHome(dir) {
29637
30100
  const done = [];
29638
- const primary = dir === join15(homedir4(), ".claude");
30101
+ const primary = dir === join16(homedir4(), ".claude");
29639
30102
  const env = { ...process.env };
29640
30103
  if (primary) delete env.CLAUDE_CONFIG_DIR;
29641
30104
  else env.CLAUDE_CONFIG_DIR = dir;
@@ -29648,7 +30111,7 @@ function installIntoClaudeHome(dir) {
29648
30111
  if (/already exists/i.test(said)) done.push(`Claude Code: MCP server "slop" already registered (user scope${where})`);
29649
30112
  else done.push(`Claude Code: run \`${primary ? "" : `CLAUDE_CONFIG_DIR=${dir} `}claude mcp add --scope user slop -- slop mcp\` (${said.trim() || "the claude command was not found here"})`);
29650
30113
  }
29651
- const path2 = join15(dir, "settings.json");
30114
+ const path2 = join16(dir, "settings.json");
29652
30115
  mkdirSync9(dir, { recursive: true });
29653
30116
  const settings = fetchJsonFile(path2);
29654
30117
  settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
@@ -29677,8 +30140,8 @@ function mergeHooks(hooks, harness) {
29677
30140
  function installIntoCodexHome(dir) {
29678
30141
  const done = [];
29679
30142
  mkdirSync9(dir, { recursive: true });
29680
- const configTomlPath = join15(dir, "config.toml");
29681
- const current = existsSync11(configTomlPath) ? readFileSync16(configTomlPath, "utf8") : "";
30143
+ const configTomlPath = join16(dir, "config.toml");
30144
+ const current = existsSync12(configTomlPath) ? readFileSync17(configTomlPath, "utf8") : "";
29682
30145
  if (/^\[mcp_servers\.slop\]/m.test(current)) {
29683
30146
  if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync8(configTomlPath, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
29684
30147
  done.push(`Codex: MCP server already in ${configTomlPath}`);
@@ -29690,7 +30153,7 @@ tool_timeout_sec = 21600
29690
30153
  `);
29691
30154
  done.push(`Codex: MCP server "slop" added to ${configTomlPath}`);
29692
30155
  }
29693
- const hooksPath = join15(dir, "hooks.json");
30156
+ const hooksPath = join16(dir, "hooks.json");
29694
30157
  const file2 = fetchJsonFile(hooksPath);
29695
30158
  file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
29696
30159
  writeFileSync8(hooksPath, `${JSON.stringify(file2, null, 2)}
@@ -29700,7 +30163,7 @@ tool_timeout_sec = 21600
29700
30163
  }
29701
30164
  async function trustCodexHooks(codexHome) {
29702
30165
  const server = spawn3("codex", ["app-server", "-c", "mcp_servers={}"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
29703
- const where = codexHome === join15(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
30166
+ const where = codexHome === join16(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
29704
30167
  let buffer = "";
29705
30168
  const pending = /* @__PURE__ */ new Map();
29706
30169
  server.stdout.on("data", (chunk) => {
@@ -29735,7 +30198,7 @@ async function trustCodexHooks(codexHome) {
29735
30198
  const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
29736
30199
  `);
29737
30200
  try {
29738
- await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.11" }, capabilities: { experimentalApi: true } });
30201
+ await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.13" }, capabilities: { experimentalApi: true } });
29739
30202
  sendNotification("initialized", {});
29740
30203
  const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
29741
30204
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
@@ -29766,7 +30229,7 @@ async function parseHookInput(harness, remember = true) {
29766
30229
  conversation = parsed.session_id ?? null;
29767
30230
  } catch {
29768
30231
  }
29769
- const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join15(home, "sessions")), conversation);
30232
+ const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join16(home, "sessions")), conversation);
29770
30233
  if (remember && conversation && ownThread && !isNonInteractiveHarness()) persistRememberedConversation(cwd, { id: conversation, harness: harness ?? "other" });
29771
30234
  return { cwd, conversation };
29772
30235
  }
@@ -29872,7 +30335,7 @@ function buildTools() {
29872
30335
  ),
29873
30336
  buildGroupedTool(
29874
30337
  "task",
29875
- "One task. show; create; update (only given fields change); move; link; archive/restore; history; finish (first done column, releases your session).",
30338
+ "One task. show; create; update (only given fields change); move; link; archive/restore; history; finish (first done column, releases your session: link the pull request and ask the person before you finish, see the slop-working skill).",
29876
30339
  { show: "show_task", create: "create_task", update: "update_task", move: "move_task", link: "link_task", archive: "archive_task", restore: "archive_task", history: "task_history", finish: "finish_task" },
29877
30340
  { task: buildStringSchema("Key, e.g. WEB-12 (all but create)"), title: buildStringSchema(""), description: buildStringSchema("Markdown"), notes: buildStringSchema("scratchpad, Markdown"), severity: buildStringSchema("low|medium|high|critical"), column: buildStringSchema("column key or label"), assignee: buildStringSchema("email, name, me, or none"), url: buildStringSchema("link: the URL"), limit: buildIntegerSchema("history: newest N (default 50)"), before: buildStringSchema("history: page back from this time") }
29878
30341
  ),
@@ -29884,7 +30347,7 @@ function buildTools() {
29884
30347
  ),
29885
30348
  buildGroupedTool(
29886
30349
  "session",
29887
- "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.",
30350
+ "Being live. claim: told to work a ticket, do this first, before reading any code: it assigns the ticket to you, moves it to in progress, goes live on it and returns what is unread (messages then reach you on their own). Nothing else makes you live. 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.",
29888
30351
  { claim: "claim_task", release: "release_task", who: "sessions", check: "check_messages", wait: "wait_for_message" },
29889
30352
  { task: buildStringSchema("Task key"), thread: buildStringSchema("wait: only a message in this thread"), minutes: buildIntegerSchema("wait: give up after (default 300; 20 with thread)") }
29890
30353
  ),
@@ -30017,13 +30480,13 @@ ${written.map((path2) => ` ${path2}`).join("\n")}`);
30017
30480
  }
30018
30481
 
30019
30482
  // src/index.ts
30020
- 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.11").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => updateJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
30483
+ 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.13").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => updateJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
30021
30484
  registerAuthCommands(program2);
30022
30485
  registerProjectCommands(program2);
30023
30486
  var taskCommand = registerTaskCommands(program2);
30024
30487
  registerSessionCommands(program2);
30025
30488
  registerThreadCommands(program2, taskCommand);
30026
- registerMcpCommands(program2, "0.4.11");
30489
+ registerMcpCommands(program2, "0.4.13");
30027
30490
  registerGuideCommand(program2);
30028
30491
  registerSkillCommands(program2);
30029
30492
  program2.parseAsync().catch(exitWithError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",