slopwaresystems 0.2.2 → 0.2.3

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 +69 -47
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3385,7 +3385,7 @@ var slop_cli_default = '---\nname: slop-cli\ndescription: How to use the slop CL
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: `slop task show <KEY>`. 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```bash\nslop task plan <KEY> - <<\'EOF\'\nInventory every Intercom object we rely on (conversations, users, tags)\nDecide the target schema for the support agent\nWrite and test the export script against a sample\nRun the full export; record counts\nImport into the support agent; verify counts match\nSwitch inbound routing; keep Intercom read-only for a week\nRemove the Intercom SDK and env vars\nEOF\n```\n\n\n## Working\n\n- Before an item: `slop task item note <KEY> <n> "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: `slop task item done <KEY> <n>`. If it turned out to be two things, add the second: `slop task item add <KEY> "\u2026"`.\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: `slop task create "\u2026" --status backlog -d "Found while doing <KEY>: \u2026"`.\n- Something to check later on this task is an item: `slop task item add <KEY> "Confirm Intercom exports stayed empty for a week"`.\n\n## Finishing\n\n`slop task items <KEY>` should show every item done or removed. Put what changed and where at the end of the description, link the PR (`slop task link`), then `slop task done <KEY>`.\n\n## Picking up a task you did not start\n\n`slop task show`, `slop task items`, `slop task history`, in that order. The notes tell you the present; the history tells you how it got there. Then continue from the first item that is not done.\n';
3386
3386
 
3387
3387
  // skills/slop-sessions.md
