slopwaresystems 0.4.9 → 0.4.10

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 +375 -145
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3379,7 +3379,7 @@ import { homedir } from "os";
3379
3379
  import { join } from "path";
3380
3380
 
3381
3381
  // skills/slop-cli.md
3382
- var slop_cli_default = "---\nname: slop-cli\ndescription: How an agent works the Slopware Tasks board through the slop MCP tools (board, task, plan, session, threads, say), and what the slop command is for (people and machines: login, link, install). Load when asked to create, update, plan, discuss or finish tasks.\n---\n\n# The board, as six tools\n\nYour harness has the `slop` MCP server. Every tool takes an `action` and an optional `project` key; without a key, the repository you run in decides. Descriptions and notes are Markdown. Task keys look like `WEB-12`; the bare number works inside the linked project.\n\n| Tool | Actions |\n|---|---|\n| `board` | `list` (`mine`, `status`, `all`, `archived`; `limit`/`offset` to page), `columns`, `members`, `projects` |\n| `task` | `show`, `create`, `update` (only given fields change), `move`, `link`, `archive`, `restore`, `history` (`limit`/`before` to page back), `finish` |\n| `plan` | `add` (`items`), `note`, `done`, `reopen`, `rename`, `remove` (by `item` number) |\n| `session` | `claim`, `release`, `who` (every live agent by name, you marked), `check`, `wait` (Claude Code only, and rarely; messages reach you on their own; never on Codex; see `slop-sessions`) |\n| `threads` | `list`, `read` (newest 40; `limit`/`before` to page back) |\n| `say` | `post` (new thread; with `task` it reaches whoever is live on it; with `to: <name>` it reaches that one agent; with neither it is general), `reply`. Messages are letters: read when the other agent gets to them, not at once; write once, reply only when it changes something (see `slop-sessions`) |\n\nYou have a name, given at session start (the `SessionStart` hook says it) and shown by `session who`. People and other agents address you by it. Long answers are cut at 16k characters and say so; ask for less with `limit`, `offset` or `before`.\n\nDo not shell out to `slop` for board work; the tools are the way.\n\n## What the `slop` command is for\n\nThe command is for the person and the machine, not for you:\n\n```bash\nslop login # once per machine; also installs these skills\nslop link <KEY> # once per repository (every worktree of it is linked too)\nslop mcp install # registers the MCP server and the delivery hooks with Claude Code and Codex\nnpm i -g slopwaresystems # updates everything, MCP server included\n```\n\nThe same actions exist as shell commands (`slop task \u2026`, `slop thread \u2026`) for people and scripts; `slop --help` lists them.\n\n## Rules of the road\n\n- Do not delete tasks you did not create. Archive finished work instead.\n- Keep titles short and imperative; put the reasoning in the description or the plan's notes.\n- When you open a pull request for a task, link it: `task` with `action: link`.\n\nWhen a wake says messages are waiting, use `session` `check` with the project named in the wake. Each conversation and project has a separate inbox, including when chats share a checkout.\n\nLaunchers can set `SLOP_CONVERSATION_ID` to the host's stable chat ID and `SLOP_HARNESS` to `claude`, `codex`, or `other`. Host-provided conversation IDs are detected automatically when available. An ambiguous directory is not enough to identify a chat; supply its ID when running several chats there without host-provided identity.\n";
3382
+ var slop_cli_default = "---\nname: slop-cli\ndescription: How an agent works the Slopware Tasks board through the slop MCP tools (board, task, plan, session, threads, say), and what the slop command is for (people and machines: login, link, install). Load when asked to create, update, plan, discuss or finish tasks.\n---\n\n# The board, as six tools\n\nYour harness has the `slop` MCP server. Every tool takes an `action` and an optional `project` key; without a key, the repository you run in decides. Descriptions and notes are Markdown. Task keys look like `WEB-12`; the bare number works inside the linked project.\n\n| Tool | Actions |\n|---|---|\n| `board` | `list` (`mine`, `status`, `all`, `archived`; `limit`/`offset` to page), `columns`, `members`, `projects` |\n| `task` | `show`, `create`, `update` (only given fields change), `move`, `link`, `archive`, `restore`, `history` (`limit`/`before` to page back), `finish` |\n| `plan` | `add` (`items`), `note`, `done`, `reopen`, `rename`, `remove` (by `item` number) |\n| `session` | `claim`, `release`, `who` (every live agent by name, you marked), `check`, `wait` (Claude Code only, and rarely; messages reach you on their own; never on Codex; see `slop-sessions`) |\n| `threads` | `list`, `read` (newest 40; `limit`/`before` to page back) |\n| `say` | `post` (new thread; with `task` it reaches whoever is live on it; with `to: <name>` it reaches that one agent; with neither it is general), `reply`. Messages are letters: read when the other agent gets to them, not at once; write once, reply only when it changes something (see `slop-sessions`) |\n\nYou have a name, given at session start (the `SessionStart` hook says it) and shown by `session who`. People and other agents address you by it. Long ordinary answers are cut at 16k characters and say so; ask for less with `limit`, `offset` or `before`. Incoming messages are kept whole in a separate batch. Use `session check` to read any remaining messages.\n\nDo not shell out to `slop` for board work; the tools are the way.\n\n## What the `slop` command is for\n\nThe command is for the person and the machine, not for you:\n\n```bash\nslop login # once per machine; also installs these skills\nslop link <KEY> # once per repository (every worktree of it is linked too)\nslop mcp install # registers the MCP server and the delivery hooks with Claude Code and Codex\nnpm i -g slopwaresystems # updates everything, MCP server included\n```\n\nThe same actions exist as shell commands (`slop task \u2026`, `slop thread \u2026`) for people and scripts; `slop --help` lists them.\n\n## Rules of the road\n\n- Do not delete tasks you did not create. Archive finished work instead.\n- Keep titles short and imperative; put the reasoning in the description or the plan's notes.\n- When you open a pull request for a task, link it: `task` with `action: link`.\n\nWhen a wake says messages are waiting, use `session` `check` with the project named in the wake. Each conversation and project has a separate inbox, including when chats share a checkout. A failed handoff is retried; a crash just after output can repeat a message, so do not repeat an action you already completed.\n\nLaunchers can set `SLOP_CONVERSATION_ID` to the host's stable chat ID and `SLOP_HARNESS` to `claude`, `codex`, or `other`. Host-provided conversation IDs are detected automatically when available. An ambiguous directory is not enough to identify a chat; supply its ID when running several chats there without host-provided identity.\n";
3383
3383
 
3384
3384
  // skills/slop-planning.md
3385
3385
  var slop_planning_default = '---\nname: slop-planning\ndescription: How to break long or multi-session work into a plan on its task, keep each item\'s note current, and pick up where you left off. Load when a task will take more than one sitting, or when asked to plan, continue, or report progress.\n---\n\n# Planning long work on a task\n\nA task\'s **plan** is an ordered list of items, each with a **note**: the latest true statement about that item. The plan plus the task\'s timeline is your memory between sessions. Write to it as you work, not at the end.\n\n## Starting\n\n1. Read the task: `task` `show`. If it has a plan, read the notes; they are what the last session knew.\n2. If there is no plan, write one. Items are concrete, checkable, and ordered by what unlocks what. Eight to fifteen is typical; more means the task should be split.\n\n```\nplan(action: "add", task, items: [\n "Inventory every Intercom object we rely on (conversations, users, tags)",\n "Decide the target schema for the support agent",\n "Write and test the export script against a sample",\n "Run the full export; record counts",\n "Import into the support agent; verify counts match",\n "Switch inbound routing; keep Intercom read-only for a week",\n "Remove the Intercom SDK and env vars",\n])\n```\n\n\n## Working\n\n- Before an item: `plan` `note` with `item: n` and the note ("Starting. Approach: \u2026")\n- As facts arrive, replace the note. Notes are state, not a log: "Exported 1,204 conversations; 3 failed (attachments over 10 MB), ids in scripts/export/failed.json".\n- When an item is done: `plan` `done` with `item: n`. If it turned out to be two things, add the second with `plan` `add`.\n- Blocked? Put the blocker in the note, then move on to an item that is not blocked. Do not mark blocked items done.\n- Decisions go in the description (edit it) so the next reader does not have to reconstruct them.\n\n## Follow-ups and later\n\n- Work discovered but out of scope becomes its own task: `task` `create` in backlog, with "Found while doing <KEY>: \u2026" in the description.\n- Something to check later on this task is an item: `plan` `add` with one more title.\n\n## Finishing\n\n`task` `show` should show every item done or removed. Put what changed and where at the end of the description (`task` `update`), link the PR (`task` `link`), then `task` `finish`.\n\n## Picking up a task you did not start\n\n`task` `show`, then `task` `history`. The notes tell you the present; the history tells you how it got there. Then continue from the first item that is not done.\n';
