slopwaresystems 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +460 -123
- package/package.json +3 -2
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`), `columns`, `members`, `projects` |\n| `task` | `show`, `create`, `update` (only given fields change), `move`, `link`, `archive`, `restore`, `history
|
|
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";
|
|
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
|
|
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";
|
|
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,
|
|
8441
|
+
function isRecursive(inst, stack, resolve3) {
|
|
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, resolve2) {
|
|
|
8448
8448
|
let result = NONE;
|
|
8449
8449
|
const check2 = (child) => {
|
|
8450
8450
|
if (result !== PROVEN && child?._zod) {
|
|
8451
|
-
const answer = isRecursive(child, stack,
|
|
8451
|
+
const answer = isRecursive(child, stack, resolve3);
|
|
8452
8452
|
if (answer > result)
|
|
8453
8453
|
result = answer;
|
|
8454
8454
|
}
|
|
@@ -8459,7 +8459,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
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,
|
|
8462
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
|
|
8463
8463
|
if (child > answer)
|
|
8464
8464
|
answer = child;
|
|
8465
8465
|
}
|
|
@@ -8523,7 +8523,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
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 ?? (
|
|
8526
|
+
const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
|
|
8527
8527
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
8528
8528
|
break;
|
|
8529
8529
|
}
|
|
@@ -23130,9 +23130,13 @@ var User = external_exports.object({
|
|
|
23130
23130
|
avatarUrl: external_exports.string().nullable(),
|
|
23131
23131
|
/** False for accounts that only ever signed in with a code. */
|
|
23132
23132
|
hasPassword: external_exports.boolean(),
|
|
23133
|
+
/** New sessions of this person's agents start reachable by teammates (visible, messaged, messaging). */
|
|
23134
|
+
shareAgents: external_exports.boolean(),
|
|
23135
|
+
/** The first sign-in's welcome (the agent prompt) has been shown. */
|
|
23136
|
+
welcomed: external_exports.boolean(),
|
|
23133
23137
|
createdAt: Timestamp
|
|
23134
23138
|
});
|
|
23135
|
-
var UpdateMeInput = external_exports.object({ name: external_exports.string().trim().min(1).max(80) });
|
|
23139
|
+
var UpdateMeInput = external_exports.object({ name: external_exports.string().trim().min(1).max(80).optional(), shareAgents: external_exports.boolean().optional(), welcomed: external_exports.literal(true).optional() });
|
|
23136
23140
|
var AVATAR_CONTENT_TYPES = ["image/webp", "image/jpeg", "image/png"];
|
|
23137
23141
|
var AVATAR_MAX_BYTES = 1e5;
|
|
23138
23142
|
var SetAvatarInput = external_exports.object({
|
|
@@ -23209,12 +23213,14 @@ var Invitation = external_exports.object({
|
|
|
23209
23213
|
});
|
|
23210
23214
|
var InviteLink = external_exports.object({
|
|
23211
23215
|
id: Id,
|
|
23216
|
+
/** Who it is for, as typed when it was made; shown in the members table so a link can be told from another. */
|
|
23217
|
+
label: external_exports.string().nullable(),
|
|
23212
23218
|
role: OrgRole,
|
|
23213
23219
|
createdAt: Timestamp,
|
|
23214
23220
|
expiresAt: Timestamp.nullable(),
|
|
23215
23221
|
uses: external_exports.number()
|
|
23216
23222
|
});
|
|
23217
|
-
var CreateInviteLinkInput = external_exports.object({ role: OrgRole.default("member"), expiresInHours: external_exports.number().int().min(1).max(24 * 30).nullable().default(24) });
|
|
23223
|
+
var CreateInviteLinkInput = external_exports.object({ role: OrgRole.default("member"), expiresInHours: external_exports.number().int().min(1).max(24 * 30).nullable().default(24), label: external_exports.string().trim().max(120).optional() });
|
|
23218
23224
|
var CreatedInviteLink = InviteLink.extend({ url: external_exports.string() });
|
|
23219
23225
|
var JoinPreview = external_exports.object({ org: external_exports.object({ id: Id, name: external_exports.string() }), role: OrgRole, alreadyMember: external_exports.boolean() });
|
|
23220
23226
|
var MembersResult = external_exports.object({
|
|
@@ -23324,6 +23330,8 @@ var Thread = external_exports.object({
|
|
|
23324
23330
|
taskId: Id.nullable(),
|
|
23325
23331
|
taskKey: external_exports.string().nullable(),
|
|
23326
23332
|
subject: external_exports.string(),
|
|
23333
|
+
/** Addressed to one live agent (by its session); replies reach it wherever it is. Null: to the task's agents, or general. */
|
|
23334
|
+
to: external_exports.object({ sessionId: external_exports.string(), agent: external_exports.string() }).nullable(),
|
|
23327
23335
|
createdBy: UserRef,
|
|
23328
23336
|
createdAt: Timestamp,
|
|
23329
23337
|
lastMessageAt: Timestamp,
|
|
@@ -23332,12 +23340,14 @@ var Thread = external_exports.object({
|
|
|
23332
23340
|
unread: external_exports.number(),
|
|
23333
23341
|
last: external_exports.object({ author: UserRef, via: MessageVia, body: external_exports.string() }).nullable()
|
|
23334
23342
|
});
|
|
23335
|
-
var ThreadDetail = Thread.extend({ messages: external_exports.array(ThreadMessage) });
|
|
23343
|
+
var ThreadDetail = Thread.extend({ messages: external_exports.array(ThreadMessage), earlier: external_exports.number().default(0) });
|
|
23336
23344
|
var body = external_exports.string().trim().min(1, "Say something").max(8e3);
|
|
23337
23345
|
var CreateThreadInput = external_exports.object({
|
|
23338
23346
|
subject: external_exports.string().trim().min(1, "Give it a subject").max(200),
|
|
23339
23347
|
/** A task key or number; absent means general. */
|
|
23340
23348
|
task: external_exports.string().trim().min(1).optional(),
|
|
23349
|
+
/** A live agent, by session id or name (as `session who` lists them). */
|
|
23350
|
+
to: external_exports.string().trim().min(1).max(200).optional(),
|
|
23341
23351
|
body
|
|
23342
23352
|
});
|
|
23343
23353
|
var ReplyInput = external_exports.object({ body });
|
|
@@ -23440,14 +23450,35 @@ var TaskEventRecord = external_exports.object({
|
|
|
23440
23450
|
});
|
|
23441
23451
|
|
|
23442
23452
|
// ../contract/src/work/sessions.ts
|
|
23453
|
+
var AgentHarness = external_exports.enum(["claude", "codex", "other"]);
|
|
23443
23454
|
var Session = external_exports.object({
|
|
23455
|
+
/** Derived from the person, the machine and the agent's conversation: the same across reconnects and resumes. */
|
|
23444
23456
|
id: Id,
|
|
23445
23457
|
/** 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
23458
|
taskId: Id.nullable(),
|
|
23447
23459
|
taskKey: external_exports.string().nullable(),
|
|
23448
23460
|
user: UserRef,
|
|
23449
23461
|
machine: external_exports.object({ id: Id, name: external_exports.string() }),
|
|
23450
|
-
|
|
23462
|
+
/** The agent itself: a generated name (see agent-names) and which harness runs it. */
|
|
23463
|
+
agent: external_exports.object({ name: external_exports.string(), harness: AgentHarness.nullable() }),
|
|
23464
|
+
/** Where it runs, to tell two agents under one person apart. */
|
|
23465
|
+
where: external_exports.object({ directory: external_exports.string().nullable(), branch: external_exports.string().nullable() }),
|
|
23466
|
+
startedAt: Timestamp,
|
|
23467
|
+
/** "reconnecting": its socket dropped and the room is holding the session open for it to come back. */
|
|
23468
|
+
state: external_exports.enum(["live", "reconnecting"]),
|
|
23469
|
+
/**
|
|
23470
|
+
* Reachable by teammates: listed to them, messaged by them and by their agents, and able to message. Off, the agent
|
|
23471
|
+
* still works the board but talks to nobody and is listed only to its own person. Starts from the person's default.
|
|
23472
|
+
*/
|
|
23473
|
+
reachable: external_exports.boolean().default(true)
|
|
23474
|
+
});
|
|
23475
|
+
var ReachableInput = external_exports.object({ on: external_exports.boolean() });
|
|
23476
|
+
var SessionClaimInput = external_exports.object({
|
|
23477
|
+
/** Stable for one conversation of the agent (the harness's own session id), so a resumed chat is the same session and keeps its name. */
|
|
23478
|
+
conversation: external_exports.string().trim().min(1).max(200).optional(),
|
|
23479
|
+
harness: AgentHarness.nullable().default(null),
|
|
23480
|
+
directory: external_exports.string().trim().max(200).nullable().default(null),
|
|
23481
|
+
branch: external_exports.string().trim().max(200).nullable().default(null)
|
|
23451
23482
|
});
|
|
23452
23483
|
var ConnectTicket = external_exports.object({ ticket: external_exports.string(), path: external_exports.string() });
|
|
23453
23484
|
var AskInput = external_exports.object({ body: external_exports.string().trim().min(1, "Say something").max(4e3) });
|
|
@@ -23455,13 +23486,127 @@ var SessionPush = external_exports.discriminatedUnion("type", [
|
|
|
23455
23486
|
external_exports.object({ type: external_exports.literal("hello"), session: Session }),
|
|
23456
23487
|
external_exports.object({ type: external_exports.literal("event"), record: TaskEventRecord }),
|
|
23457
23488
|
/** A message in a thread about the task this session is on. */
|
|
23458
|
-
external_exports.object({ type: external_exports.literal("thread"), thread: external_exports.object({ id: Id, subject: external_exports.string(), taskKey: external_exports.string().nullable() }), message: ThreadMessage }),
|
|
23489
|
+
external_exports.object({ type: external_exports.literal("thread"), thread: external_exports.object({ id: Id, subject: external_exports.string(), taskKey: external_exports.string().nullable(), to: external_exports.string().nullable().optional() }), message: ThreadMessage }),
|
|
23459
23490
|
/** The room is closing this session (someone ran `slop session end`). */
|
|
23460
23491
|
external_exports.object({ type: external_exports.literal("end") }),
|
|
23492
|
+
/** Reachability was switched from the web. Off releases a waiting agent: nothing can reach it any more. */
|
|
23493
|
+
external_exports.object({ type: external_exports.literal("reachable"), on: external_exports.boolean() }),
|
|
23461
23494
|
/** To board watchers: something in the project changed; refetch. */
|
|
23462
23495
|
external_exports.object({ type: external_exports.literal("changed"), scope: external_exports.string() })
|
|
23463
23496
|
]);
|
|
23464
23497
|
|
|
23498
|
+
// ../contract/src/work/agent-names.ts
|
|
23499
|
+
var AGENT_NAMES = [
|
|
23500
|
+
"Albert",
|
|
23501
|
+
"Alma",
|
|
23502
|
+
"Amos",
|
|
23503
|
+
"Astrid",
|
|
23504
|
+
"August",
|
|
23505
|
+
"Beatrix",
|
|
23506
|
+
"Bruno",
|
|
23507
|
+
"Cedric",
|
|
23508
|
+
"Clara",
|
|
23509
|
+
"Cosmo",
|
|
23510
|
+
"Dahlia",
|
|
23511
|
+
"Dexter",
|
|
23512
|
+
"Edie",
|
|
23513
|
+
"Elio",
|
|
23514
|
+
"Ember",
|
|
23515
|
+
"Felix",
|
|
23516
|
+
"Fern",
|
|
23517
|
+
"Flora",
|
|
23518
|
+
"Gus",
|
|
23519
|
+
"Hazel",
|
|
23520
|
+
"Hugo",
|
|
23521
|
+
"Ida",
|
|
23522
|
+
"Ingrid",
|
|
23523
|
+
"Iris",
|
|
23524
|
+
"Jasper",
|
|
23525
|
+
"Juno",
|
|
23526
|
+
"Kit",
|
|
23527
|
+
"Leo",
|
|
23528
|
+
"Lila",
|
|
23529
|
+
"Linus",
|
|
23530
|
+
"Lotte",
|
|
23531
|
+
"Mabel",
|
|
23532
|
+
"Marlow",
|
|
23533
|
+
"Milo",
|
|
23534
|
+
"Minnie",
|
|
23535
|
+
"Nell",
|
|
23536
|
+
"Nico",
|
|
23537
|
+
"Nina",
|
|
23538
|
+
"Odette",
|
|
23539
|
+
"Olive",
|
|
23540
|
+
"Oscar",
|
|
23541
|
+
"Otto",
|
|
23542
|
+
"Pearl",
|
|
23543
|
+
"Percy",
|
|
23544
|
+
"Pip",
|
|
23545
|
+
"Quill",
|
|
23546
|
+
"Remy",
|
|
23547
|
+
"Rosa",
|
|
23548
|
+
"Rufus",
|
|
23549
|
+
"Sable",
|
|
23550
|
+
"Silas",
|
|
23551
|
+
"Sky",
|
|
23552
|
+
"Sol",
|
|
23553
|
+
"Tess",
|
|
23554
|
+
"Theo",
|
|
23555
|
+
"Tilly",
|
|
23556
|
+
"Ulla",
|
|
23557
|
+
"Vera",
|
|
23558
|
+
"Vito",
|
|
23559
|
+
"Wren",
|
|
23560
|
+
"Xavi",
|
|
23561
|
+
"Yara",
|
|
23562
|
+
"Zed",
|
|
23563
|
+
"Zora",
|
|
23564
|
+
"Arlo",
|
|
23565
|
+
"Bea",
|
|
23566
|
+
"Cyrus",
|
|
23567
|
+
"Della",
|
|
23568
|
+
"Enzo",
|
|
23569
|
+
"Frida",
|
|
23570
|
+
"Gilda",
|
|
23571
|
+
"Hal",
|
|
23572
|
+
"Imre",
|
|
23573
|
+
"Joss",
|
|
23574
|
+
"Kai",
|
|
23575
|
+
"Lena",
|
|
23576
|
+
"Mona",
|
|
23577
|
+
"Nate",
|
|
23578
|
+
"Opal",
|
|
23579
|
+
"Pia",
|
|
23580
|
+
"Rae",
|
|
23581
|
+
"Sacha",
|
|
23582
|
+
"Tove",
|
|
23583
|
+
"Uma",
|
|
23584
|
+
"Vince",
|
|
23585
|
+
"Willa",
|
|
23586
|
+
"Yuki",
|
|
23587
|
+
"Zelda",
|
|
23588
|
+
"Ansel",
|
|
23589
|
+
"Birdie",
|
|
23590
|
+
"Cleo",
|
|
23591
|
+
"Dov",
|
|
23592
|
+
"Esme",
|
|
23593
|
+
"Fitz",
|
|
23594
|
+
"Greta",
|
|
23595
|
+
"Hollis",
|
|
23596
|
+
"Inez",
|
|
23597
|
+
"Jude",
|
|
23598
|
+
"Kip",
|
|
23599
|
+
"Lark"
|
|
23600
|
+
];
|
|
23601
|
+
function agentNameFor(key) {
|
|
23602
|
+
let hash2 = 2166136261;
|
|
23603
|
+
for (let index = 0; index < key.length; index += 1) {
|
|
23604
|
+
hash2 ^= key.charCodeAt(index);
|
|
23605
|
+
hash2 = Math.imul(hash2, 16777619) >>> 0;
|
|
23606
|
+
}
|
|
23607
|
+
return AGENT_NAMES[hash2 % AGENT_NAMES.length] ?? AGENT_NAMES[0];
|
|
23608
|
+
}
|
|
23609
|
+
|
|
23465
23610
|
// ../contract/src/work/bindings.ts
|
|
23466
23611
|
var RepoInfo = external_exports.object({
|
|
23467
23612
|
/**
|
|
@@ -23520,7 +23665,7 @@ function resolveSession() {
|
|
|
23520
23665
|
|
|
23521
23666
|
// src/platform/session-file.ts
|
|
23522
23667
|
import { createHash } from "crypto";
|
|
23523
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
23668
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
23524
23669
|
import { dirname as dirname2, join as join3 } from "path";
|
|
23525
23670
|
var sessionsDir = () => join3(dirname2(configPath()), "sessions");
|
|
23526
23671
|
var realCwd = (cwd) => {
|
|
@@ -23540,6 +23685,21 @@ function rememberedSession(cwd) {
|
|
|
23540
23685
|
const path3 = sessionFile(cwd);
|
|
23541
23686
|
return existsSync3(path3) ? readFileSync3(path3, "utf8").trim() || null : null;
|
|
23542
23687
|
}
|
|
23688
|
+
var conversationFile = (cwd) => join3(sessionsDir(), `${createHash("sha256").update(realCwd(cwd)).digest("hex").slice(0, 16)}.conversation.json`);
|
|
23689
|
+
function rememberConversation(cwd, conversation) {
|
|
23690
|
+
mkdirSync3(sessionsDir(), { recursive: true });
|
|
23691
|
+
writeFileSync3(conversationFile(cwd), JSON.stringify(conversation));
|
|
23692
|
+
}
|
|
23693
|
+
function rememberedConversation(cwd, notBefore) {
|
|
23694
|
+
const path3 = conversationFile(cwd);
|
|
23695
|
+
try {
|
|
23696
|
+
if (statSync(path3).mtimeMs < notBefore) return null;
|
|
23697
|
+
const parsed = JSON.parse(readFileSync3(path3, "utf8"));
|
|
23698
|
+
return typeof parsed.id === "string" && parsed.id ? { id: parsed.id, harness: parsed.harness ?? "other" } : null;
|
|
23699
|
+
} catch {
|
|
23700
|
+
return null;
|
|
23701
|
+
}
|
|
23702
|
+
}
|
|
23543
23703
|
|
|
23544
23704
|
// src/platform/errors.ts
|
|
23545
23705
|
var CliError = class extends Error {
|
|
@@ -23775,7 +23935,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
|
|
|
23775
23935
|
- Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
|
|
23776
23936
|
- When it is finished and verified: \`slop task done <KEY>\`.
|
|
23777
23937
|
- Descriptions are Markdown and are shown in full on the board. Write real ones: context, file paths, what done means. Pipe long ones in with \`-d -\`.
|
|
23778
|
-
- Agents use the six slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, each with an \`action\`. Messages
|
|
23938
|
+
- Agents use the six slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, each with an \`action\`. You have a name (session start tells you; \`session who\` lists everyone's); \`say post\` with \`to\` reaches one agent by name. Messages for you reach you after your next tool call. The \`slop\` command is for people and scripts. Talk through threads: \`slop task ask <KEY> "\u2026"\` reaches whoever is live on that task, \`slop thread reply <id> "\u2026"\` answers, \`slop threads\` lists them; general threads are for people and for context. Method: \`slop skill slop-sessions\`.
|
|
23779
23939
|
- Work that outlasts one session gets a plan on its task: \`slop task plan <KEY> -\` (one item per line), then \`slop task item note|done <KEY> <n>\` as you go; \`slop task items <KEY>\` shows where you are. \`slop skill slop-planning\` is the method; \`slop skills install\` puts the skills in this repository.
|
|
23780
23940
|
- Everything is editable: \`slop task update <KEY> --title \u2026 --severity \u2026 --status \u2026 --assignee \u2026\`. Assign with \`slop task assign <KEY> <email|me|none>\`; see who exists with \`slop members\`. \`slop task list --mine\` shows your user's tasks.
|
|
23781
23941
|
- Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
|
|
@@ -23805,6 +23965,10 @@ function findRepo() {
|
|
|
23805
23965
|
const root = basename(commonDir) === ".git" ? resolve(commonDir, "..") : worktree ?? commonDir;
|
|
23806
23966
|
return { root, worktree: worktree ?? root, linkPath: join5(commonDir, "slop.json") };
|
|
23807
23967
|
}
|
|
23968
|
+
function currentBranch(cwd = process.cwd()) {
|
|
23969
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
23970
|
+
return branch && branch !== "HEAD" ? branch : null;
|
|
23971
|
+
}
|
|
23808
23972
|
function requireRepo() {
|
|
23809
23973
|
const repo = findRepo();
|
|
23810
23974
|
if (!repo) throw new CliError("Not inside a git repository", "cd into your repository, or pass --project KEY", "no_repo");
|
|
@@ -23980,23 +24144,25 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
|
23980
24144
|
|
|
23981
24145
|
// src/commands/mcp.ts
|
|
23982
24146
|
import { execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
|
|
23983
|
-
import {
|
|
23984
|
-
import
|
|
23985
|
-
import { dirname as dirname5 } from "path";
|
|
24147
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
|
|
24148
|
+
import "path";
|
|
23986
24149
|
import { homedir as homedir3 } from "os";
|
|
23987
|
-
import { join as
|
|
24150
|
+
import { join as join8 } from "path";
|
|
23988
24151
|
|
|
23989
24152
|
// src/platform/live.ts
|
|
24153
|
+
var PING_EVERY_MS = 25e3;
|
|
24154
|
+
var PONG_WITHIN_MS = 1e4;
|
|
23990
24155
|
async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
23991
|
-
const ticket = await api.post(`/projects/${projectId}/tasks/${encodeURIComponent(taskKey)}/session/ticket
|
|
24156
|
+
const ticket = await api.post(`/projects/${projectId}/tasks/${encodeURIComponent(taskKey)}/session/ticket`, options.claim ?? {});
|
|
23992
24157
|
const socket = new WebSocket(`${api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
23993
|
-
const outcome = await new Promise((
|
|
23994
|
-
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() =>
|
|
24158
|
+
const outcome = await new Promise((resolve3) => {
|
|
24159
|
+
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => resolve3({ kind: "timeout" }), options.timeoutMs) : null;
|
|
23995
24160
|
const done = (result) => {
|
|
23996
24161
|
if (timer) clearTimeout(timer);
|
|
23997
|
-
|
|
24162
|
+
resolve3(result);
|
|
23998
24163
|
};
|
|
23999
24164
|
socket.addEventListener("message", (event) => {
|
|
24165
|
+
if (String(event.data) === "pong") return;
|
|
24000
24166
|
const push = JSON.parse(String(event.data));
|
|
24001
24167
|
if (push.type === "hello") {
|
|
24002
24168
|
api.sessionId = push.session.id;
|
|
@@ -24018,28 +24184,33 @@ async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
|
24018
24184
|
if (outcome.kind !== "message") rememberSession(process.cwd(), null);
|
|
24019
24185
|
if (outcome.kind === "message" || outcome.kind === "timeout") {
|
|
24020
24186
|
socket.close(1e3, outcome.kind === "message" ? "handled" : "timeout");
|
|
24021
|
-
await new Promise((
|
|
24022
|
-
socket.addEventListener("close", () =>
|
|
24023
|
-
setTimeout(
|
|
24187
|
+
await new Promise((resolve3) => {
|
|
24188
|
+
socket.addEventListener("close", () => resolve3(), { once: true });
|
|
24189
|
+
setTimeout(resolve3, 500);
|
|
24024
24190
|
});
|
|
24025
24191
|
}
|
|
24026
24192
|
return outcome;
|
|
24027
24193
|
}
|
|
24028
24194
|
var HeldSession = class {
|
|
24029
|
-
constructor(api, projectId, taskKey,
|
|
24195
|
+
constructor(api, projectId, taskKey, hooks) {
|
|
24030
24196
|
this.api = api;
|
|
24031
24197
|
this.projectId = projectId;
|
|
24032
24198
|
this.taskKey = taskKey;
|
|
24033
|
-
this.
|
|
24199
|
+
this.hooks = hooks;
|
|
24034
24200
|
}
|
|
24035
24201
|
api;
|
|
24036
24202
|
projectId;
|
|
24037
24203
|
taskKey;
|
|
24038
|
-
|
|
24204
|
+
hooks;
|
|
24039
24205
|
socket = null;
|
|
24040
24206
|
stopped = false;
|
|
24041
24207
|
timer = null;
|
|
24208
|
+
heartbeat = null;
|
|
24209
|
+
pongDue = null;
|
|
24042
24210
|
attempt = 0;
|
|
24211
|
+
connected = 0;
|
|
24212
|
+
session = null;
|
|
24213
|
+
/** Whoever is sitting in `session wait`. Handed the next message, or null when standby is switched off under them. */
|
|
24043
24214
|
waiters = /* @__PURE__ */ new Set();
|
|
24044
24215
|
async start() {
|
|
24045
24216
|
await this.connect();
|
|
@@ -24047,30 +24218,48 @@ var HeldSession = class {
|
|
|
24047
24218
|
async connect() {
|
|
24048
24219
|
if (this.stopped) return;
|
|
24049
24220
|
try {
|
|
24050
|
-
const ticket = await this.api.post(this.taskKey ? `/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket` : `/projects/${this.projectId}/session/ticket
|
|
24221
|
+
const ticket = await this.api.post(this.taskKey ? `/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket` : `/projects/${this.projectId}/session/ticket`, this.hooks.claim());
|
|
24051
24222
|
const socket = new WebSocket(`${this.api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
24052
24223
|
this.socket = socket;
|
|
24053
24224
|
socket.addEventListener("open", () => {
|
|
24054
24225
|
this.attempt = 0;
|
|
24226
|
+
this.startHeartbeat(socket);
|
|
24055
24227
|
});
|
|
24056
24228
|
socket.addEventListener("message", (event) => {
|
|
24057
|
-
const
|
|
24229
|
+
const data = String(event.data);
|
|
24230
|
+
if (data === "pong") {
|
|
24231
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
24232
|
+
this.pongDue = null;
|
|
24233
|
+
return;
|
|
24234
|
+
}
|
|
24235
|
+
const push = JSON.parse(data);
|
|
24058
24236
|
if (push.type === "hello") {
|
|
24059
24237
|
this.api.sessionId = push.session.id;
|
|
24238
|
+
this.session = push.session;
|
|
24239
|
+
this.taskKey = push.session.taskKey;
|
|
24060
24240
|
rememberSession(process.cwd(), push.session.id);
|
|
24241
|
+
this.connected += 1;
|
|
24242
|
+
if (this.connected > 1) void this.hooks.onReconnect?.(push.session);
|
|
24061
24243
|
return;
|
|
24062
24244
|
}
|
|
24063
24245
|
if (push.type === "end") {
|
|
24064
24246
|
this.stopped = true;
|
|
24065
24247
|
return;
|
|
24066
24248
|
}
|
|
24249
|
+
if (push.type === "reachable") {
|
|
24250
|
+
if (this.session) this.session = { ...this.session, reachable: push.on };
|
|
24251
|
+
if (!push.on) for (const waiter of [...this.waiters]) waiter(null);
|
|
24252
|
+
return;
|
|
24253
|
+
}
|
|
24067
24254
|
if (push.type !== "thread") return;
|
|
24068
24255
|
const delivered = { thread: push.thread, message: push.message };
|
|
24069
|
-
|
|
24070
|
-
|
|
24256
|
+
const taken = this.waiters.size > 0;
|
|
24257
|
+
for (const waiter of [...this.waiters]) waiter(delivered);
|
|
24258
|
+
if (!taken) this.hooks.onMessage(delivered);
|
|
24071
24259
|
});
|
|
24072
24260
|
socket.addEventListener("close", () => {
|
|
24073
|
-
this.
|
|
24261
|
+
this.stopHeartbeat();
|
|
24262
|
+
if (this.socket === socket) this.socket = null;
|
|
24074
24263
|
if (!this.stopped) this.schedule();
|
|
24075
24264
|
});
|
|
24076
24265
|
socket.addEventListener("error", () => {
|
|
@@ -24079,6 +24268,26 @@ var HeldSession = class {
|
|
|
24079
24268
|
this.schedule();
|
|
24080
24269
|
}
|
|
24081
24270
|
}
|
|
24271
|
+
/** A ping every 25s; no pong within 10s means the socket is dead however open it looks. Close it and come back. */
|
|
24272
|
+
startHeartbeat(socket) {
|
|
24273
|
+
this.stopHeartbeat();
|
|
24274
|
+
this.heartbeat = setInterval(() => {
|
|
24275
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
24276
|
+
socket.send("ping");
|
|
24277
|
+
if (!this.pongDue) {
|
|
24278
|
+
this.pongDue = setTimeout(() => {
|
|
24279
|
+
this.pongDue = null;
|
|
24280
|
+
socket.close(4e3, "no pong");
|
|
24281
|
+
}, PONG_WITHIN_MS);
|
|
24282
|
+
}
|
|
24283
|
+
}, PING_EVERY_MS);
|
|
24284
|
+
}
|
|
24285
|
+
stopHeartbeat() {
|
|
24286
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
24287
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
24288
|
+
this.heartbeat = null;
|
|
24289
|
+
this.pongDue = null;
|
|
24290
|
+
}
|
|
24082
24291
|
schedule() {
|
|
24083
24292
|
this.attempt += 1;
|
|
24084
24293
|
this.timer = setTimeout(() => void this.connect(), Math.min(3e4, 1e3 * 2 ** Math.min(this.attempt, 5)));
|
|
@@ -24088,10 +24297,36 @@ var HeldSession = class {
|
|
|
24088
24297
|
this.api.sessionId = null;
|
|
24089
24298
|
rememberSession(process.cwd(), null);
|
|
24090
24299
|
if (this.timer) clearTimeout(this.timer);
|
|
24300
|
+
this.stopHeartbeat();
|
|
24091
24301
|
this.socket?.close(1e3, "released");
|
|
24092
24302
|
}
|
|
24093
24303
|
};
|
|
24094
24304
|
|
|
24305
|
+
// src/platform/agent-identity.ts
|
|
24306
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
24307
|
+
import { basename as basename2, resolve as resolve2 } from "path";
|
|
24308
|
+
var processConversation = randomUUID3();
|
|
24309
|
+
var startedAt = Date.now();
|
|
24310
|
+
function agentIdentity(cwd, fallback = "process") {
|
|
24311
|
+
const conversation = rememberedConversation(cwd, startedAt - 12e4);
|
|
24312
|
+
const repo = findRepo();
|
|
24313
|
+
return {
|
|
24314
|
+
conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve2(cwd)}`),
|
|
24315
|
+
harness: conversation?.harness ?? null,
|
|
24316
|
+
directory: repo ? basename2(repo.worktree) : basename2(cwd),
|
|
24317
|
+
branch: currentBranch(cwd)
|
|
24318
|
+
};
|
|
24319
|
+
}
|
|
24320
|
+
|
|
24321
|
+
// src/platform/bounded.ts
|
|
24322
|
+
var RESULT_CAP = 16e3;
|
|
24323
|
+
function bounded(result) {
|
|
24324
|
+
if (result.length <= RESULT_CAP) return result;
|
|
24325
|
+
return `${result.slice(0, RESULT_CAP)}
|
|
24326
|
+
|
|
24327
|
+
[cut: ${result.length - RESULT_CAP} more characters; ask for less with limit, offset or before]`;
|
|
24328
|
+
}
|
|
24329
|
+
|
|
24095
24330
|
// src/platform/mcp.ts
|
|
24096
24331
|
import { createInterface } from "readline";
|
|
24097
24332
|
function serveMcp(tools2, info) {
|
|
@@ -24439,7 +24674,7 @@ var readText2 = (value) => value === "-" ? readFileSync9(0, "utf8").trim() : val
|
|
|
24439
24674
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
24440
24675
|
var who = (message) => `${message.author.name}${message.via === "agent" ? ` (agent${message.machine ? ` \xB7 ${message.machine}` : ""})` : ""}`;
|
|
24441
24676
|
function threadLine(thread) {
|
|
24442
|
-
const scope = thread.taskKey ?? "general";
|
|
24677
|
+
const scope = [thread.taskKey ?? (thread.to ? null : "general"), thread.to ? `to ${thread.to.agent}` : null].filter(Boolean).join(", ");
|
|
24443
24678
|
const unread = thread.unread > 0 ? ` ${thread.unread} unread` : "";
|
|
24444
24679
|
const last = thread.last ? `
|
|
24445
24680
|
${who(thread.last)}: ${thread.last.body.split("\n")[0]}` : "";
|
|
@@ -24453,9 +24688,10 @@ function threadBlock(thread) {
|
|
|
24453
24688
|
return [`${thread.subject} [${thread.taskKey ?? "general"}] ${thread.id}`, "", ...thread.messages.map(messageBlock)].join("\n\n").replace("\n\n\n", "\n\n");
|
|
24454
24689
|
}
|
|
24455
24690
|
function pushBlock(push) {
|
|
24456
|
-
|
|
24691
|
+
const scope = [push.thread.taskKey ?? (push.thread.to ? null : "general"), push.thread.to ? `to ${push.thread.to}` : null].filter(Boolean).join(", ");
|
|
24692
|
+
return `Thread ${push.thread.id} [${scope}] ${push.thread.subject}
|
|
24457
24693
|
${messageBlock(push.message)}
|
|
24458
|
-
Reply:
|
|
24694
|
+
Reply: say reply (thread ${push.thread.id})`;
|
|
24459
24695
|
}
|
|
24460
24696
|
function registerThreadCommands(program3, task) {
|
|
24461
24697
|
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
@@ -24470,10 +24706,10 @@ function registerThreadCommands(program3, task) {
|
|
|
24470
24706
|
emit(threads, () => threads.length === 0 ? "No threads." : threads.map(threadLine).join("\n"));
|
|
24471
24707
|
});
|
|
24472
24708
|
const thread = program3.command("thread").description("One thread: start, read, reply.");
|
|
24473
|
-
withProject(thread.command("new <subject>")).description('Start a thread. General by default; --task makes it about a task (and reaches whoever is live on it). The first message is -m, or "-" for stdin.').requiredOption("-m, --message <text>", 'the first message; "-" reads stdin').option("-t, --task <key>", "about this task").action(async (subject, options) => {
|
|
24709
|
+
withProject(thread.command("new <subject>")).description('Start a thread. General by default; --task makes it about a task (and reaches whoever is live on it). The first message is -m, or "-" for stdin.').requiredOption("-m, --message <text>", 'the first message; "-" reads stdin').option("-t, --task <key>", "about this task").option("--to <agent>", "to one live agent, by name or session id (slop sessions)").action(async (subject, options) => {
|
|
24474
24710
|
const api = authedApi();
|
|
24475
24711
|
const project = await resolveProject(api, options.project);
|
|
24476
|
-
const result = await api.post(`/projects/${project.id}/threads`, { subject, task: options.task, body: readText2(options.message) });
|
|
24712
|
+
const result = await api.post(`/projects/${project.id}/threads`, { subject, task: options.task, to: options.to, body: readText2(options.message) });
|
|
24477
24713
|
emit(result, () => started(result));
|
|
24478
24714
|
});
|
|
24479
24715
|
withProject(thread.command("show <id>")).description("Read a thread (and mark it read).").action(async (id, options) => {
|
|
@@ -24508,14 +24744,12 @@ var subjectOf = (body2) => {
|
|
|
24508
24744
|
if (!line) throw new CliError("Say something", "the first line becomes the subject");
|
|
24509
24745
|
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
24510
24746
|
};
|
|
24511
|
-
var started = (result) => 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.`;
|
|
24747
|
+
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.`;
|
|
24512
24748
|
|
|
24513
|
-
// src/
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
var PROJECT = str("Project key. Defaults to the project this repository is linked to.");
|
|
24518
|
-
var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
24749
|
+
// src/platform/inbox.ts
|
|
24750
|
+
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";
|
|
24519
24753
|
var inboxDir = () => join7(dirname5(configPath()), "inbox");
|
|
24520
24754
|
var realCwd2 = (cwd) => {
|
|
24521
24755
|
try {
|
|
@@ -24534,10 +24768,40 @@ function inboxWrite(cwd, push) {
|
|
|
24534
24768
|
function inboxDrain(cwd) {
|
|
24535
24769
|
const path3 = inboxFile(cwd);
|
|
24536
24770
|
if (!existsSync7(path3)) return [];
|
|
24537
|
-
const
|
|
24538
|
-
|
|
24539
|
-
|
|
24771
|
+
const taken = `${path3}.${process.pid}.${Date.now()}.draining`;
|
|
24772
|
+
try {
|
|
24773
|
+
renameSync(path3, taken);
|
|
24774
|
+
} catch {
|
|
24775
|
+
return [];
|
|
24776
|
+
}
|
|
24777
|
+
const lines = readFileSync10(taken, "utf8").split("\n").filter(Boolean);
|
|
24778
|
+
rmSync4(taken, { force: true });
|
|
24779
|
+
return lines.flatMap((line) => {
|
|
24780
|
+
try {
|
|
24781
|
+
return [JSON.parse(line)];
|
|
24782
|
+
} catch {
|
|
24783
|
+
return [];
|
|
24784
|
+
}
|
|
24785
|
+
});
|
|
24540
24786
|
}
|
|
24787
|
+
|
|
24788
|
+
// src/commands/mcp.ts
|
|
24789
|
+
var str = (description2) => ({ type: "string", description: description2 });
|
|
24790
|
+
var int2 = (description2) => ({ type: "integer", description: description2 });
|
|
24791
|
+
var schema = (properties, required2 = []) => ({ type: "object", properties, required: required2, additionalProperties: false });
|
|
24792
|
+
var PROJECT = str("Project key. Defaults to the project this repository is linked to.");
|
|
24793
|
+
var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
24794
|
+
var sessionLine = (session, mine = null) => [
|
|
24795
|
+
// Optional chaining on purpose: a server from before agents had names answers without these, and a tool must not crash on it.
|
|
24796
|
+
(session.agent?.name ?? "agent") + (session.id === mine ? " (you)" : ""),
|
|
24797
|
+
session.taskKey ?? "(no task)",
|
|
24798
|
+
session.user.name,
|
|
24799
|
+
session.machine.name,
|
|
24800
|
+
session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-",
|
|
24801
|
+
`since ${session.startedAt}`,
|
|
24802
|
+
session.state === "reconnecting" ? "(reconnecting)" : "",
|
|
24803
|
+
session.reachable ? "" : "(not visible to teammates)"
|
|
24804
|
+
].filter(Boolean).join(" ");
|
|
24541
24805
|
async function digest(api, projectId, taskKey) {
|
|
24542
24806
|
const [about, mine, general] = await Promise.all([
|
|
24543
24807
|
taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1&read=1`) : Promise.resolve([]),
|
|
@@ -24551,6 +24815,19 @@ async function digest(api, projectId, taskKey) {
|
|
|
24551
24815
|
if (general.length) lines.push("Unread general threads (context; nobody has to answer):", ...general.map(threadLine));
|
|
24552
24816
|
return lines.length ? lines.join("\n") : "Nothing unread.";
|
|
24553
24817
|
}
|
|
24818
|
+
async function catchUp(api, projectId, taskKey, cwd) {
|
|
24819
|
+
const [mine, about] = await Promise.all([
|
|
24820
|
+
api.get(`/projects/${projectId}/threads?participant=1&unread=1`),
|
|
24821
|
+
taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1`) : Promise.resolve([])
|
|
24822
|
+
]);
|
|
24823
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24824
|
+
for (const thread of [...about, ...mine]) {
|
|
24825
|
+
if (seen.has(thread.id) || thread.unread === 0) continue;
|
|
24826
|
+
seen.add(thread.id);
|
|
24827
|
+
const detail = await api.get(`/projects/${projectId}/threads/${thread.id}?read=0`);
|
|
24828
|
+
for (const message of detail.messages.slice(-thread.unread)) inboxWrite(cwd, { thread: { id: detail.id, subject: detail.subject, taskKey: detail.taskKey, to: detail.to?.agent ?? null }, message });
|
|
24829
|
+
}
|
|
24830
|
+
}
|
|
24554
24831
|
function handlers() {
|
|
24555
24832
|
const api = authedApi();
|
|
24556
24833
|
const resolveOnly = (args) => resolveProject(api, args.project);
|
|
@@ -24558,7 +24835,11 @@ function handlers() {
|
|
|
24558
24835
|
const hold = async (projectId, taskKey) => {
|
|
24559
24836
|
if (held && held.projectId === projectId && held.taskKey === taskKey) return;
|
|
24560
24837
|
held?.stop();
|
|
24561
|
-
held = new HeldSession(api, projectId, taskKey,
|
|
24838
|
+
held = new HeldSession(api, projectId, taskKey, {
|
|
24839
|
+
onMessage: (push) => inboxWrite(process.cwd(), push),
|
|
24840
|
+
claim: () => agentIdentity(process.cwd()),
|
|
24841
|
+
onReconnect: (session) => catchUp(api, projectId, session.taskKey, process.cwd()).catch(() => void 0)
|
|
24842
|
+
});
|
|
24562
24843
|
await held.start();
|
|
24563
24844
|
};
|
|
24564
24845
|
const project = async (args) => {
|
|
@@ -24572,26 +24853,28 @@ function handlers() {
|
|
|
24572
24853
|
return [
|
|
24573
24854
|
{
|
|
24574
24855
|
name: "wait_for_message",
|
|
24575
|
-
description: "
|
|
24576
|
-
inputSchema: schema({ task: str("Task key
|
|
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.",
|
|
24857
|
+
inputSchema: schema({ task: str("Task key to go live on first (optional)"), minutes: int2("Give up after (default 300)"), project: PROJECT }),
|
|
24577
24858
|
run: async (args) => {
|
|
24578
24859
|
const target = await project(args);
|
|
24579
|
-
await hold(target.id, String(args.task));
|
|
24860
|
+
if (args.task) await hold(target.id, String(args.task));
|
|
24580
24861
|
const queued = inboxDrain(process.cwd());
|
|
24581
24862
|
if (queued.length) return queued.map(pushBlock).join("\n\n");
|
|
24582
|
-
const minutes = typeof args.minutes === "number" ? args.minutes :
|
|
24583
|
-
const
|
|
24863
|
+
const minutes = typeof args.minutes === "number" ? args.minutes : 300;
|
|
24864
|
+
const outcome = await new Promise((resolve3) => {
|
|
24584
24865
|
const waiter = (delivered) => {
|
|
24585
24866
|
held?.waiters.delete(waiter);
|
|
24586
|
-
|
|
24867
|
+
resolve3({ push: delivered, released: delivered === null });
|
|
24587
24868
|
};
|
|
24588
24869
|
held?.waiters.add(waiter);
|
|
24589
24870
|
setTimeout(() => {
|
|
24590
24871
|
held?.waiters.delete(waiter);
|
|
24591
|
-
|
|
24872
|
+
resolve3({ push: null, released: false });
|
|
24592
24873
|
}, minutes * 6e4);
|
|
24593
24874
|
});
|
|
24594
|
-
|
|
24875
|
+
if (outcome.push) return pushBlock(outcome.push);
|
|
24876
|
+
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.`;
|
|
24595
24878
|
}
|
|
24596
24879
|
},
|
|
24597
24880
|
{
|
|
@@ -24621,8 +24904,8 @@ function handlers() {
|
|
|
24621
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"]),
|
|
24622
24905
|
run: async (args) => {
|
|
24623
24906
|
const target = await project(args);
|
|
24624
|
-
const result = await api.post(`/projects/${target.id}/threads`, { subject: String(args.subject), body: String(args.body), task: args.task });
|
|
24625
|
-
return `Started ${result.thread.id}${result.thread.taskKey ? ` on ${result.thread.taskKey}` : " (general)"}${result.delivered ? `; reached ${result.delivered} live session(s)` : ""}.`;
|
|
24907
|
+
const result = await api.post(`/projects/${target.id}/threads`, { subject: String(args.subject), body: String(args.body), task: args.task, to: args.to });
|
|
24908
|
+
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)` : ""}.`;
|
|
24626
24909
|
}
|
|
24627
24910
|
},
|
|
24628
24911
|
{
|
|
@@ -24642,7 +24925,15 @@ function handlers() {
|
|
|
24642
24925
|
name: "thread",
|
|
24643
24926
|
description: "Read a thread in full (marks it read).",
|
|
24644
24927
|
inputSchema: schema({ id: str("Thread id"), project: PROJECT }, ["id"]),
|
|
24645
|
-
run: async (args) =>
|
|
24928
|
+
run: async (args) => {
|
|
24929
|
+
const query = new URLSearchParams({ limit: String(Math.max(1, Number(args.limit ?? 40))) });
|
|
24930
|
+
if (args.before) query.set("before", String(args.before));
|
|
24931
|
+
const detail = await api.get(`/projects/${(await project(args)).id}/threads/${String(args.id)}?${query}`);
|
|
24932
|
+
const earlier = detail.earlier > 0 && detail.messages[0] ? `\u2026 ${detail.earlier} earlier (before: ${detail.messages[0].createdAt})
|
|
24933
|
+
|
|
24934
|
+
` : "";
|
|
24935
|
+
return earlier + threadBlock(detail);
|
|
24936
|
+
}
|
|
24646
24937
|
},
|
|
24647
24938
|
{
|
|
24648
24939
|
name: "sessions",
|
|
@@ -24650,7 +24941,8 @@ function handlers() {
|
|
|
24650
24941
|
inputSchema: schema({ project: PROJECT }),
|
|
24651
24942
|
run: async (args) => {
|
|
24652
24943
|
const list = await api.get(`/projects/${(await project(args)).id}/sessions`);
|
|
24653
|
-
|
|
24944
|
+
const mine = held?.session?.id ?? null;
|
|
24945
|
+
return list.length ? list.map((session) => sessionLine(session, mine)).join("\n") : "Nobody is live.";
|
|
24654
24946
|
}
|
|
24655
24947
|
},
|
|
24656
24948
|
{
|
|
@@ -24730,7 +25022,15 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
24730
25022
|
const doneKeys = new Set(target.statuses.filter((status) => status.category === "done").map((status) => status.key));
|
|
24731
25023
|
const only = args.status ? resolveStatus(target, String(args.status)) : null;
|
|
24732
25024
|
const rows = list.filter((task) => (args.all || args.archived || !doneKeys.has(task.status)) && (!me || task.assignee?.userId === me) && (!only || task.status === only));
|
|
24733
|
-
|
|
25025
|
+
const offset = Math.max(0, Number(args.offset ?? 0));
|
|
25026
|
+
const limit = Math.max(1, Number(args.limit ?? 50));
|
|
25027
|
+
const page = rows.slice(offset, offset + limit);
|
|
25028
|
+
const more = rows.length - offset - page.length;
|
|
25029
|
+
if (rows.length === 0) return "No tasks.";
|
|
25030
|
+
return [
|
|
25031
|
+
...page.map((task) => `${task.key} ${target.statuses.find((status) => status.key === task.status)?.label ?? task.status} ${task.severity} ${task.assignee?.name ?? "-"} ${task.title}${task.progress.total ? ` (${task.progress.done}/${task.progress.total})` : ""}`),
|
|
25032
|
+
...more > 0 ? [`\u2026 ${more} more (offset: ${offset + page.length})`] : []
|
|
25033
|
+
].join("\n");
|
|
24734
25034
|
}
|
|
24735
25035
|
},
|
|
24736
25036
|
{
|
|
@@ -24805,8 +25105,13 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
24805
25105
|
inputSchema: schema({ task: str("Task key"), project: PROJECT }, ["task"]),
|
|
24806
25106
|
run: async (args) => {
|
|
24807
25107
|
const target = await project(args);
|
|
24808
|
-
const
|
|
24809
|
-
|
|
25108
|
+
const query = new URLSearchParams({ limit: String(Math.max(1, Number(args.limit ?? 50))) });
|
|
25109
|
+
if (args.before) query.set("before", String(args.before));
|
|
25110
|
+
const events = await api.get(`${taskPath(target.id, String(args.task))}/events?${query}`);
|
|
25111
|
+
if (events.length === 0) return "No history yet.";
|
|
25112
|
+
const lines = events.map((record2) => `${record2.createdAt.slice(0, 16).replace("T", " ")} ${record2.actor.name} ${JSON.stringify(record2.event)}`);
|
|
25113
|
+
const full = events.length >= Number(query.get("limit"));
|
|
25114
|
+
return [...full ? [`\u2026 earlier history: pass before: ${events[0].createdAt}`] : [], ...lines].join("\n");
|
|
24810
25115
|
}
|
|
24811
25116
|
},
|
|
24812
25117
|
{
|
|
@@ -24887,7 +25192,7 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
24887
25192
|
}
|
|
24888
25193
|
function readJson(path3) {
|
|
24889
25194
|
try {
|
|
24890
|
-
return JSON.parse(
|
|
25195
|
+
return JSON.parse(readFileSync11(path3, "utf8"));
|
|
24891
25196
|
} catch {
|
|
24892
25197
|
return {};
|
|
24893
25198
|
}
|
|
@@ -24900,49 +25205,54 @@ function installClaude() {
|
|
|
24900
25205
|
} catch {
|
|
24901
25206
|
done.push("Claude Code: run `claude mcp add --scope user slop -- slop mcp` (the claude command was not found here)");
|
|
24902
25207
|
}
|
|
24903
|
-
const dir =
|
|
24904
|
-
const path3 =
|
|
24905
|
-
|
|
25208
|
+
const dir = join8(homedir3(), ".claude");
|
|
25209
|
+
const path3 = join8(dir, "settings.json");
|
|
25210
|
+
mkdirSync7(dir, { recursive: true });
|
|
24906
25211
|
const settings = readJson(path3);
|
|
24907
|
-
settings.hooks = mergeHooks(settings.hooks ?? {});
|
|
24908
|
-
settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "
|
|
25212
|
+
settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
|
|
25213
|
+
settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "21600000" };
|
|
24909
25214
|
writeFileSync7(path3, `${JSON.stringify(settings, null, 2)}
|
|
24910
25215
|
`);
|
|
24911
|
-
done.push(`Claude Code: PostToolUse and Stop hooks and MCP_TOOL_TIMEOUT written to ${path3}`);
|
|
25216
|
+
done.push(`Claude Code: SessionStart, PostToolUse and Stop hooks and MCP_TOOL_TIMEOUT written to ${path3}`);
|
|
24912
25217
|
return done;
|
|
24913
25218
|
}
|
|
24914
|
-
function mergeHooks(hooks) {
|
|
24915
|
-
const
|
|
24916
|
-
|
|
24917
|
-
|
|
24918
|
-
|
|
24919
|
-
|
|
24920
|
-
|
|
24921
|
-
|
|
24922
|
-
}
|
|
24923
|
-
|
|
25219
|
+
function mergeHooks(hooks, harness) {
|
|
25220
|
+
const ensure = (event, verb, timeout, statusMessage) => {
|
|
25221
|
+
const command = `slop hook ${verb} --harness ${harness}`;
|
|
25222
|
+
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 }] });
|
|
25226
|
+
hooks[event] = entries;
|
|
25227
|
+
};
|
|
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");
|
|
25231
|
+
return hooks;
|
|
24924
25232
|
}
|
|
24925
25233
|
function installCodex() {
|
|
24926
25234
|
const done = [];
|
|
24927
|
-
const dir =
|
|
24928
|
-
|
|
24929
|
-
const
|
|
24930
|
-
const current =
|
|
24931
|
-
if (/^\[mcp_servers\.slop\]/m.test(current))
|
|
24932
|
-
|
|
24933
|
-
|
|
25235
|
+
const dir = join8(homedir3(), ".codex");
|
|
25236
|
+
mkdirSync7(dir, { recursive: true });
|
|
25237
|
+
const configPath3 = join8(dir, "config.toml");
|
|
25238
|
+
const current = existsSync8(configPath3) ? readFileSync11(configPath3, "utf8") : "";
|
|
25239
|
+
if (/^\[mcp_servers\.slop\]/m.test(current)) {
|
|
25240
|
+
if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync7(configPath3, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
|
|
25241
|
+
done.push(`Codex: MCP server already in ${configPath3}`);
|
|
25242
|
+
} else {
|
|
25243
|
+
writeFileSync7(configPath3, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}[mcp_servers.slop]
|
|
24934
25244
|
command = "slop"
|
|
24935
25245
|
args = ["mcp"]
|
|
24936
|
-
tool_timeout_sec =
|
|
25246
|
+
tool_timeout_sec = 21600
|
|
24937
25247
|
`);
|
|
24938
|
-
done.push(`Codex: MCP server "slop" added to ${
|
|
25248
|
+
done.push(`Codex: MCP server "slop" added to ${configPath3}`);
|
|
24939
25249
|
}
|
|
24940
|
-
const hooksPath =
|
|
25250
|
+
const hooksPath = join8(dir, "hooks.json");
|
|
24941
25251
|
const file2 = readJson(hooksPath);
|
|
24942
|
-
file2.hooks = mergeHooks(file2.hooks ?? {});
|
|
25252
|
+
file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
|
|
24943
25253
|
writeFileSync7(hooksPath, `${JSON.stringify(file2, null, 2)}
|
|
24944
25254
|
`);
|
|
24945
|
-
done.push(`Codex: PostToolUse and Stop hooks written to ${hooksPath}`);
|
|
25255
|
+
done.push(`Codex: SessionStart, PostToolUse and Stop hooks written to ${hooksPath}`);
|
|
24946
25256
|
return done;
|
|
24947
25257
|
}
|
|
24948
25258
|
async function trustCodexHooks() {
|
|
@@ -24964,7 +25274,7 @@ async function trustCodexHooks() {
|
|
|
24964
25274
|
}
|
|
24965
25275
|
});
|
|
24966
25276
|
let nextId = 1;
|
|
24967
|
-
const call = (method, params) => new Promise((
|
|
25277
|
+
const call = (method, params) => new Promise((resolve3, reject) => {
|
|
24968
25278
|
const id = nextId++;
|
|
24969
25279
|
const timer = setTimeout(() => {
|
|
24970
25280
|
pending.delete(id);
|
|
@@ -24973,7 +25283,7 @@ async function trustCodexHooks() {
|
|
|
24973
25283
|
pending.set(id, (message) => {
|
|
24974
25284
|
clearTimeout(timer);
|
|
24975
25285
|
if (message.error) reject(new Error(message.error.message ?? `${method} failed`));
|
|
24976
|
-
else
|
|
25286
|
+
else resolve3(message.result);
|
|
24977
25287
|
});
|
|
24978
25288
|
server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
24979
25289
|
`);
|
|
@@ -24981,10 +25291,10 @@ async function trustCodexHooks() {
|
|
|
24981
25291
|
const notify = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
24982
25292
|
`);
|
|
24983
25293
|
try {
|
|
24984
|
-
await call("initialize", { clientInfo: { name: "slop", version: "0.
|
|
25294
|
+
await call("initialize", { clientInfo: { name: "slop", version: "0.4.0" }, capabilities: { experimentalApi: true } });
|
|
24985
25295
|
notify("initialized", {});
|
|
24986
25296
|
const listed = await call("hooks/list", { cwds: [process.cwd()] });
|
|
24987
|
-
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command
|
|
25297
|
+
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
24988
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";
|
|
24989
25299
|
const untrusted = ours.filter((hook) => hook.trustStatus !== "trusted");
|
|
24990
25300
|
if (untrusted.length === 0) return "Codex: the slop hooks are already trusted";
|
|
@@ -24997,21 +25307,33 @@ async function trustCodexHooks() {
|
|
|
24997
25307
|
server.kill();
|
|
24998
25308
|
}
|
|
24999
25309
|
}
|
|
25000
|
-
async function
|
|
25001
|
-
const input2 = await new Promise((
|
|
25310
|
+
async function hookInput(harness) {
|
|
25311
|
+
const input2 = await new Promise((resolve3) => {
|
|
25002
25312
|
let data = "";
|
|
25003
25313
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
25004
|
-
process.stdin.on("end", () =>
|
|
25005
|
-
setTimeout(() =>
|
|
25314
|
+
process.stdin.on("end", () => resolve3(data));
|
|
25315
|
+
setTimeout(() => resolve3(data), 2e3);
|
|
25006
25316
|
});
|
|
25317
|
+
let cwd = process.cwd();
|
|
25318
|
+
let conversation = null;
|
|
25007
25319
|
try {
|
|
25008
|
-
|
|
25320
|
+
const parsed = JSON.parse(input2);
|
|
25321
|
+
cwd = parsed.cwd ?? cwd;
|
|
25322
|
+
conversation = parsed.session_id ?? null;
|
|
25009
25323
|
} catch {
|
|
25010
|
-
return process.cwd();
|
|
25011
25324
|
}
|
|
25325
|
+
if (conversation) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
|
|
25326
|
+
return { cwd, conversation };
|
|
25327
|
+
}
|
|
25328
|
+
async function startHook(harness) {
|
|
25329
|
+
const { conversation } = await hookInput(harness);
|
|
25330
|
+
if (!conversation) return;
|
|
25331
|
+
const text = `On Slopware Tasks you are ${agentNameFor(conversation)}: that is how people and other agents address you (session who lists everyone).`;
|
|
25332
|
+
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } })}
|
|
25333
|
+
`);
|
|
25012
25334
|
}
|
|
25013
|
-
async function tickHook() {
|
|
25014
|
-
const arrived = inboxDrain(await
|
|
25335
|
+
async function tickHook(harness) {
|
|
25336
|
+
const arrived = inboxDrain((await hookInput(harness)).cwd);
|
|
25015
25337
|
if (arrived.length === 0) return;
|
|
25016
25338
|
const text = `Messages arrived on Slopware Tasks while you worked:
|
|
25017
25339
|
|
|
@@ -25021,8 +25343,8 @@ Act or answer with the slop MCP \`reply\` tool, then carry on.`;
|
|
|
25021
25343
|
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } })}
|
|
25022
25344
|
`);
|
|
25023
25345
|
}
|
|
25024
|
-
async function stopHook() {
|
|
25025
|
-
const cwd = await
|
|
25346
|
+
async function stopHook(harness) {
|
|
25347
|
+
const { cwd } = await hookInput(harness);
|
|
25026
25348
|
process.chdir(cwd);
|
|
25027
25349
|
const parts = inboxDrain(cwd).map(pushBlock);
|
|
25028
25350
|
const repo = findRepo();
|
|
@@ -25044,8 +25366,20 @@ ${parts.join("\n\n")}` })}
|
|
|
25044
25366
|
}
|
|
25045
25367
|
const general = await api.get(`/projects/${project.id}/threads?scope=general&unread=1&read=1`);
|
|
25046
25368
|
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:
|
|
25373
|
+
|
|
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.` })}
|
|
25378
|
+
`);
|
|
25379
|
+
return;
|
|
25380
|
+
}
|
|
25047
25381
|
if (parts.length === 0) return;
|
|
25048
|
-
process.stdout.write(`${JSON.stringify({ decision: "block", reason: `Messages arrived on Slopware Tasks while you worked. Read them (
|
|
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.
|
|
25049
25383
|
|
|
25050
25384
|
${parts.join("\n")}` })}
|
|
25051
25385
|
`);
|
|
@@ -25072,13 +25406,13 @@ function tools() {
|
|
|
25072
25406
|
"board",
|
|
25073
25407
|
"The project: tasks on the board, columns, members, projects.",
|
|
25074
25408
|
{ list: "list_tasks", columns: "columns", members: "members", projects: "projects" },
|
|
25075
|
-
{ status: str("list: only this column"), mine: { type: "boolean" }, all: { type: "boolean", description: "list: include done" }, archived: { type: "boolean" } }
|
|
25409
|
+
{ status: str("list: only this column"), mine: { type: "boolean" }, all: { type: "boolean", description: "list: include done" }, archived: { type: "boolean" }, limit: int2("list: rows (default 50)"), offset: int2("list: skip") }
|
|
25076
25410
|
),
|
|
25077
25411
|
group(
|
|
25078
25412
|
"task",
|
|
25079
25413
|
"One task. show; create; update (only given fields change); move; link; archive/restore; history; finish (first done column, releases your session).",
|
|
25080
25414
|
{ show: "show_task", create: "create_task", update: "update_task", move: "move_task", link: "link_task", archive: "archive_task", restore: "archive_task", history: "task_history", finish: "finish_task" },
|
|
25081
|
-
{ task: str("Key, e.g. WEB-12 (all but create)"), title: str(""), description: str("Markdown"), notes: str("scratchpad, Markdown"), severity: str("low|medium|high|critical"), column: str("column key or label"), assignee: str("email, name, me, or none"), url: str("link: the URL") }
|
|
25415
|
+
{ task: str("Key, e.g. WEB-12 (all but create)"), title: str(""), description: str("Markdown"), notes: str("scratchpad, Markdown"), severity: str("low|medium|high|critical"), column: str("column key or label"), assignee: str("email, name, me, or none"), url: str("link: the URL"), limit: int2("history: newest N (default 50)"), before: str("history: page back from this time") }
|
|
25082
25416
|
),
|
|
25083
25417
|
group(
|
|
25084
25418
|
"plan",
|
|
@@ -25088,16 +25422,16 @@ function tools() {
|
|
|
25088
25422
|
),
|
|
25089
25423
|
group(
|
|
25090
25424
|
"session",
|
|
25091
|
-
"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
|
|
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).",
|
|
25092
25426
|
{ claim: "claim_task", release: "release_task", who: "sessions", check: "check_messages", wait: "wait_for_message" },
|
|
25093
|
-
{ task: str("Task key"), minutes: int2("wait: give up after (default
|
|
25427
|
+
{ task: str("Task key"), minutes: int2("wait: give up after (default 300)") }
|
|
25094
25428
|
),
|
|
25095
|
-
group("threads", "Talk between agents and people. list threads; read one (marks it read).", { list: "threads", read: "thread" }, { task: str("list: about this task"), unread: { type: "boolean" }, id: str("read: thread id") }),
|
|
25429
|
+
group("threads", "Talk between agents and people. list threads; read one (marks it read; newest 40 unless limit/before).", { list: "threads", read: "thread" }, { task: str("list: about this task"), unread: { type: "boolean" }, id: str("read: thread id"), limit: int2("read: newest N"), before: str("read: page back from this time") }),
|
|
25096
25430
|
group(
|
|
25097
25431
|
"say",
|
|
25098
|
-
"post: start a thread (with task: reaches whoever is live on it; without: general, for people). reply: in a thread; reaches its task's live sessions and its
|
|
25432
|
+
"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.",
|
|
25099
25433
|
{ post: "post", reply: "reply" },
|
|
25100
|
-
{ subject: str("post"), body: str("Markdown"), task: str("post: about this task"), thread: str("reply: thread id") }
|
|
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") }
|
|
25101
25435
|
)
|
|
25102
25436
|
];
|
|
25103
25437
|
}
|
|
@@ -25105,7 +25439,7 @@ function withInbox(list, version2) {
|
|
|
25105
25439
|
return list.map((tool) => ({
|
|
25106
25440
|
...tool,
|
|
25107
25441
|
run: async (args) => {
|
|
25108
|
-
const result = await tool.run(args);
|
|
25442
|
+
const result = bounded(await tool.run(args));
|
|
25109
25443
|
const parts = [result];
|
|
25110
25444
|
if (!(tool.name === "session" && (args.action === "check" || args.action === "wait"))) {
|
|
25111
25445
|
const arrived = inboxDrain(process.cwd());
|
|
@@ -25133,14 +25467,16 @@ function registerMcpCommands(program3, version2) {
|
|
|
25133
25467
|
emit({ done: lines }, () => lines.join("\n"));
|
|
25134
25468
|
});
|
|
25135
25469
|
const hook = program3.command("hook").description("Harness hooks (installed by slop mcp install).");
|
|
25136
|
-
|
|
25137
|
-
hook.command("
|
|
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)));
|
|
25138
25474
|
}
|
|
25139
25475
|
|
|
25140
25476
|
// src/commands/sessions.ts
|
|
25141
|
-
import { readFileSync as
|
|
25477
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
25142
25478
|
var taskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
25143
|
-
var
|
|
25479
|
+
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"];
|
|
25144
25480
|
function registerSessionCommands(program3, task) {
|
|
25145
25481
|
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
25146
25482
|
const session = program3.command("session").description("Claim a task and be reachable on it while you work.");
|
|
@@ -25171,7 +25507,8 @@ function registerSessionCommands(program3, task) {
|
|
|
25171
25507
|
`);
|
|
25172
25508
|
const outcome = await waitForMessage(api, project.id, key, {
|
|
25173
25509
|
timeoutMs: options.timeout ? Number(options.timeout) * 1e3 : 0,
|
|
25174
|
-
|
|
25510
|
+
claim: agentIdentity(process.cwd(), "directory"),
|
|
25511
|
+
onHello: (session2) => note2(isJsonMode() ? JSON.stringify({ live: session2 }) : `live on ${session2.taskKey} as ${session2.agent?.name ?? "agent"} for ${session2.user.name} (${session2.machine.name}); waiting\u2026`),
|
|
25175
25512
|
onMessage: options.follow ? (push) => {
|
|
25176
25513
|
emit(push, () => `${pushBlock(push)}
|
|
25177
25514
|
`);
|
|
@@ -25196,7 +25533,7 @@ function registerSessionCommands(program3, task) {
|
|
|
25196
25533
|
const api = authedApi();
|
|
25197
25534
|
const project = await resolveProject(api, options.project);
|
|
25198
25535
|
const sessions = await api.get(`/projects/${project.id}/sessions`);
|
|
25199
|
-
emit(sessions, () => sessions.length === 0 ? "Nobody is live." : table([["TASK", "WHO", "MACHINE", "SINCE"], ...sessions.map(
|
|
25536
|
+
emit(sessions, () => sessions.length === 0 ? "Nobody is live." : table([["AGENT", "TASK", "WHO", "MACHINE", "WHERE", "SINCE", "STATE"], ...sessions.map(sessionLine2)]));
|
|
25200
25537
|
});
|
|
25201
25538
|
}
|
|
25202
25539
|
|
|
@@ -25222,13 +25559,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
|
|
|
25222
25559
|
}
|
|
25223
25560
|
|
|
25224
25561
|
// src/index.ts
|
|
25225
|
-
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.
|
|
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)");
|
|
25226
25563
|
registerAuthCommands(program2);
|
|
25227
25564
|
registerProjectCommands(program2);
|
|
25228
25565
|
var taskCommand = registerTaskCommands(program2);
|
|
25229
25566
|
registerSessionCommands(program2, taskCommand);
|
|
25230
25567
|
registerThreadCommands(program2, taskCommand);
|
|
25231
|
-
registerMcpCommands(program2, "0.
|
|
25568
|
+
registerMcpCommands(program2, "0.4.0");
|
|
25232
25569
|
registerGuideCommand(program2);
|
|
25233
25570
|
registerSkillCommands(program2);
|
|
25234
25571
|
program2.parseAsync().catch(fail);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slopwaresystems",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@types/node": "^26.6.1",
|
|
19
|
+
"commander": "^15.0.0",
|
|
20
|
+
"fast-check": "^4.10.1",
|
|
19
21
|
"tsup": "^8.5.1",
|
|
20
22
|
"typescript": "^6.0.3",
|
|
21
23
|
"vitest": "^5.0.1",
|
|
22
|
-
"commander": "^15.0.0",
|
|
23
24
|
"zod": "^4.6.5",
|
|
24
25
|
"@slop/contract": "0.1.0"
|
|
25
26
|
},
|