slopwaresystems 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -146,7 +146,7 @@ function fetchLaunchCandidate(launcher2) {
146
146
  var path = fileURLToPath(import.meta.url);
147
147
  var launcher = {
148
148
  path,
149
- bundledVersion: "0.9.3",
149
+ bundledVersion: "0.9.5",
150
150
  bundledMain: fileURLToPath(new URL("./main.js", import.meta.url)),
151
151
  installed: isInstalledLauncherPath(path)
152
152
  };
package/dist/main.js CHANGED
@@ -3389,7 +3389,7 @@ var slop_operations_default = '---\nname: slop-operations\ndescription: How to p
3389
3389
  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\nA plan across tickets (phases, several agents, work that waits on other work) is an **operation**, not a longer plan: see `slop-operations`. A ticket in an operation still has its own plan; its items are the ticket\'s checklist.\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';
3390
3390
 
3391
3391
  // skills/slop-sessions.md
3392
- 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.** 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\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## Operations: available and orchestrator\n\nAn operation (see `slop-operations`) is run by one agent session, its **orchestrator**, which proposes who does each ready ticket; a person approves every proposal. Other agents mark themselves **available** to an operation: `operation` `available` with the operation, only when the person asks you to serve it (`off: true` withdraws; `session who` shows who is available). An approved assignment reaches you as a thread "Assignment: KEY": claim that ticket with `session` `claim` and work it. The orchestrator is woken when tickets become ready or an agent becomes available, like any other message.\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';
3392
+ 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.** 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, its model; 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\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## Operations: available and orchestrator\n\nAn operation (see `slop-operations`) is run by one agent session, its **orchestrator**, which proposes who does each ready ticket; a person approves every proposal. Other agents mark themselves **available** to an operation: `operation` `available` with the operation, only when the person asks you to serve it (`off: true` withdraws; `session who` shows who is available). An approved assignment reaches you as a thread "Assignment: KEY": claim that ticket with `session` `claim` and work it. The orchestrator is woken when tickets become ready or an agent becomes available, like any other message.\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, the worktree they work in and the model each runs on, 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';
3393
3393
 
3394
3394
  // skills/slop-splat.md
3395
3395
  var slop_splat_default = '---\nname: slop-splat\ndescription: How an agent uses Splat, the project\'s app rendered on the team\'s shared canvas: every route at every viewport (mobile, tablet, desktop), captured on a machine running the app, seen by everyone. Load when asked to show, capture, compare or check the UI; after changing UI; or when asked to point Splat at a dev server or port.\n---\n\n# Splat: the app on the team\'s canvas\n\nA Splat canvas belongs to a project (the web app\'s Splat tab). A **route** is a path in the app (`/settings/billing`). Each route has one **frame** per viewport (by default mobile 375, tablet 768, desktop 1280). A **capture** is one render of a frame. It is taken on a machine that runs the app, stored with the branch and commit it came from, and shown to the whole team. The canvas is seen through a branch: a frame shows that branch\'s newest capture, else the default branch\'s.\n\nEverything goes through the `splat` MCP tool. Each call is an `action` and its fields in `args`, for example `{ "action": "target", "args": { "port": 3100 } }` or `{ "action": "capture", "args": { "paths": ["/settings"], "full": true } }`. `project` defaults to the linked one.\n\n## Point it at the app\n\nSplat renders a dev server on this machine. It finds servers running inside the repository\'s worktrees by itself. Anything else (a tunnel, a container, a port you were told about) is pinned:\n\n- `splat target` `port: 3100` (optional `branch`, `label`). The pin is kept on this machine for this project, and every renderer here picks it up. `remove: true` unpins it.\n- `splat canvas` lists the routes, what each frame shows, who can render now, and this machine\'s dev servers.\n\nWhile this chat is open and the project uses Splat, the chat is also a **renderer**. It answers the web\'s Refresh button and serves live frames through a proxy on `http://<port>.localhost:7421`. You do nothing for this.\n\n## The loop after a UI change\n\n1. `splat add` `paths: ["/settings", "/settings/billing"]` puts routes on the canvas (a frame per viewport). A path already there is reused. For a file-based router, `splat discover` lists the routes and `add: true` adds the static ones. For a router defined in code (a routes array, a `<Route>` tree), read it and add the paths yourself. Dynamic segments need an example value: `/projects/abc123`.\n2. `splat capture` `paths: [...]` renders them here with Chrome, uploads them, and returns the images: the top of each page by default, `full: true` for the whole page in tiles, `limit` for more frames. Look at them, critique them against what was asked, and fix what is off.\n3. Capture again after fixing. Every capture is on the canvas for the team at once, stamped with the branch, the commit, and "uncommitted" when the working tree was dirty.\n\nIf a render landed on a sign-in page, the result says so. Ask the person to run `slop splat login --port <port>` once: it opens Chrome on Splat\'s own profile to sign in, and every capture after that is signed in (the cookies stay in that profile). On a machine with no screen (a VM), `slop splat login --port <port> --cookie name=value` sets the app\'s session cookie instead.\n\n## Workspaces and published apps\n\nA project can have several **workspaces**, each its own canvas (routes, layout, notes, pins, viewports). Every action takes `workspace` (a name or id); without it, the project\'s default. `splat workspaces` lists them; `name: "Checkout redesign"` makes one.\n\nA workspace holds **sources**: its shared source (captured by whichever renderer has the branch, as above) and the apps people **publish** into it. A published app runs on its publisher\'s machine (a laptop, a VM) and the team uses it **live** from their browsers through Slopware, with no tunnel, open port or local server of their own. Publishing is a person\'s (or a VM\'s) long-running command, not a chat\'s:\n\n slop splat serve --workspace "Checkout redesign" --name "Checkout experiment" --port 3100\n\nThe same name in the same workspace is the same source across restarts and port or branch changes. Several `serve --name` on one machine are several sources. A new source has no frames: `splat sources` lists the workspace\'s sources, and `frames: "Checkout experiment"` puts that source\'s frames on the canvas (every route at every viewport). `splat add` with the workspace\'s sources shown adds frames for them too. Its captures (`refresh` with `sources: ["Checkout experiment"]`) are taken by its own publisher.\n\nFrames say which source they show, and two sources\' frames of one route sit side by side after `slop splat tidy`, so a change is compared against main at a glance. Which sources a person sees is their own choice in the web\'s Sources menu; nobody else\'s view changes.\n\n## Keeping it current\n\n- `slop splat schedule 1h` (or `15m`, `6h`, `1d`, `off`) recaptures every frame on the default branch on a schedule. A `slop splat serve` with a dev server on that branch does the rounds (a VM that always runs the app); the server gives each round to one renderer. An agent\'s chat never takes rounds.\n- `slop splat tidy` lays the canvas out again: sections in order, each route\'s viewports side by side, routes as a grid. Captures, notes and pins stay.\n\n## Other actions\n\n- `view` `route` (optional `viewport`, `branch`): the team\'s latest render of a route, as images. Use it to see what the others see without running the app.\n- `refresh` (optional `paths`, `viewports`, `branch`, `renderer`, `sources`): ask a renderer on the team to capture, for a branch you do not run; a published source\'s frames are captured by its publisher.\n- `renderers`: who can render now, with their dev servers and branches.\n- `section` `title`, `paths`: a labelled group (a user journey, a comparison). `remove: <section id>` removes it with its frames.\n- `note` `markdown` or `file`: a spec or plan beside the frames. `image` `file`: a reference or mock.\n- `pin` `route`, `viewport`, `x` (0-1 across), `y` (px down the page), plus `task` (an existing key) or `title` (a new task), optional `source`: a task on a spot of the page. The task links back to the pin.\n- `viewport` `key`, `width`, `height` (or `remove: true`): the project\'s viewports. A new one gets a frame for every route.\n- `update` `route`, `path`: change a path. `remove` `route`, `confirm: true`: take it off, captures and all.\n\nRenders are Chrome\'s. Use them to check layout and design. They are not pixel-perfect cross-browser QA.\n';
@@ -26542,7 +26542,7 @@ var TaskEventRecord = external_exports.object({
26542
26542
  });
26543
26543
 
26544
26544
  // ../contract/src/work/cli-release.ts
26545
- var CLI_RELEASE = "0.9.3";
26545
+ var CLI_RELEASE = "0.9.5";
26546
26546
  var CLI_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
26547
26547
  var CLI_VERSION_MAX = 64;
26548
26548
  function parseCliVersion(version2) {
@@ -26592,18 +26592,22 @@ var AgentHarness = external_exports.enum(["claude", "codex", "cursor", "other"])
26592
26592
  var WORKERS_PER_SESSION = 200;
26593
26593
  var WORKER_LABEL_MAX = 80;
26594
26594
  var DOING_MAX = 120;
26595
+ var MODEL_MAX = 80;
26596
+ var ModelClaim = external_exports.string().trim().max(MODEL_MAX).nullable().default(null);
26595
26597
  var SessionWorker = external_exports.object({
26596
26598
  ref: external_exports.string().trim().min(1).max(100),
26597
26599
  /** What it was spawned to do, in the harness's own words. */
26598
26600
  label: external_exports.string().trim().min(1).max(WORKER_LABEL_MAX),
26599
26601
  directory: external_exports.string().trim().max(120).nullable().default(null),
26600
26602
  branch: external_exports.string().trim().max(120).nullable().default(null),
26603
+ /** The model it runs on, which need not be its parent's: a subagent may be spawned on a smaller one. */
26604
+ model: ModelClaim,
26601
26605
  doing: external_exports.string().trim().max(DOING_MAX).nullable().default(null),
26602
26606
  since: Timestamp
26603
26607
  });
26604
26608
  var SessionWorkerEntry = SessionWorker.omit({ doing: true });
26605
26609
  var SessionWorkerLine = external_exports.object({ ref: SessionWorker.shape.ref, doing: SessionWorker.shape.doing });
26606
- var SessionRoster = external_exports.object({ workers: external_exports.array(SessionWorkerEntry).max(WORKERS_PER_SESSION) });
26610
+ var SessionRoster = external_exports.object({ workers: external_exports.array(SessionWorkerEntry).max(WORKERS_PER_SESSION), model: ModelClaim });
26607
26611
  var SessionDoing = external_exports.object({
26608
26612
  doing: external_exports.string().trim().max(DOING_MAX).nullable().default(null),
26609
26613
  lines: external_exports.array(SessionWorkerLine).max(WORKERS_PER_SESSION).default([])
@@ -26621,10 +26625,11 @@ var Session = external_exports.object({
26621
26625
  user: UserRef,
26622
26626
  machine: external_exports.object({ id: Id, name: external_exports.string() }),
26623
26627
  /**
26624
- * The agent itself: a generated name (see agent-names), which harness runs it, and the version of the CLI that holds
26625
- * its session. Null version: a CLI from before they said (see `isCliVersionOutdated`).
26628
+ * The agent itself: a generated name (see agent-names), which harness runs it, the version of the CLI that holds
26629
+ * its session, and the model it is running on now. Null version: a CLI from before they said (see
26630
+ * `isCliVersionOutdated`). Null model: it has not written anything yet, or its CLI is from before they said.
26626
26631
  */
26627
- agent: external_exports.object({ name: external_exports.string(), harness: AgentHarness.nullable(), cliVersion: external_exports.string().nullable().default(null) }),
26632
+ agent: external_exports.object({ name: external_exports.string(), harness: AgentHarness.nullable(), cliVersion: external_exports.string().nullable().default(null), model: ModelClaim }),
26628
26633
  /** Where it runs, to tell two agents under one person apart. */
26629
26634
  where: external_exports.object({ directory: external_exports.string().nullable(), branch: external_exports.string().nullable() }),
26630
26635
  startedAt: Timestamp,
@@ -28755,6 +28760,25 @@ function persistRememberedConversation(cwd, conversation) {
28755
28760
  const directory = `${buildConversationPath(cwd)}.d`;
28756
28761
  persistLocalState(join3(directory, `${generateLocalStateKey(cwd, conversation.id)}.json`), JSON.stringify(conversation));
28757
28762
  }
28763
+ var buildRelayConversationPath = (relay) => join3(buildSessionsDir(), `relay-${relay.relayPid}-${relay.harnessPid}.conversation.json`);
28764
+ function persistRelayConversation(relay, conversation) {
28765
+ try {
28766
+ persistLocalState(buildRelayConversationPath(relay), JSON.stringify(conversation));
28767
+ } catch {
28768
+ }
28769
+ }
28770
+ function fetchRelayConversation(relay) {
28771
+ try {
28772
+ const parsed = JSON.parse(readFileSync4(buildRelayConversationPath(relay), "utf8"));
28773
+ const harness = AgentHarness.safeParse(parsed.harness);
28774
+ return typeof parsed.id === "string" && parsed.id && harness.success ? { id: parsed.id, harness: harness.data } : null;
28775
+ } catch {
28776
+ return null;
28777
+ }
28778
+ }
28779
+ function deleteRelayConversation(relay) {
28780
+ rmSync4(buildRelayConversationPath(relay), { force: true });
28781
+ }
28758
28782
  function fetchRememberedConversation(cwd, notBefore, harness) {
28759
28783
  const path3 = buildConversationPath(cwd);
28760
28784
  const candidates = [];
@@ -29373,6 +29397,24 @@ function isMainChatThread(roots, threadId) {
29373
29397
  const head = fetchFileHead(path3, HEAD_BYTES);
29374
29398
  return head === "" ? true : isMainChatRollout(head.split("\n")[0] ?? "");
29375
29399
  }
29400
+ function parseCodexThreadFromToolCallMeta(meta3) {
29401
+ const turn = meta3["x-codex-turn-metadata"];
29402
+ if (typeof turn !== "object" || turn === null || Array.isArray(turn)) return null;
29403
+ const thread = turn.thread_id ?? meta3.threadId;
29404
+ return typeof thread === "string" && thread.trim() ? thread.trim() : null;
29405
+ }
29406
+ var PARENT_THREAD_HOPS = 8;
29407
+ function fetchCodexChatThreadId(roots, threadId) {
29408
+ let current = threadId;
29409
+ for (let hop = 0; hop < PARENT_THREAD_HOPS; hop += 1) {
29410
+ const path3 = fetchCodexRolloutPath(roots, current);
29411
+ const line = path3 ? fetchFileHead(path3, HEAD_BYTES).split("\n")[0] ?? "" : "";
29412
+ const parent = /"parent_thread_id"\s*:\s*"([^"]+)"/.exec(line)?.[1];
29413
+ if (!parent || parent === current) return current;
29414
+ current = parent;
29415
+ }
29416
+ return current;
29417
+ }
29376
29418
  function fetchCodexRolloutPath(roots, threadId) {
29377
29419
  const suffix = `-${threadId}.jsonl`;
29378
29420
  let best = null;
@@ -29446,15 +29488,14 @@ function fetchKnownConversation(cwd) {
29446
29488
  if (remembered?.harness === "codex" && isMainChatThread(fetchCodexHomes().map((home) => join9(home, "sessions")), remembered.id)) return remembered;
29447
29489
  return remembered?.harness === "codex" ? null : remembered;
29448
29490
  }
29449
- function resolveAgentIdentity(cwd, fallback = "process") {
29450
- const conversation = fetchKnownConversation(cwd);
29491
+ function resolveAgentIdentity(cwd, fallback = "process", conversation = fetchKnownConversation(cwd)) {
29451
29492
  const repo = fetchRepo();
29452
29493
  return {
29453
29494
  conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve4(cwd)}`),
29454
29495
  harness: conversation?.harness ?? null,
29455
29496
  directory: repo ? basename3(repo.worktree) : basename3(cwd),
29456
29497
  branch: fetchCurrentBranch(cwd),
29457
- cliVersion: "0.9.3"
29498
+ cliVersion: "0.9.5"
29458
29499
  };
29459
29500
  }
29460
29501
 
@@ -29734,7 +29775,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
29734
29775
  - Shell project commands for people and scripts: \`slop project bindings --project <KEY>\` lists repository bindings, including other checkouts; \`slop project binding delete <ID> --project <KEY>\` removes one if you created it or are an org admin. \`slop session reachable <AGENT|SESSION_ID> on|off\` changes visibility for one of your live sessions. \`slop task move <KEY> <COLUMN> --before <NEIGHBOR>\` places the task immediately before that neighbor; \`--after <NEIGHBOR>\` places it immediately after. Use both only for adjacent neighbors: \`--after <PREVIOUS> --before <NEXT>\`. With neither, the task goes to the end of the column. \`slop task delete <KEY...>\` accepts explicit task keys; delete only tasks you created. Each deletion is separate, so a later server rejection can follow earlier successful deletions.
29735
29776
  - Other shell project actions for people and scripts: \`slop task unlink <KEY> <LINK>\` removes one task link by ID, title or URL; \`slop task archive-done\` archives every task in a done column; \`slop project delete <KEY|ID>\` permanently deletes a project and its tasks (admin only, no undo).
29736
29777
  - Agents use the eight slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, \`operation\`, \`splat\`, 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. slop updates itself (the CLI, the MCP server and the skills) and open chats switch to a new version on their own: never update or restart for it. 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\`.
29737
- - 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.
29778
+ - Subagents you spawn show under you on the board (what you asked them to do, the worktree, the model, 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.
29738
29779
  - 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.
29739
29780
  - An operation is a phased plan across tickets: requirements, a spec per ticket, typed edges between tickets, files owned. The \`operation\` tool plans and reads one; \`slop op show <op>\` from a shell; \`slop skill slop-operations\` is the method.
29740
29781
  - Splat is the project's app on a shared canvas: every route at every viewport, rendered on a machine running the app, seen by the whole team. The \`splat\` tool points it at a dev server on any port (\`target\`), renders here and shows you the images (\`capture\`), and shows what the team sees (\`view\`). After a UI change, \`splat capture\` the routes you touched and look. A project has workspaces (separate canvases; \`workspace\` on any action), and people publish running apps into them (\`slop splat serve --name <app> --port <port>\`, from any machine or VM) that the team then uses live in their browsers. From a shell: \`slop splat\`; \`slop skill slop-splat\` is the method.
@@ -29919,6 +29960,9 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
29919
29960
  });
29920
29961
  }
29921
29962
 
29963
+ // src/commands/mcp.ts
29964
+ import { join as join26 } from "path";
29965
+
29922
29966
  // src/platform/mcp.ts
29923
29967
  import { createInterface } from "readline";
29924
29968
 
@@ -30003,6 +30047,7 @@ function buildMcpRequestHandler(tools, info, sendJsonRpcMessage, options2 = {})
30003
30047
  const output2 = new McpOutput();
30004
30048
  let text;
30005
30049
  try {
30050
+ options2.onToolCall?.(isRecord(params._meta) ? params._meta : {});
30006
30051
  const suppliedArguments = params.arguments ?? {};
30007
30052
  const args2 = tool.parseInput ? tool.parseInput(suppliedArguments) : suppliedArguments;
30008
30053
  text = output2.formatResult(await tool.run(args2, output2));
@@ -30118,7 +30163,7 @@ function buildUpdateNotice() {
30118
30163
  }
30119
30164
  function resolveLatestCliVersion() {
30120
30165
  const deprecated = fetchDeprecatedVersions();
30121
- return sortCliVersions([CLI_RELEASE, "0.9.3", fetchUpdateStatus().latest ?? CLI_RELEASE]).filter((version2) => !deprecated.has(version2)).at(-1) ?? CLI_RELEASE;
30166
+ return sortCliVersions([CLI_RELEASE, "0.9.5", fetchUpdateStatus().latest ?? CLI_RELEASE]).filter((version2) => !deprecated.has(version2)).at(-1) ?? CLI_RELEASE;
30122
30167
  }
30123
30168
  function formatAgentCliVersion(version2, latest = resolveLatestCliVersion()) {
30124
30169
  const label = version2 ? `slop ${version2}` : "an older slop";
@@ -30554,7 +30599,7 @@ async function updateInstalledCliFromRegistry(launcher, options2) {
30554
30599
  const running = fetchLaunchCandidate(launcher).version;
30555
30600
  const action = running === previous ? "current" : isNewerVersion(running, previous) ? "updated" : "rolled-back";
30556
30601
  const latest = resolveNewestOfferedRelease(index);
30557
- deleteUnusedVersions(/* @__PURE__ */ new Set([running, previous, current, launcher.bundledVersion, "0.9.3"]));
30602
+ deleteUnusedVersions(/* @__PURE__ */ new Set([running, previous, current, launcher.bundledVersion, "0.9.5"]));
30558
30603
  persistUpdateStatus({ checkedAt: now(), latest, failures: 0, lastError: null });
30559
30604
  return { action, running, previous, latest };
30560
30605
  } catch (error62) {
@@ -31361,7 +31406,7 @@ function watchLaunchTarget(onChange) {
31361
31406
  }
31362
31407
  function runMcpRelay() {
31363
31408
  const launcher = readLauncherInfo();
31364
- const own2 = { version: "0.9.3", main: fileURLToPath(import.meta.url), downloaded: false };
31409
+ const own2 = { version: "0.9.5", main: fileURLToPath(import.meta.url), downloaded: false };
31365
31410
  const relay = new McpRelay({
31366
31411
  input: process.stdin,
31367
31412
  output: process.stdout,
@@ -31802,7 +31847,7 @@ async function waitForMessage(api, projectId, taskKey, options2 = {}) {
31802
31847
  setTimeout(() => resolve6(outcome), 50);
31803
31848
  };
31804
31849
  const held = new HeldSession(api, projectId, taskKey, {
31805
- claim: () => options2.claim ?? { harness: null, directory: null, branch: null, cliVersion: "0.9.3" },
31850
+ claim: () => options2.claim ?? { harness: null, directory: null, branch: null, cliVersion: "0.9.5" },
31806
31851
  reconnect: false,
31807
31852
  onHello: options2.onHello,
31808
31853
  onEnd: () => finishWait({ kind: "ended" }),
@@ -32223,6 +32268,31 @@ function parseToolCallFromCodexLine(line) {
32223
32268
  }
32224
32269
  return null;
32225
32270
  }
32271
+ function parseModelFromClaudeLine(line) {
32272
+ if (!line.includes('"assistant"')) return null;
32273
+ let entry;
32274
+ try {
32275
+ entry = JSON.parse(line);
32276
+ } catch {
32277
+ return null;
32278
+ }
32279
+ const model = entry.message?.model;
32280
+ if (entry.type !== "assistant" || typeof model !== "string" || !model.trim() || model === "<synthetic>") return null;
32281
+ return truncate(model.trim(), MODEL_MAX);
32282
+ }
32283
+ function parseModelFromCodexLine(line) {
32284
+ if (!line.includes('"turn_context"')) return null;
32285
+ let entry;
32286
+ try {
32287
+ entry = JSON.parse(line);
32288
+ } catch {
32289
+ return null;
32290
+ }
32291
+ const model = entry.payload?.model;
32292
+ if (entry.type !== "turn_context" || typeof model !== "string" || !model.trim()) return null;
32293
+ return truncate(model.trim(), MODEL_MAX);
32294
+ }
32295
+ var resolveLineParsers = (harness) => harness === "codex" ? { parseToolCall: parseToolCallFromCodexLine, parseModel: parseModelFromCodexLine } : { parseToolCall: parseToolCallFromClaudeLine, parseModel: parseModelFromClaudeLine };
32226
32296
  var fetchFileSize = (path3) => {
32227
32297
  try {
32228
32298
  return statSync7(path3).size;
@@ -32269,6 +32339,14 @@ var FileTail = class {
32269
32339
  return lines2;
32270
32340
  }
32271
32341
  };
32342
+ function fetchModelFromFileTail(path3, parseModel) {
32343
+ const lines2 = new FileTail(path3, "start").fetchNewLines();
32344
+ for (let index = lines2.length - 1; index >= 0; index -= 1) {
32345
+ const model = parseModel(lines2[index]);
32346
+ if (model) return model;
32347
+ }
32348
+ return null;
32349
+ }
32272
32350
  var fetchChangedAt = (path3) => {
32273
32351
  try {
32274
32352
  return statSync7(path3).mtimeMs;
@@ -32376,21 +32454,26 @@ function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
32376
32454
  }
32377
32455
  return found;
32378
32456
  }
32457
+ var EMPTY_ROSTER = JSON.stringify({ workers: [], model: null });
32379
32458
  var HarnessActivity = class {
32380
32459
  constructor(conversation, onReport) {
32381
32460
  this.conversation = conversation;
32382
32461
  this.onReport = onReport;
32462
+ this.parsers = resolveLineParsers(conversation.harness);
32383
32463
  }
32384
32464
  conversation;
32385
32465
  onReport;
32386
32466
  workers = /* @__PURE__ */ new Map();
32467
+ parsers;
32387
32468
  own = null;
32469
+ /** The model this agent itself runs on: the newest its own record names. */
32470
+ model = null;
32388
32471
  doing = null;
32389
32472
  watched = false;
32390
32473
  discoveredAt = 0;
32391
32474
  timer = null;
32392
- /** What the room knows: the roster it was last told, what this agent is doing, and each worker's line. */
32393
- toldRoster = "[]";
32475
+ /** What the room knows: the roster and model it was last told, what this agent is doing, and each worker's line. */
32476
+ toldRoster = EMPTY_ROSTER;
32394
32477
  toldDoing = null;
32395
32478
  toldLines = /* @__PURE__ */ new Map();
32396
32479
  stopped = false;
@@ -32409,7 +32492,7 @@ var HarnessActivity = class {
32409
32492
  }
32410
32493
  /** Say it again: a new socket starts knowing nothing, and only the agent can refill it. */
32411
32494
  repeat() {
32412
- this.toldRoster = "[]";
32495
+ this.toldRoster = EMPTY_ROSTER;
32413
32496
  this.toldDoing = null;
32414
32497
  this.toldLines.clear();
32415
32498
  this.tick();
@@ -32434,12 +32517,12 @@ var HarnessActivity = class {
32434
32517
  }
32435
32518
  }
32436
32519
  const roster = this.buildRoster();
32437
- const said = JSON.stringify(roster);
32520
+ const said = JSON.stringify({ workers: roster, model: this.model });
32438
32521
  if (said !== this.toldRoster) {
32439
32522
  this.toldRoster = said;
32440
32523
  const refs = new Set(roster.map((worker) => worker.ref));
32441
32524
  for (const ref of [...this.toldLines.keys()]) if (!refs.has(ref)) this.toldLines.delete(ref);
32442
- this.onReport({ type: "workers", workers: roster });
32525
+ this.onReport({ type: "workers", workers: roster, model: this.model });
32443
32526
  }
32444
32527
  const doing = this.watched ? this.doing : null;
32445
32528
  const changed = this.buildWorkerLines(roster, now).filter((line) => (this.toldLines.get(line.ref) ?? null) !== line.doing);
@@ -32454,7 +32537,7 @@ var HarnessActivity = class {
32454
32537
  for (const file2 of found) {
32455
32538
  const known = this.workers.get(file2.ref);
32456
32539
  if (known) Object.assign(known, { label: file2.label, directory: file2.directory, branch: file2.branch, changedAt: file2.changedAt });
32457
- else this.workers.set(file2.ref, { ...file2, since: new Date(file2.changedAt).toISOString(), doing: null, tail: new FileTail(file2.path, "end") });
32540
+ else this.workers.set(file2.ref, { ...file2, since: new Date(file2.changedAt).toISOString(), model: fetchModelFromFileTail(file2.path, this.parsers.parseModel), doing: null, tail: new FileTail(file2.path, "end") });
32458
32541
  }
32459
32542
  const live = new Set(found.map((file2) => file2.ref));
32460
32543
  for (const ref of [...this.workers.keys()]) if (!live.has(ref)) this.workers.delete(ref);
@@ -32462,38 +32545,52 @@ var HarnessActivity = class {
32462
32545
  findWorkerFiles(changedAfter) {
32463
32546
  if (this.conversation.harness === "claude") {
32464
32547
  const transcript = findClaudeTranscriptPath(fetchClaudeHomes(), this.conversation.id);
32465
- this.own ??= transcript ? new FileTail(transcript, "end") : null;
32548
+ if (transcript && !this.own) {
32549
+ this.own = new FileTail(transcript, "end");
32550
+ this.model = fetchModelFromFileTail(transcript, this.parsers.parseModel);
32551
+ }
32466
32552
  return transcript ? findClaudeWorkerFiles(transcript, this.conversation.id, changedAfter) : [];
32467
32553
  }
32468
32554
  if (this.conversation.harness === "codex") {
32469
32555
  const roots = fetchCodexHomes().map((home) => join18(home, "sessions"));
32470
32556
  const rollout = fetchCodexRolloutPath(roots, this.conversation.id);
32471
- this.own ??= rollout ? new FileTail(rollout, "end") : null;
32557
+ if (rollout && !this.own) {
32558
+ this.own = new FileTail(rollout, "end");
32559
+ this.model = fetchModelFromFileTail(rollout, this.parsers.parseModel);
32560
+ }
32472
32561
  return findCodexChildRollouts(roots, this.conversation.id, changedAfter);
32473
32562
  }
32474
32563
  return [];
32475
32564
  }
32476
- /** The newest tool call in everything that has been written since the last read. */
32565
+ /** The newest tool call and the newest model in everything that has been written since the last read. */
32477
32566
  read() {
32478
- const parse3 = this.conversation.harness === "codex" ? parseToolCallFromCodexLine : parseToolCallFromClaudeLine;
32479
- const findNewestCall = (tail) => {
32480
- let newest = null;
32481
- for (const line of tail.fetchNewLines()) newest = parse3(line) ?? newest;
32482
- return newest;
32567
+ const { parseToolCall, parseModel } = this.parsers;
32568
+ const fetchNewestCallAndModel = (tail) => {
32569
+ let call = null;
32570
+ let model = null;
32571
+ for (const line of tail.fetchNewLines()) {
32572
+ call = parseToolCall(line) ?? call;
32573
+ model = parseModel(line) ?? model;
32574
+ }
32575
+ return { call, model };
32483
32576
  };
32484
32577
  if (this.own) {
32485
- let newest = null;
32486
- for (const line of this.own.fetchNewLines()) newest = parse3(line) ?? newest;
32487
- this.doing = newest ?? this.doing;
32578
+ const { call, model } = fetchNewestCallAndModel(this.own);
32579
+ this.doing = call ?? this.doing;
32580
+ this.model = model ?? this.model;
32581
+ }
32582
+ for (const worker of this.workers.values()) {
32583
+ const { call, model } = fetchNewestCallAndModel(worker.tail);
32584
+ worker.doing = call ?? worker.doing;
32585
+ worker.model = model ?? worker.model;
32488
32586
  }
32489
- for (const worker of this.workers.values()) worker.doing = findNewestCall(worker.tail) ?? worker.doing;
32490
32587
  }
32491
32588
  /**
32492
32589
  * Every worker, oldest first, so the roster changes only when one starts or finishes and never because one got
32493
32590
  * busy. The contract's guard is far past what a harness runs; past it, the oldest are the ones listed.
32494
32591
  */
32495
32592
  buildRoster() {
32496
- return [...this.workers.values()].sort((a, b) => a.since.localeCompare(b.since) || a.ref.localeCompare(b.ref)).slice(0, WORKERS_PER_SESSION).map((worker) => ({ ref: worker.ref, label: worker.label, directory: worker.directory, branch: worker.branch, since: worker.since }));
32593
+ return [...this.workers.values()].sort((a, b) => a.since.localeCompare(b.since) || a.ref.localeCompare(b.ref)).slice(0, WORKERS_PER_SESSION).map((worker) => ({ ref: worker.ref, label: worker.label, directory: worker.directory, branch: worker.branch, model: worker.model, since: worker.since }));
32497
32594
  }
32498
32595
  /** Quiet for a while: still listed, because it is probably thinking, but saying nothing about what it is doing. */
32499
32596
  buildWorkerLines(roster, now) {
@@ -35388,7 +35485,7 @@ var SplatRendererConnection = class {
35388
35485
  try {
35389
35486
  let path3;
35390
35487
  try {
35391
- const ticket = await this.api.post(`/projects/${this.projectId}/splat/renderer/ticket`, ApiResponses.ConnectTicket, { cliVersion: "0.9.3", onlyMe: this.options.onlyMe ?? false, agent: this.options.agent ?? null, capabilities: ["workspaces"] });
35488
+ const ticket = await this.api.post(`/projects/${this.projectId}/splat/renderer/ticket`, ApiResponses.ConnectTicket, { cliVersion: "0.9.5", onlyMe: this.options.onlyMe ?? false, agent: this.options.agent ?? null, capabilities: ["workspaces"] });
35392
35489
  path3 = ticket.path;
35393
35490
  } catch (error62) {
35394
35491
  if (error62 instanceof CliError && ["unauthenticated", "not_found", "forbidden", "bad_request"].includes(error62.code)) {
@@ -36244,6 +36341,7 @@ var formatSessionLine = (session, mine = null, latestCli = resolveLatestCliVersi
36244
36341
  session.user.name,
36245
36342
  session.machine.name,
36246
36343
  session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-",
36344
+ session.agent?.model ?? "",
36247
36345
  formatAgentCliVersion(session.agent?.cliVersion, latestCli),
36248
36346
  `since ${session.startedAt}`,
36249
36347
  session.state === "reconnecting" ? "(reconnecting)" : "",
@@ -36251,7 +36349,7 @@ var formatSessionLine = (session, mine = null, latestCli = resolveLatestCliVersi
36251
36349
  session.doing ? `doing ${session.doing}` : "",
36252
36350
  session.available ? `available to operation ${session.available}` : ""
36253
36351
  ].filter(Boolean).join(" "),
36254
- ...(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}` : ""}`)
36352
+ ...(session.workers ?? []).map((worker) => ` ${generateWorkerName(session.agent?.name ?? "agent", worker.ref)} (worker) ${worker.label}${worker.directory ? ` ${worker.directory}${worker.branch ? `@${worker.branch}` : ""}` : ""}${worker.model ? ` ${worker.model}` : ""}${worker.doing ? ` doing ${worker.doing}` : ""}`)
36255
36353
  ].join("\n");
36256
36354
  async function buildUnreadDigest(api, projectId, taskKey) {
36257
36355
  const [about, general] = await Promise.all([
@@ -37421,17 +37519,17 @@ function hasHarnessSetup() {
37421
37519
  function syncMachineSetupAfterUpdate() {
37422
37520
  const nothing = { skills: [], retired: [], reinstalling: false };
37423
37521
  try {
37424
- if (!isUpdateCheckEnabled() || fetchMachineSetupState()?.version === "0.9.3") return nothing;
37522
+ if (!isUpdateCheckEnabled() || fetchMachineSetupState()?.version === "0.9.5") return nothing;
37425
37523
  const lock = claimFileLock(buildMachineSetupLockDirectory(), { staleAfterMs: 6e4 });
37426
37524
  if (!lock) return nothing;
37427
37525
  try {
37428
37526
  const state = fetchMachineSetupState();
37429
- if (state?.version === "0.9.3") return nothing;
37527
+ if (state?.version === "0.9.5") return nothing;
37430
37528
  const skills = hasInstalledSkills() ? installSkills() : [];
37431
37529
  const shipped = Object.keys(SKILLS);
37432
37530
  const retired = deleteRetiredSkills((state?.skills ?? []).filter((name) => !shipped.includes(name)));
37433
37531
  const setupRevision = state?.setupRevision ?? (hasHarnessSetup() ? UNRECORDED_SETUP_REVISION : null);
37434
- persistMachineSetupState({ version: "0.9.3", setupRevision, skills: hasInstalledSkills() ? shipped : [] });
37532
+ persistMachineSetupState({ version: "0.9.5", setupRevision, skills: hasInstalledSkills() ? shipped : [] });
37435
37533
  const reinstalling = setupRevision !== null && setupRevision < SETUP_REVISION && runMcpInstallInBackground();
37436
37534
  return { skills, retired, reinstalling };
37437
37535
  } finally {
@@ -37598,7 +37696,7 @@ async function trustCodexHooks(codexHome) {
37598
37696
  const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
37599
37697
  `);
37600
37698
  try {
37601
- await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.9.3" }, capabilities: { experimentalApi: true } });
37699
+ await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.9.5" }, capabilities: { experimentalApi: true } });
37602
37700
  sendNotification("initialized", {});
37603
37701
  const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
37604
37702
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
@@ -37631,18 +37729,27 @@ function registerMcpInstallCommand(mcp) {
37631
37729
  }
37632
37730
 
37633
37731
  // src/commands/mcp.ts
37634
- function buildTools(lifecycle) {
37635
- if (isNonInteractiveHarness()) return { tools: [], startPresence: () => void 0, releaseSessions: () => void 0 };
37732
+ function buildTools(lifecycle, relay) {
37733
+ if (isNonInteractiveHarness()) return { tools: [], startPresence: () => void 0, releaseSessions: () => void 0, observeToolCall: () => void 0 };
37636
37734
  let api = null;
37637
37735
  const requireApi = () => api ??= buildAuthedApiClient();
37638
37736
  let identity = null;
37737
+ let called = relay ? fetchRelayConversation(relay) : null;
37738
+ const observeToolCall = (meta3) => {
37739
+ if (called) return;
37740
+ const thread = parseCodexThreadFromToolCallMeta(meta3);
37741
+ if (!thread) return;
37742
+ called = { id: fetchCodexChatThreadId(fetchCodexHomes().map((home) => join26(home, "sessions")), thread), harness: "codex" };
37743
+ if (relay) persistRelayConversation(relay, called);
37744
+ };
37745
+ const fetchOwnConversation = () => called ?? fetchKnownConversation(process.cwd());
37639
37746
  let stopped = false;
37640
37747
  const projects = /* @__PURE__ */ new Map();
37641
37748
  const requireProjectTools = (project) => {
37642
37749
  if (stopped) throw new Error("The MCP server is stopping");
37643
37750
  let context2 = projects.get(project.id);
37644
37751
  if (!context2) {
37645
- identity ??= resolveAgentIdentity(process.cwd());
37752
+ identity ??= resolveAgentIdentity(process.cwd(), "process", fetchOwnConversation());
37646
37753
  context2 = createProjectTools(buildAuthedApiClient(), project, identity);
37647
37754
  projects.set(project.id, context2);
37648
37755
  }
@@ -37661,8 +37768,9 @@ function buildTools(lifecycle) {
37661
37768
  if (presenceStarted) return;
37662
37769
  presenceStarted = true;
37663
37770
  void (async () => {
37664
- for (let waited = 0; waited < 1e4 && !fetchKnownConversation(process.cwd()) && !stopped; waited += 250) await new Promise((resolve6) => setTimeout(resolve6, 250));
37771
+ for (let waited = 0; waited < 1e4 && !fetchOwnConversation() && !stopped; waited += 250) await new Promise((resolve6) => setTimeout(resolve6, 250));
37665
37772
  if (stopped) return;
37773
+ if (!identity && !fetchOwnConversation() && resolveParentHarness() === "codex") return;
37666
37774
  await requireProjectTools(await resolveProject(requireApi())).start();
37667
37775
  })().catch(() => void 0);
37668
37776
  };
@@ -37670,6 +37778,7 @@ function buildTools(lifecycle) {
37670
37778
  if (stopped) return;
37671
37779
  stopped = true;
37672
37780
  for (const context2 of projects.values()) context2.stop(reason);
37781
+ if (relay && reason === "released") deleteRelayConversation(relay);
37673
37782
  };
37674
37783
  let exiting = false;
37675
37784
  const endSessionAndExit = (reason) => {
@@ -37681,7 +37790,7 @@ function buildTools(lifecycle) {
37681
37790
  process.stdin.once("end", () => endSessionAndExit(lifecycle.onInputEnd));
37682
37791
  process.stdin.once("close", () => endSessionAndExit(lifecycle.onInputEnd));
37683
37792
  for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, () => endSessionAndExit("released"));
37684
- return { tools: createProjectGroupedTools(runHandlerByName), startPresence, releaseSessions };
37793
+ return { tools: createProjectGroupedTools(runHandlerByName), startPresence, releaseSessions, observeToolCall };
37685
37794
  }
37686
37795
  function attachUpdateNoticeToTools(list) {
37687
37796
  return list.map((tool) => ({
@@ -37696,10 +37805,12 @@ ${notice}` : result;
37696
37805
  }));
37697
37806
  }
37698
37807
  function serveBoardTools(version2) {
37699
- const underRelay = drainRelayChildContextFromEnvironment() !== null;
37808
+ const relay = drainRelayChildContextFromEnvironment();
37809
+ const underRelay = relay !== null;
37700
37810
  const lifecycle = { onInputEnd: underRelay ? "orphaned" : "released" };
37701
- const { tools, startPresence, releaseSessions } = buildTools(lifecycle);
37811
+ const { tools, startPresence, releaseSessions, observeToolCall } = buildTools(lifecycle, relay);
37702
37812
  serveMcp(attachUpdateNoticeToTools(tools), { name: "slop", version: version2 }, {
37813
+ onToolCall: observeToolCall,
37703
37814
  onNotification: (method) => {
37704
37815
  if (method === RELAY_START_NOTIFICATION) startPresence();
37705
37816
  else if (method === RELAY_HANDOVER_NOTIFICATION) {
@@ -37722,7 +37833,7 @@ function registerMcpCommands(program3, version2) {
37722
37833
 
37723
37834
  // src/commands/sessions.ts
37724
37835
  var buildTaskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
37725
- var formatSessionLine2 = (session, latestCli) => [session.agent?.name ?? "agent", session.taskKey ?? "(no task)", session.user.name, session.machine.name, session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-", formatAgentCliVersion(session.agent?.cliVersion, latestCli), session.startedAt, session.state === "reconnecting" ? "reconnecting" : "live"];
37836
+ var formatSessionLine2 = (session, latestCli) => [session.agent?.name ?? "agent", session.agent?.model ?? "-", session.taskKey ?? "(no task)", session.user.name, session.machine.name, session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-", formatAgentCliVersion(session.agent?.cliVersion, latestCli), session.startedAt, session.state === "reconnecting" ? "reconnecting" : "live"];
37726
37837
  function registerSessionCommands(program3) {
37727
37838
  const registerProjectOption = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
37728
37839
  const session = program3.command("session").description("Claim a task and be reachable on it while you work.");
@@ -37782,7 +37893,7 @@ function registerSessionCommands(program3) {
37782
37893
  emitOutput(sessions, () => {
37783
37894
  if (sessions.length === 0) return "Nobody is live.";
37784
37895
  const latestCli = resolveLatestCliVersion();
37785
- return formatTable([["AGENT", "TASK", "WHO", "MACHINE", "WHERE", "CLI", "SINCE", "STATE"], ...sessions.map((session2) => formatSessionLine2(session2, latestCli))]);
37896
+ return formatTable([["AGENT", "MODEL", "TASK", "WHO", "MACHINE", "WHERE", "CLI", "SINCE", "STATE"], ...sessions.map((session2) => formatSessionLine2(session2, latestCli))]);
37786
37897
  });
37787
37898
  });
37788
37899
  registerProjectOption(session.command("reachable <agent> <state>")).description("Make one of your live agents visible to teammates (on) or visible only to you (off).").action(async (reference, state, options2) => {
@@ -37863,7 +37974,7 @@ var options = optionEnd < 0 ? args : args.slice(0, optionEnd);
37863
37974
  updateJsonMode(options.includes("--json"));
37864
37975
  syncMachineSetupAfterUpdate();
37865
37976
  if (!options.includes("--background") && !options.includes("--child")) scheduleBackgroundUpdate();
37866
- 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.9.3").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").configureOutput({
37977
+ 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.9.5").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").configureOutput({
37867
37978
  outputError: (message, write) => {
37868
37979
  if (!isJsonMode()) write(message);
37869
37980
  },
@@ -37880,7 +37991,7 @@ var sessionCommand = registerSessionCommands(program2);
37880
37991
  registerThreadCommands(program2, taskCommand);
37881
37992
  registerOperationCommands(program2, taskCommand, sessionCommand);
37882
37993
  registerSplatCommands(program2);
37883
- registerMcpCommands(program2, "0.9.3");
37994
+ registerMcpCommands(program2, "0.9.5");
37884
37995
  registerGuideCommand(program2);
37885
37996
  registerSkillCommands(program2);
37886
37997
  registerUpdateCommand(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",