@@ -24825,7 +24825,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
24825
24825
  - Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
24826
24826
  - When it is finished and verified: \`slop task done <KEY>\`.
24827
24827
  - 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 -\`.
24828
- - 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 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\`.
24828
+ - 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\`.
24829
24829
  - 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.
24830
24830
  - 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.
24831
24831
  - Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
@@ -24968,20 +24968,137 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
24968
24968
  }
24969
24969
 
24970
24970
  // src/commands/mcp.ts
24971
- import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
24972
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
24971
+ import { execFileSync as execFileSync6, spawn as spawn3 } from "child_process";
24972
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
24973
24973
  import { homedir as homedir4 } from "os";
24974
- import { join as join13 } from "path";
24974
+ import { join as join15 } from "path";
24975
24975
 
24976
24976
  // src/platform/session-recovery.ts
24977
- import { readFileSync as readFileSync9 } from "fs";
24978
- import { dirname as dirname6, join as join11 } from "path";
24977
+ import { readFileSync as readFileSync11 } from "fs";
24978
+ import { dirname as dirname6, join as join13 } from "path";
24979
+
24980
+ // src/platform/inbox.ts
24981
+ import { createHash as createHash6, randomUUID as randomUUID7 } from "crypto";
24982
+ import { existsSync as existsSync9, linkSync as linkSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync10, readdirSync as readdirSync6, realpathSync as realpathSync4, renameSync as renameSync2, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "fs";
24983
+ import { dirname as dirname5, join as join12 } from "path";
24984
+
24985
+ // src/platform/inbox-claims.ts
24986
+ import { execFileSync as execFileSync5 } from "child_process";
24987
+ import { randomUUID as randomUUID6 } from "crypto";
24988
+ import { existsSync as existsSync7, linkSync as linkSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync8, readdirSync as readdirSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
24989
+ import { join as join10 } from "path";
24990
+ var processOwner = null;
24991
+ function fetchProcessStart(pid) {
24992
+ try {
24993
+ return execFileSync5("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", env: { ...process.env, LC_ALL: "C", TZ: "UTC" }, stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).trim() || null;
24994
+ } catch {
24995
+ return null;
24996
+ }
24997
+ }
24998
+ function isClaimOwnerRunning(owner) {
24999
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw new Error("Invalid inbox claim owner");
25000
+ try {
25001
+ process.kill(owner.pid, 0);
25002
+ } catch (error62) {
25003
+ if (error62.code === "ESRCH") return false;
25004
+ return true;
25005
+ }
25006
+ if (owner.pid === process.pid) return !owner.started || !processOwner?.started || owner.started === processOwner.started;
25007
+ const started = owner.started && fetchProcessStart(owner.pid);
25008
+ return !started || started === owner.started;
25009
+ }
25010
+ function claimInboxMessage(directory) {
25011
+ mkdirSync5(directory, { recursive: true });
25012
+ processOwner ??= { pid: process.pid, started: fetchProcessStart(process.pid) };
25013
+ for (; ; ) {
25014
+ let names;
25015
+ try {
25016
+ names = readdirSync4(directory).filter((name) => /^\d+\.json$/.test(name)).sort((a, b) => Number(a.slice(0, -5)) - Number(b.slice(0, -5)));
25017
+ } catch (error62) {
25018
+ if (error62.code === "ENOENT") return null;
25019
+ throw error62;
25020
+ }
25021
+ const last = names.at(-1);
25022
+ if (last && !existsSync7(join10(directory, `${last}.released`))) {
25023
+ let owner;
25024
+ try {
25025
+ owner = JSON.parse(readFileSync8(join10(directory, last), "utf8"));
25026
+ } catch (error62) {
25027
+ if (error62.code === "ENOENT") return null;
25028
+ throw error62;
25029
+ }
25030
+ if (isClaimOwnerRunning(owner)) return null;
25031
+ }
25032
+ const next = join10(directory, `${last ? Number(last.slice(0, -5)) + 1 : 0}.json`);
25033
+ const temporary = join10(directory, `${randomUUID6()}.writing`);
25034
+ try {
25035
+ writeFileSync6(temporary, JSON.stringify(processOwner), { flag: "wx", mode: 384 });
25036
+ linkSync2(temporary, next);
25037
+ return { release: () => {
25038
+ try {
25039
+ writeFileSync6(`${next}.released`, "", { mode: 384 });
25040
+ } catch (error62) {
25041
+ if (error62.code !== "ENOENT") throw error62;
25042
+ }
25043
+ } };
25044
+ } catch (error62) {
25045
+ if (error62.code === "ENOENT") return null;
25046
+ if (error62.code !== "EEXIST") throw error62;
25047
+ } finally {
25048
+ rmSync5(temporary, { force: true });
25049
+ }
25050
+ }
25051
+ }
25052
+
25053
+ // src/platform/inbox-receipts.ts
25054
+ import { createHash as createHash5 } from "crypto";
25055
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync9, readdirSync as readdirSync5, rmSync as rmSync6 } from "fs";
25056
+ import { join as join11 } from "path";
25057
+ var COMPACTION_SIZE = 128;
25058
+ var archives = /* @__PURE__ */ new Map();
25059
+ function fetchArchivedReceipts(directory) {
25060
+ let cached2 = archives.get(directory);
25061
+ if (!cached2) {
25062
+ cached2 = { files: /* @__PURE__ */ new Set(), keys: /* @__PURE__ */ new Set() };
25063
+ archives.set(directory, cached2);
25064
+ }
25065
+ let names;
25066
+ try {
25067
+ names = readdirSync5(join11(directory, "compacted"));
25068
+ } catch (error62) {
25069
+ if (error62.code === "ENOENT") return cached2.keys;
25070
+ throw error62;
25071
+ }
25072
+ for (const name of names) {
25073
+ if (!name.endsWith(".json") || cached2.files.has(name)) continue;
25074
+ const keys = JSON.parse(readFileSync9(join11(directory, "compacted", name), "utf8"));
25075
+ if (!Array.isArray(keys) || !keys.every((key) => typeof key === "string" && /^[a-f0-9]{64}$/.test(key))) throw new Error("Invalid inbox receipt archive");
25076
+ for (const key of keys) cached2.keys.add(key);
25077
+ cached2.files.add(name);
25078
+ }
25079
+ return cached2.keys;
25080
+ }
25081
+ function hasInboxReceipt(directory, key) {
25082
+ return existsSync8(join11(directory, key)) || fetchArchivedReceipts(directory).has(key);
25083
+ }
25084
+ function compactInboxReceipts(directory) {
25085
+ let keys;
25086
+ try {
25087
+ keys = readdirSync5(directory).filter((name2) => /^[a-f0-9]{64}$/.test(name2)).sort();
25088
+ } catch (error62) {
25089
+ if (error62.code === "ENOENT") return;
25090
+ throw error62;
25091
+ }
25092
+ if (keys.length < COMPACTION_SIZE) return;
25093
+ mkdirSync6(join11(directory, "compacted"), { recursive: true });
25094
+ const text = JSON.stringify(keys);
25095
+ const name = createHash5("sha256").update(text).digest("hex");
25096
+ persistLocalState(join11(directory, "compacted", `${name}.json`), text, false);
25097
+ for (const key of keys) rmSync6(join11(directory, key), { force: true });
25098
+ }
24979
25099
 
24980
25100
  // src/platform/inbox.ts
24981
- import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
24982
- import { existsSync as existsSync7, linkSync as linkSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync8, readdirSync as readdirSync4, realpathSync as realpathSync4, renameSync as renameSync2, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
24983
- import { dirname as dirname5, join as join10 } from "path";
24984
- var buildInboxDir = () => join10(dirname5(buildConfigPath()), "inbox");
25101
+ var buildInboxDir = () => join12(dirname5(buildConfigPath()), "inbox");
24985
25102
  var resolveRealCwd2 = (cwd) => {
24986
25103
  try {
24987
25104
  return realpathSync4(cwd);
@@ -24989,100 +25106,169 @@ var resolveRealCwd2 = (cwd) => {
24989
25106
  return cwd;
24990
25107
  }
24991
25108
  };
24992
- var generateInboxKey = (cwd, owner) => owner ? generateLocalStateKey(cwd, JSON.stringify([owner.baseUrl.replace(/\/$/, ""), owner.projectId, owner.conversation])) : createHash5("sha256").update(resolveRealCwd2(cwd)).digest("hex").slice(0, 16);
24993
- var buildInboxPath = (cwd) => join10(buildInboxDir(), `${generateInboxKey(cwd)}.jsonl`);
24994
- var buildInboxQueuePath = (cwd, owner) => join10(buildInboxDir(), generateInboxKey(cwd, owner));
25109
+ var generateInboxKey = (cwd, owner) => owner ? generateLocalStateKey(cwd, JSON.stringify([owner.baseUrl.replace(/\/$/, ""), owner.projectId, owner.conversation])) : createHash6("sha256").update(resolveRealCwd2(cwd)).digest("hex").slice(0, 16);
25110
+ var buildInboxPath = (cwd) => join12(buildInboxDir(), `${generateInboxKey(cwd)}.jsonl`);
25111
+ var buildInboxQueuePath = (cwd, owner) => join12(buildInboxDir(), generateInboxKey(cwd, owner));
24995
25112
  var sequence = 0;
24996
25113
  function persistInboxPush(cwd, push, owner) {
24997
25114
  const directory = buildInboxQueuePath(cwd, owner);
24998
- mkdirSync5(directory, { recursive: true });
24999
- const name = `${Date.now()}.${process.pid}.${String(sequence++).padStart(12, "0")}.${randomUUID6()}`;
25000
- const temporary = join10(directory, `${name}.writing`);
25001
- writeFileSync6(temporary, JSON.stringify(push), { flag: "wx" });
25115
+ mkdirSync7(directory, { recursive: true });
25116
+ const name = `${Date.now()}.${process.pid}.${String(sequence++).padStart(12, "0")}.${randomUUID7()}`;
25117
+ const temporary = join12(directory, `${name}.writing`);
25118
+ writeFileSync7(temporary, JSON.stringify(push), { flag: "wx" });
25002
25119
  if (owner) {
25003
- const key = createHash5("sha256").update(push.message.id).digest("hex");
25004
- const published = join10(directory, `${key}.json`);
25120
+ const key = createHash6("sha256").update(push.message.id).digest("hex");
25121
+ const published = join12(directory, `${key}.json`);
25005
25122
  try {
25006
- if (existsSync7(join10(directory, "receipts", key))) return false;
25007
- linkSync2(temporary, published);
25123
+ if (hasInboxReceipt(join12(directory, "receipts"), key)) return false;
25124
+ linkSync3(temporary, published);
25008
25125
  return true;
25009
25126
  } catch (error62) {
25010
- if (error62.code === "EEXIST") return !existsSync7(join10(directory, "receipts", key));
25127
+ if (error62.code === "EEXIST") return !hasInboxReceipt(join12(directory, "receipts"), key);
25011
25128
  throw error62;
25012
25129
  } finally {
25013
- rmSync5(temporary, { force: true });
25130
+ rmSync7(temporary, { force: true });
25014
25131
  }
25015
25132
  }
25016
- renameSync2(temporary, join10(directory, `${name}.json`));
25133
+ renameSync2(temporary, join12(directory, `${name}.json`));
25017
25134
  return true;
25018
25135
  }
25019
- function drainInbox(cwd, owner, accept = () => true) {
25020
- const pushes = owner ? [] : drainLegacyInbox(cwd);
25021
- const directory = buildInboxQueuePath(cwd, owner);
25136
+ function deleteAcknowledgedInboxClaims(directory) {
25137
+ const claims = join12(directory, "claims");
25022
25138
  let names;
25023
25139
  try {
25024
- names = readdirSync4(directory).filter((file2) => file2.endsWith(".json")).sort();
25140
+ names = readdirSync6(claims);
25025
25141
  } catch (error62) {
25026
- if (error62.code === "ENOENT") return pushes;
25142
+ if (error62.code === "ENOENT") return;
25027
25143
  throw error62;
25028
25144
  }
25029
- if (owner) {
25030
- const receipts = join10(directory, "receipts");
25031
- mkdirSync5(receipts, { recursive: true });
25145
+ for (const key of names) {
25146
+ if (!/^[a-f0-9]{64}$/.test(key) || !hasInboxReceipt(join12(directory, "receipts"), key)) continue;
25147
+ try {
25148
+ rmSync7(join12(claims, key), { recursive: true, force: true });
25149
+ } catch (error62) {
25150
+ if (error62.code !== "ENOTEMPTY") throw error62;
25151
+ }
25152
+ }
25153
+ }
25154
+ function claimInbox(cwd, owner, accept = () => true) {
25155
+ const directory = buildInboxQueuePath(cwd, owner);
25156
+ const receipts = join12(directory, "receipts");
25157
+ const claims = [];
25158
+ let settled = false;
25159
+ const release = () => {
25160
+ if (settled) return;
25161
+ for (const claim2 of claims) claim2.release();
25162
+ settled = true;
25163
+ };
25164
+ const delivery = {
25165
+ pushes: [],
25166
+ release,
25167
+ acknowledge: () => {
25168
+ if (settled) return;
25169
+ mkdirSync7(receipts, { recursive: true });
25170
+ for (const { key } of claims) {
25171
+ try {
25172
+ writeFileSync7(join12(receipts, key), "", { flag: "wx", mode: 384 });
25173
+ } catch (error62) {
25174
+ if (error62.code !== "EEXIST") throw error62;
25175
+ }
25176
+ }
25177
+ settled = true;
25178
+ try {
25179
+ for (const { key } of claims) rmSync7(join12(directory, `${key}.json`), { force: true });
25180
+ deleteAcknowledgedInboxClaims(directory);
25181
+ compactInboxReceipts(receipts);
25182
+ } catch {
25183
+ }
25184
+ }
25185
+ };
25186
+ try {
25187
+ let names;
25188
+ try {
25189
+ names = readdirSync6(directory).filter((file2) => /^[a-f0-9]{64}\.json$/.test(file2));
25190
+ } catch (error62) {
25191
+ if (error62.code === "ENOENT") return delivery;
25192
+ throw error62;
25193
+ }
25032
25194
  const available = [];
25033
25195
  for (const name of names) {
25034
25196
  try {
25035
- available.push({ name, push: JSON.parse(readFileSync8(join10(directory, name), "utf8")) });
25197
+ available.push({ key: name.slice(0, -5), push: JSON.parse(readFileSync10(join12(directory, name), "utf8")) });
25036
25198
  } catch (error62) {
25037
25199
  if (error62.code !== "ENOENT") throw error62;
25038
25200
  }
25039
25201
  }
25040
25202
  available.sort((a, b) => a.push.message.createdAt.localeCompare(b.push.message.createdAt) || a.push.message.id.localeCompare(b.push.message.id));
25041
- for (const { name, push } of available) {
25203
+ for (const { key, push } of available) {
25204
+ if (hasInboxReceipt(receipts, key)) {
25205
+ rmSync7(join12(directory, `${key}.json`), { force: true });
25206
+ continue;
25207
+ }
25042
25208
  if (!accept(push)) continue;
25043
- const path2 = join10(directory, name);
25044
- const receipt = join10(receipts, name.slice(0, -5));
25209
+ let claim2;
25045
25210
  try {
25046
- writeFileSync6(receipt, "", { flag: "wx", mode: 384 });
25211
+ claim2 = claimInboxMessage(join12(directory, "claims", key));
25047
25212
  } catch (error62) {
25048
- if (error62.code === "EEXIST") {
25049
- rmSync5(path2, { force: true });
25050
- continue;
25051
- }
25213
+ if (hasInboxReceipt(receipts, key)) continue;
25052
25214
  throw error62;
25053
25215
  }
25054
- pushes.push(push);
25055
- rmSync5(path2, { force: true });
25216
+ if (!claim2) continue;
25217
+ claims.push({ key, ...claim2 });
25218
+ if (!hasInboxReceipt(receipts, key)) delivery.pushes.push(push);
25056
25219
  }
25057
- return pushes;
25220
+ return delivery;
25221
+ } catch (error62) {
25222
+ release();
25223
+ throw error62;
25224
+ }
25225
+ }
25226
+ function hasPendingInboxMessages(cwd, owner) {
25227
+ const directory = buildInboxQueuePath(cwd, owner);
25228
+ try {
25229
+ return readdirSync6(directory).some((name) => /^[a-f0-9]{64}\.json$/.test(name) && !hasInboxReceipt(join12(directory, "receipts"), name.slice(0, -5)));
25230
+ } catch (error62) {
25231
+ if (error62.code === "ENOENT") return false;
25232
+ throw error62;
25233
+ }
25234
+ }
25235
+ function drainInbox(cwd) {
25236
+ const pushes = drainLegacyInbox(cwd);
25237
+ const directory = buildInboxQueuePath(cwd);
25238
+ let names;
25239
+ try {
25240
+ names = readdirSync6(directory).filter((file2) => file2.endsWith(".json")).sort();
25241
+ } catch (error62) {
25242
+ if (error62.code === "ENOENT") return pushes;
25243
+ throw error62;
25058
25244
  }
25059
25245
  for (const name of names) {
25060
- const taken = join10(directory, `${name}.${randomUUID6()}.draining`);
25246
+ const taken = join12(directory, `${name}.${randomUUID7()}.draining`);
25061
25247
  try {
25062
- renameSync2(join10(directory, name), taken);
25248
+ renameSync2(join12(directory, name), taken);
25063
25249
  } catch (error62) {
25064
25250
  if (error62.code === "ENOENT") continue;
25065
25251
  throw error62;
25066
25252
  }
25067
25253
  try {
25068
- pushes.push(JSON.parse(readFileSync8(taken, "utf8")));
25254
+ pushes.push(JSON.parse(readFileSync10(taken, "utf8")));
25069
25255
  } finally {
25070
- rmSync5(taken, { force: true });
25256
+ rmSync7(taken, { force: true });
25071
25257
  }
25072
25258
  }
25073
25259
  return pushes;
25074
25260
  }
25075
25261
  function drainLegacyInbox(cwd) {
25076
25262
  const path2 = buildInboxPath(cwd);
25077
- if (!existsSync7(path2)) return [];
25263
+ if (!existsSync9(path2)) return [];
25078
25264
  const taken = `${path2}.${process.pid}.${Date.now()}.draining`;
25079
25265
  try {
25080
25266
  renameSync2(path2, taken);
25081
25267
  } catch {
25082
25268
  return [];
25083
25269
  }
25084
- const lines = readFileSync8(taken, "utf8").split("\n").filter(Boolean);
25085
- rmSync5(taken, { force: true });
25270
+ const lines = readFileSync10(taken, "utf8").split("\n").filter(Boolean);
25271
+ rmSync7(taken, { force: true });
25086
25272
  return lines.flatMap((line) => {
25087
25273
  try {
25088
25274
  return [JSON.parse(line)];
@@ -25098,7 +25284,7 @@ var SessionRecovery = class {
25098
25284
  this.api = api;
25099
25285
  this.owner = owner;
25100
25286
  this.deliverPush = deliverPush;
25101
- this.path = join11(dirname6(buildConfigPath()), "cursors", `${generateInboxKey(cwd, owner)}.json`);
25287
+ this.path = join13(dirname6(buildConfigPath()), "cursors", `${generateInboxKey(cwd, owner)}.json`);
25102
25288
  }
25103
25289
  api;
25104
25290
  owner;
@@ -25118,7 +25304,7 @@ var SessionRecovery = class {
25118
25304
  }
25119
25305
  async fetchCursor() {
25120
25306
  try {
25121
- const value = JSON.parse(readFileSync9(this.path, "utf8"));
25307
+ const value = JSON.parse(readFileSync11(this.path, "utf8"));
25122
25308
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("Invalid saved message cursor");
25123
25309
  this.cursor = value;
25124
25310
  } catch (error62) {
@@ -25471,63 +25657,98 @@ function truncateToolResult(result) {
25471
25657
 
25472
25658
  // src/platform/mcp.ts
25473
25659
  import { createInterface } from "readline";
25660
+ var McpOutput = class {
25661
+ deliveries = [];
25662
+ remainingMessageChars = RESULT_CAP;
25663
+ registerDelivery(delivery) {
25664
+ this.deliveries.push(delivery);
25665
+ this.remainingMessageChars -= delivery.text.length;
25666
+ }
25667
+ formatResult(text) {
25668
+ const messages = this.deliveries.map((delivery) => delivery.text).filter(Boolean);
25669
+ return [truncateToolResult(text), ...messages.length ? ["--- Messages for you (read once; do not acknowledge). Use session check for any remaining messages. ---", ...messages] : []].filter(Boolean).join("\n\n");
25670
+ }
25671
+ acknowledgeDeliveries() {
25672
+ for (const delivery of this.deliveries) delivery.acknowledge();
25673
+ }
25674
+ releaseDeliveries() {
25675
+ for (const delivery of this.deliveries) delivery.release();
25676
+ }
25677
+ };
25474
25678
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
25475
25679
  function buildMcpRequestHandler(tools, info, sendJsonRpcMessage) {
25476
25680
  const sendJsonRpcResult = (id, result) => sendJsonRpcMessage({ jsonrpc: "2.0", id, result });
25477
25681
  const sendJsonRpcError = (id, code, message) => sendJsonRpcMessage({ jsonrpc: "2.0", id, error: { code, message } });
25478
25682
  return async (value) => {
25479
25683
  if (!isRecord(value) || value.jsonrpc !== "2.0" || typeof value.method !== "string" || value.id !== void 0 && value.id !== null && typeof value.id !== "string" && typeof value.id !== "number") {
25480
- sendJsonRpcError(null, -32600, "Invalid request");
25684
+ await sendJsonRpcError(null, -32600, "Invalid request");
25481
25685
  return;
25482
25686
  }
25483
25687
  const request = value;
25484
25688
  const { id, method } = request;
25485
25689
  if (id === void 0) return;
25486
25690
  if (request.params !== void 0 && !isRecord(request.params)) {
25487
- sendJsonRpcError(id, -32602, "Params must be an object");
25691
+ await sendJsonRpcError(id, -32602, "Params must be an object");
25488
25692
  return;
25489
25693
  }
25490
25694
  const params = request.params ?? {};
25491
25695
  switch (method) {
25492
25696
  case "initialize":
25493
- sendJsonRpcResult(id, { protocolVersion: params.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: info });
25697
+ await sendJsonRpcResult(id, { protocolVersion: params.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: info });
25494
25698
  return;
25495
25699
  case "notifications/initialized":
25496
25700
  case "notifications/cancelled":
25497
25701
  return;
25498
25702
  case "ping":
25499
- sendJsonRpcResult(id, {});
25703
+ await sendJsonRpcResult(id, {});
25500
25704
  return;
25501
25705
  case "tools/list":
25502
- sendJsonRpcResult(id, { tools: tools.map(({ name, description: description2, inputSchema }) => ({ name, description: description2, inputSchema })) });
25706
+ await sendJsonRpcResult(id, { tools: tools.map(({ name, description: description2, inputSchema }) => ({ name, description: description2, inputSchema })) });
25503
25707
  return;
25504
25708
  case "tools/call": {
25505
25709
  const name = params.name;
25506
25710
  if (typeof name !== "string" || params.arguments !== void 0 && !isRecord(params.arguments)) {
25507
- sendJsonRpcError(id, -32602, "Tool name and arguments are invalid");
25711
+ await sendJsonRpcError(id, -32602, "Tool name and arguments are invalid");
25508
25712
  return;
25509
25713
  }
25510
25714
  const tool = tools.find((candidate) => candidate.name === name);
25511
25715
  if (!tool) {
25512
- sendJsonRpcError(id, -32602, `No tool named ${name}`);
25716
+ await sendJsonRpcError(id, -32602, `No tool named ${name}`);
25513
25717
  return;
25514
25718
  }
25719
+ const output2 = new McpOutput();
25720
+ let text;
25515
25721
  try {
25516
- const text = await tool.run(params.arguments ?? {});
25517
- sendJsonRpcResult(id, { content: [{ type: "text", text }] });
25722
+ text = output2.formatResult(await tool.run(params.arguments ?? {}, output2));
25518
25723
  } catch (error62) {
25519
- sendJsonRpcResult(id, { content: [{ type: "text", text: error62 instanceof Error ? error62.message : String(error62) }], isError: true });
25724
+ output2.releaseDeliveries();
25725
+ await sendJsonRpcResult(id, { content: [{ type: "text", text: error62 instanceof Error ? error62.message : String(error62) }], isError: true });
25726
+ return;
25727
+ }
25728
+ try {
25729
+ await sendJsonRpcResult(id, { content: [{ type: "text", text }] });
25730
+ output2.acknowledgeDeliveries();
25731
+ } catch (error62) {
25732
+ output2.releaseDeliveries();
25733
+ throw error62;
25520
25734
  }
25521
25735
  return;
25522
25736
  }
25523
25737
  default:
25524
- if (id !== void 0) sendJsonRpcError(id, -32601, `Unknown method ${method}`);
25738
+ if (id !== void 0) await sendJsonRpcError(id, -32601, `Unknown method ${method}`);
25525
25739
  }
25526
25740
  };
25527
25741
  }
25528
25742
  function serveMcp(tools, info) {
25529
- const sendJsonRpcMessage = (message) => process.stdout.write(`${JSON.stringify(message)}
25530
- `);
25743
+ const sendJsonRpcMessage = (message) => new Promise((resolve6, reject) => {
25744
+ process.stdout.write(`${JSON.stringify(message)}
25745
+ `, (error62) => error62 ? reject(error62) : resolve6());
25746
+ });
25747
+ const endFailedOutput = () => {
25748
+ process.exitCode = 1;
25749
+ process.stdin.destroy();
25750
+ };
25751
+ process.stdout.on("error", endFailedOutput);
25531
25752
  const handleJsonRpcRequest = buildMcpRequestHandler(tools, info, sendJsonRpcMessage);
25532
25753
  const lines = createInterface({ input: process.stdin });
25533
25754
  lines.on("line", (line) => {
@@ -25536,19 +25757,19 @@ function serveMcp(tools, info) {
25536
25757
  try {
25537
25758
  request = JSON.parse(line);
25538
25759
  } catch {
25539
- sendJsonRpcMessage({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
25760
+ void sendJsonRpcMessage({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }).catch(endFailedOutput);
25540
25761
  return;
25541
25762
  }
25542
- void handleJsonRpcRequest(request);
25763
+ void handleJsonRpcRequest(request).catch(endFailedOutput);
25543
25764
  });
25544
25765
  }
25545
25766
 
25546
25767
  // src/commands/tasks.ts
25547
- import { readFileSync as readFileSync11 } from "fs";
25768
+ import { readFileSync as readFileSync13 } from "fs";
25548
25769
 
25549
25770
  // src/commands/items.ts
25550
- import { readFileSync as readFileSync10 } from "fs";
25551
- var fetchTextArgument = (value) => value === "-" ? readFileSync10(0, "utf8").trim() : value;
25771
+ import { readFileSync as readFileSync12 } from "fs";
25772
+ var fetchTextArgument = (value) => value === "-" ? readFileSync12(0, "utf8").trim() : value;
25552
25773
  var buildItemsPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
25553
25774
  function formatPlanItems(items) {
25554
25775
  if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
@@ -25575,7 +25796,7 @@ function registerItemCommands(task, registerProjectOption) {
25575
25796
  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) => {
25576
25797
  const api = buildAuthedApiClient();
25577
25798
  const project = await resolveProject(api, options.project);
25578
- const source = lines.length === 1 && lines[0] === "-" ? readFileSync10(0, "utf8") : lines.join("\n");
25799
+ const source = lines.length === 1 && lines[0] === "-" ? readFileSync12(0, "utf8") : lines.join("\n");
25579
25800
  const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
25580
25801
  if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
25581
25802
  const items = await api.post(buildItemsPath(project.id, key), ApiResponses.TaskItems, { items: titles.map((title3) => ({ title: title3 })) });
@@ -25632,7 +25853,7 @@ function parseOneOf(value, allowed, what) {
25632
25853
  if (allowed.includes(normalized)) return normalized;
25633
25854
  throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
25634
25855
  }
25635
- var fetchTextArgument2 = (value) => value === "-" ? readFileSync11(0, "utf8").trim() : value;
25856
+ var fetchTextArgument2 = (value) => value === "-" ? readFileSync13(0, "utf8").trim() : value;
25636
25857
  var buildTaskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
25637
25858
  var fetchProject = (api, id) => api.get(`/projects/${id}`, ApiResponses.Project);
25638
25859
  function resolveStatus(project, ref) {
@@ -25783,21 +26004,21 @@ ${buildTaskUrl(api.baseUrl, project.id, created)}`);
25783
26004
  }
