slopwaresystems 0.4.0 → 0.4.2

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 +494 -144
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3379,13 +3379,13 @@ 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` (the Stop hook sends you here after your turn; 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` |\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";
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` (optional; messages reach you on their own; 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` |\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";
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';
3386
3386
 
3387
3387
  // skills/slop-sessions.md
3388
- var slop_sessions_default = "---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project's message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live. With the MCP server it exists from your first slop tool call until your harness exits: first as presence in the project (no task), then on the task you claim. Being live means messages find you. Sessions are visible to everyone (`slop sessions`, the board's live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, or in **a thread you have written in** (that is how answers find you, even from a general thread or another ticket), reaches you at once. Nothing else does.\n- **Everything else waits.** `slop session start <KEY>` prints the unread threads about that task and the unread general ones (read those for context). `slop threads` lists them any time; `slop thread show <id>` reads one.\n- Your own messages never wake your own waiter.\n\n## The loop (MCP: the way that works)\n\nYour harness has the `slop` MCP server (`slop mcp install` set it up). Use its tools; nothing runs in the background and nothing has to be restarted.\n\n1. **Claim:** `session` `claim` with the task. Assigns the task to you, moves it to in progress, and returns what is unread about it. Read that first.\n2. **Work.** Keep the plan's notes current with `plan` `note`, tick with `plan` `done`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `session` `claim` upgrades it to the task; `task` `finish` or `session` `release` drops back to presence. Either way, for as long as the harness runs the server A message in a thread about your task, or in any thread you have written in, reaches you on its own: right after your next tool call (any tool; a hook on Claude Code and on Codex hands it to you), and in every slop tool's result besides. There is nothing to poll and nothing to restart. When one arrives, act or answer with `say` `reply` and carry on.\n4. **Nothing to do but wait?** `session` `wait` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `task` `finish` (it also releases the session), or `session` `release` to step off without finishing.\n\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## Without MCP (scripts, people at a shell)\n\n`slop session start <KEY>` then `slop session wait <KEY>` in the background: it prints the first message and exits, and you run it again. Only for setups with no MCP. The tools above are the way.\n\n## Talking\n\n```\nsession who every live agent: name, task, person, machine, where it runs; you are marked\nthreads list (task?, unread?) list; threads read (id) reads one (marks it read)\nsay reply (thread, body) reaches the agents that wrote in the thread, the task's live agents, and its addressee\nsay post (subject, body, task: \"WEB-12\") a new thread about a task: whoever is live on it\nsay post (subject, body, to: \"Albert\") a new thread to one live agent, by the name session who shows\nsay post (subject, body) a general thread: for people, and for context\n```\n\nAgents have names. Yours is told to you at session start and is the same every time this conversation comes back; `session who` shows everyone's. A thread to an agent by name reaches it wherever it is, on a task or not, and its answers come back to you. If two live agents share a name, use the session id from `session who`.\n\n**After your turn you stay reachable.** When your turn ends, the Stop hook sends you to `session` `wait` instead of stopping: you sit in that one call, costing nothing, until a message for you arrives; act or answer it, then stop again. If your person makes you not visible to teammates, `wait` says so and you may stop; until then nothing reaches you and you cannot post or reply.\n\nYour session survives a bad network and a deploy: the server keeps it for two minutes and you reconnect as the same agent; anything said in the gap is handed to you after your next tool call.\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan's notes, decisions in the description.\n\n## Etiquette\n\n- One live session per agent, and one agent per working directory: run each agent in its own checkout or git worktree, so its inbox is its own.\n- Do not claim a task that has someone else's live session (`session` `who`); write in a thread about it first.\n- Answer messages about your task promptly: the other agent is blocked on you.\n- Report progress in the plan's notes, not in threads. Threads are for talking to someone.\n- If you stop working on a task, end the session so nobody waits on you.\n";
3388
+ var slop_sessions_default = "---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project's message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live. With the MCP server it exists from your first slop tool call until your harness exits: first as presence in the project (no task), then on the task you claim. Being live means messages find you. Sessions are visible to everyone (`slop sessions`, the board's live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, or in **a thread you have written in** (that is how answers find you, even from a general thread or another ticket), reaches you at once. Nothing else does.\n- **Everything else waits.** `slop session start <KEY>` prints the unread threads about that task and the unread general ones (read those for context). `slop threads` lists them any time; `slop thread show <id>` reads one.\n- Your own messages never wake your own waiter.\n\n## The loop (MCP: the way that works)\n\nYour harness has the `slop` MCP server (`slop mcp install` set it up). Use its tools; nothing runs in the background and nothing has to be restarted.\n\n1. **Claim:** `session` `claim` with the task. Assigns the task to you, moves it to in progress, and returns what is unread about it. Read that first.\n2. **Work.** Keep the plan's notes current with `plan` `note`, tick with `plan` `done`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `session` `claim` upgrades it to the task; `task` `finish` or `session` `release` drops back to presence. Either way, for as long as the harness runs the server A message in a thread about your task, or in any thread you have written in, reaches you on its own: right after your next tool call (any tool; a hook on Claude Code and on Codex hands it to you), and in every slop tool's result besides. There is nothing to poll and nothing to restart. When one arrives, act or answer with `say` `reply` and carry on.\n4. **Nothing to do but wait?** `session` `wait` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `task` `finish` (it also releases the session), or `session` `release` to step off without finishing.\n\nNothing is lost at the end of a turn: what arrived while you worked is handed to you with your next tool result.\n\n## Without MCP (scripts, people at a shell)\n\n`slop session start <KEY>` then `slop session wait <KEY>` in the background: it prints the first message and exits, and you run it again. Only for setups with no MCP. The tools above are the way.\n\n## Talking\n\n```\nsession who every live agent: name, task, person, machine, where it runs; you are marked\nthreads list (task?, unread?) list; threads read (id) reads one (marks it read)\nsay reply (thread, body) reaches the agents that wrote in the thread, the task's live agents, and its addressee\nsay post (body, task: \"WEB-12\") a new thread about a task: whoever is live on it\nsay post (body, to: \"Albert\") a new thread to one live agent, by the name session who shows\nsay post (body) a general thread: for people, and for context\n (subject is optional: without one, the first line of the body)\n```\n\nAgents have names. Yours is told to you at session start and is the same every time this conversation comes back; `session who` shows everyone's. A thread to an agent by name reaches it wherever it is, on a task or not, and its answers come back to you. If two live agents share a name, use the session id from `session who`.\n\n**Messages find you.** Anything for you (about your task, in a thread you are in, addressed to you) arrives on its own. On Claude Code it arrives as a turn of yours, even while you are idle. On Codex it waits for you: it is handed to you with your next tool result, so check `session check` when you start a turn if you expect something.\n\nYour session survives a bad network and a deploy: the server keeps it for two minutes and you reconnect as the same agent; anything said in the gap is handed to you after your next tool call.\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan's notes, decisions in the description.\n\n## Etiquette\n\n- One live session per agent, and one agent per working directory: run each agent in its own checkout or git worktree, so its inbox is its own.\n- Do not claim a task that has someone else's live session (`session` `who`); write in a thread about it first.\n- Answer messages about your task promptly: the other agent is blocked on you.\n- Report progress in the plan's notes, not in threads. Threads are for talking to someone.\n- If you stop working on a task, end the session so nobody waits on you.\n";
3389
3389
 
3390
3390
  // skills/slop-working.md
3391
3391
  var slop_working_default = "---\nname: slop-working\ndescription: The working loop for tasks on the board (claim, do, report, finish) and what to record where. Load at the start of any coding session in a repository linked with slop.\n---\n\n# The loop\n\n1. **What is there:** `board` `list` with `mine`, or plain `list`. If nothing is assigned to you, take the top item of the first \"to do\" column.\n2. **Claim it:** `session` `claim`. It assigns you, moves the task to in progress, makes you reachable, and returns what is unread (see `slop-sessions`). Then `task` `show` and read all of it, plan and notes included.\n3. **Do it.** Long work: follow `slop-planning`. Short work: just do it.\n4. **Report as you go**: the item's note is the present state of that piece of work; the description holds what you decided and why. Write in sentences a teammate can act on.\n5. **Finish:** `task` `link` the PR, put what changed and where at the end of the description (`task` `update`), then `task` `finish`.\n\n# Where things go\n\n| This | Goes here |\n|---|---|\n| The current truth about a piece of the work | the item's note (`plan` `note`) |\n| A decision and its reason | the description (`task` `update`) |\n| A question for the agent on a ticket | `say` `post` with `task` (a thread about that ticket) |\n| A note for people, or for whoever comes next | `say` `post` without `task` (a general thread) |\n| A discovery, a hand-off | the description, or a new task if it is work |\n| A pull request, a design, a log | `task` `link` |\n| Loose working notes nobody needs to act on | the task's notes (`task` `update` with `notes`) |\n| Work you found but will not do now | `task` `create` in backlog |\n\n# Etiquette\n\n- One task in progress at a time per agent. `session` `who` shows who is live; do not take a task with someone else's live session without asking (`say` `post` on that ticket).\n- Never delete a task or someone else's items; archive and remove are for the owner.\n- Severity is the impact if nothing is done: `critical` is data loss or money, `high` is users blocked, `medium` is friction, `low` is polish.\n";