3388
- var slop_sessions_default = '---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project\'s message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live on one task. It exists while `slop session wait` is running: when that process ends (you exit, or your thread closes), the session ends with it. Sessions are visible to everyone (`slop sessions`, the board\'s live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, or in **a thread you have written in** (that is how answers find you, even from a general thread or another ticket), reaches you at once. Nothing else does.\n- **Everything else waits.** `slop session start <KEY>` prints the unread threads about that task and the unread general ones (read those for context). `slop threads` lists them any time; `slop thread show <id>` reads one.\n- Your own messages never wake your own waiter.\n\n## The loop (MCP: the way that works)\n\nYour harness has the `slop` MCP server (`slop mcp install` set it up). Use its tools; nothing runs in the background and nothing has to be restarted.\n\n1. **Claim:** `claim_task(task)`. Assigns the task to you, moves it to in progress, and returns what is unread about it. Read that first.\n2. **Work.** Keep the plan\'s notes current with `note_item`, tick with `finish_item`.\n3. **You are live from now on.** `claim_task` holds your session for as long as the harness runs the server. A message in a thread about your task, or in any thread you have written in, reaches you on its own: right after your next tool call (any tool; a hook on Claude Code and on Codex hands it to you), and in every slop tool\'s result besides. There is nothing to poll and nothing to restart. When one arrives, act or answer with `reply(thread, body)` and carry on.\n4. **Nothing to do but wait?** `wait_for_message(task, minutes)` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `finish_task(task)` (it also releases the session), or `release_task` to step off without finishing.\n\nBoth harnesses also refuse to stop while messages are unread about your task (a Stop hook), so nothing is lost at the end of a turn.\n\n## The loop without MCP (scripts, older setups)\n\n`slop session start <KEY>`, then `slop session wait <KEY>` in the background. It prints the first message and exits; **the exit is the message**: read it, reply with `slop thread reply <id> "\u2026"`, run `slop session wait <KEY>` again at once. Exit codes: 0 message, 2 ended on purpose (stop), 3 timeout, 4 dropped (relaunch). `--follow` streams instead of exiting, for harnesses that watch output.\n\n## Talking\n\n```bash\nslop sessions # who is live on what\nslop threads [--task KEY] [--general] [--unread]\nslop thread show <id> # read it (marks it read)\nslop thread reply <id> "\u2026" # in a task thread: reaches whoever is live on it, or waits for them\nslop thread new "Subject" -m "\u2026" --task WEB-12 # a new thread about a task\nslop thread new "Subject" -m "\u2026" # a general thread: for people, and for context\nslop task ask WEB-12 "\u2026" # shorthand: a new thread about WEB-12 from one message\n```\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan\'s notes, decisions in the description.\n\n## Etiquette\n\n- One live session per agent. Do not claim a task that has someone else\'s live session (`sessions`); write in a thread about it first.\n- Answer messages about your task promptly: the other agent is blocked on you.\n- Report progress in the plan\'s notes, not in threads. Threads are for talking to someone.\n- If you stop working on a task, end the session so nobody waits on you.\n';
3388
+ var slop_sessions_default = '---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project\'s message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live. With the MCP server it exists from your first slop tool call until your harness exits: first as presence in the project (no task), then on the task you claim. Being live means messages find you. Sessions are visible to everyone (`slop sessions`, the board\'s live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, or in **a thread you have written in** (that is how answers find you, even from a general thread or another ticket), reaches you at once. Nothing else does.\n- **Everything else waits.** `slop session start <KEY>` prints the unread threads about that task and the unread general ones (read those for context). `slop threads` lists them any time; `slop thread show <id>` reads one.\n- Your own messages never wake your own waiter.\n\n## The loop (MCP: the way that works)\n\nYour harness has the `slop` MCP server (`slop mcp install` set it up). Use its tools; nothing runs in the background and nothing has to be restarted.\n\n1. **Claim:** `claim_task(task)`. Assigns the task to you, moves it to in progress, and returns what is unread about it. Read that first.\n2. **Work.** Keep the plan\'s notes current with `note_item`, tick with `finish_item`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `claim_task` upgrades it to the task; `finish_task` or `release_task` drops back to presence. Either way, for as long as the harness runs the server A message in a thread about your task, or in any thread you have written in, reaches you on its own: right after your next tool call (any tool; a hook on Claude Code and on Codex hands it to you), and in every slop tool\'s result besides. There is nothing to poll and nothing to restart. When one arrives, act or answer with `reply(thread, body)` and carry on.\n4. **Nothing to do but wait?** `wait_for_message(task, minutes)` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `finish_task(task)` (it also releases the session), or `release_task` to step off without finishing.\n\nBoth harnesses also refuse to stop while messages are unread about your task (a Stop hook), so nothing is lost at the end of a turn.\n\n## The loop without MCP (scripts, older setups)\n\n`slop session start <KEY>`, then `slop session wait <KEY>` in the background. It prints the first message and exits; **the exit is the message**: read it, reply with `slop thread reply <id> "\u2026"`, run `slop session wait <KEY>` again at once. Exit codes: 0 message, 2 ended on purpose (stop), 3 timeout, 4 dropped (relaunch). `--follow` streams instead of exiting, for harnesses that watch output.\n\n## Talking\n\n```bash\nslop sessions # who is live on what\nslop threads [--task KEY] [--general] [--unread]\nslop thread show <id> # read it (marks it read)\nslop thread reply <id> "\u2026" # in a task thread: reaches whoever is live on it, or waits for them\nslop thread new "Subject" -m "\u2026" --task WEB-12 # a new thread about a task\nslop thread new "Subject" -m "\u2026" # a general thread: for people, and for context\nslop task ask WEB-12 "\u2026" # shorthand: a new thread about WEB-12 from one message\n```\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan\'s notes, decisions in the description.\n\n## Etiquette\n\n- One live session per agent, and one agent per working directory: run each agent in its own checkout or git worktree, so its inbox is its own.\n- Do not claim a task that has someone else\'s live session (`sessions`); write in a thread about it first.\n- Answer messages about your task promptly: the other agent is blocked on you.\n- Report progress in the plan\'s notes, not in threads. Threads are for talking to someone.\n- If you stop working on a task, end the session so nobody waits on you.\n';
3389
3389
 
3390
3390
  // skills/slop-working.md