25784
26005
 
25785
26006
  // src/platform/update-check.ts
25786
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
25787
- import { dirname as dirname7, join as join12 } from "path";
26007
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
26008
+ import { dirname as dirname7, join as join14 } from "path";
25788
26009
  var CHECK_EVERY_MS = 60 * 60 * 1e3;
25789
26010
  var REGISTRY = "https://registry.npmjs.org/slopwaresystems/latest";
25790
- var buildUpdateCachePath = () => join12(dirname7(buildConfigPath()), "update-check.json");
26011
+ var buildUpdateCachePath = () => join14(dirname7(buildConfigPath()), "update-check.json");
25791
26012
  function fetchUpdateCache() {
25792
26013
  try {
25793
- return JSON.parse(readFileSync12(buildUpdateCachePath(), "utf8"));
26014
+ return JSON.parse(readFileSync14(buildUpdateCachePath(), "utf8"));
25794
26015
  } catch {
25795
26016
  return { checkedAt: 0, latest: null };
25796
26017
  }
25797
26018
  }
25798
26019
  function persistUpdateCache(cache) {
25799
- mkdirSync6(dirname7(buildUpdateCachePath()), { recursive: true });
25800
- writeFileSync7(buildUpdateCachePath(), JSON.stringify(cache));
26020
+ mkdirSync8(dirname7(buildUpdateCachePath()), { recursive: true });
26021
+ writeFileSync8(buildUpdateCachePath(), JSON.stringify(cache));
25801
26022
  }