@@ -8438,7 +8438,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
8438
8438
  var NONE = 0;
8439
8439
  var ASSUMED = 1;
8440
8440
  var PROVEN = 2;
8441
- function isRecursive(inst, stack, resolve3) {
8441
+ function isRecursive(inst, stack, resolve5) {
8442
8442
  const cached2 = recursive.get(inst);
8443
8443
  if (cached2 !== void 0)
8444
8444
  return cached2 ? PROVEN : NONE;
@@ -8448,7 +8448,7 @@ function isRecursive(inst, stack, resolve3) {
8448
8448
  let result = NONE;
8449
8449
  const check2 = (child) => {
8450
8450
  if (result !== PROVEN && child?._zod) {
8451
- const answer = isRecursive(child, stack, resolve3);
8451
+ const answer = isRecursive(child, stack, resolve5);
8452
8452
  if (answer > result)
8453
8453
  result = answer;
8454
8454
  }
@@ -8459,7 +8459,7 @@ function isRecursive(inst, stack, resolve3) {
8459
8459
  const desc = Object.getOwnPropertyDescriptor(sh, key);
8460
8460
  if (spread && !desc.enumerable)
8461
8461
  continue;
8462
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
8462
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
8463
8463
  if (child > answer)
8464
8464
  answer = child;
8465
8465
  }
@@ -8523,7 +8523,7 @@ function isRecursive(inst, stack, resolve3) {
8523
8523
  break;
8524
8524
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
8525
8525
  case "lazy": {
8526
- const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
8526
+ const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
8527
8527
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
8528
8528
  break;
8529
8529
  }
@@ -19054,12 +19054,12 @@ function processSchema(schema2, ctx, _params = { path: [], schemaPath: [] }) {
19054
19054
  }
19055
19055
  processor(schema2, ctx, _json, params);
19056
19056
  }
19057
- const parent = schema2._zod.parent;
19058
- if (parent) {
19057
+ const parent2 = schema2._zod.parent;
19058
+ if (parent2) {
19059
19059
  if (!result.ref)
19060
- result.ref = parent;
19061
- processSchema(parent, ctx, params);
19062
- ctx.seen.get(parent).isParent = true;
19060
+ result.ref = parent2;
19061
+ processSchema(parent2, ctx, params);
19062
+ ctx.seen.get(parent2).isParent = true;
19063
19063
  }
19064
19064
  }
19065
19065
  const meta3 = ctx.metadataRegistry.get(schema2);
@@ -19322,10 +19322,10 @@ function finalize(ctx, schema2) {
19322
19322
  }
19323
19323
  }
19324
19324
  }
19325
- const parent = zodSchema._zod.parent;
19326
- if (parent && parent !== ref) {
19327
- flattenRef(parent);
19328
- const parentSeen = ctx.seen.get(parent);
19325
+ const parent2 = zodSchema._zod.parent;
19326
+ if (parent2 && parent2 !== ref) {
19327
+ flattenRef(parent2);
19328
+ const parentSeen = ctx.seen.get(parent2);
19329
19329
  if (parentSeen?.schema.$ref) {
19330
19330
  schema3.$ref = parentSeen.schema.$ref;
19331
19331
  if (parentSeen.def) {
@@ -23320,6 +23320,8 @@ var ThreadMessage = external_exports.object({
23320
23320
  /** A person on the web, or an agent through the CLI (with its machine). */
23321
23321
  via: MessageVia,
23322
23322
  machine: external_exports.string().nullable(),
23323
+ /** The agent's name, when a live session wrote it; null for a person, or an older CLI. */
23324
+ agent: external_exports.string().nullable(),
23323
23325
  body: external_exports.string(),
23324
23326
  createdAt: Timestamp
23325
23327
  });
@@ -23338,12 +23340,13 @@ var Thread = external_exports.object({
23338
23340
  messageCount: external_exports.number(),
23339
23341
  /** Messages the viewer has not read. Their own count as read. */
23340
23342
  unread: external_exports.number(),
23341
- last: external_exports.object({ author: UserRef, via: MessageVia, body: external_exports.string() }).nullable()
23343
+ last: external_exports.object({ author: UserRef, via: MessageVia, agent: external_exports.string().nullable(), body: external_exports.string() }).nullable()
23342
23344
  });
23343
23345
  var ThreadDetail = Thread.extend({ messages: external_exports.array(ThreadMessage), earlier: external_exports.number().default(0) });
23344
23346
  var body = external_exports.string().trim().min(1, "Say something").max(8e3);
23345
23347
  var CreateThreadInput = external_exports.object({
23346
- subject: external_exports.string().trim().min(1, "Give it a subject").max(200),
23348
+ /** Absent, the first line of the body is the subject. */
23349
+ subject: external_exports.string().trim().min(1, "Give it a subject").max(200).optional(),
23347
23350
  /** A task key or number; absent means general. */
23348
23351
  task: external_exports.string().trim().min(1).optional(),
23349
23352
  /** A live agent, by session id or name (as `session who` lists them). */
@@ -24143,23 +24146,24 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
24143
24146
  }
24144
24147
 
24145
24148
  // src/commands/mcp.ts
24146
- import { execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
24147
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
24149
+ import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
24150
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
24148
24151
  import "path";
24149
- import { homedir as homedir3 } from "os";
24150
- import { join as join8 } from "path";
24152
+ import { homedir as homedir4 } from "os";
24153
+ import { join as join11 } from "path";
24151
24154
 
24152
24155
  // src/platform/live.ts
24153
24156
  var PING_EVERY_MS = 25e3;
24154
24157
  var PONG_WITHIN_MS = 1e4;
24158
+ var STANDBY_POLL_MS = 2e4;
24155
24159
  async function waitForMessage(api, projectId, taskKey, options = {}) {
24156
24160
  const ticket = await api.post(`/projects/${projectId}/tasks/${encodeURIComponent(taskKey)}/session/ticket`, options.claim ?? {});
24157
24161
  const socket = new WebSocket(`${api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
24158
- const outcome = await new Promise((resolve3) => {
24159
- const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => resolve3({ kind: "timeout" }), options.timeoutMs) : null;
24162
+ const outcome = await new Promise((resolve5) => {
24163
+ const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => resolve5({ kind: "timeout" }), options.timeoutMs) : null;
24160
24164
  const done = (result) => {
24161
24165
  if (timer) clearTimeout(timer);
24162
- resolve3(result);
24166
+ resolve5(result);
24163
24167
  };
24164
24168
  socket.addEventListener("message", (event) => {
24165
24169
  if (String(event.data) === "pong") return;
@@ -24184,9 +24188,9 @@ async function waitForMessage(api, projectId, taskKey, options = {}) {
24184
24188
  if (outcome.kind !== "message") rememberSession(process.cwd(), null);
24185
24189
  if (outcome.kind === "message" || outcome.kind === "timeout") {
24186
24190
  socket.close(1e3, outcome.kind === "message" ? "handled" : "timeout");
24187
- await new Promise((resolve3) => {
24188
- socket.addEventListener("close", () => resolve3(), { once: true });
24189
- setTimeout(resolve3, 500);
24191
+ await new Promise((resolve5) => {
24192
+ socket.addEventListener("close", () => resolve5(), { once: true });
24193
+ setTimeout(resolve5, 500);
24190
24194
  });
24191
24195
  }
24192
24196
  return outcome;
@@ -24249,6 +24253,7 @@ var HeldSession = class {
24249
24253
  if (push.type === "reachable") {
24250
24254
  if (this.session) this.session = { ...this.session, reachable: push.on };
24251
24255
  if (!push.on) for (const waiter of [...this.waiters]) waiter(null);
24256
+ this.hooks.onReachable?.(push.on);
24252
24257
  return;
24253
24258
  }
24254
24259
  if (push.type !== "thread") return;
@@ -24257,10 +24262,12 @@ var HeldSession = class {
24257
24262
  for (const waiter of [...this.waiters]) waiter(delivered);
24258
24263
  if (!taken) this.hooks.onMessage(delivered);
24259
24264
  });
24260
- socket.addEventListener("close", () => {
24265
+ socket.addEventListener("close", (event) => {
24261
24266
  this.stopHeartbeat();
24262
24267
  if (this.socket === socket) this.socket = null;
24263
- if (!this.stopped) this.schedule();
24268
+ if (this.stopped) return;
24269
+ if (event.reason === "replaced") this.standBy();
24270
+ else this.schedule();
24264
24271
  });
24265
24272
  socket.addEventListener("error", () => {
24266
24273
  });
@@ -24292,10 +24299,23 @@ var HeldSession = class {
24292
24299
  this.attempt += 1;
24293
24300
  this.timer = setTimeout(() => void this.connect(), Math.min(3e4, 1e3 * 2 ** Math.min(this.attempt, 5)));
24294
24301
  }
24302
+ /** Another process holds this session now: look now and then, and take it back once it is no longer live. */
24303
+ standBy() {
24304
+ this.timer = setTimeout(async () => {
24305
+ if (this.stopped) return;
24306
+ let holder = true;
24307
+ try {
24308
+ const sessions = await this.api.get(`/projects/${this.projectId}/sessions`);
24309
+ holder = sessions.some((session) => session.id === this.session?.id && session.state === "live");
24310
+ } catch {
24311
+ }
24312
+ if (holder) this.standBy();
24313
+ else void this.connect();
24314
+ }, STANDBY_POLL_MS);
24315
+ }
24295
24316
  stop() {
24296
24317
  this.stopped = true;
24297
24318
  this.api.sessionId = null;
24298
- rememberSession(process.cwd(), null);
24299
24319
  if (this.timer) clearTimeout(this.timer);
24300
24320
  this.stopHeartbeat();
24301
24321
  this.socket?.close(1e3, "released");
@@ -24303,17 +24323,316 @@ var HeldSession = class {
24303
24323
  };
24304
24324
 
24305
24325
  // src/platform/agent-identity.ts
24326
+ import { randomUUID as randomUUID4 } from "crypto";
24327
+ import { basename as basename3, resolve as resolve3 } from "path";
24328
+
24329
+ // src/platform/harness.ts
24330
+ import { execFileSync as execFileSync4 } from "child_process";
24331
+ import { closeSync, openSync, readdirSync as readdirSync2, readSync, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
24332
+ import { basename as basename2, join as join8 } from "path";
24333
+
24334
+ // src/platform/harness-wake.ts
24335
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
24336
+ import { join as join7 } from "path";
24337
+ import { connect } from "net";
24338
+ import { spawn as spawn2 } from "child_process";
24306
24339
  import { randomUUID as randomUUID3 } from "crypto";
24307
- import { basename as basename2, resolve as resolve2 } from "path";
24308
- var processConversation = randomUUID3();
24340
+
24341
+ // src/platform/homes.ts
24342
+ import { execFileSync as execFileSync3 } from "child_process";
24343
+ import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
24344
+ import { homedir as homedir3 } from "os";
24345
+ import { join as join6, resolve as resolve2 } from "path";
24346
+ var unique = (paths) => [...new Set(paths.map((path3) => resolve2(path3)))];
24347
+ function environmentsSeen() {
24348
+ if (process.platform === "win32") return [];
24349
+ const seen = [];
24350
+ try {
24351
+ if (process.platform === "linux") {
24352
+ let pid2 = process.ppid;
24353
+ for (let hop = 0; hop < 6 && pid2 > 1; hop += 1) {
24354
+ seen.push(readFileSync6(`/proc/${pid2}/environ`, "latin1").replace(/\0/g, " "));
24355
+ const stat = readFileSync6(`/proc/${pid2}/stat`, "utf8");
24356
+ pid2 = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
24357
+ }
24358
+ for (const entry of readdirSync("/proc")) {
24359
+ if (!/^\d+$/.test(entry)) continue;
24360
+ try {
24361
+ seen.push(readFileSync6(`/proc/${entry}/environ`, "latin1").replace(/\0/g, " "));
24362
+ } catch {
24363
+ }
24364
+ }
24365
+ return seen;
24366
+ }
24367
+ const ps = (args) => execFileSync3("ps", args, { encoding: "utf8", maxBuffer: 64 << 20, stdio: ["ignore", "pipe", "ignore"] });
24368
+ let pid = process.ppid;
24369
+ for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
24370
+ seen.push(ps(["eww", "-o", "command=", "-p", String(pid)]));
24371
+ pid = Number(ps(["-o", "ppid=", "-p", String(pid)]).trim());
24372
+ }
24373
+ seen.push(ps(["axeww", "-o", "command="]));
24374
+ } catch {
24375
+ }
24376
+ return seen;
24377
+ }
24378
+ var environments = null;
24379
+ function homesInUse(variable) {
24380
+ environments ??= environmentsSeen();
24381
+ const found = [];
24382
+ const pattern = new RegExp(`(?:^|\\s)${variable}=(\\S+)`, "g");
24383
+ for (const text of environments) for (const match of text.matchAll(pattern)) found.push(match[1] ?? "");
24384
+ return found.filter(Boolean);
24385
+ }
24386
+ var directoriesNamed = (root, prefix) => {
24387
+ try {
24388
+ return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.name.startsWith(prefix) && (entry.isDirectory() || entry.isSymbolicLink())).map((entry) => join6(root, entry.name)).filter((path3) => {
24389
+ try {
24390
+ return statSync2(path3).isDirectory();
24391
+ } catch {
24392
+ return false;
24393
+ }
24394
+ });
24395
+ } catch {
24396
+ return [];
24397
+ }
24398
+ };
24399
+ var hasAny = (dir, files) => files.some((file2) => existsSync6(join6(dir, file2)));
24400
+ function claudeHomes(root = homedir3(), inUse = homesInUse) {
24401
+ if (process.env.SLOP_CLAUDE_HOME) return [process.env.SLOP_CLAUDE_HOME];
24402
+ const named = process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : [];
24403
+ return unique([join6(root, ".claude"), ...named, ...inUse("CLAUDE_CONFIG_DIR"), ...directoriesNamed(root, ".claude").filter((dir) => hasAny(dir, ["settings.json", ".claude.json", "sessions"]))]);
24404
+ }
24405
+ function codexHomes(root = homedir3(), inUse = homesInUse) {
24406
+ if (process.env.SLOP_CODEX_HOME) return [process.env.SLOP_CODEX_HOME];
24407
+ const named = process.env.CODEX_HOME ? [process.env.CODEX_HOME] : [];
24408
+ return unique([join6(root, ".codex"), ...named, ...inUse("CODEX_HOME"), ...directoriesNamed(root, ".codex").filter((dir) => hasAny(dir, ["config.toml", "auth.json", "sessions"]))]);
24409
+ }
24410
+ function ownHarnessHome(variable) {
24411
+ if (process.platform === "win32") return null;
24412
+ try {
24413
+ let pid = process.ppid;
24414
+ for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
24415
+ const text = process.platform === "linux" ? readFileSync6(`/proc/${pid}/environ`, "latin1").replace(/\0/g, " ") : execFileSync3("ps", ["eww", "-o", "command=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
24416
+ const match = new RegExp(`(?:^|\\s)${variable}=(\\S+)`).exec(text);
24417
+ if (match?.[1]) return resolve2(match[1]);
24418
+ if (process.platform === "linux") {
24419
+ const stat = readFileSync6(`/proc/${pid}/stat`, "utf8");
24420
+ pid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
24421
+ } else pid = Number(execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
24422
+ }
24423
+ } catch {
24424
+ }
24425
+ return null;
24426
+ }
24427
+
24428
+ // src/platform/harness-wake.ts
24429
+ function harnessRegistration(pid = process.ppid) {
24430
+ for (const home of claudeHomes()) {
24431
+ const path3 = join7(home, "sessions", `${pid}.json`);
24432
+ if (!existsSync7(path3)) continue;
24433
+ try {
24434
+ return JSON.parse(readFileSync7(path3, "utf8"));
24435
+ } catch {
24436
+ return null;
24437
+ }
24438
+ }
24439
+ return null;
24440
+ }
24441
+ function wakeHarness(text, pid = Number(process.env.SLOP_HARNESS_PID) || process.ppid) {
24442
+ const registration = harnessRegistration(pid);
24443
+ if (!registration?.sessionId || !registration.messagingSocketPath) return Promise.resolve(false);
24444
+ const frame = {
24445
+ uuid: randomUUID3(),
24446
+ session_id: registration.sessionId,
24447
+ type: "user",
24448
+ parent_tool_use_id: null,
24449
+ message: { role: "user", content: text }
24450
+ };
24451
+ return new Promise((resolve5) => {
24452
+ const socket = connect({ path: registration.messagingSocketPath });
24453
+ const done = (ok) => {
24454
+ clearTimeout(timer);
24455
+ socket.destroy();
24456
+ resolve5(ok);
24457
+ };
24458
+ const timer = setTimeout(() => done(false), 3e3);
24459
+ socket.on("connect", () => {
24460
+ socket.write(`${JSON.stringify(frame)}
24461
+ `, (error62) => error62 ? done(false) : setTimeout(() => done(true), 200));
24462
+ });
24463
+ socket.on("error", () => done(false));
24464
+ });
24465
+ }
24466
+ function wakeCodex(text, threadId) {
24467
+ const endpoint = process.env.SLOP_CODEX_APP_SERVER;
24468
+ if (endpoint) return wakeCodexOverWs(text, threadId, endpoint);
24469
+ return Promise.resolve(false);
24470
+ }
24471
+ function wakeCodexOverWs(text, threadId, endpoint) {
24472
+ return new Promise((resolve5) => {
24473
+ let socket;
24474
+ try {
24475
+ socket = new WebSocket(endpoint);
24476
+ } catch {
24477
+ resolve5(false);
24478
+ return;
24479
+ }
24480
+ let nextId = 1;
24481
+ const pending = /* @__PURE__ */ new Map();
24482
+ const finish = (ok) => {
24483
+ clearTimeout(timer);
24484
+ try {
24485
+ socket.close();
24486
+ } catch {
24487
+ }
24488
+ resolve5(ok);
24489
+ };
24490
+ const timer = setTimeout(() => finish(false), 8e3);
24491
+ const call = (method, params) => new Promise((ok, fail2) => {
24492
+ const id = nextId++;
24493
+ pending.set(id, (message) => message.error ? fail2(new Error(method)) : ok(message.result));
24494
+ socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
24495
+ });
24496
+ socket.addEventListener("error", () => finish(false));
24497
+ socket.addEventListener("message", (event) => {
24498
+ try {
24499
+ const message = JSON.parse(String(event.data));
24500
+ if (typeof message.id === "number") pending.get(message.id)?.(message);
24501
+ } catch {
24502
+ }
24503
+ });
24504
+ socket.addEventListener("open", () => {
24505
+ void (async () => {
24506
+ try {
24507
+ await call("initialize", { clientInfo: { name: "slop", version: "0" }, capabilities: { experimentalApi: true } });
24508
+ socket.send(JSON.stringify({ jsonrpc: "2.0", method: "initialized", params: {} }));
24509
+ const loaded = await call("thread/loaded/list", {});
24510
+ if (!loaded.data.includes(threadId)) {
24511
+ finish(false);
24512
+ return;
24513
+ }
24514
+ await call("turn/start", { threadId, input: [{ type: "text", text }] });
24515
+ finish(true);
24516
+ } catch {
24517
+ finish(false);
24518
+ }
24519
+ })();
24520
+ });
24521
+ });
24522
+ }
24523
+
24524
+ // src/platform/harness.ts
24525
+ var parent = null;
24526
+ function parentCommand() {
24527
+ if (parent !== null) return parent;
24528
+ try {
24529
+ parent = process.platform === "win32" ? "" : execFileSync4("ps", ["-o", "command=", "-p", String(process.ppid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
24530
+ } catch {
24531
+ parent = "";
24532
+ }
24533
+ return parent;
24534
+ }
24535
+ function harnessOf(command) {
24536
+ const first = basename2(command.trim().split(/\s+/)[0] ?? "");
24537
+ if (/^claude(\b|$)/.test(first)) return "claude";
24538
+ if (/^codex(\b|$)/.test(first)) return "codex";
24539
+ if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
24540
+ return null;
24541
+ }
24542
+ function isNonInteractive(parentCommand2) {
24543
+ return /(^|[\s/])codex(\S*)?\s+exec(\s|$)/.test(parentCommand2);
24544
+ }
24545
+ function nonInteractiveHarness() {
24546
+ return isNonInteractive(parentCommand());
24547
+ }
24548
+ var same = (a, b) => {
24549
+ const real = (path3) => {
24550
+ try {
24551
+ return realpathSync2(path3);
24552
+ } catch {
24553
+ return path3;
24554
+ }
24555
+ };
24556
+ return real(a) === real(b);
24557
+ };
24558
+ function codexConversation(roots, cwd) {
24559
+ const rollouts = [];
24560
+ const walk = (dir, depth) => {
24561
+ let entries;
24562
+ try {
24563
+ entries = readdirSync2(dir, { withFileTypes: true });
24564
+ } catch {
24565
+ return;
24566
+ }
24567
+ for (const entry of entries) {
24568
+ const path3 = join8(dir, entry.name);
24569
+ if (entry.isDirectory()) {
24570
+ if (depth < 3) walk(path3, depth + 1);
24571
+ continue;
24572
+ }
24573
+ const match = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/.exec(entry.name);
24574
+ if (!match) continue;
24575
+ try {
24576
+ rollouts.push({ path: path3, id: match[1] ?? "", mtime: statSync3(path3).mtimeMs });
24577
+ } catch {
24578
+ }
24579
+ }
24580
+ };
24581
+ for (const root of roots) walk(root, 0);
24582
+ rollouts.sort((a, b) => b.mtime - a.mtime);
24583
+ for (const rollout of rollouts.slice(0, 300)) {
24584
+ let head = "";
24585
+ try {
24586
+ const fd = openSync(rollout.path, "r");
24587
+ try {
24588
+ const buffer = Buffer.alloc(8192);
24589
+ head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
24590
+ } finally {
24591
+ closeSync(fd);
24592
+ }
24593
+ } catch {
24594
+ continue;
24595
+ }
24596
+ const line = head.split("\n")[0] ?? "";
24597
+ if (!line.includes('"session_meta"')) continue;
24598
+ const found = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(line);
24599
+ if (!found) continue;
24600
+ let dir;
24601
+ try {
24602
+ dir = JSON.parse(`"${found[1]}"`);
24603
+ } catch {
24604
+ continue;
24605
+ }
24606
+ if (same(dir, cwd)) return rollout.id;
24607
+ }
24608
+ return null;
24609
+ }
24610
+ function harnessConversation(cwd) {
24611
+ const harness = harnessOf(parentCommand());
24612
+ if (harness === "claude") {
24613
+ const id = harnessRegistration()?.sessionId;
24614
+ return id ? { id, harness } : null;
24615
+ }
24616
+ if (harness === "codex" && !nonInteractiveHarness()) {
24617
+ const id = codexConversation(codexHomes().map((home) => join8(home, "sessions")), cwd);
24618
+ return id ? { id, harness } : null;
24619
+ }
24620
+ return null;
24621
+ }
24622
+
24623
+ // src/platform/agent-identity.ts
24624
+ var processConversation = randomUUID4();
24309
24625
  var startedAt = Date.now();
24626
+ function knownConversation(cwd) {
24627
+ return harnessConversation(cwd) ?? rememberedConversation(cwd, startedAt - 12e4);
24628
+ }
24310
24629
  function agentIdentity(cwd, fallback = "process") {
24311
- const conversation = rememberedConversation(cwd, startedAt - 12e4);
24630
+ const conversation = knownConversation(cwd);
24312
24631
  const repo = findRepo();
24313
24632
  return {
24314
- conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve2(cwd)}`),
24633
+ conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve3(cwd)}`),
24315
24634
  harness: conversation?.harness ?? null,
24316
- directory: repo ? basename2(repo.worktree) : basename2(cwd),
24635
+ directory: repo ? basename3(repo.worktree) : basename3(cwd),
24317
24636
  branch: currentBranch(cwd)
24318
24637
  };
24319
24638
  }
@@ -24384,11 +24703,11 @@ function serveMcp(tools2, info) {
24384
24703
  }
24385
24704
 
24386
24705
  // src/commands/tasks.ts
24387
- import { readFileSync as readFileSync7 } from "fs";
24706
+ import { readFileSync as readFileSync9 } from "fs";
24388
24707
 
24389
24708
  // src/commands/items.ts
24390
- import { readFileSync as readFileSync6 } from "fs";
24391
- var readText = (value) => value === "-" ? readFileSync6(0, "utf8").trim() : value;
24709
+ import { readFileSync as readFileSync8 } from "fs";
24710
+ var readText = (value) => value === "-" ? readFileSync8(0, "utf8").trim() : value;
24392
24711
  var path2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
24393
24712
  function renderItems(items) {
24394
24713
  if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
@@ -24415,7 +24734,7 @@ function registerItemCommands(task, withProject) {
24415
24734
  withProject(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) => {
24416
24735
  const api = authedApi();
24417
24736
  const project = await resolveProject(api, options.project);
24418
- const source = lines.length === 1 && lines[0] === "-" ? readFileSync6(0, "utf8") : lines.join("\n");
24737
+ const source = lines.length === 1 && lines[0] === "-" ? readFileSync8(0, "utf8") : lines.join("\n");
24419
24738
  const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
24420
24739
  if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
24421
24740
  const items = await api.post(path2(project.id, key), { items: titles.map((title3) => ({ title: title3 })) });
@@ -24472,7 +24791,7 @@ function oneOf(value, allowed, what) {
24472
24791
  if (allowed.includes(normalized)) return normalized;
24473
24792
  throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
24474
24793
  }
24475
- var readDescription = (value) => value === "-" ? readFileSync7(0, "utf8").trim() : value;
24794
+ var readDescription = (value) => value === "-" ? readFileSync9(0, "utf8").trim() : value;
24476
24795
  var taskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
24477
24796
  var loadProject = (api, id) => api.get(`/projects/${id}`);
24478
24797
  function resolveStatus(project, ref) {
@@ -24623,14 +24942,14 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
24623
24942
  }
24624
24943
 
24625
24944
  // src/platform/update-check.ts
24626
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
24627
- import { dirname as dirname4, join as join6 } from "path";
24945
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
24946
+ import { dirname as dirname4, join as join9 } from "path";
24628
24947
  var CHECK_EVERY_MS = 60 * 60 * 1e3;
24629
24948
  var REGISTRY = "https://registry.npmjs.org/slopwaresystems/latest";
24630
- var cacheFile = () => join6(dirname4(configPath()), "update-check.json");
24949
+ var cacheFile = () => join9(dirname4(configPath()), "update-check.json");
24631
24950
  function readCache() {
24632
24951
  try {
24633
- return JSON.parse(readFileSync8(cacheFile(), "utf8"));
24952
+ return JSON.parse(readFileSync10(cacheFile(), "utf8"));
24634
24953
  } catch {
24635
24954
  return { checkedAt: 0, latest: null };
24636
24955
  }
@@ -24666,13 +24985,13 @@ function updateNotice(current) {
24666
24985
  if (!latest || !isNewer(latest, current)) return null;
24667
24986
  return `Update available: slopwaresystems ${latest} (this is ${current}). Ask your person to run: npm i -g slopwaresystems && slop mcp install`;
24668
24987
  }
24669
- var updateChecksEnabled = () => !existsSync6(join6(dirname4(configPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
24988
+ var updateChecksEnabled = () => !existsSync8(join9(dirname4(configPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
24670
24989
 
24671
24990
  // src/commands/threads.ts
24672
- import { readFileSync as readFileSync9 } from "fs";
24673
- var readText2 = (value) => value === "-" ? readFileSync9(0, "utf8").trim() : value;
24991
+ import { readFileSync as readFileSync11 } from "fs";
24992
+ var readText2 = (value) => value === "-" ? readFileSync11(0, "utf8").trim() : value;
24674
24993
  var when = (iso) => iso.slice(0, 16).replace("T", " ");
24675
- var who = (message) => `${message.author.name}${message.via === "agent" ? ` (agent${message.machine ? ` \xB7 ${message.machine}` : ""})` : ""}`;
24994
+ var who = (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}` : ""})`;
24676
24995
  function threadLine(thread) {
24677
24996
  const scope = [thread.taskKey ?? (thread.to ? null : "general"), thread.to ? `to ${thread.to.agent}` : null].filter(Boolean).join(", ");
24678
24997
  const unread = thread.unread > 0 ? ` ${thread.unread} unread` : "";
@@ -24728,38 +25047,33 @@ function registerThreadCommands(program3, task) {
24728
25047
  const api = authedApi();
24729
25048
  const project = await resolveProject(api, options.project);
24730
25049
  const body2 = readText2(text);
24731
- const result = await api.post(`/projects/${project.id}/threads`, { subject: subjectOf(body2), task: options.task, body: body2 });
25050
+ const result = await api.post(`/projects/${project.id}/threads`, { task: options.task, body: body2 });
24732
25051
  emit(result, () => started(result));
24733
25052
  });
24734
25053
  withProject(task.command("ask <key> <text>")).description('Start a thread about the task and say something to whoever is live on it. "-" reads stdin.').action(async (key, text, options) => {
24735
25054
  const api = authedApi();
24736
25055
  const project = await resolveProject(api, options.project);
24737
25056
  const body2 = readText2(text);
24738
- const result = await api.post(`/projects/${project.id}/threads`, { subject: subjectOf(body2), task: key, body: body2 });
25057
+ const result = await api.post(`/projects/${project.id}/threads`, { task: key, body: body2 });
24739
25058
  emit(result, () => started(result));
24740
25059
  });
24741
25060
  }
24742
- var subjectOf = (body2) => {
24743
- const line = body2.split("\n")[0]?.trim() ?? "";
24744
- if (!line) throw new CliError("Say something", "the first line becomes the subject");
24745
- return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
24746
- };
24747
25061
  var started = (result) => result.thread.to ? `Started ${result.thread.id} to ${result.thread.to.agent}${result.delivered ? "; it has it now." : "; it reads it when it is back."}` : result.thread.taskKey ? result.delivered ? `Started ${result.thread.id} on ${result.thread.taskKey}; reached ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : `Started ${result.thread.id} on ${result.thread.taskKey}. Nobody is live on it; they see it when they start.` : `Started ${result.thread.id} (general). People see it on the web; agents at their next session start.`;
24748
25062
 
24749
25063
  // src/platform/inbox.ts
24750
25064
  import { createHash as createHash4 } from "crypto";
24751
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync10, realpathSync as realpathSync2, renameSync, rmSync as rmSync4 } from "fs";
24752
- import { dirname as dirname5, join as join7 } from "path";
24753
- var inboxDir = () => join7(dirname5(configPath()), "inbox");
25065
+ import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync3, renameSync, rmSync as rmSync4 } from "fs";
25066
+ import { dirname as dirname5, join as join10 } from "path";
25067
+ var inboxDir = () => join10(dirname5(configPath()), "inbox");
24754
25068
  var realCwd2 = (cwd) => {
24755
25069
  try {
24756
- return realpathSync2(cwd);
25070
+ return realpathSync3(cwd);
24757
25071
  } catch {
24758
25072
  return cwd;
24759
25073
  }
24760
25074
  };
24761
25075
  var inboxKey = (cwd) => createHash4("sha256").update(realCwd2(cwd)).digest("hex").slice(0, 16);
24762
- var inboxFile = (cwd) => join7(inboxDir(), `${inboxKey(cwd)}.jsonl`);
25076
+ var inboxFile = (cwd) => join10(inboxDir(), `${inboxKey(cwd)}.jsonl`);
24763
25077
  function inboxWrite(cwd, push) {
24764
25078
  mkdirSync6(inboxDir(), { recursive: true });
24765
25079
  appendFileSync(inboxFile(cwd), `${JSON.stringify(push)}
@@ -24767,14 +25081,14 @@ function inboxWrite(cwd, push) {
24767
25081
  }
24768
25082
  function inboxDrain(cwd) {
24769
25083
  const path3 = inboxFile(cwd);
24770
- if (!existsSync7(path3)) return [];
25084
+ if (!existsSync9(path3)) return [];
24771
25085
  const taken = `${path3}.${process.pid}.${Date.now()}.draining`;
24772
25086
  try {
24773
25087
  renameSync(path3, taken);
24774
25088
  } catch {
24775
25089
  return [];
24776
25090
  }
24777
- const lines = readFileSync10(taken, "utf8").split("\n").filter(Boolean);
25091
+ const lines = readFileSync12(taken, "utf8").split("\n").filter(Boolean);
24778
25092
  rmSync4(taken, { force: true });
24779
25093
  return lines.flatMap((line) => {
24780
25094
  try {
@@ -24832,12 +25146,28 @@ function handlers() {
24832
25146
  const api = authedApi();
24833
25147
  const resolveOnly = (args) => resolveProject(api, args.project);
24834
25148
  let held = null;
25149
+ let identity = null;
25150
+ const whoAmI = () => identity ??= agentIdentity(process.cwd());
24835
25151
  const hold = async (projectId, taskKey) => {
24836
25152
  if (held && held.projectId === projectId && held.taskKey === taskKey) return;
24837
25153
  held?.stop();
24838
25154
  held = new HeldSession(api, projectId, taskKey, {
24839
- onMessage: (push) => inboxWrite(process.cwd(), push),
24840
- claim: () => agentIdentity(process.cwd()),
25155
+ // A message for the agent wakes its harness when the harness can be woken (Claude Code's session socket): it
25156
+ // arrives as a turn, now, whether the agent is idle or mid-turn. Otherwise the inbox and the hooks carry it.
25157
+ // Claude Code is woken over its session socket: the message arrives as a turn, now, idle or not. Codex has no
25158
+ // such door (an app server named by SLOP_CODEX_APP_SERVER aside), so the message waits in the inbox and rides
25159
+ // the chat's next tool result. Nothing holds a turn open, nothing runs on the chat's behalf.
25160
+ onMessage: (push) => {
25161
+ const text = `A message for you on Slopware Tasks. Act or answer it (say reply), then carry on.
25162
+
25163
+ ${pushBlock(push)}`;
25164
+ const me = whoAmI();
25165
+ const wake = me.harness === "codex" && me.conversation ? wakeCodex(text, me.conversation) : wakeHarness(text);
25166
+ void wake.then((woken) => {
25167
+ if (!woken) inboxWrite(process.cwd(), push);
25168
+ });
25169
+ },
25170
+ claim: whoAmI,
24841
25171
  onReconnect: (session) => catchUp(api, projectId, session.taskKey, process.cwd()).catch(() => void 0)
24842
25172
  });
24843
25173
  await held.start();
@@ -24850,10 +25180,25 @@ function handlers() {
24850
25180
  const release = async () => {
24851
25181
  if (held?.taskKey) await hold(held.projectId, null);
24852
25182
  };
25183
+ if (!nonInteractiveHarness())
25184
+ void (async () => {
25185
+ for (let waited = 0; waited < 1e4 && !knownConversation(process.cwd()); waited += 250) await new Promise((resolve5) => setTimeout(resolve5, 250));
25186
+ whoAmI();
25187
+ const target = await resolveOnly({});
25188
+ if (!held) await hold(target.id, null);
25189
+ })().catch(() => void 0);
25190
+ if (!nonInteractiveHarness()) void healCodexTrust().catch(() => void 0);
25191
+ const goodbye = () => {
25192
+ held?.stop();
25193
+ held = null;
25194
+ setTimeout(() => process.exit(0), 300).unref();
25195
+ };
25196
+ process.stdin.once("end", goodbye);
25197
+ for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, goodbye);
24853
25198
  return [
24854
25199
  {
24855
25200
  name: "wait_for_message",
24856
- description: "Block until the next message for you (on your task, in a thread you are in, or addressed to you), or until `minutes` pass. After your turn the Stop hook sends you here, so a message from the web wakes you; nothing runs while you wait.",
25201
+ description: "Block until the next message for you (on your task, in a thread you are in, or addressed to you), or until `minutes` pass. Optional: messages reach you on their own; use this only when you have nothing else to do and nobody is at your keyboard.",
24857
25202
  inputSchema: schema({ task: str("Task key to go live on first (optional)"), minutes: int2("Give up after (default 300)"), project: PROJECT }),
24858
25203
  run: async (args) => {
24859
25204
  const target = await project(args);
@@ -24861,20 +25206,21 @@ function handlers() {
24861
25206
  const queued = inboxDrain(process.cwd());
24862
25207
  if (queued.length) return queued.map(pushBlock).join("\n\n");
24863
25208
  const minutes = typeof args.minutes === "number" ? args.minutes : 300;
24864
- const outcome = await new Promise((resolve3) => {
25209
+ const outcome = await new Promise((resolve5) => {
24865
25210
  const waiter = (delivered) => {
24866
25211
  held?.waiters.delete(waiter);
24867
- resolve3({ push: delivered, released: delivered === null });
25212
+ clearTimeout(timer);
25213
+ resolve5({ push: delivered, released: delivered === null });
24868
25214
  };
24869
25215
  held?.waiters.add(waiter);
24870
- setTimeout(() => {
25216
+ const timer = setTimeout(() => {
24871
25217
  held?.waiters.delete(waiter);
24872
- resolve3({ push: null, released: false });
25218
+ resolve5({ push: null, released: false });
24873
25219
  }, minutes * 6e4);
24874
25220
  });
24875
25221
  if (outcome.push) return pushBlock(outcome.push);
24876
25222
  if (outcome.released) return "Your person made you not visible to teammates: nothing can reach you now. You may stop.";
24877
- return `No message in ${minutes} minutes. Wait again.`;
25223
+ return `No message in ${minutes} minutes.`;
24878
25224
  }
24879
25225
  },
24880
25226
  {
@@ -24901,10 +25247,10 @@ function handlers() {
24901
25247
  {
24902
25248
  name: "post",
24903
25249
  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.",
24904
- inputSchema: schema({ subject: str("Subject"), body: str("First message (Markdown)"), task: str("Task key, to make it about that task"), project: PROJECT }, ["subject", "body"]),
25250
+ inputSchema: schema({ subject: str("Subject (default: the first line of the body)"), body: str("First message (Markdown)"), task: str("Task key, to make it about that task"), project: PROJECT }, ["body"]),
24905
25251
  run: async (args) => {
24906
25252
  const target = await project(args);
24907
- const result = await api.post(`/projects/${target.id}/threads`, { subject: String(args.subject), body: String(args.body), task: args.task, to: args.to });
25253
+ const result = await api.post(`/projects/${target.id}/threads`, { subject: args.subject ? String(args.subject) : void 0, body: String(args.body), task: args.task, to: args.to });
24908
25254
  return `Started ${result.thread.id}${result.thread.to ? ` to ${result.thread.to.agent}` : ""}${result.thread.taskKey ? ` on ${result.thread.taskKey}` : result.thread.to ? "" : " (general)"}${result.delivered ? `; reached ${result.delivered} live session(s)` : ""}.`;
24909
25255
  }
24910
25256
  },
@@ -25192,50 +25538,57 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
25192
25538
  }
25193
25539
  function readJson(path3) {
25194
25540
  try {
25195
- return JSON.parse(readFileSync11(path3, "utf8"));
25541
+ return JSON.parse(readFileSync13(path3, "utf8"));
25196
25542
  } catch {
25197
25543
  return {};
25198
25544
  }
25199
25545
  }
25200
- function installClaude() {
25546
+ function installClaude(dir) {
25201
25547
  const done = [];
25548
+ const primary = dir === join11(homedir4(), ".claude");
25549
+ const env = { ...process.env };
25550
+ if (primary) delete env.CLAUDE_CONFIG_DIR;
25551
+ else env.CLAUDE_CONFIG_DIR = dir;
25552
+ const where = primary ? "" : ` for ${dir}`;
25202
25553
  try {
25203
- execFileSync3("claude", ["mcp", "add", "--scope", "user", "slop", "--", "slop", "mcp"], { stdio: "ignore" });
25204
- done.push('Claude Code: MCP server "slop" registered (user scope)');
25205
- } catch {
25206
- done.push("Claude Code: run `claude mcp add --scope user slop -- slop mcp` (the claude command was not found here)");
25554
+ execFileSync5("claude", ["mcp", "add", "--scope", "user", "slop", "--", "slop", "mcp"], { stdio: ["ignore", "ignore", "pipe"], env });
25555
+ done.push(`Claude Code: MCP server "slop" registered (user scope${where})`);
25556
+ } catch (error62) {
25557
+ const said = String(error62.stderr ?? "") + String(error62.stdout ?? "");
25558
+ if (/already exists/i.test(said)) done.push(`Claude Code: MCP server "slop" already registered (user scope${where})`);
25559
+ 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"})`);
25207
25560
  }
25208
- const dir = join8(homedir3(), ".claude");
25209
- const path3 = join8(dir, "settings.json");
25561
+ const path3 = join11(dir, "settings.json");
25210
25562
  mkdirSync7(dir, { recursive: true });
25211
25563
  const settings = readJson(path3);
25212
25564
  settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
25213
25565
  settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "21600000" };
25214
25566
  writeFileSync7(path3, `${JSON.stringify(settings, null, 2)}
25215
25567
  `);
25216
- done.push(`Claude Code: SessionStart, PostToolUse and Stop hooks and MCP_TOOL_TIMEOUT written to ${path3}`);
25568
+ done.push(`Claude Code: SessionStart hook and MCP_TOOL_TIMEOUT written to ${path3}; other slop hooks removed (its session socket wakes it)`);
25217
25569
  return done;
25218
25570
  }
25219
25571
  function mergeHooks(hooks, harness) {
25220
- const ensure = (event, verb, timeout, statusMessage) => {
25221
- const command = `slop hook ${verb} --harness ${harness}`;
25572
+ const ours = (hook) => typeof hook.command === "string" && /^slop hook /.test(hook.command);
25573
+ for (const event of Object.keys(hooks)) {
25574
+ const entries = hooks[event] ?? [];
25575
+ for (const entry of entries) entry.hooks = (entry.hooks ?? []).filter((hook) => !ours(hook));
25576
+ hooks[event] = entries.filter((entry) => (entry.hooks ?? []).length > 0);
25577
+ if (hooks[event].length === 0) delete hooks[event];
25578
+ }
25579
+ const add = (event, verb, timeout, statusMessage) => {
25222
25580
  const entries = hooks[event] ?? [];
25223
- const ours = entries.flatMap((entry) => entry.hooks ?? []).find((hook) => hook.command === `slop hook ${verb}` || hook.command?.startsWith(`slop hook ${verb} `));
25224
- if (ours) Object.assign(ours, { command, timeout, statusMessage });
25225
- else entries.push({ hooks: [{ type: "command", command, timeout, statusMessage }] });
25581
+ entries.push({ hooks: [{ type: "command", command: `slop hook ${verb} --harness ${harness}`, timeout, statusMessage }] });
25226
25582
  hooks[event] = entries;
25227
25583
  };
25228
- ensure("SessionStart", "start", 5, "Slopware Tasks");
25229
- ensure("PostToolUse", "tick", 5, "Slopware Tasks inbox");
25230
- ensure("Stop", "stop", 30, "Checking Slopware Tasks for messages");
25584
+ add("SessionStart", "start", 5, "Slopware Tasks");
25231
25585
  return hooks;
25232
25586
  }
25233
- function installCodex() {
25587
+ function installCodex(dir) {
25234
25588
  const done = [];
25235
- const dir = join8(homedir3(), ".codex");
25236
25589
  mkdirSync7(dir, { recursive: true });
25237
- const configPath3 = join8(dir, "config.toml");
25238
- const current = existsSync8(configPath3) ? readFileSync11(configPath3, "utf8") : "";
25590
+ const configPath3 = join11(dir, "config.toml");
25591
+ const current = existsSync10(configPath3) ? readFileSync13(configPath3, "utf8") : "";
25239
25592
  if (/^\[mcp_servers\.slop\]/m.test(current)) {
25240
25593
  if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync7(configPath3, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
25241
25594
  done.push(`Codex: MCP server already in ${configPath3}`);
@@ -25247,16 +25600,17 @@ tool_timeout_sec = 21600
25247
25600
  `);
25248
25601
  done.push(`Codex: MCP server "slop" added to ${configPath3}`);
25249
25602
  }
25250
- const hooksPath = join8(dir, "hooks.json");
25603
+ const hooksPath = join11(dir, "hooks.json");
25251
25604
  const file2 = readJson(hooksPath);
25252
25605
  file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
25253
25606
  writeFileSync7(hooksPath, `${JSON.stringify(file2, null, 2)}
25254
25607
  `);
25255
- done.push(`Codex: SessionStart, PostToolUse and Stop hooks written to ${hooksPath}`);
25608
+ done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (a stand-in run answers for an idle chat)`);
25256
25609
  return done;
25257
25610
  }
25258
- async function trustCodexHooks() {
25259
- const server = spawn2("codex", ["app-server"], { stdio: ["pipe", "pipe", "ignore"] });
25611
+ async function trustCodexHooks(codexHome) {
25612
+ const server = spawn3("codex", ["app-server", "-c", "mcp_servers={}"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
25613
+ const where = codexHome === join11(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
25260
25614
  let buffer = "";
25261
25615
  const pending = /* @__PURE__ */ new Map();
25262
25616
  server.stdout.on("data", (chunk) => {
@@ -25274,7 +25628,7 @@ async function trustCodexHooks() {
25274
25628
  }
25275
25629
  });
25276
25630
  let nextId = 1;
25277
- const call = (method, params) => new Promise((resolve3, reject) => {
25631
+ const call = (method, params) => new Promise((resolve5, reject) => {
25278
25632
  const id = nextId++;
25279
25633
  const timer = setTimeout(() => {
25280
25634
  pending.delete(id);
@@ -25283,7 +25637,7 @@ async function trustCodexHooks() {
25283
25637
  pending.set(id, (message) => {
25284
25638
  clearTimeout(timer);
25285
25639
  if (message.error) reject(new Error(message.error.message ?? `${method} failed`));
25286
- else resolve3(message.result);
25640
+ else resolve5(message.result);
25287
25641
  });
25288
25642
  server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
25289
25643
  `);
@@ -25291,28 +25645,38 @@ async function trustCodexHooks() {
25291
25645
  const notify = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
25292
25646
  `);
25293
25647
  try {
25294
- await call("initialize", { clientInfo: { name: "slop", version: "0.4.0" }, capabilities: { experimentalApi: true } });
25648
+ await call("initialize", { clientInfo: { name: "slop", version: "0.4.2" }, capabilities: { experimentalApi: true } });
25295
25649
  notify("initialized", {});
25296
25650
  const listed = await call("hooks/list", { cwds: [process.cwd()] });
25297
25651
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
25298
- if (ours.length === 0) return "Codex: could not find the slop hooks in its list; trust them once with /hooks in a Codex session";
25652
+ if (ours.length === 0) return `Codex: could not find the slop hooks in its list${where}; trust them once with /hooks in a Codex session`;
25299
25653
  const untrusted = ours.filter((hook) => hook.trustStatus !== "trusted");
25300
- if (untrusted.length === 0) return "Codex: the slop hooks are already trusted";
25654
+ if (untrusted.length === 0) return `Codex: the slop hooks are already trusted${where}`;
25301
25655
  const value = Object.fromEntries(untrusted.map((hook) => [hook.key, { trusted_hash: hook.currentHash }]));
25302
25656
  await call("config/batchWrite", { edits: [{ keyPath: "hooks.state", value, mergeStrategy: "upsert" }], reloadUserConfig: true });
25303
- return `Codex: trusted ${untrusted.length} slop hook(s), so it will not ask`;
25657
+ return `Codex: trusted ${untrusted.length} slop hook(s)${where}, so it will not ask`;
25304
25658
  } catch (error62) {
25305
- return `Codex: could not trust the hooks automatically (${error62 instanceof Error ? error62.message : String(error62)}); trust them once with /hooks in a Codex session`;
25659
+ return `Codex: could not trust the hooks automatically${where} (${error62 instanceof Error ? error62.message : String(error62)}); trust them once with /hooks in a Codex session`;
25306
25660
  } finally {
25307
25661
  server.kill();
25308
25662
  }
25309
25663
  }
25664
+ async function healCodexTrust() {
25665
+ const home = ownHarnessHome("CODEX_HOME");
25666
+ if (!home) return;
25667
+ const hooksPath = join11(home, "hooks.json");
25668
+ const configPath3 = join11(home, "config.toml");
25669
+ if (!existsSync10(hooksPath) || !existsSync10(configPath3)) return;
25670
+ if (!readFileSync13(hooksPath, "utf8").includes("slop hook stop")) return;
25671
+ if (readFileSync13(configPath3, "utf8").includes(`hooks.state."${hooksPath}:stop:`)) return;
25672
+ await trustCodexHooks(home);
25673
+ }
25310
25674
  async function hookInput(harness) {
25311
- const input2 = await new Promise((resolve3) => {
25675
+ const input2 = await new Promise((resolve5) => {
25312
25676
  let data = "";
25313
25677
  process.stdin.on("data", (chunk) => data += chunk);
25314
- process.stdin.on("end", () => resolve3(data));
25315
- setTimeout(() => resolve3(data), 2e3);
25678
+ process.stdin.on("end", () => resolve5(data));
25679
+ setTimeout(() => resolve5(data), 2e3);
25316
25680
  });
25317
25681
  let cwd = process.cwd();
25318
25682
  let conversation = null;
@@ -25322,28 +25686,22 @@ async function hookInput(harness) {
25322
25686
  conversation = parsed.session_id ?? null;
25323
25687
  } catch {
25324
25688
  }
25325
- if (conversation) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
25689
+ if (conversation && !nonInteractiveHarness()) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
25326
25690
  return { cwd, conversation };
25327
25691
  }
25692
+ async function promptHook(harness) {
25693
+ await hookInput(harness);
25694
+ }
25328
25695
  async function startHook(harness) {
25696
+ if (nonInteractiveHarness()) return;
25329
25697
  const { conversation } = await hookInput(harness);
25330
25698
  if (!conversation) return;
25331
25699
  const text = `On Slopware Tasks you are ${agentNameFor(conversation)}: that is how people and other agents address you (session who lists everyone).`;
25332
25700
  process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } })}
25333
25701
  `);
25334
25702
  }
25335
- async function tickHook(harness) {
25336
- const arrived = inboxDrain((await hookInput(harness)).cwd);
25337
- if (arrived.length === 0) return;
25338
- const text = `Messages arrived on Slopware Tasks while you worked:
25339
-
25340
- ${arrived.map(pushBlock).join("\n\n")}
25341
-
25342
- Act or answer with the slop MCP \`reply\` tool, then carry on.`;
25343
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } })}
25344
- `);
25345
- }
25346
25703
  async function stopHook(harness) {
25704
+ if (nonInteractiveHarness()) return;
25347
25705
  const { cwd } = await hookInput(harness);
25348
25706
  process.chdir(cwd);
25349
25707
  const parts = inboxDrain(cwd).map(pushBlock);
@@ -25366,25 +25724,16 @@ ${parts.join("\n\n")}` })}
25366
25724
  }
25367
25725
  const general = await api.get(`/projects/${project.id}/threads?scope=general&unread=1&read=1`);
25368
25726
  if (general.length) parts.push("Unread general threads (context):", ...general.map(threadLine));
25369
- const sessionId = rememberedSession(cwd);
25370
- const own2 = sessionId ? (await api.get(`/projects/${project.id}/sessions`)).find((session) => session.id === sessionId) : void 0;
25371
- if (own2?.reachable) {
25372
- const unread = parts.length ? `First, these arrived while you worked:
25727
+ if (parts.length) {
25728
+ process.stdout.write(`${JSON.stringify({ decision: "block", reason: `Messages arrived on Slopware Tasks while you worked. Read them (threads read), act or reply (say reply), then continue or stop.
25373
25729
 
25374
- ${parts.join("\n")}
25375
-
25376
- ` : "";
25377
- process.stdout.write(`${JSON.stringify({ decision: "block", reason: `${unread}You are reachable on Slopware Tasks, so stay listening: call the slop session tool with action wait (no other arguments). When it returns a message, act or answer it (say reply), then stop again; if it returns with nothing, call wait again. It tells you if you may stop.` })}
25730
+ ${parts.join("\n")}` })}
25378
25731
  `);
25379
25732
  return;
25380
25733
  }
25381
- if (parts.length === 0) return;
25382
- process.stdout.write(`${JSON.stringify({ decision: "block", reason: `Messages arrived on Slopware Tasks while you worked. Read them (threads read), act or reply (say reply), then continue or stop.
25383
-
25384
- ${parts.join("\n")}` })}
25385
- `);
25386
25734
  }
25387
25735
  function tools() {
25736
+ if (nonInteractiveHarness()) return [];
25388
25737
  const fine = handlers();
25389
25738
  const run2 = (name, args) => {
25390
25739
  const handler = fine.find((candidate) => candidate.name === name);
@@ -25422,7 +25771,7 @@ function tools() {
25422
25771
  ),
25423
25772
  group(
25424
25773
  "session",
25425
- "Being live. claim: take a task, go live on it, get what is unread (messages then reach you after any tool call). release; finish is on task. who: live agents by name (you included). check: unread now. wait: block for the next message (the Stop hook sends you here after your turn, so you stay reachable).",
25774
+ "Being live. claim: take a task, go live on it, get what is unread (messages then reach you on their own). release; finish is on task. who: live agents by name (you included). check: unread now. wait: block for the next message (optional).",
25426
25775
  { claim: "claim_task", release: "release_task", who: "sessions", check: "check_messages", wait: "wait_for_message" },
25427
25776
  { task: str("Task key"), minutes: int2("wait: give up after (default 300)") }
25428
25777
  ),
@@ -25431,7 +25780,7 @@ function tools() {
25431
25780
  "say",
25432
25781
  "post: start a thread (with task: reaches whoever is live on it; with to: one live agent by name; without either: general, for people). reply: in a thread; reaches its task's live sessions, its participants and its addressee.",
25433
25782
  { post: "post", reply: "reply" },
25434
- { subject: str("post"), body: str("Markdown"), task: str("post: about this task"), to: str("post: an agent's name (session who)"), thread: str("reply: thread id") }
25783
+ { subject: str("post: optional, else the first line"), body: str("Markdown"), task: str("post: about this task"), to: str("post: an agent's name (session who)"), thread: str("reply: thread id") }
25435
25784
  )
25436
25785
  ];
25437
25786
  }
@@ -25456,25 +25805,26 @@ function registerMcpCommands(program3, version2) {
25456
25805
  const mcp = program3.command("mcp").description("The MCP server: the board and its threads as tools for Claude Code, Codex and any MCP client. Holds your live session while it runs.");
25457
25806
  mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(withInbox(tools(), version2), { name: "slop", version: version2 }));
25458
25807
  mcp.command("install").description("Register the server and the two hooks with Claude Code and Codex on this machine.").action(async () => {
25459
- const lines = [...installClaude(), ...installCodex()];
25808
+ const homes = codexHomes();
25809
+ const lines = [...claudeHomes().flatMap((home) => installClaude(home)), ...homes.flatMap((home) => installCodex(home))];
25460
25810
  let codexPresent = true;
25461
25811
  try {
25462
- execFileSync3("codex", ["--version"], { stdio: "ignore" });
25812
+ execFileSync5("codex", ["--version"], { stdio: "ignore" });
25463
25813
  } catch {
25464
25814
  codexPresent = false;
25465
25815
  }
25466
- if (codexPresent) lines.push(await trustCodexHooks());
25816
+ if (codexPresent) for (const home of homes) lines.push(await trustCodexHooks(home));
25467
25817
  emit({ done: lines }, () => lines.join("\n"));
25468
25818
  });
25469
25819
  const hook = program3.command("hook").description("Harness hooks (installed by slop mcp install).");
25470
- const harnessOf = (options) => options.harness === "claude" || options.harness === "codex" ? options.harness : options.harness ? "other" : null;
25471
- hook.command("start").description("SessionStart hook (Claude Code and Codex): writes the conversation down so the MCP server is its session, and tells the agent its name.").option("--harness <name>", "claude or codex").action((options) => startHook(harnessOf(options)));
25472
- hook.command("stop").description("Stop hook (Claude Code and Codex): hands over unread messages instead of letting the agent stop.").option("--harness <name>", "claude or codex").action((options) => stopHook(harnessOf(options)));
25473
- hook.command("tick").description("PostToolUse hook (Claude Code and Codex): hands over messages the held session received, as context, right after any tool call.").option("--harness <name>", "claude or codex").action((options) => tickHook(harnessOf(options)));
25820
+ const harnessOf2 = (options) => options.harness === "claude" || options.harness === "codex" ? options.harness : options.harness ? "other" : null;
25821
+ hook.command("start").description("SessionStart hook (Claude Code and Codex): writes the conversation down so the MCP server is its session, and tells the agent its name.").option("--harness <name>", "claude or codex").action((options) => startHook(harnessOf2(options)));
25822
+ hook.command("prompt").description("UserPromptSubmit hook (Codex): the person typed; a waiting agent returns to handle it.").option("--harness <name>", "claude or codex").action((options) => promptHook(harnessOf2(options)));
25823
+ hook.command("stop").description("Stop hook (Codex): the wait. Holds the turn open at no cost until a message arrives, the person types, or the agent is made unreachable.").option("--harness <name>", "claude or codex").action((options) => stopHook(harnessOf2(options)));
25474
25824
  }
25475
25825
 
25476
25826
  // src/commands/sessions.ts
25477
- import { readFileSync as readFileSync12 } from "fs";
25827
+ import { readFileSync as readFileSync14 } from "fs";
25478
25828
  var taskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
25479
25829
  var sessionLine2 = (session) => [session.agent?.name ?? "agent", session.taskKey ?? "(no task)", session.user.name, session.machine.name, session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-", session.startedAt, session.state === "reconnecting" ? "reconnecting" : "live"];
25480
25830
  function registerSessionCommands(program3, task) {
@@ -25559,13 +25909,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
25559
25909
  }
25560
25910
 
25561
25911
  // src/index.ts
25562
- 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.0").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)");
25912
+ var program2 = new Command().name("slop").description("Slopware Tasks from the terminal. Everything a person can do on the board, an agent can do here.").version("0.4.2").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => setJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
25563
25913
  registerAuthCommands(program2);
25564
25914
  registerProjectCommands(program2);
25565
25915
  var taskCommand = registerTaskCommands(program2);
25566
25916
  registerSessionCommands(program2, taskCommand);
25567
25917
  registerThreadCommands(program2, taskCommand);
25568
- registerMcpCommands(program2, "0.4.0");
25918
+ registerMcpCommands(program2, "0.4.2");
25569
25919
  registerGuideCommand(program2);
25570
25920
  registerSkillCommands(program2);
25571
25921
  program2.parseAsync().catch(fail);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",