3391
3391
  var slop_working_default = '---\nname: slop-working\ndescription: The working loop for tasks on the board (claim, do, report, finish) and what to record where. Load at the start of any coding session in a repository linked with slop.\n---\n\n# The loop\n\n1. **What is there:** `slop task list --assignee me` (or `slop task list`). If nothing is assigned to you, take the top item from the first "to do" column: `slop task assign <KEY> me`.\n2. **Claim it:** `slop session start <KEY>` (assigns you, moves it to in progress), then run `slop session wait <KEY>` in the background so other agents can reach you (see `slop-sessions`). Read `slop task show <KEY>` fully, including the plan and its notes.\n3. **Do it.** Long work: follow `slop-planning`. Short work: just do it.\n4. **Report as you go**: the item\'s note is the present state of that piece of work; the description holds what you decided and why. Write in sentences a teammate can act on.\n5. **Finish:** link the PR, put what changed and where at the end of the description, `slop task done <KEY>`, `slop session end <KEY>`.\n\n# Where things go\n\n| This | Goes here |\n|---|---|\n| The current truth about a piece of the work | the item\'s note |\n| A decision and its reason | the description (edit it) |\n| A question for the agent on a ticket | a thread about that ticket (`slop task ask KEY "\u2026"`) |\n| A note for people, or for whoever comes next | a general thread (`slop post "\u2026"`) |\n| A discovery, a hand-off | the description, or a new task if it is work |\n| A pull request, a design, a log | a link |\n| Loose working notes nobody needs to act on | the task\'s notes (`slop task notes`) |\n| Work you found but will not do now | a new task in backlog |\n\n# Etiquette\n\n- One task in progress at a time per agent. `slop sessions` shows who is live; do not take a task with someone else\'s live session without asking (`slop task ask`).\n- Never delete a task or someone else\'s items; archive and remove are for the owner.\n- Severity is the impact if nothing is done: `critical` is data loss or money, `high` is users blocked, `medium` is friction, `low` is polish.\n';