25802
26023
  function isNewerVersion(a, b) {
25803
26024
  const parseVersionParts = (version2) => version2.split(".").map((part) => Number.parseInt(part, 10));
@@ -25826,11 +26047,11 @@ function buildUpdateNotice(current) {
25826
26047
  if (!latest || !isNewerVersion(latest, current)) return null;
25827
26048
  return `Update available: slopwaresystems ${latest} (this is ${current}). Ask your person to run: npm i -g slopwaresystems && slop mcp install`;
25828
26049
  }
25829
- var isUpdateCheckEnabled = () => !existsSync8(join12(dirname7(buildConfigPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
26050
+ var isUpdateCheckEnabled = () => !existsSync10(join14(dirname7(buildConfigPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
25830
26051
 
25831
26052
  // src/commands/threads.ts
25832
- import { readFileSync as readFileSync13 } from "fs";
25833
- var fetchTextArgument3 = (value) => value === "-" ? readFileSync13(0, "utf8").trim() : value;
26053
+ import { readFileSync as readFileSync15 } from "fs";
26054
+ var fetchTextArgument3 = (value) => value === "-" ? readFileSync15(0, "utf8").trim() : value;
25834
26055
  var formatMessageTime = (iso) => iso.slice(0, 16).replace("T", " ");
25835
26056
  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}` : ""})`;
25836
26057
  function formatThreadLine(thread) {
@@ -25941,12 +26162,32 @@ function createProjectTools(api, project, identity) {
25941
26162
  api.sessionId = null;
25942
26163
  const resolveProjectFromArgs = async (_args) => project;
25943
26164
  const owner = { baseUrl: api.baseUrl, projectId: project.id, conversation: identity.conversation };
25944
- const drainOwnInbox = (thread = null) => drainInbox(process.cwd(), owner, (push) => !thread || push.thread.id === thread);
26165
+ const claimOwnInbox = (output2, accept = () => true) => {
26166
+ let remaining = output2.remainingMessageChars;
26167
+ const delivery = claimInbox(process.cwd(), owner, (push) => {
26168
+ if (!accept(push)) return false;
26169
+ const length = formatPushBlock(push).length + 2;
26170
+ if (length > remaining && remaining !== RESULT_CAP) return false;
26171
+ remaining -= length;
26172
+ return true;
26173
+ });
26174
+ output2.registerDelivery({ ...delivery, text: delivery.pushes.map(formatPushBlock).join("\n\n") });
26175
+ return delivery.pushes.length > 0;
26176
+ };
25945
26177
  const persistOwnInboxPush = (push) => persistInboxPush(process.cwd(), push, owner);
25946
26178
  let held = null;
25947
26179
  let stopped = false;
25948
26180
  const recovery = new SessionRecovery(api, owner, process.cwd(), (push) => held?.deliverPush(push));
25949
26181
  let waking = false;
26182
+ const wakeOwnHarness = () => {
26183
+ if (waking || stopped) return;
26184
+ waking = true;
26185
+ const text = `Messages are waiting for you on Slopware Tasks in project ${project.key}. Use session check with project ${project.id} to read them, then act or reply only if it changes something. Do not acknowledge.`;
26186
+ const wake = identity.harness === "codex" && identity.conversation ? wakeCodex(text, identity.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : identity.harness === "claude" ? wakeHarness(text, void 0, identity.conversation) : Promise.resolve(false);
26187
+ void wake.catch(() => false).finally(() => {
26188
+ waking = false;
26189
+ });
26190
+ };
25950
26191
  const resolveOwnAgentIdentity = () => identity;
25951
26192
  const holdSession = async (projectId, taskKey) => {
25952
26193
  if (stopped) throw new Error("The project session is stopping");
@@ -25963,16 +26204,7 @@ function createProjectTools(api, project, identity) {
25963
26204
  held?.stop();
25964
26205
  held = new HeldSession(api, projectId, taskKey, {
25965
26206
  onPush: persistOwnInboxPush,
25966
- onMessage: () => {
25967
- if (waking) return;
25968
- waking = true;
25969
- const text = `Messages are waiting for you on Slopware Tasks in project ${project.key}. Use session check with project ${project.id} to read them, then act or reply only if it changes something. Do not acknowledge.`;
25970
- const me = resolveOwnAgentIdentity();
25971
- const wake = me.harness === "codex" && me.conversation ? wakeCodex(text, me.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : me.harness === "claude" ? wakeHarness(text, void 0, me.conversation) : Promise.resolve(false);
25972
- void wake.catch(() => false).finally(() => {
25973
- waking = false;
25974
- });
25975
- },
26207
+ onMessage: wakeOwnHarness,
25976
26208
  claim: resolveOwnAgentIdentity,
25977
26209
  onReconnect: () => recovery.sync()
25978
26210
  });
@@ -25980,6 +26212,7 @@ function createProjectTools(api, project, identity) {
25980
26212
  await current.start();
25981
26213
  await current.requireSession();
25982
26214
  await recovery.sync();
26215
+ if (held === current && !current.isStopped && hasPendingInboxMessages(process.cwd(), owner)) wakeOwnHarness();
25983
26216
  };
25984
26217
  const resolveProjectWithPresence = async (args) => {
25985
26218
  const target = await resolveProjectFromArgs(args);
@@ -25990,29 +26223,28 @@ function createProjectTools(api, project, identity) {
25990
26223
  const releaseTaskSession = async () => {
25991
26224
  if (held?.taskKey) await holdSession(held.projectId, null);
25992
26225
  };
25993
- const waitForNextMessage = async (thread, minutes) => {
25994
- const wanted = drainOwnInbox(thread);
25995
- if (wanted.length) return { pushes: wanted, released: false };
26226
+ const waitForNextMessage = async (output2, thread, minutes) => {
26227
+ if (claimOwnInbox(output2, (push) => !thread || push.thread.id === thread)) return { received: true, released: false };
25996
26228
  const waitingOn = held;
25997
- if (!waitingOn || waitingOn.isStopped || !waitingOn.session?.reachable) return { pushes: [], released: true };
26229
+ if (!waitingOn || waitingOn.isStopped || !waitingOn.session?.reachable) return { received: false, released: true };
25998
26230
  return new Promise((resolve6) => {
25999
26231
  const acceptDeliveredPush = (delivered) => {
26000
26232
  if (delivered && thread && delivered.thread.id !== thread) return false;
26001
- if (delivered && drainInbox(process.cwd(), owner, (push) => push.message.id === delivered.message.id).length === 0) return false;
26233
+ if (delivered && !claimOwnInbox(output2, (push) => push.message.id === delivered.message.id)) return false;
26002
26234
  waitingOn.waiters.delete(acceptDeliveredPush);
26003
26235
  clearTimeout(timer);
26004
- resolve6({ pushes: delivered ? [delivered] : [], released: delivered === null });
26236
+ resolve6({ received: delivered !== null, released: delivered === null });
26005
26237
  return true;
26006
26238
  };
26007
26239
  waitingOn.waiters.add(acceptDeliveredPush);
26008
26240
  const timer = setTimeout(() => {
26009
26241
  waitingOn.waiters.delete(acceptDeliveredPush);
26010
- resolve6({ pushes: [], released: false });
26242
+ resolve6({ received: false, released: false });
26011
26243
  }, minutes * 6e4);
26012
26244
  });
26013
26245
  };
26014
26246
  const formatWaitOutcome = (outcome, minutes, thread) => {
26015
- if (outcome.pushes.length) return outcome.pushes.map(formatPushBlock).join("\n\n");
26247
+ if (outcome.received) return "";
26016
26248
  if (outcome.released) return "This wait ended because the session was released, replaced, or made unreachable.";
26017
26249
  return thread ? `No reply in ${thread} in ${minutes} minutes.` : `No message in ${minutes} minutes.`;
26018
26250
  };
@@ -26021,30 +26253,29 @@ function createProjectTools(api, project, identity) {
26021
26253
  name: "wait_for_message",
26022
26254
  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.",
26023
26255
  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 }),
26024
- run: async (args) => {
26256
+ run: async (args, output2) => {
26025
26257
  const target = await resolveProjectWithPresence(args);
26026
26258
  if (args.task) await holdSession(target.id, String(args.task));
26027
26259
  const thread = args.thread ? String(args.thread) : null;
26028
26260
  const minutes = typeof args.minutes === "number" ? args.minutes : thread ? 20 : 300;
26029
- return formatWaitOutcome(await waitForNextMessage(thread, minutes), minutes, thread);
26261
+ return formatWaitOutcome(await waitForNextMessage(output2, thread, minutes), minutes, thread);
26030
26262
  }
26031
26263
  },
26032
26264
  {
26033
26265
  name: "check_messages",
26034
26266
  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.",
26035
26267
  inputSchema: buildToolSchema({ task: buildStringSchema("Task key"), project: PROJECT }),
26036
- run: async (args) => {
26268
+ run: async (args, output2) => {
26037
26269
  const target = await resolveProjectWithPresence(args);
26038
- const queued = drainOwnInbox().map(formatPushBlock);
26039
- const rest = await buildUnreadDigest(api, target.id, args.task ?? null);
26040
- return [...queued, rest].join("\n\n");
26270
+ claimOwnInbox(output2);
26271
+ return buildUnreadDigest(api, target.id, args.task ?? null);
26041
26272
  }
26042
26273
  },
26043
26274
  {
26044
26275
  name: "reply",
26045
26276
  description: "Reply in a thread. In a thread about a task, it reaches whoever is live on that task now, or waits for them.",
26046
26277
  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"]),
26047
- run: async (args) => {
26278
+ run: async (args, output2) => {
26048
26279
  const target = await resolveProjectWithPresence(args);
26049
26280
  const thread = String(args.thread);
26050
26281
  const result = await api.post(`/projects/${target.id}/threads/${thread}/messages`, ApiResponses.CreatedThreadMessage, { body: String(args.body) });
@@ -26052,14 +26283,14 @@ function createProjectTools(api, project, identity) {
26052
26283
  if (typeof args.wait !== "number" || args.wait <= 0) return replied;
26053
26284
  return `${replied}
26054
26285
 
26055
- ${formatWaitOutcome(await waitForNextMessage(thread, args.wait), args.wait, thread)}`;
26286
+ ${formatWaitOutcome(await waitForNextMessage(output2, thread, args.wait), args.wait, thread)}`;
26056
26287
  }
26057
26288
  },
26058
26289
  {
26059
26290
  name: "post",
26060
26291
  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.",
26061
26292
  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"]),
26062
- run: async (args) => {
26293
+ run: async (args, output2) => {
26063
26294
  const target = await resolveProjectWithPresence(args);
26064
26295
  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 });
26065
26296
  const names = result.thread.to.map((recipient) => recipient.agent).join(", ");
@@ -26067,7 +26298,7 @@ ${formatWaitOutcome(await waitForNextMessage(thread, args.wait), args.wait, thre
26067
26298
  if (typeof args.wait !== "number" || args.wait <= 0) return started;
26068
26299
  return `${started}
26069
26300
 
26070
- ${formatWaitOutcome(await waitForNextMessage(result.thread.id, args.wait), args.wait, result.thread.id)}`;
26301
+ ${formatWaitOutcome(await waitForNextMessage(output2, result.thread.id, args.wait), args.wait, result.thread.id)}`;
26071
26302
  }
26072
26303
  },
26073
26304
  {
@@ -26343,44 +26574,41 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
26343
26574
  recovery.stop();
26344
26575
  held?.stop();
26345
26576
  },
26346
- tools: tools.map((tool) => ({ ...tool, run: async (args) => {
26347
- const result = truncateToolResult(await tool.run(args));
26348
- const arrived = tool.name === "check_messages" || tool.name === "wait_for_message" ? [] : drainOwnInbox();
26349
- return arrived.length ? `${result}
26350
-
26351
- --- Messages arrived while you worked (answer what is still open, once; do not acknowledge) ---
26352
- ${arrived.map(formatPushBlock).join("\n\n")}` : result;
26577
+ tools: tools.map((tool) => ({ ...tool, run: async (args, output2) => {
26578
+ const result = await tool.run(args, output2);
26579
+ if (tool.name !== "check_messages" && tool.name !== "wait_for_message") claimOwnInbox(output2);
26580
+ return result;
26353
26581
  } }))
26354
26582
  };
26355
26583
  }
26356
26584
  function fetchJsonFile(path2) {
26357
26585
  try {
26358
- return JSON.parse(readFileSync14(path2, "utf8"));
26586
+ return JSON.parse(readFileSync16(path2, "utf8"));
26359
26587
  } catch {
26360
26588
  return {};
26361
26589
  }
26362
26590
  }
26363
26591
  function installIntoClaudeHome(dir) {
26364
26592
  const done = [];
26365
- const primary = dir === join13(homedir4(), ".claude");
26593
+ const primary = dir === join15(homedir4(), ".claude");
26366
26594
  const env = { ...process.env };
26367
26595
  if (primary) delete env.CLAUDE_CONFIG_DIR;
26368
26596
  else env.CLAUDE_CONFIG_DIR = dir;
26369
26597
  const where = primary ? "" : ` for ${dir}`;
26370
26598
  try {
26371
- execFileSync5("claude", ["mcp", "add", "--scope", "user", "slop", "--", "slop", "mcp"], { stdio: ["ignore", "ignore", "pipe"], env });
26599
+ execFileSync6("claude", ["mcp", "add", "--scope", "user", "slop", "--", "slop", "mcp"], { stdio: ["ignore", "ignore", "pipe"], env });
26372
26600
  done.push(`Claude Code: MCP server "slop" registered (user scope${where})`);
26373
26601
  } catch (error62) {
26374
26602
  const said = String(error62.stderr ?? "") + String(error62.stdout ?? "");
26375
26603
  if (/already exists/i.test(said)) done.push(`Claude Code: MCP server "slop" already registered (user scope${where})`);
26376
26604
  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"})`);
26377
26605
  }
26378
- const path2 = join13(dir, "settings.json");
26379
- mkdirSync7(dir, { recursive: true });
26606
+ const path2 = join15(dir, "settings.json");
26607
+ mkdirSync9(dir, { recursive: true });
26380
26608
  const settings = fetchJsonFile(path2);
26381
26609
  settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
26382
26610
  settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "21600000" };
26383
- writeFileSync8(path2, `${JSON.stringify(settings, null, 2)}
26611
+ writeFileSync9(path2, `${JSON.stringify(settings, null, 2)}
26384
26612
  `);
26385
26613
  done.push(`Claude Code: SessionStart hook and MCP_TOOL_TIMEOUT written to ${path2}; other slop hooks removed (its session socket wakes it)`);
26386
26614
  return done;
@@ -26403,31 +26631,31 @@ function mergeHooks(hooks, harness) {
26403
26631
  }
26404
26632
  function installIntoCodexHome(dir) {
26405
26633
  const done = [];
26406
- mkdirSync7(dir, { recursive: true });
26407
- const configTomlPath = join13(dir, "config.toml");
26408
- const current = existsSync9(configTomlPath) ? readFileSync14(configTomlPath, "utf8") : "";
26634
+ mkdirSync9(dir, { recursive: true });
26635
+ const configTomlPath = join15(dir, "config.toml");
26636
+ const current = existsSync11(configTomlPath) ? readFileSync16(configTomlPath, "utf8") : "";
26409
26637
  if (/^\[mcp_servers\.slop\]/m.test(current)) {
26410
- if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync8(configTomlPath, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
26638
+ if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync9(configTomlPath, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
26411
26639
  done.push(`Codex: MCP server already in ${configTomlPath}`);
26412
26640
  } else {
26413
- writeFileSync8(configTomlPath, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}[mcp_servers.slop]
26641
+ writeFileSync9(configTomlPath, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}[mcp_servers.slop]
26414
26642
  command = "slop"
26415
26643
  args = ["mcp"]
26416
26644
  tool_timeout_sec = 21600
26417
26645
  `);
26418
26646
  done.push(`Codex: MCP server "slop" added to ${configTomlPath}`);
26419
26647
  }
26420
- const hooksPath = join13(dir, "hooks.json");
26648
+ const hooksPath = join15(dir, "hooks.json");
26421
26649
  const file2 = fetchJsonFile(hooksPath);
26422
26650
  file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
26423
- writeFileSync8(hooksPath, `${JSON.stringify(file2, null, 2)}
26651
+ writeFileSync9(hooksPath, `${JSON.stringify(file2, null, 2)}
26424
26652
  `);
26425
26653
  done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (Codex is woken with codex queue; no hook holds a turn open)`);
26426
26654
  return done;
26427
26655
  }
26428
26656
  async function trustCodexHooks(codexHome) {
26429
26657
  const server = spawn3("codex", ["app-server", "-c", "mcp_servers={}"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
26430
- const where = codexHome === join13(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
26658
+ const where = codexHome === join15(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
26431
26659
  let buffer = "";
26432
26660
  const pending = /* @__PURE__ */ new Map();
26433
26661
  server.stdout.on("data", (chunk) => {
@@ -26462,7 +26690,7 @@ async function trustCodexHooks(codexHome) {
26462
26690
  const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
26463
26691
  `);
26464
26692
  try {
26465
- await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.9" }, capabilities: { experimentalApi: true } });
26693
+ await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.10" }, capabilities: { experimentalApi: true } });
26466
26694
  sendNotification("initialized", {});
26467
26695
  const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
26468
26696
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
@@ -26493,7 +26721,7 @@ async function parseHookInput(harness, remember = true) {
26493
26721
  conversation = parsed.session_id ?? null;
26494
26722
  } catch {
26495
26723
  }
26496
- const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join13(home, "sessions")), conversation);
26724
+ const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join15(home, "sessions")), conversation);
26497
26725
  if (remember && conversation && ownThread && !isNonInteractiveHarness()) persistRememberedConversation(cwd, { id: conversation, harness: harness ?? "other" });
26498
26726
  return { cwd, conversation };
26499
26727
  }
@@ -26559,12 +26787,12 @@ function buildTools() {
26559
26787
  }
26560
26788
  return context;
26561
26789
  };
26562
- const runHandlerByName = async (name, args) => {
26790
+ const runHandlerByName = async (name, args, output2) => {
26563
26791
  if (name === "projects") return fetchProjectsOverview(api);
26564
26792
  const project = await resolveProject(api, args.project);
26565
26793
  const handler = requireProjectTools(project).tools.find((candidate) => candidate.name === name);
26566
26794
  if (!handler) throw new Error("No such action");
26567
- return handler.run(args);
26795
+ return handler.run(args, output2);
26568
26796
  };
26569
26797
  void (async () => {
26570
26798
  for (let waited = 0; waited < 1e4 && !fetchKnownConversation(process.cwd()) && !stopped; waited += 250) await new Promise((resolve6) => setTimeout(resolve6, 250));
@@ -26572,20 +26800,22 @@ function buildTools() {
26572
26800
  await requireProjectTools(await resolveProject(api)).start();
26573
26801
  })().catch(() => void 0);
26574
26802
  const endSessionAndExit = () => {
26803
+ if (stopped) return;
26575
26804
  stopped = true;
26576
26805
  for (const context of projects.values()) context.stop();
26577
- setTimeout(() => process.exit(0), 300).unref();
26806
+ setTimeout(() => process.exit(process.exitCode ?? 0), 300).unref();
26578
26807
  };
26579
26808
  process.stdin.once("end", endSessionAndExit);
26809
+ process.stdin.once("close", endSessionAndExit);
26580
26810
  for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, endSessionAndExit);
26581
26811
  const buildGroupedTool = (name, description2, actions, properties) => ({
26582
26812
  name,
26583
26813
  description: description2,
26584
26814
  inputSchema: buildToolSchema({ action: { type: "string", enum: Object.keys(actions) }, ...properties, project: PROJECT }, ["action"]),
26585
- run: (args) => {
26815
+ run: (args, output2) => {
26586
26816
  const target = actions[String(args.action)];
26587
26817
  if (!target) throw new Error(`action must be one of: ${Object.keys(actions).join(", ")}`);
26588
- return runHandlerByName(target, args);
26818
+ return runHandlerByName(target, args, output2);
26589
26819
  }
26590
26820
  });
26591
26821
  return [
@@ -26625,8 +26855,8 @@ function buildTools() {
26625
26855
  function attachUpdateNoticeToTools(list, version2) {
26626
26856
  return list.map((tool) => ({
26627
26857
  ...tool,
26628
- run: async (args) => {
26629
- const result = truncateToolResult(await tool.run(args));
26858
+ run: async (args, output2) => {
26859
+ const result = await tool.run(args, output2);
26630
26860
  const parts = [result];
26631
26861
  const notice = isUpdateCheckEnabled() ? buildUpdateNotice(version2) : null;
26632
26862
  if (notice) parts.push(notice);
@@ -26642,7 +26872,7 @@ function registerMcpCommands(program3, version2) {
26642
26872
  const lines = [...fetchClaudeHomes().flatMap((home) => installIntoClaudeHome(home)), ...homes.flatMap((home) => installIntoCodexHome(home))];
26643
26873
  let codexPresent = true;
26644
26874
  try {
26645
- execFileSync5("codex", ["--version"], { stdio: "ignore" });
26875
+ execFileSync6("codex", ["--version"], { stdio: "ignore" });
26646
26876
  } catch {
26647
26877
  codexPresent = false;
26648
26878
  }
@@ -26742,13 +26972,13 @@ ${written.map((path2) => ` ${path2}`).join("\n")}`);
26742
26972
  }
26743
26973
 
26744
26974
  // src/index.ts
26745
- 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.9").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)");
26975
+ 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.10").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)");
26746
26976
  registerAuthCommands(program2);
26747
26977
  registerProjectCommands(program2);
26748
26978
  var taskCommand = registerTaskCommands(program2);
26749
26979
  registerSessionCommands(program2);
26750
26980
  registerThreadCommands(program2, taskCommand);
26751
- registerMcpCommands(program2, "0.4.9");
26981
+ registerMcpCommands(program2, "0.4.10");
26752
26982
  registerGuideCommand(program2);
26753
26983
  registerSkillCommands(program2);
26754
26984
  program2.parseAsync().catch(exitWithError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",