@@ -23442,8 +23442,9 @@ var TaskEventRecord = external_exports.object({
23442
23442
  // ../contract/src/work/sessions.ts
23443
23443
  var Session = external_exports.object({
23444
23444
  id: Id,
23445
- taskId: Id,
23446
- taskKey: external_exports.string(),
23445
+ /** Null: the agent is live in the project but on no task yet (presence). Replies in threads it is part of still reach it. */
23446
+ taskId: Id.nullable(),
23447
+ taskKey: external_exports.string().nullable(),
23447
23448
  user: UserRef,
23448
23449
  machine: external_exports.object({ id: Id, name: external_exports.string() }),
23449
23450
  startedAt: Timestamp
@@ -23536,6 +23537,7 @@ function createApi(options) {
23536
23537
  const headers = { accept: "application/json" };
23537
23538
  if (body2 !== void 0) headers["content-type"] = "application/json";
23538
23539
  if (options.token) headers.authorization = `Bearer ${options.token}`;
23540
+ if (client.sessionId) headers["x-slop-session"] = client.sessionId;
23539
23541
  let response;
23540
23542
  try {
23541
23543
  response = await fetch(`${options.baseUrl}/api${path3}`, {
@@ -23561,14 +23563,16 @@ function createApi(options) {
23561
23563
  }
23562
23564
  throw new CliError(`${method} ${path3} failed with HTTP ${response.status}`, void 0, "http");
23563
23565
  }
23564
- return {
23566
+ const client = {
23565
23567
  baseUrl: options.baseUrl,
23568
+ sessionId: null,
23566
23569
  get: (path3) => request("GET", path3),
23567
23570
  post: (path3, body2) => request("POST", path3, body2 ?? {}),
23568
23571
  patch: (path3, body2) => request("PATCH", path3, body2),
23569
23572
  put: (path3, body2) => request("PUT", path3, body2),
23570
23573
  delete: (path3) => request("DELETE", path3)
23571
23574
  };
23575
+ return client;
23572
23576
  }
23573
23577
  function authedApi() {
23574
23578
  const session = resolveSession();
@@ -23951,7 +23955,8 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
23951
23955
 
23952
23956
  // src/commands/mcp.ts
23953
23957
  import { execFileSync as execFileSync3 } from "child_process";
23954
- import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, readdirSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
23958
+ import { createHash as createHash3 } from "crypto";
23959
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, readdirSync, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
23955
23960
  import { dirname as dirname3 } from "path";
23956
23961
  import { homedir as homedir3 } from "os";
23957
23962
  import { join as join5 } from "path";
@@ -23969,6 +23974,7 @@ async function waitForMessage(api, projectId, taskKey, options = {}) {
23969
23974
  socket.addEventListener("message", (event) => {
23970
23975
  const push = JSON.parse(String(event.data));
23971
23976
  if (push.type === "hello") {
23977
+ api.sessionId = push.session.id;
23972
23978
  options.onHello?.(push.session);
23973
23979
  return;
23974
23980
  }
@@ -24014,7 +24020,7 @@ var HeldSession = class {
24014
24020
  async connect() {
24015
24021
  if (this.stopped) return;
24016
24022
  try {
24017
- const ticket = await this.api.post(`/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket`);
24023
+ const ticket = await this.api.post(this.taskKey ? `/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket` : `/projects/${this.projectId}/session/ticket`);
24018
24024
  const socket = new WebSocket(`${this.api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
24019
24025
  this.socket = socket;
24020
24026
  socket.addEventListener("open", () => {
@@ -24022,6 +24028,10 @@ var HeldSession = class {
24022
24028
  });
24023
24029
  socket.addEventListener("message", (event) => {
24024
24030
  const push = JSON.parse(String(event.data));
24031
+ if (push.type === "hello") {
24032
+ this.api.sessionId = push.session.id;
24033
+ return;
24034
+ }
24025
24035
  if (push.type === "end") {
24026
24036
  this.stopped = true;
24027
24037
  return;
@@ -24047,6 +24057,7 @@ var HeldSession = class {
24047
24057
  }
24048
24058
  stop() {
24049
24059
  this.stopped = true;
24060
+ this.api.sessionId = null;
24050
24061
  if (this.timer) clearTimeout(this.timer);
24051
24062
  this.socket?.close(1e3, "released");
24052
24063
  }
@@ -24431,23 +24442,27 @@ var schema = (properties, required2 = []) => ({ type: "object", properties, requ
24431
24442
  var PROJECT = str("Project key. Defaults to the project this repository is linked to.");
24432
24443
  var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
24433
24444
  var inboxDir = () => join5(dirname3(configPath()), "inbox");
24434
- var inboxFile = (projectId) => join5(inboxDir(), `${projectId}.jsonl`);
24435
- function inboxWrite(projectId, push) {
24445
+ var realCwd = (cwd) => {
24446
+ try {
24447
+ return realpathSync(cwd);
24448
+ } catch {
24449
+ return cwd;
24450
+ }
24451
+ };
24452
+ var inboxKey = (cwd) => createHash3("sha256").update(realCwd(cwd)).digest("hex").slice(0, 16);
24453
+ var inboxFile = (cwd) => join5(inboxDir(), `${inboxKey(cwd)}.jsonl`);
24454
+ function inboxWrite(cwd, push) {
24436
24455
  mkdirSync4(inboxDir(), { recursive: true });
24437
- appendFileSync(inboxFile(projectId), `${JSON.stringify(push)}
24456
+ appendFileSync(inboxFile(cwd), `${JSON.stringify(push)}
24438
24457
  `);
24439
24458
  }
24440
- function inboxDrain(projectId) {
24441
- const path3 = inboxFile(projectId);
24459
+ function inboxDrain(cwd) {
24460
+ const path3 = inboxFile(cwd);
24442
24461
  if (!existsSync5(path3)) return [];
24443
24462
  const lines = readFileSync8(path3, "utf8").split("\n").filter(Boolean);
24444
24463
  rmSync3(path3, { force: true });
24445
24464
  return lines.map((line) => JSON.parse(line));
24446
24465
  }
24447
- function inboxDrainAll() {
24448
- if (!existsSync5(inboxDir())) return [];
24449
- return readdirSync(inboxDir()).flatMap((name) => inboxDrain(name.replace(/\.jsonl$/, "")));
24450
- }
24451
24466
  async function digest(api, projectId, taskKey) {
24452
24467
  const [about, mine, general] = await Promise.all([
24453
24468
  taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1&read=1`) : Promise.resolve([]),
@@ -24463,17 +24478,21 @@ async function digest(api, projectId, taskKey) {
24463
24478
  }
24464
24479
  function tools() {
24465
24480
  const api = authedApi();
24466
- const project = (args) => resolveProject(api, args.project);
24481
+ const resolveOnly = (args) => resolveProject(api, args.project);
24467
24482
  let held = null;
24468
24483
  const hold = async (projectId, taskKey) => {
24469
24484
  if (held && held.projectId === projectId && held.taskKey === taskKey) return;
24470
24485
  held?.stop();
24471
- held = new HeldSession(api, projectId, taskKey, (push) => inboxWrite(projectId, push));
24486
+ held = new HeldSession(api, projectId, taskKey, (push) => inboxWrite(process.cwd(), push));
24472
24487
  await held.start();
24473
24488
  };
24474
- const release = () => {
24475
- held?.stop();
24476
- held = null;
24489
+ const project = async (args) => {
24490
+ const target = await resolveOnly(args);
24491
+ if (!held) await hold(target.id, null);
24492
+ return target;
24493
+ };
24494
+ const release = async () => {
24495
+ if (held?.taskKey) await hold(held.projectId, null);
24477
24496
  };
24478
24497
  return [
24479
24498
  {
@@ -24483,7 +24502,7 @@ function tools() {
24483
24502
  run: async (args) => {
24484
24503
  const target = await project(args);
24485
24504
  await hold(target.id, String(args.task));
24486
- const queued = inboxDrain(target.id);
24505
+ const queued = inboxDrain(process.cwd());
24487
24506
  if (queued.length) return queued.map(pushBlock).join("\n\n");
24488
24507
  const minutes = typeof args.minutes === "number" ? args.minutes : 10;
24489
24508
  const push = await new Promise((resolve2) => {
@@ -24506,7 +24525,7 @@ function tools() {
24506
24525
  inputSchema: schema({ task: str("Task key"), project: PROJECT }),
24507
24526
  run: async (args) => {
24508
24527
  const target = await project(args);
24509
- const queued = inboxDrain(target.id).map(pushBlock);
24528
+ const queued = inboxDrain(process.cwd()).map(pushBlock);
24510
24529
  const rest = await digest(api, target.id, args.task ?? null);
24511
24530
  return [...queued, rest].join("\n\n");
24512
24531
  }
@@ -24552,11 +24571,11 @@ function tools() {
24552
24571
  },
24553
24572
  {
24554
24573
  name: "sessions",
24555
- description: "Who is live on which task right now.",
24574
+ description: "Who is live right now: on which task, or present in the project on no task yet.",
24556
24575
  inputSchema: schema({ project: PROJECT }),
24557
24576
  run: async (args) => {
24558
24577
  const list = await api.get(`/projects/${(await project(args)).id}/sessions`);
24559
- return list.length ? list.map((session) => `${session.taskKey} ${session.user.name} ${session.machine.name} since ${session.startedAt}`).join("\n") : "Nobody is live.";
24578
+ return list.length ? list.map((session) => `${session.taskKey ?? "(no task)"} ${session.user.name} ${session.machine.name} since ${session.startedAt}`).join("\n") : "Nobody is live.";
24560
24579
  }
24561
24580
  },
24562
24581
  {
@@ -24575,11 +24594,11 @@ ${await digest(api, target.id, task.key)}`;
24575
24594
  },
24576
24595
  {
24577
24596
  name: "release_task",
24578
- description: "Stop being live on the task you hold (without finishing it).",
24597
+ description: "Step off the task you hold without finishing it. You stay live in the project, reachable in threads you are part of.",
24579
24598
  inputSchema: schema({ project: PROJECT }),
24580
24599
  run: async () => {
24581
- release();
24582
- return "Released.";
24600
+ await release();
24601
+ return "Released. Still live in the project.";
24583
24602
  }
24584
24603
  },
24585
24604
  {
@@ -24631,8 +24650,8 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
24631
24650
  run: async (args) => {
24632
24651
  const target = await loadProject(api, (await project(args)).id);
24633
24652
  const task = await api.patch(taskPath(target.id, String(args.task)), { status: statusInCategory(target, "done") });
24634
- release();
24635
- return `${task.key} is done.`;
24653
+ await release();
24654
+ return `${task.key} is done. You are still live in the project, reachable in threads you are part of.`;
24636
24655
  }
24637
24656
  }
24638
24657
  ];
@@ -24697,8 +24716,21 @@ tool_timeout_sec = 1800
24697
24716
  done.push(`Codex: PostToolUse and Stop hooks written to ${hooksPath}. Codex asks you to trust new hooks once (/hooks in a session).`);
24698
24717
  return done;
24699
24718
  }
24700
- function tickHook() {
24701
- const arrived = inboxDrainAll();
24719
+ async function hookCwd() {
24720
+ const input2 = await new Promise((resolve2) => {
24721
+ let data = "";
24722
+ process.stdin.on("data", (chunk) => data += chunk);
24723
+ process.stdin.on("end", () => resolve2(data));
24724
+ setTimeout(() => resolve2(data), 2e3);
24725
+ });
24726
+ try {
24727
+ return JSON.parse(input2).cwd ?? process.cwd();
24728
+ } catch {
24729
+ return process.cwd();
24730
+ }
24731
+ }
24732
+ async function tickHook() {
24733
+ const arrived = inboxDrain(await hookCwd());
24702
24734
  if (arrived.length === 0) return;
24703
24735
  const text = `Messages arrived on Slopware Tasks while you worked:
24704
24736
 
@@ -24709,19 +24741,9 @@ Act or answer with the slop MCP \`reply\` tool, then carry on.`;
24709
24741
  `);
24710
24742
  }
24711
24743
  async function stopHook() {
24712
- const input2 = await new Promise((resolve2) => {
24713
- let data = "";
24714
- process.stdin.on("data", (chunk) => data += chunk);
24715
- process.stdin.on("end", () => resolve2(data));
24716
- setTimeout(() => resolve2(data), 2e3);
24717
- });
24718
- let cwd = process.cwd();
24719
- try {
24720
- cwd = JSON.parse(input2).cwd ?? cwd;
24721
- } catch {
24722
- }
24744
+ const cwd = await hookCwd();
24723
24745
  process.chdir(cwd);
24724
- const parts = inboxDrainAll().map(pushBlock);
24746
+ const parts = inboxDrain(cwd).map(pushBlock);
24725
24747
  const repo = findRepo();
24726
24748
  const link = repo ? readLink(repo) : null;
24727
24749
  if (!link) {
@@ -24753,7 +24775,7 @@ function withInbox(list) {
24753
24775
  ...tool,
24754
24776
  run: async (args) => {
24755
24777
  const result = await tool.run(args);
24756
- const arrived = inboxDrainAll();
24778
+ const arrived = inboxDrain(process.cwd());
24757
24779
  return arrived.length ? `${result}
24758
24780
 
24759
24781
  --- Messages arrived while you worked ---
@@ -24777,7 +24799,7 @@ function registerMcpCommands(program3, version2) {
24777
24799
  // src/commands/sessions.ts
24778
24800
  import { readFileSync as readFileSync9 } from "fs";
24779
24801
  var taskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
24780
- var sessionLine = (session) => [session.taskKey, session.user.name, session.machine.name, session.startedAt];
24802
+ var sessionLine = (session) => [session.taskKey ?? "(no task)", session.user.name, session.machine.name, session.startedAt];
24781
24803
  function registerSessionCommands(program3, task) {
24782
24804
  const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
24783
24805
  const session = program3.command("session").description("Claim a task and be reachable on it while you work.");
@@ -24859,13 +24881,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
24859
24881
  }
24860
24882
 
24861
24883
  // src/index.ts
24862
- 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.2.2").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => setJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
24884
+ 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.2.3").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => setJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
24863
24885
  registerAuthCommands(program2);
24864
24886
  registerProjectCommands(program2);
24865
24887
  var taskCommand = registerTaskCommands(program2);
24866
24888
  registerSessionCommands(program2, taskCommand);
24867
24889
  registerThreadCommands(program2, taskCommand);
24868
- registerMcpCommands(program2, "0.2.2");
24890
+ registerMcpCommands(program2, "0.2.3");
24869
24891
  registerGuideCommand(program2);
24870
24892
  registerSkillCommands(program2);
24871
24893
  program2.parseAsync().catch(fail);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",