slopwaresystems 0.3.0 → 0.4.1
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 +908 -190
- 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` (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
|
|
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 (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, when your turn ends the Stop hook waits on your behalf, costing nothing, and continues your turn the moment a message arrives; your person typing comes first and you wait again after. Otherwise it arrives with your next tool result. Act or answer it (`say` `reply`), then carry on. If your person makes you not visible to teammates, nothing reaches you and you cannot post or reply until they switch it back.\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";
|
|
@@ -3398,11 +3398,11 @@ function installSkills({ force = false } = {}) {
|
|
|
3398
3398
|
const written = [];
|
|
3399
3399
|
for (const home of SKILL_HOMES) {
|
|
3400
3400
|
for (const [name, markdown] of Object.entries(SKILLS)) {
|
|
3401
|
-
const
|
|
3402
|
-
if (!force && existsSync(
|
|
3401
|
+
const file3 = join(home, name, "SKILL.md");
|
|
3402
|
+
if (!force && existsSync(file3) && readFileSync(file3, "utf8") === markdown) continue;
|
|
3403
3403
|
mkdirSync(join(home, name), { recursive: true });
|
|
3404
|
-
writeFileSync(
|
|
3405
|
-
written.push(
|
|
3404
|
+
writeFileSync(file3, markdown);
|
|
3405
|
+
written.push(file3);
|
|
3406
3406
|
}
|
|
3407
3407
|
}
|
|
3408
3408
|
return written;
|
|
@@ -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, 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, 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, resolve5);
|
|
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, resolve5) : 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 ?? (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
|
|
19058
|
-
if (
|
|
19057
|
+
const parent2 = schema2._zod.parent;
|
|
19058
|
+
if (parent2) {
|
|
19059
19059
|
if (!result.ref)
|
|
19060
|
-
result.ref =
|
|
19061
|
-
processSchema(
|
|
19062
|
-
ctx.seen.get(
|
|
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
|
|
19326
|
-
if (
|
|
19327
|
-
flattenRef(
|
|
19328
|
-
const parentSeen = ctx.seen.get(
|
|
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) {
|
|
@@ -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({
|
|
@@ -23314,6 +23320,8 @@ var ThreadMessage = external_exports.object({
|
|
|
23314
23320
|
/** A person on the web, or an agent through the CLI (with its machine). */
|
|
23315
23321
|
via: MessageVia,
|
|
23316
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(),
|
|
23317
23325
|
body: external_exports.string(),
|
|
23318
23326
|
createdAt: Timestamp
|
|
23319
23327
|
});
|
|
@@ -23324,20 +23332,25 @@ var Thread = external_exports.object({
|
|
|
23324
23332
|
taskId: Id.nullable(),
|
|
23325
23333
|
taskKey: external_exports.string().nullable(),
|
|
23326
23334
|
subject: external_exports.string(),
|
|
23335
|
+
/** Addressed to one live agent (by its session); replies reach it wherever it is. Null: to the task's agents, or general. */
|
|
23336
|
+
to: external_exports.object({ sessionId: external_exports.string(), agent: external_exports.string() }).nullable(),
|
|
23327
23337
|
createdBy: UserRef,
|
|
23328
23338
|
createdAt: Timestamp,
|
|
23329
23339
|
lastMessageAt: Timestamp,
|
|
23330
23340
|
messageCount: external_exports.number(),
|
|
23331
23341
|
/** Messages the viewer has not read. Their own count as read. */
|
|
23332
23342
|
unread: external_exports.number(),
|
|
23333
|
-
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()
|
|
23334
23344
|
});
|
|
23335
|
-
var ThreadDetail = Thread.extend({ messages: external_exports.array(ThreadMessage) });
|
|
23345
|
+
var ThreadDetail = Thread.extend({ messages: external_exports.array(ThreadMessage), earlier: external_exports.number().default(0) });
|
|
23336
23346
|
var body = external_exports.string().trim().min(1, "Say something").max(8e3);
|
|
23337
23347
|
var CreateThreadInput = external_exports.object({
|
|
23338
|
-
|
|
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(),
|
|
23339
23350
|
/** A task key or number; absent means general. */
|
|
23340
23351
|
task: external_exports.string().trim().min(1).optional(),
|
|
23352
|
+
/** A live agent, by session id or name (as `session who` lists them). */
|
|
23353
|
+
to: external_exports.string().trim().min(1).max(200).optional(),
|
|
23341
23354
|
body
|
|
23342
23355
|
});
|
|
23343
23356
|
var ReplyInput = external_exports.object({ body });
|
|
@@ -23440,14 +23453,35 @@ var TaskEventRecord = external_exports.object({
|
|
|
23440
23453
|
});
|
|
23441
23454
|
|
|
23442
23455
|
// ../contract/src/work/sessions.ts
|
|
23456
|
+
var AgentHarness = external_exports.enum(["claude", "codex", "other"]);
|
|
23443
23457
|
var Session = external_exports.object({
|
|
23458
|
+
/** Derived from the person, the machine and the agent's conversation: the same across reconnects and resumes. */
|
|
23444
23459
|
id: Id,
|
|
23445
23460
|
/** 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
23461
|
taskId: Id.nullable(),
|
|
23447
23462
|
taskKey: external_exports.string().nullable(),
|
|
23448
23463
|
user: UserRef,
|
|
23449
23464
|
machine: external_exports.object({ id: Id, name: external_exports.string() }),
|
|
23450
|
-
|
|
23465
|
+
/** The agent itself: a generated name (see agent-names) and which harness runs it. */
|
|
23466
|
+
agent: external_exports.object({ name: external_exports.string(), harness: AgentHarness.nullable() }),
|
|
23467
|
+
/** Where it runs, to tell two agents under one person apart. */
|
|
23468
|
+
where: external_exports.object({ directory: external_exports.string().nullable(), branch: external_exports.string().nullable() }),
|
|
23469
|
+
startedAt: Timestamp,
|
|
23470
|
+
/** "reconnecting": its socket dropped and the room is holding the session open for it to come back. */
|
|
23471
|
+
state: external_exports.enum(["live", "reconnecting"]),
|
|
23472
|
+
/**
|
|
23473
|
+
* Reachable by teammates: listed to them, messaged by them and by their agents, and able to message. Off, the agent
|
|
23474
|
+
* still works the board but talks to nobody and is listed only to its own person. Starts from the person's default.
|
|
23475
|
+
*/
|
|
23476
|
+
reachable: external_exports.boolean().default(true)
|
|
23477
|
+
});
|
|
23478
|
+
var ReachableInput = external_exports.object({ on: external_exports.boolean() });
|
|
23479
|
+
var SessionClaimInput = external_exports.object({
|
|
23480
|
+
/** 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. */
|
|
23481
|
+
conversation: external_exports.string().trim().min(1).max(200).optional(),
|
|
23482
|
+
harness: AgentHarness.nullable().default(null),
|
|
23483
|
+
directory: external_exports.string().trim().max(200).nullable().default(null),
|
|
23484
|
+
branch: external_exports.string().trim().max(200).nullable().default(null)
|
|
23451
23485
|
});
|
|
23452
23486
|
var ConnectTicket = external_exports.object({ ticket: external_exports.string(), path: external_exports.string() });
|
|
23453
23487
|
var AskInput = external_exports.object({ body: external_exports.string().trim().min(1, "Say something").max(4e3) });
|
|
@@ -23455,13 +23489,127 @@ var SessionPush = external_exports.discriminatedUnion("type", [
|
|
|
23455
23489
|
external_exports.object({ type: external_exports.literal("hello"), session: Session }),
|
|
23456
23490
|
external_exports.object({ type: external_exports.literal("event"), record: TaskEventRecord }),
|
|
23457
23491
|
/** 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 }),
|
|
23492
|
+
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
23493
|
/** The room is closing this session (someone ran `slop session end`). */
|
|
23460
23494
|
external_exports.object({ type: external_exports.literal("end") }),
|
|
23495
|
+
/** Reachability was switched from the web. Off releases a waiting agent: nothing can reach it any more. */
|
|
23496
|
+
external_exports.object({ type: external_exports.literal("reachable"), on: external_exports.boolean() }),
|
|
23461
23497
|
/** To board watchers: something in the project changed; refetch. */
|
|
23462
23498
|
external_exports.object({ type: external_exports.literal("changed"), scope: external_exports.string() })
|
|
23463
23499
|
]);
|
|
23464
23500
|
|
|
23501
|
+
// ../contract/src/work/agent-names.ts
|
|
23502
|
+
var AGENT_NAMES = [
|
|
23503
|
+
"Albert",
|
|
23504
|
+
"Alma",
|
|
23505
|
+
"Amos",
|
|
23506
|
+
"Astrid",
|
|
23507
|
+
"August",
|
|
23508
|
+
"Beatrix",
|
|
23509
|
+
"Bruno",
|
|
23510
|
+
"Cedric",
|
|
23511
|
+
"Clara",
|
|
23512
|
+
"Cosmo",
|
|
23513
|
+
"Dahlia",
|
|
23514
|
+
"Dexter",
|
|
23515
|
+
"Edie",
|
|
23516
|
+
"Elio",
|
|
23517
|
+
"Ember",
|
|
23518
|
+
"Felix",
|
|
23519
|
+
"Fern",
|
|
23520
|
+
"Flora",
|
|
23521
|
+
"Gus",
|
|
23522
|
+
"Hazel",
|
|
23523
|
+
"Hugo",
|
|
23524
|
+
"Ida",
|
|
23525
|
+
"Ingrid",
|
|
23526
|
+
"Iris",
|
|
23527
|
+
"Jasper",
|
|
23528
|
+
"Juno",
|
|
23529
|
+
"Kit",
|
|
23530
|
+
"Leo",
|
|
23531
|
+
"Lila",
|
|
23532
|
+
"Linus",
|
|
23533
|
+
"Lotte",
|
|
23534
|
+
"Mabel",
|
|
23535
|
+
"Marlow",
|
|
23536
|
+
"Milo",
|
|
23537
|
+
"Minnie",
|
|
23538
|
+
"Nell",
|
|
23539
|
+
"Nico",
|
|
23540
|
+
"Nina",
|
|
23541
|
+
"Odette",
|
|
23542
|
+
"Olive",
|
|
23543
|
+
"Oscar",
|
|
23544
|
+
"Otto",
|
|
23545
|
+
"Pearl",
|
|
23546
|
+
"Percy",
|
|
23547
|
+
"Pip",
|
|
23548
|
+
"Quill",
|
|
23549
|
+
"Remy",
|
|
23550
|
+
"Rosa",
|
|
23551
|
+
"Rufus",
|
|
23552
|
+
"Sable",
|
|
23553
|
+
"Silas",
|
|
23554
|
+
"Sky",
|
|
23555
|
+
"Sol",
|
|
23556
|
+
"Tess",
|
|
23557
|
+
"Theo",
|
|
23558
|
+
"Tilly",
|
|
23559
|
+
"Ulla",
|
|
23560
|
+
"Vera",
|
|
23561
|
+
"Vito",
|
|
23562
|
+
"Wren",
|
|
23563
|
+
"Xavi",
|
|
23564
|
+
"Yara",
|
|
23565
|
+
"Zed",
|
|
23566
|
+
"Zora",
|
|
23567
|
+
"Arlo",
|
|
23568
|
+
"Bea",
|
|
23569
|
+
"Cyrus",
|
|
23570
|
+
"Della",
|
|
23571
|
+
"Enzo",
|
|
23572
|
+
"Frida",
|
|
23573
|
+
"Gilda",
|
|
23574
|
+
"Hal",
|
|
23575
|
+
"Imre",
|
|
23576
|
+
"Joss",
|
|
23577
|
+
"Kai",
|
|
23578
|
+
"Lena",
|
|
23579
|
+
"Mona",
|
|
23580
|
+
"Nate",
|
|
23581
|
+
"Opal",
|
|
23582
|
+
"Pia",
|
|
23583
|
+
"Rae",
|
|
23584
|
+
"Sacha",
|
|
23585
|
+
"Tove",
|
|
23586
|
+
"Uma",
|
|
23587
|
+
"Vince",
|
|
23588
|
+
"Willa",
|
|
23589
|
+
"Yuki",
|
|
23590
|
+
"Zelda",
|
|
23591
|
+
"Ansel",
|
|
23592
|
+
"Birdie",
|
|
23593
|
+
"Cleo",
|
|
23594
|
+
"Dov",
|
|
23595
|
+
"Esme",
|
|
23596
|
+
"Fitz",
|
|
23597
|
+
"Greta",
|
|
23598
|
+
"Hollis",
|
|
23599
|
+
"Inez",
|
|
23600
|
+
"Jude",
|
|
23601
|
+
"Kip",
|
|
23602
|
+
"Lark"
|
|
23603
|
+
];
|
|
23604
|
+
function agentNameFor(key) {
|
|
23605
|
+
let hash2 = 2166136261;
|
|
23606
|
+
for (let index = 0; index < key.length; index += 1) {
|
|
23607
|
+
hash2 ^= key.charCodeAt(index);
|
|
23608
|
+
hash2 = Math.imul(hash2, 16777619) >>> 0;
|
|
23609
|
+
}
|
|
23610
|
+
return AGENT_NAMES[hash2 % AGENT_NAMES.length] ?? AGENT_NAMES[0];
|
|
23611
|
+
}
|
|
23612
|
+
|
|
23465
23613
|
// ../contract/src/work/bindings.ts
|
|
23466
23614
|
var RepoInfo = external_exports.object({
|
|
23467
23615
|
/**
|
|
@@ -23520,7 +23668,7 @@ function resolveSession() {
|
|
|
23520
23668
|
|
|
23521
23669
|
// src/platform/session-file.ts
|
|
23522
23670
|
import { createHash } from "crypto";
|
|
23523
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
23671
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
23524
23672
|
import { dirname as dirname2, join as join3 } from "path";
|
|
23525
23673
|
var sessionsDir = () => join3(dirname2(configPath()), "sessions");
|
|
23526
23674
|
var realCwd = (cwd) => {
|
|
@@ -23540,6 +23688,21 @@ function rememberedSession(cwd) {
|
|
|
23540
23688
|
const path3 = sessionFile(cwd);
|
|
23541
23689
|
return existsSync3(path3) ? readFileSync3(path3, "utf8").trim() || null : null;
|
|
23542
23690
|
}
|
|
23691
|
+
var conversationFile = (cwd) => join3(sessionsDir(), `${createHash("sha256").update(realCwd(cwd)).digest("hex").slice(0, 16)}.conversation.json`);
|
|
23692
|
+
function rememberConversation(cwd, conversation) {
|
|
23693
|
+
mkdirSync3(sessionsDir(), { recursive: true });
|
|
23694
|
+
writeFileSync3(conversationFile(cwd), JSON.stringify(conversation));
|
|
23695
|
+
}
|
|
23696
|
+
function rememberedConversation(cwd, notBefore) {
|
|
23697
|
+
const path3 = conversationFile(cwd);
|
|
23698
|
+
try {
|
|
23699
|
+
if (statSync(path3).mtimeMs < notBefore) return null;
|
|
23700
|
+
const parsed = JSON.parse(readFileSync3(path3, "utf8"));
|
|
23701
|
+
return typeof parsed.id === "string" && parsed.id ? { id: parsed.id, harness: parsed.harness ?? "other" } : null;
|
|
23702
|
+
} catch {
|
|
23703
|
+
return null;
|
|
23704
|
+
}
|
|
23705
|
+
}
|
|
23543
23706
|
|
|
23544
23707
|
// src/platform/errors.ts
|
|
23545
23708
|
var CliError = class extends Error {
|
|
@@ -23775,7 +23938,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
|
|
|
23775
23938
|
- Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
|
|
23776
23939
|
- When it is finished and verified: \`slop task done <KEY>\`.
|
|
23777
23940
|
- 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
|
|
23941
|
+
- 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
23942
|
- 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
23943
|
- 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
23944
|
- Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
|
|
@@ -23805,6 +23968,10 @@ function findRepo() {
|
|
|
23805
23968
|
const root = basename(commonDir) === ".git" ? resolve(commonDir, "..") : worktree ?? commonDir;
|
|
23806
23969
|
return { root, worktree: worktree ?? root, linkPath: join5(commonDir, "slop.json") };
|
|
23807
23970
|
}
|
|
23971
|
+
function currentBranch(cwd = process.cwd()) {
|
|
23972
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
23973
|
+
return branch && branch !== "HEAD" ? branch : null;
|
|
23974
|
+
}
|
|
23808
23975
|
function requireRepo() {
|
|
23809
23976
|
const repo = findRepo();
|
|
23810
23977
|
if (!repo) throw new CliError("Not inside a git repository", "cd into your repository, or pass --project KEY", "no_repo");
|
|
@@ -23979,24 +24146,26 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
|
23979
24146
|
}
|
|
23980
24147
|
|
|
23981
24148
|
// src/commands/mcp.ts
|
|
23982
|
-
import { execFileSync as
|
|
23983
|
-
import {
|
|
23984
|
-
import
|
|
23985
|
-
import {
|
|
23986
|
-
import {
|
|
23987
|
-
import { join as join7 } from "path";
|
|
24149
|
+
import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
|
|
24150
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
24151
|
+
import "path";
|
|
24152
|
+
import { homedir as homedir4 } from "os";
|
|
24153
|
+
import { join as join12 } from "path";
|
|
23988
24154
|
|
|
23989
24155
|
// src/platform/live.ts
|
|
24156
|
+
var PING_EVERY_MS = 25e3;
|
|
24157
|
+
var PONG_WITHIN_MS = 1e4;
|
|
23990
24158
|
async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
23991
|
-
const ticket = await api.post(`/projects/${projectId}/tasks/${encodeURIComponent(taskKey)}/session/ticket
|
|
24159
|
+
const ticket = await api.post(`/projects/${projectId}/tasks/${encodeURIComponent(taskKey)}/session/ticket`, options.claim ?? {});
|
|
23992
24160
|
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(() =>
|
|
24161
|
+
const outcome = await new Promise((resolve5) => {
|
|
24162
|
+
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => resolve5({ kind: "timeout" }), options.timeoutMs) : null;
|
|
23995
24163
|
const done = (result) => {
|
|
23996
24164
|
if (timer) clearTimeout(timer);
|
|
23997
|
-
|
|
24165
|
+
resolve5(result);
|
|
23998
24166
|
};
|
|
23999
24167
|
socket.addEventListener("message", (event) => {
|
|
24168
|
+
if (String(event.data) === "pong") return;
|
|
24000
24169
|
const push = JSON.parse(String(event.data));
|
|
24001
24170
|
if (push.type === "hello") {
|
|
24002
24171
|
api.sessionId = push.session.id;
|
|
@@ -24018,28 +24187,33 @@ async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
|
24018
24187
|
if (outcome.kind !== "message") rememberSession(process.cwd(), null);
|
|
24019
24188
|
if (outcome.kind === "message" || outcome.kind === "timeout") {
|
|
24020
24189
|
socket.close(1e3, outcome.kind === "message" ? "handled" : "timeout");
|
|
24021
|
-
await new Promise((
|
|
24022
|
-
socket.addEventListener("close", () =>
|
|
24023
|
-
setTimeout(
|
|
24190
|
+
await new Promise((resolve5) => {
|
|
24191
|
+
socket.addEventListener("close", () => resolve5(), { once: true });
|
|
24192
|
+
setTimeout(resolve5, 500);
|
|
24024
24193
|
});
|
|
24025
24194
|
}
|
|
24026
24195
|
return outcome;
|
|
24027
24196
|
}
|
|
24028
24197
|
var HeldSession = class {
|
|
24029
|
-
constructor(api, projectId, taskKey,
|
|
24198
|
+
constructor(api, projectId, taskKey, hooks) {
|
|
24030
24199
|
this.api = api;
|
|
24031
24200
|
this.projectId = projectId;
|
|
24032
24201
|
this.taskKey = taskKey;
|
|
24033
|
-
this.
|
|
24202
|
+
this.hooks = hooks;
|
|
24034
24203
|
}
|
|
24035
24204
|
api;
|
|
24036
24205
|
projectId;
|
|
24037
24206
|
taskKey;
|
|
24038
|
-
|
|
24207
|
+
hooks;
|
|
24039
24208
|
socket = null;
|
|
24040
24209
|
stopped = false;
|
|
24041
24210
|
timer = null;
|
|
24211
|
+
heartbeat = null;
|
|
24212
|
+
pongDue = null;
|
|
24042
24213
|
attempt = 0;
|
|
24214
|
+
connected = 0;
|
|
24215
|
+
session = null;
|
|
24216
|
+
/** Whoever is sitting in `session wait`. Handed the next message, or null when standby is switched off under them. */
|
|
24043
24217
|
waiters = /* @__PURE__ */ new Set();
|
|
24044
24218
|
async start() {
|
|
24045
24219
|
await this.connect();
|
|
@@ -24047,30 +24221,49 @@ var HeldSession = class {
|
|
|
24047
24221
|
async connect() {
|
|
24048
24222
|
if (this.stopped) return;
|
|
24049
24223
|
try {
|
|
24050
|
-
const ticket = await this.api.post(this.taskKey ? `/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket` : `/projects/${this.projectId}/session/ticket
|
|
24224
|
+
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
24225
|
const socket = new WebSocket(`${this.api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
24052
24226
|
this.socket = socket;
|
|
24053
24227
|
socket.addEventListener("open", () => {
|
|
24054
24228
|
this.attempt = 0;
|
|
24229
|
+
this.startHeartbeat(socket);
|
|
24055
24230
|
});
|
|
24056
24231
|
socket.addEventListener("message", (event) => {
|
|
24057
|
-
const
|
|
24232
|
+
const data = String(event.data);
|
|
24233
|
+
if (data === "pong") {
|
|
24234
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
24235
|
+
this.pongDue = null;
|
|
24236
|
+
return;
|
|
24237
|
+
}
|
|
24238
|
+
const push = JSON.parse(data);
|
|
24058
24239
|
if (push.type === "hello") {
|
|
24059
24240
|
this.api.sessionId = push.session.id;
|
|
24241
|
+
this.session = push.session;
|
|
24242
|
+
this.taskKey = push.session.taskKey;
|
|
24060
24243
|
rememberSession(process.cwd(), push.session.id);
|
|
24244
|
+
this.connected += 1;
|
|
24245
|
+
if (this.connected > 1) void this.hooks.onReconnect?.(push.session);
|
|
24061
24246
|
return;
|
|
24062
24247
|
}
|
|
24063
24248
|
if (push.type === "end") {
|
|
24064
24249
|
this.stopped = true;
|
|
24065
24250
|
return;
|
|
24066
24251
|
}
|
|
24252
|
+
if (push.type === "reachable") {
|
|
24253
|
+
if (this.session) this.session = { ...this.session, reachable: push.on };
|
|
24254
|
+
if (!push.on) for (const waiter of [...this.waiters]) waiter(null);
|
|
24255
|
+
this.hooks.onReachable?.(push.on);
|
|
24256
|
+
return;
|
|
24257
|
+
}
|
|
24067
24258
|
if (push.type !== "thread") return;
|
|
24068
24259
|
const delivered = { thread: push.thread, message: push.message };
|
|
24069
|
-
|
|
24070
|
-
|
|
24260
|
+
const taken = this.waiters.size > 0;
|
|
24261
|
+
for (const waiter of [...this.waiters]) waiter(delivered);
|
|
24262
|
+
if (!taken) this.hooks.onMessage(delivered);
|
|
24071
24263
|
});
|
|
24072
24264
|
socket.addEventListener("close", () => {
|
|
24073
|
-
this.
|
|
24265
|
+
this.stopHeartbeat();
|
|
24266
|
+
if (this.socket === socket) this.socket = null;
|
|
24074
24267
|
if (!this.stopped) this.schedule();
|
|
24075
24268
|
});
|
|
24076
24269
|
socket.addEventListener("error", () => {
|
|
@@ -24079,6 +24272,26 @@ var HeldSession = class {
|
|
|
24079
24272
|
this.schedule();
|
|
24080
24273
|
}
|
|
24081
24274
|
}
|
|
24275
|
+
/** A ping every 25s; no pong within 10s means the socket is dead however open it looks. Close it and come back. */
|
|
24276
|
+
startHeartbeat(socket) {
|
|
24277
|
+
this.stopHeartbeat();
|
|
24278
|
+
this.heartbeat = setInterval(() => {
|
|
24279
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
24280
|
+
socket.send("ping");
|
|
24281
|
+
if (!this.pongDue) {
|
|
24282
|
+
this.pongDue = setTimeout(() => {
|
|
24283
|
+
this.pongDue = null;
|
|
24284
|
+
socket.close(4e3, "no pong");
|
|
24285
|
+
}, PONG_WITHIN_MS);
|
|
24286
|
+
}
|
|
24287
|
+
}, PING_EVERY_MS);
|
|
24288
|
+
}
|
|
24289
|
+
stopHeartbeat() {
|
|
24290
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
24291
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
24292
|
+
this.heartbeat = null;
|
|
24293
|
+
this.pongDue = null;
|
|
24294
|
+
}
|
|
24082
24295
|
schedule() {
|
|
24083
24296
|
this.attempt += 1;
|
|
24084
24297
|
this.timer = setTimeout(() => void this.connect(), Math.min(3e4, 1e3 * 2 ** Math.min(this.attempt, 5)));
|
|
@@ -24088,10 +24301,335 @@ var HeldSession = class {
|
|
|
24088
24301
|
this.api.sessionId = null;
|
|
24089
24302
|
rememberSession(process.cwd(), null);
|
|
24090
24303
|
if (this.timer) clearTimeout(this.timer);
|
|
24304
|
+
this.stopHeartbeat();
|
|
24091
24305
|
this.socket?.close(1e3, "released");
|
|
24092
24306
|
}
|
|
24093
24307
|
};
|
|
24094
24308
|
|
|
24309
|
+
// src/platform/agent-identity.ts
|
|
24310
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
24311
|
+
import { basename as basename3, resolve as resolve3 } from "path";
|
|
24312
|
+
|
|
24313
|
+
// src/platform/harness.ts
|
|
24314
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
24315
|
+
import { closeSync, openSync, readdirSync as readdirSync2, readSync, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
|
|
24316
|
+
import { basename as basename2, join as join8 } from "path";
|
|
24317
|
+
|
|
24318
|
+
// src/platform/harness-wake.ts
|
|
24319
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
24320
|
+
import { join as join7 } from "path";
|
|
24321
|
+
import { connect } from "net";
|
|
24322
|
+
import { spawn as spawn2 } from "child_process";
|
|
24323
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
24324
|
+
|
|
24325
|
+
// src/platform/homes.ts
|
|
24326
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
24327
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
24328
|
+
import { homedir as homedir3 } from "os";
|
|
24329
|
+
import { join as join6, resolve as resolve2 } from "path";
|
|
24330
|
+
var unique = (paths) => [...new Set(paths.map((path3) => resolve2(path3)))];
|
|
24331
|
+
function environmentsSeen() {
|
|
24332
|
+
if (process.platform === "win32") return [];
|
|
24333
|
+
const seen = [];
|
|
24334
|
+
try {
|
|
24335
|
+
if (process.platform === "linux") {
|
|
24336
|
+
let pid2 = process.ppid;
|
|
24337
|
+
for (let hop = 0; hop < 6 && pid2 > 1; hop += 1) {
|
|
24338
|
+
seen.push(readFileSync6(`/proc/${pid2}/environ`, "latin1").replace(/\0/g, " "));
|
|
24339
|
+
const stat = readFileSync6(`/proc/${pid2}/stat`, "utf8");
|
|
24340
|
+
pid2 = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24341
|
+
}
|
|
24342
|
+
for (const entry of readdirSync("/proc")) {
|
|
24343
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
24344
|
+
try {
|
|
24345
|
+
seen.push(readFileSync6(`/proc/${entry}/environ`, "latin1").replace(/\0/g, " "));
|
|
24346
|
+
} catch {
|
|
24347
|
+
}
|
|
24348
|
+
}
|
|
24349
|
+
return seen;
|
|
24350
|
+
}
|
|
24351
|
+
const ps = (args) => execFileSync3("ps", args, { encoding: "utf8", maxBuffer: 64 << 20, stdio: ["ignore", "pipe", "ignore"] });
|
|
24352
|
+
let pid = process.ppid;
|
|
24353
|
+
for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
|
|
24354
|
+
seen.push(ps(["eww", "-o", "command=", "-p", String(pid)]));
|
|
24355
|
+
pid = Number(ps(["-o", "ppid=", "-p", String(pid)]).trim());
|
|
24356
|
+
}
|
|
24357
|
+
seen.push(ps(["axeww", "-o", "command="]));
|
|
24358
|
+
} catch {
|
|
24359
|
+
}
|
|
24360
|
+
return seen;
|
|
24361
|
+
}
|
|
24362
|
+
var environments = null;
|
|
24363
|
+
function homesInUse(variable) {
|
|
24364
|
+
environments ??= environmentsSeen();
|
|
24365
|
+
const found = [];
|
|
24366
|
+
const pattern = new RegExp(`(?:^|\\s)${variable}=(\\S+)`, "g");
|
|
24367
|
+
for (const text of environments) for (const match of text.matchAll(pattern)) found.push(match[1] ?? "");
|
|
24368
|
+
return found.filter(Boolean);
|
|
24369
|
+
}
|
|
24370
|
+
var directoriesNamed = (root, prefix) => {
|
|
24371
|
+
try {
|
|
24372
|
+
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.name.startsWith(prefix) && (entry.isDirectory() || entry.isSymbolicLink())).map((entry) => join6(root, entry.name)).filter((path3) => {
|
|
24373
|
+
try {
|
|
24374
|
+
return statSync2(path3).isDirectory();
|
|
24375
|
+
} catch {
|
|
24376
|
+
return false;
|
|
24377
|
+
}
|
|
24378
|
+
});
|
|
24379
|
+
} catch {
|
|
24380
|
+
return [];
|
|
24381
|
+
}
|
|
24382
|
+
};
|
|
24383
|
+
var hasAny = (dir2, files) => files.some((file3) => existsSync6(join6(dir2, file3)));
|
|
24384
|
+
function claudeHomes(root = homedir3(), inUse = homesInUse) {
|
|
24385
|
+
if (process.env.SLOP_CLAUDE_HOME) return [process.env.SLOP_CLAUDE_HOME];
|
|
24386
|
+
const named = process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : [];
|
|
24387
|
+
return unique([join6(root, ".claude"), ...named, ...inUse("CLAUDE_CONFIG_DIR"), ...directoriesNamed(root, ".claude").filter((dir2) => hasAny(dir2, ["settings.json", ".claude.json", "sessions"]))]);
|
|
24388
|
+
}
|
|
24389
|
+
function codexHomes(root = homedir3(), inUse = homesInUse) {
|
|
24390
|
+
if (process.env.SLOP_CODEX_HOME) return [process.env.SLOP_CODEX_HOME];
|
|
24391
|
+
const named = process.env.CODEX_HOME ? [process.env.CODEX_HOME] : [];
|
|
24392
|
+
return unique([join6(root, ".codex"), ...named, ...inUse("CODEX_HOME"), ...directoriesNamed(root, ".codex").filter((dir2) => hasAny(dir2, ["config.toml", "auth.json", "sessions"]))]);
|
|
24393
|
+
}
|
|
24394
|
+
function ownHarnessHome(variable) {
|
|
24395
|
+
if (process.platform === "win32") return null;
|
|
24396
|
+
try {
|
|
24397
|
+
let pid = process.ppid;
|
|
24398
|
+
for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
|
|
24399
|
+
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"] });
|
|
24400
|
+
const match = new RegExp(`(?:^|\\s)${variable}=(\\S+)`).exec(text);
|
|
24401
|
+
if (match?.[1]) return resolve2(match[1]);
|
|
24402
|
+
if (process.platform === "linux") {
|
|
24403
|
+
const stat = readFileSync6(`/proc/${pid}/stat`, "utf8");
|
|
24404
|
+
pid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24405
|
+
} else pid = Number(execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
24406
|
+
}
|
|
24407
|
+
} catch {
|
|
24408
|
+
}
|
|
24409
|
+
return null;
|
|
24410
|
+
}
|
|
24411
|
+
|
|
24412
|
+
// src/platform/harness-wake.ts
|
|
24413
|
+
function harnessRegistration(pid = process.ppid) {
|
|
24414
|
+
for (const home of claudeHomes()) {
|
|
24415
|
+
const path3 = join7(home, "sessions", `${pid}.json`);
|
|
24416
|
+
if (!existsSync7(path3)) continue;
|
|
24417
|
+
try {
|
|
24418
|
+
return JSON.parse(readFileSync7(path3, "utf8"));
|
|
24419
|
+
} catch {
|
|
24420
|
+
return null;
|
|
24421
|
+
}
|
|
24422
|
+
}
|
|
24423
|
+
return null;
|
|
24424
|
+
}
|
|
24425
|
+
function wakeHarness(text, pid = Number(process.env.SLOP_HARNESS_PID) || process.ppid) {
|
|
24426
|
+
const registration = harnessRegistration(pid);
|
|
24427
|
+
if (!registration?.sessionId || !registration.messagingSocketPath) return Promise.resolve(false);
|
|
24428
|
+
const frame = {
|
|
24429
|
+
uuid: randomUUID3(),
|
|
24430
|
+
session_id: registration.sessionId,
|
|
24431
|
+
type: "user",
|
|
24432
|
+
parent_tool_use_id: null,
|
|
24433
|
+
message: { role: "user", content: text }
|
|
24434
|
+
};
|
|
24435
|
+
return new Promise((resolve5) => {
|
|
24436
|
+
const socket = connect({ path: registration.messagingSocketPath });
|
|
24437
|
+
const done = (ok) => {
|
|
24438
|
+
clearTimeout(timer);
|
|
24439
|
+
socket.destroy();
|
|
24440
|
+
resolve5(ok);
|
|
24441
|
+
};
|
|
24442
|
+
const timer = setTimeout(() => done(false), 3e3);
|
|
24443
|
+
socket.on("connect", () => {
|
|
24444
|
+
socket.write(`${JSON.stringify(frame)}
|
|
24445
|
+
`, (error62) => error62 ? done(false) : setTimeout(() => done(true), 200));
|
|
24446
|
+
});
|
|
24447
|
+
socket.on("error", () => done(false));
|
|
24448
|
+
});
|
|
24449
|
+
}
|
|
24450
|
+
function wakeCodex(text, threadId) {
|
|
24451
|
+
const endpoint = process.env.SLOP_CODEX_APP_SERVER;
|
|
24452
|
+
if (endpoint) return wakeCodexOverWs(text, threadId, endpoint);
|
|
24453
|
+
return Promise.resolve(false);
|
|
24454
|
+
}
|
|
24455
|
+
function wakeCodexOverWs(text, threadId, endpoint) {
|
|
24456
|
+
return new Promise((resolve5) => {
|
|
24457
|
+
let socket;
|
|
24458
|
+
try {
|
|
24459
|
+
socket = new WebSocket(endpoint);
|
|
24460
|
+
} catch {
|
|
24461
|
+
resolve5(false);
|
|
24462
|
+
return;
|
|
24463
|
+
}
|
|
24464
|
+
let nextId = 1;
|
|
24465
|
+
const pending = /* @__PURE__ */ new Map();
|
|
24466
|
+
const finish = (ok) => {
|
|
24467
|
+
clearTimeout(timer);
|
|
24468
|
+
try {
|
|
24469
|
+
socket.close();
|
|
24470
|
+
} catch {
|
|
24471
|
+
}
|
|
24472
|
+
resolve5(ok);
|
|
24473
|
+
};
|
|
24474
|
+
const timer = setTimeout(() => finish(false), 8e3);
|
|
24475
|
+
const call = (method, params) => new Promise((ok, fail2) => {
|
|
24476
|
+
const id = nextId++;
|
|
24477
|
+
pending.set(id, (message) => message.error ? fail2(new Error(method)) : ok(message.result));
|
|
24478
|
+
socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
24479
|
+
});
|
|
24480
|
+
socket.addEventListener("error", () => finish(false));
|
|
24481
|
+
socket.addEventListener("message", (event) => {
|
|
24482
|
+
try {
|
|
24483
|
+
const message = JSON.parse(String(event.data));
|
|
24484
|
+
if (typeof message.id === "number") pending.get(message.id)?.(message);
|
|
24485
|
+
} catch {
|
|
24486
|
+
}
|
|
24487
|
+
});
|
|
24488
|
+
socket.addEventListener("open", () => {
|
|
24489
|
+
void (async () => {
|
|
24490
|
+
try {
|
|
24491
|
+
await call("initialize", { clientInfo: { name: "slop", version: "0" }, capabilities: { experimentalApi: true } });
|
|
24492
|
+
socket.send(JSON.stringify({ jsonrpc: "2.0", method: "initialized", params: {} }));
|
|
24493
|
+
const loaded = await call("thread/loaded/list", {});
|
|
24494
|
+
if (!loaded.data.includes(threadId)) {
|
|
24495
|
+
finish(false);
|
|
24496
|
+
return;
|
|
24497
|
+
}
|
|
24498
|
+
await call("turn/start", { threadId, input: [{ type: "text", text }] });
|
|
24499
|
+
finish(true);
|
|
24500
|
+
} catch {
|
|
24501
|
+
finish(false);
|
|
24502
|
+
}
|
|
24503
|
+
})();
|
|
24504
|
+
});
|
|
24505
|
+
});
|
|
24506
|
+
}
|
|
24507
|
+
|
|
24508
|
+
// src/platform/harness.ts
|
|
24509
|
+
var parent = null;
|
|
24510
|
+
function parentCommand() {
|
|
24511
|
+
if (parent !== null) return parent;
|
|
24512
|
+
try {
|
|
24513
|
+
parent = process.platform === "win32" ? "" : execFileSync4("ps", ["-o", "command=", "-p", String(process.ppid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
24514
|
+
} catch {
|
|
24515
|
+
parent = "";
|
|
24516
|
+
}
|
|
24517
|
+
return parent;
|
|
24518
|
+
}
|
|
24519
|
+
function harnessOf(command) {
|
|
24520
|
+
const first = basename2(command.trim().split(/\s+/)[0] ?? "");
|
|
24521
|
+
if (/^claude(\b|$)/.test(first)) return "claude";
|
|
24522
|
+
if (/^codex(\b|$)/.test(first)) return "codex";
|
|
24523
|
+
if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
|
|
24524
|
+
return null;
|
|
24525
|
+
}
|
|
24526
|
+
function isNonInteractive(parentCommand2) {
|
|
24527
|
+
return /(^|[\s/])codex(\S*)?\s+exec(\s|$)/.test(parentCommand2);
|
|
24528
|
+
}
|
|
24529
|
+
function nonInteractiveHarness() {
|
|
24530
|
+
return isNonInteractive(parentCommand());
|
|
24531
|
+
}
|
|
24532
|
+
var same = (a, b) => {
|
|
24533
|
+
const real = (path3) => {
|
|
24534
|
+
try {
|
|
24535
|
+
return realpathSync2(path3);
|
|
24536
|
+
} catch {
|
|
24537
|
+
return path3;
|
|
24538
|
+
}
|
|
24539
|
+
};
|
|
24540
|
+
return real(a) === real(b);
|
|
24541
|
+
};
|
|
24542
|
+
function codexConversation(roots, cwd) {
|
|
24543
|
+
const rollouts = [];
|
|
24544
|
+
const walk = (dir2, depth) => {
|
|
24545
|
+
let entries;
|
|
24546
|
+
try {
|
|
24547
|
+
entries = readdirSync2(dir2, { withFileTypes: true });
|
|
24548
|
+
} catch {
|
|
24549
|
+
return;
|
|
24550
|
+
}
|
|
24551
|
+
for (const entry of entries) {
|
|
24552
|
+
const path3 = join8(dir2, entry.name);
|
|
24553
|
+
if (entry.isDirectory()) {
|
|
24554
|
+
if (depth < 3) walk(path3, depth + 1);
|
|
24555
|
+
continue;
|
|
24556
|
+
}
|
|
24557
|
+
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);
|
|
24558
|
+
if (!match) continue;
|
|
24559
|
+
try {
|
|
24560
|
+
rollouts.push({ path: path3, id: match[1] ?? "", mtime: statSync3(path3).mtimeMs });
|
|
24561
|
+
} catch {
|
|
24562
|
+
}
|
|
24563
|
+
}
|
|
24564
|
+
};
|
|
24565
|
+
for (const root of roots) walk(root, 0);
|
|
24566
|
+
rollouts.sort((a, b) => b.mtime - a.mtime);
|
|
24567
|
+
for (const rollout of rollouts.slice(0, 300)) {
|
|
24568
|
+
let head = "";
|
|
24569
|
+
try {
|
|
24570
|
+
const fd = openSync(rollout.path, "r");
|
|
24571
|
+
try {
|
|
24572
|
+
const buffer = Buffer.alloc(8192);
|
|
24573
|
+
head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
|
|
24574
|
+
} finally {
|
|
24575
|
+
closeSync(fd);
|
|
24576
|
+
}
|
|
24577
|
+
} catch {
|
|
24578
|
+
continue;
|
|
24579
|
+
}
|
|
24580
|
+
const line = head.split("\n")[0] ?? "";
|
|
24581
|
+
if (!line.includes('"session_meta"')) continue;
|
|
24582
|
+
const found = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(line);
|
|
24583
|
+
if (!found) continue;
|
|
24584
|
+
let dir2;
|
|
24585
|
+
try {
|
|
24586
|
+
dir2 = JSON.parse(`"${found[1]}"`);
|
|
24587
|
+
} catch {
|
|
24588
|
+
continue;
|
|
24589
|
+
}
|
|
24590
|
+
if (same(dir2, cwd)) return rollout.id;
|
|
24591
|
+
}
|
|
24592
|
+
return null;
|
|
24593
|
+
}
|
|
24594
|
+
function harnessConversation(cwd) {
|
|
24595
|
+
const harness = harnessOf(parentCommand());
|
|
24596
|
+
if (harness === "claude") {
|
|
24597
|
+
const id = harnessRegistration()?.sessionId;
|
|
24598
|
+
return id ? { id, harness } : null;
|
|
24599
|
+
}
|
|
24600
|
+
if (harness === "codex" && !nonInteractiveHarness()) {
|
|
24601
|
+
const id = codexConversation(codexHomes().map((home) => join8(home, "sessions")), cwd);
|
|
24602
|
+
return id ? { id, harness } : null;
|
|
24603
|
+
}
|
|
24604
|
+
return null;
|
|
24605
|
+
}
|
|
24606
|
+
|
|
24607
|
+
// src/platform/agent-identity.ts
|
|
24608
|
+
var processConversation = randomUUID4();
|
|
24609
|
+
var startedAt = Date.now();
|
|
24610
|
+
function knownConversation(cwd) {
|
|
24611
|
+
return harnessConversation(cwd) ?? rememberedConversation(cwd, startedAt - 12e4);
|
|
24612
|
+
}
|
|
24613
|
+
function agentIdentity(cwd, fallback = "process") {
|
|
24614
|
+
const conversation = knownConversation(cwd);
|
|
24615
|
+
const repo = findRepo();
|
|
24616
|
+
return {
|
|
24617
|
+
conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve3(cwd)}`),
|
|
24618
|
+
harness: conversation?.harness ?? null,
|
|
24619
|
+
directory: repo ? basename3(repo.worktree) : basename3(cwd),
|
|
24620
|
+
branch: currentBranch(cwd)
|
|
24621
|
+
};
|
|
24622
|
+
}
|
|
24623
|
+
|
|
24624
|
+
// src/platform/bounded.ts
|
|
24625
|
+
var RESULT_CAP = 16e3;
|
|
24626
|
+
function bounded(result) {
|
|
24627
|
+
if (result.length <= RESULT_CAP) return result;
|
|
24628
|
+
return `${result.slice(0, RESULT_CAP)}
|
|
24629
|
+
|
|
24630
|
+
[cut: ${result.length - RESULT_CAP} more characters; ask for less with limit, offset or before]`;
|
|
24631
|
+
}
|
|
24632
|
+
|
|
24095
24633
|
// src/platform/mcp.ts
|
|
24096
24634
|
import { createInterface } from "readline";
|
|
24097
24635
|
function serveMcp(tools2, info) {
|
|
@@ -24149,11 +24687,11 @@ function serveMcp(tools2, info) {
|
|
|
24149
24687
|
}
|
|
24150
24688
|
|
|
24151
24689
|
// src/commands/tasks.ts
|
|
24152
|
-
import { readFileSync as
|
|
24690
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
24153
24691
|
|
|
24154
24692
|
// src/commands/items.ts
|
|
24155
|
-
import { readFileSync as
|
|
24156
|
-
var readText = (value) => value === "-" ?
|
|
24693
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
24694
|
+
var readText = (value) => value === "-" ? readFileSync8(0, "utf8").trim() : value;
|
|
24157
24695
|
var path2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
|
|
24158
24696
|
function renderItems(items) {
|
|
24159
24697
|
if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
|
|
@@ -24180,7 +24718,7 @@ function registerItemCommands(task, withProject) {
|
|
|
24180
24718
|
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) => {
|
|
24181
24719
|
const api = authedApi();
|
|
24182
24720
|
const project = await resolveProject(api, options.project);
|
|
24183
|
-
const source = lines.length === 1 && lines[0] === "-" ?
|
|
24721
|
+
const source = lines.length === 1 && lines[0] === "-" ? readFileSync8(0, "utf8") : lines.join("\n");
|
|
24184
24722
|
const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
|
|
24185
24723
|
if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
|
|
24186
24724
|
const items = await api.post(path2(project.id, key), { items: titles.map((title3) => ({ title: title3 })) });
|
|
@@ -24237,7 +24775,7 @@ function oneOf(value, allowed, what) {
|
|
|
24237
24775
|
if (allowed.includes(normalized)) return normalized;
|
|
24238
24776
|
throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
|
|
24239
24777
|
}
|
|
24240
|
-
var readDescription = (value) => value === "-" ?
|
|
24778
|
+
var readDescription = (value) => value === "-" ? readFileSync9(0, "utf8").trim() : value;
|
|
24241
24779
|
var taskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
|
|
24242
24780
|
var loadProject = (api, id) => api.get(`/projects/${id}`);
|
|
24243
24781
|
function resolveStatus(project, ref) {
|
|
@@ -24388,14 +24926,14 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
|
|
|
24388
24926
|
}
|
|
24389
24927
|
|
|
24390
24928
|
// src/platform/update-check.ts
|
|
24391
|
-
import { existsSync as
|
|
24392
|
-
import { dirname as dirname4, join as
|
|
24929
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
24930
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
24393
24931
|
var CHECK_EVERY_MS = 60 * 60 * 1e3;
|
|
24394
24932
|
var REGISTRY = "https://registry.npmjs.org/slopwaresystems/latest";
|
|
24395
|
-
var cacheFile = () =>
|
|
24933
|
+
var cacheFile = () => join9(dirname4(configPath()), "update-check.json");
|
|
24396
24934
|
function readCache() {
|
|
24397
24935
|
try {
|
|
24398
|
-
return JSON.parse(
|
|
24936
|
+
return JSON.parse(readFileSync10(cacheFile(), "utf8"));
|
|
24399
24937
|
} catch {
|
|
24400
24938
|
return { checkedAt: 0, latest: null };
|
|
24401
24939
|
}
|
|
@@ -24431,15 +24969,15 @@ function updateNotice(current) {
|
|
|
24431
24969
|
if (!latest || !isNewer(latest, current)) return null;
|
|
24432
24970
|
return `Update available: slopwaresystems ${latest} (this is ${current}). Ask your person to run: npm i -g slopwaresystems && slop mcp install`;
|
|
24433
24971
|
}
|
|
24434
|
-
var updateChecksEnabled = () => !
|
|
24972
|
+
var updateChecksEnabled = () => !existsSync8(join9(dirname4(configPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
|
|
24435
24973
|
|
|
24436
24974
|
// src/commands/threads.ts
|
|
24437
|
-
import { readFileSync as
|
|
24438
|
-
var readText2 = (value) => value === "-" ?
|
|
24975
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
24976
|
+
var readText2 = (value) => value === "-" ? readFileSync11(0, "utf8").trim() : value;
|
|
24439
24977
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
24440
|
-
var who = (message) => `${message.author.name}${message.
|
|
24978
|
+
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}` : ""})`;
|
|
24441
24979
|
function threadLine(thread) {
|
|
24442
|
-
const scope = thread.taskKey ?? "general";
|
|
24980
|
+
const scope = [thread.taskKey ?? (thread.to ? null : "general"), thread.to ? `to ${thread.to.agent}` : null].filter(Boolean).join(", ");
|
|
24443
24981
|
const unread = thread.unread > 0 ? ` ${thread.unread} unread` : "";
|
|
24444
24982
|
const last = thread.last ? `
|
|
24445
24983
|
${who(thread.last)}: ${thread.last.body.split("\n")[0]}` : "";
|
|
@@ -24453,9 +24991,10 @@ function threadBlock(thread) {
|
|
|
24453
24991
|
return [`${thread.subject} [${thread.taskKey ?? "general"}] ${thread.id}`, "", ...thread.messages.map(messageBlock)].join("\n\n").replace("\n\n\n", "\n\n");
|
|
24454
24992
|
}
|
|
24455
24993
|
function pushBlock(push) {
|
|
24456
|
-
|
|
24994
|
+
const scope = [push.thread.taskKey ?? (push.thread.to ? null : "general"), push.thread.to ? `to ${push.thread.to}` : null].filter(Boolean).join(", ");
|
|
24995
|
+
return `Thread ${push.thread.id} [${scope}] ${push.thread.subject}
|
|
24457
24996
|
${messageBlock(push.message)}
|
|
24458
|
-
Reply:
|
|
24997
|
+
Reply: say reply (thread ${push.thread.id})`;
|
|
24459
24998
|
}
|
|
24460
24999
|
function registerThreadCommands(program3, task) {
|
|
24461
25000
|
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
@@ -24470,10 +25009,10 @@ function registerThreadCommands(program3, task) {
|
|
|
24470
25009
|
emit(threads, () => threads.length === 0 ? "No threads." : threads.map(threadLine).join("\n"));
|
|
24471
25010
|
});
|
|
24472
25011
|
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) => {
|
|
25012
|
+
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
25013
|
const api = authedApi();
|
|
24475
25014
|
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) });
|
|
25015
|
+
const result = await api.post(`/projects/${project.id}/threads`, { subject, task: options.task, to: options.to, body: readText2(options.message) });
|
|
24477
25016
|
emit(result, () => started(result));
|
|
24478
25017
|
});
|
|
24479
25018
|
withProject(thread.command("show <id>")).description("Read a thread (and mark it read).").action(async (id, options) => {
|
|
@@ -24492,40 +25031,33 @@ function registerThreadCommands(program3, task) {
|
|
|
24492
25031
|
const api = authedApi();
|
|
24493
25032
|
const project = await resolveProject(api, options.project);
|
|
24494
25033
|
const body2 = readText2(text);
|
|
24495
|
-
const result = await api.post(`/projects/${project.id}/threads`, {
|
|
25034
|
+
const result = await api.post(`/projects/${project.id}/threads`, { task: options.task, body: body2 });
|
|
24496
25035
|
emit(result, () => started(result));
|
|
24497
25036
|
});
|
|
24498
25037
|
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) => {
|
|
24499
25038
|
const api = authedApi();
|
|
24500
25039
|
const project = await resolveProject(api, options.project);
|
|
24501
25040
|
const body2 = readText2(text);
|
|
24502
|
-
const result = await api.post(`/projects/${project.id}/threads`, {
|
|
25041
|
+
const result = await api.post(`/projects/${project.id}/threads`, { task: key, body: body2 });
|
|
24503
25042
|
emit(result, () => started(result));
|
|
24504
25043
|
});
|
|
24505
25044
|
}
|
|
24506
|
-
var
|
|
24507
|
-
const line = body2.split("\n")[0]?.trim() ?? "";
|
|
24508
|
-
if (!line) throw new CliError("Say something", "the first line becomes the subject");
|
|
24509
|
-
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
24510
|
-
};
|
|
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.`;
|
|
25045
|
+
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
25046
|
|
|
24513
|
-
// src/
|
|
24514
|
-
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
var
|
|
24518
|
-
var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
24519
|
-
var inboxDir = () => join7(dirname5(configPath()), "inbox");
|
|
25047
|
+
// src/platform/inbox.ts
|
|
25048
|
+
import { createHash as createHash4 } from "crypto";
|
|
25049
|
+
import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync3, renameSync, rmSync as rmSync4 } from "fs";
|
|
25050
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
25051
|
+
var inboxDir = () => join10(dirname5(configPath()), "inbox");
|
|
24520
25052
|
var realCwd2 = (cwd) => {
|
|
24521
25053
|
try {
|
|
24522
|
-
return
|
|
25054
|
+
return realpathSync3(cwd);
|
|
24523
25055
|
} catch {
|
|
24524
25056
|
return cwd;
|
|
24525
25057
|
}
|
|
24526
25058
|
};
|
|
24527
25059
|
var inboxKey = (cwd) => createHash4("sha256").update(realCwd2(cwd)).digest("hex").slice(0, 16);
|
|
24528
|
-
var inboxFile = (cwd) =>
|
|
25060
|
+
var inboxFile = (cwd) => join10(inboxDir(), `${inboxKey(cwd)}.jsonl`);
|
|
24529
25061
|
function inboxWrite(cwd, push) {
|
|
24530
25062
|
mkdirSync6(inboxDir(), { recursive: true });
|
|
24531
25063
|
appendFileSync(inboxFile(cwd), `${JSON.stringify(push)}
|
|
@@ -24533,11 +25065,58 @@ function inboxWrite(cwd, push) {
|
|
|
24533
25065
|
}
|
|
24534
25066
|
function inboxDrain(cwd) {
|
|
24535
25067
|
const path3 = inboxFile(cwd);
|
|
24536
|
-
if (!
|
|
24537
|
-
const
|
|
24538
|
-
|
|
24539
|
-
|
|
25068
|
+
if (!existsSync9(path3)) return [];
|
|
25069
|
+
const taken = `${path3}.${process.pid}.${Date.now()}.draining`;
|
|
25070
|
+
try {
|
|
25071
|
+
renameSync(path3, taken);
|
|
25072
|
+
} catch {
|
|
25073
|
+
return [];
|
|
25074
|
+
}
|
|
25075
|
+
const lines = readFileSync12(taken, "utf8").split("\n").filter(Boolean);
|
|
25076
|
+
rmSync4(taken, { force: true });
|
|
25077
|
+
return lines.flatMap((line) => {
|
|
25078
|
+
try {
|
|
25079
|
+
return [JSON.parse(line)];
|
|
25080
|
+
} catch {
|
|
25081
|
+
return [];
|
|
25082
|
+
}
|
|
25083
|
+
});
|
|
24540
25084
|
}
|
|
25085
|
+
|
|
25086
|
+
// src/platform/wake.ts
|
|
25087
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
25088
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
25089
|
+
var dir = (kind) => join11(dirname6(configPath()), kind);
|
|
25090
|
+
var file2 = (kind, cwd) => join11(dir(kind), inboxKey(cwd));
|
|
25091
|
+
function markWake(cwd) {
|
|
25092
|
+
mkdirSync7(dir("wake"), { recursive: true });
|
|
25093
|
+
writeFileSync7(file2("wake", cwd), String(Date.now()));
|
|
25094
|
+
}
|
|
25095
|
+
function takeWake(cwd, since) {
|
|
25096
|
+
const path3 = file2("wake", cwd);
|
|
25097
|
+
if (!existsSync10(path3)) return false;
|
|
25098
|
+
const at = statSync4(path3).mtimeMs;
|
|
25099
|
+
rmSync5(path3, { force: true });
|
|
25100
|
+
return at >= since - 1e3;
|
|
25101
|
+
}
|
|
25102
|
+
|
|
25103
|
+
// src/commands/mcp.ts
|
|
25104
|
+
var str = (description2) => ({ type: "string", description: description2 });
|
|
25105
|
+
var int2 = (description2) => ({ type: "integer", description: description2 });
|
|
25106
|
+
var schema = (properties, required2 = []) => ({ type: "object", properties, required: required2, additionalProperties: false });
|
|
25107
|
+
var PROJECT = str("Project key. Defaults to the project this repository is linked to.");
|
|
25108
|
+
var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
25109
|
+
var sessionLine = (session, mine = null) => [
|
|
25110
|
+
// Optional chaining on purpose: a server from before agents had names answers without these, and a tool must not crash on it.
|
|
25111
|
+
(session.agent?.name ?? "agent") + (session.id === mine ? " (you)" : ""),
|
|
25112
|
+
session.taskKey ?? "(no task)",
|
|
25113
|
+
session.user.name,
|
|
25114
|
+
session.machine.name,
|
|
25115
|
+
session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-",
|
|
25116
|
+
`since ${session.startedAt}`,
|
|
25117
|
+
session.state === "reconnecting" ? "(reconnecting)" : "",
|
|
25118
|
+
session.reachable ? "" : "(not visible to teammates)"
|
|
25119
|
+
].filter(Boolean).join(" ");
|
|
24541
25120
|
async function digest(api, projectId, taskKey) {
|
|
24542
25121
|
const [about, mine, general] = await Promise.all([
|
|
24543
25122
|
taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1&read=1`) : Promise.resolve([]),
|
|
@@ -24551,14 +25130,48 @@ async function digest(api, projectId, taskKey) {
|
|
|
24551
25130
|
if (general.length) lines.push("Unread general threads (context; nobody has to answer):", ...general.map(threadLine));
|
|
24552
25131
|
return lines.length ? lines.join("\n") : "Nothing unread.";
|
|
24553
25132
|
}
|
|
25133
|
+
async function catchUp(api, projectId, taskKey, cwd) {
|
|
25134
|
+
const [mine, about] = await Promise.all([
|
|
25135
|
+
api.get(`/projects/${projectId}/threads?participant=1&unread=1`),
|
|
25136
|
+
taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1`) : Promise.resolve([])
|
|
25137
|
+
]);
|
|
25138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25139
|
+
for (const thread of [...about, ...mine]) {
|
|
25140
|
+
if (seen.has(thread.id) || thread.unread === 0) continue;
|
|
25141
|
+
seen.add(thread.id);
|
|
25142
|
+
const detail = await api.get(`/projects/${projectId}/threads/${thread.id}?read=0`);
|
|
25143
|
+
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 });
|
|
25144
|
+
}
|
|
25145
|
+
}
|
|
24554
25146
|
function handlers() {
|
|
24555
25147
|
const api = authedApi();
|
|
24556
25148
|
const resolveOnly = (args) => resolveProject(api, args.project);
|
|
24557
25149
|
let held = null;
|
|
25150
|
+
let identity = null;
|
|
25151
|
+
const whoAmI = () => identity ??= agentIdentity(process.cwd());
|
|
24558
25152
|
const hold = async (projectId, taskKey) => {
|
|
24559
25153
|
if (held && held.projectId === projectId && held.taskKey === taskKey) return;
|
|
24560
25154
|
held?.stop();
|
|
24561
|
-
held = new HeldSession(api, projectId, taskKey,
|
|
25155
|
+
held = new HeldSession(api, projectId, taskKey, {
|
|
25156
|
+
// A message for the agent wakes its harness when the harness can be woken (Claude Code's session socket): it
|
|
25157
|
+
// arrives as a turn, now, whether the agent is idle or mid-turn. Otherwise the inbox and the hooks carry it.
|
|
25158
|
+
onMessage: (push) => {
|
|
25159
|
+
const text = `A message for you on Slopware Tasks. Act or answer it (say reply), then carry on.
|
|
25160
|
+
|
|
25161
|
+
${pushBlock(push)}`;
|
|
25162
|
+
const me = whoAmI();
|
|
25163
|
+
const wake = me.harness === "codex" && me.conversation ? wakeCodex(text, me.conversation) : wakeHarness(text);
|
|
25164
|
+
void wake.then((woken) => {
|
|
25165
|
+
if (!woken) inboxWrite(process.cwd(), push);
|
|
25166
|
+
});
|
|
25167
|
+
},
|
|
25168
|
+
claim: whoAmI,
|
|
25169
|
+
onReconnect: (session) => catchUp(api, projectId, session.taskKey, process.cwd()).catch(() => void 0),
|
|
25170
|
+
// Made unreachable: a Codex Stop hook holding a turn open has nothing to wait for; the marker lets it go.
|
|
25171
|
+
onReachable: (on) => {
|
|
25172
|
+
if (!on) markWake(process.cwd());
|
|
25173
|
+
}
|
|
25174
|
+
});
|
|
24562
25175
|
await held.start();
|
|
24563
25176
|
};
|
|
24564
25177
|
const project = async (args) => {
|
|
@@ -24569,29 +25182,47 @@ function handlers() {
|
|
|
24569
25182
|
const release = async () => {
|
|
24570
25183
|
if (held?.taskKey) await hold(held.projectId, null);
|
|
24571
25184
|
};
|
|
25185
|
+
if (!nonInteractiveHarness())
|
|
25186
|
+
void (async () => {
|
|
25187
|
+
for (let waited = 0; waited < 1e4 && !knownConversation(process.cwd()); waited += 250) await new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
25188
|
+
whoAmI();
|
|
25189
|
+
const target = await resolveOnly({});
|
|
25190
|
+
if (!held) await hold(target.id, null);
|
|
25191
|
+
})().catch(() => void 0);
|
|
25192
|
+
if (!nonInteractiveHarness()) void healCodexTrust().catch(() => void 0);
|
|
25193
|
+
const goodbye = () => {
|
|
25194
|
+
held?.stop();
|
|
25195
|
+
held = null;
|
|
25196
|
+
setTimeout(() => process.exit(0), 300).unref();
|
|
25197
|
+
};
|
|
25198
|
+
process.stdin.once("end", goodbye);
|
|
25199
|
+
for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, goodbye);
|
|
24572
25200
|
return [
|
|
24573
25201
|
{
|
|
24574
25202
|
name: "wait_for_message",
|
|
24575
|
-
description: "
|
|
24576
|
-
inputSchema: schema({ task: str("Task key
|
|
25203
|
+
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.",
|
|
25204
|
+
inputSchema: schema({ task: str("Task key to go live on first (optional)"), minutes: int2("Give up after (default 300)"), project: PROJECT }),
|
|
24577
25205
|
run: async (args) => {
|
|
24578
25206
|
const target = await project(args);
|
|
24579
|
-
await hold(target.id, String(args.task));
|
|
25207
|
+
if (args.task) await hold(target.id, String(args.task));
|
|
24580
25208
|
const queued = inboxDrain(process.cwd());
|
|
24581
25209
|
if (queued.length) return queued.map(pushBlock).join("\n\n");
|
|
24582
|
-
const minutes = typeof args.minutes === "number" ? args.minutes :
|
|
24583
|
-
const
|
|
25210
|
+
const minutes = typeof args.minutes === "number" ? args.minutes : 300;
|
|
25211
|
+
const outcome = await new Promise((resolve5) => {
|
|
24584
25212
|
const waiter = (delivered) => {
|
|
24585
25213
|
held?.waiters.delete(waiter);
|
|
24586
|
-
|
|
25214
|
+
clearTimeout(timer);
|
|
25215
|
+
resolve5({ push: delivered, released: delivered === null });
|
|
24587
25216
|
};
|
|
24588
25217
|
held?.waiters.add(waiter);
|
|
24589
|
-
setTimeout(() => {
|
|
25218
|
+
const timer = setTimeout(() => {
|
|
24590
25219
|
held?.waiters.delete(waiter);
|
|
24591
|
-
|
|
25220
|
+
resolve5({ push: null, released: false });
|
|
24592
25221
|
}, minutes * 6e4);
|
|
24593
25222
|
});
|
|
24594
|
-
|
|
25223
|
+
if (outcome.push) return pushBlock(outcome.push);
|
|
25224
|
+
if (outcome.released) return "Your person made you not visible to teammates: nothing can reach you now. You may stop.";
|
|
25225
|
+
return `No message in ${minutes} minutes.`;
|
|
24595
25226
|
}
|
|
24596
25227
|
},
|
|
24597
25228
|
{
|
|
@@ -24618,11 +25249,11 @@ function handlers() {
|
|
|
24618
25249
|
{
|
|
24619
25250
|
name: "post",
|
|
24620
25251
|
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.",
|
|
24621
|
-
inputSchema: schema({ subject: str("Subject"), body: str("First message (Markdown)"), task: str("Task key, to make it about that task"), project: PROJECT }, ["
|
|
25252
|
+
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"]),
|
|
24622
25253
|
run: async (args) => {
|
|
24623
25254
|
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)` : ""}.`;
|
|
25255
|
+
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 });
|
|
25256
|
+
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
25257
|
}
|
|
24627
25258
|
},
|
|
24628
25259
|
{
|
|
@@ -24642,7 +25273,15 @@ function handlers() {
|
|
|
24642
25273
|
name: "thread",
|
|
24643
25274
|
description: "Read a thread in full (marks it read).",
|
|
24644
25275
|
inputSchema: schema({ id: str("Thread id"), project: PROJECT }, ["id"]),
|
|
24645
|
-
run: async (args) =>
|
|
25276
|
+
run: async (args) => {
|
|
25277
|
+
const query = new URLSearchParams({ limit: String(Math.max(1, Number(args.limit ?? 40))) });
|
|
25278
|
+
if (args.before) query.set("before", String(args.before));
|
|
25279
|
+
const detail = await api.get(`/projects/${(await project(args)).id}/threads/${String(args.id)}?${query}`);
|
|
25280
|
+
const earlier = detail.earlier > 0 && detail.messages[0] ? `\u2026 ${detail.earlier} earlier (before: ${detail.messages[0].createdAt})
|
|
25281
|
+
|
|
25282
|
+
` : "";
|
|
25283
|
+
return earlier + threadBlock(detail);
|
|
25284
|
+
}
|
|
24646
25285
|
},
|
|
24647
25286
|
{
|
|
24648
25287
|
name: "sessions",
|
|
@@ -24650,7 +25289,8 @@ function handlers() {
|
|
|
24650
25289
|
inputSchema: schema({ project: PROJECT }),
|
|
24651
25290
|
run: async (args) => {
|
|
24652
25291
|
const list = await api.get(`/projects/${(await project(args)).id}/sessions`);
|
|
24653
|
-
|
|
25292
|
+
const mine = held?.session?.id ?? null;
|
|
25293
|
+
return list.length ? list.map((session) => sessionLine(session, mine)).join("\n") : "Nobody is live.";
|
|
24654
25294
|
}
|
|
24655
25295
|
},
|
|
24656
25296
|
{
|
|
@@ -24730,7 +25370,15 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
24730
25370
|
const doneKeys = new Set(target.statuses.filter((status) => status.category === "done").map((status) => status.key));
|
|
24731
25371
|
const only = args.status ? resolveStatus(target, String(args.status)) : null;
|
|
24732
25372
|
const rows = list.filter((task) => (args.all || args.archived || !doneKeys.has(task.status)) && (!me || task.assignee?.userId === me) && (!only || task.status === only));
|
|
24733
|
-
|
|
25373
|
+
const offset = Math.max(0, Number(args.offset ?? 0));
|
|
25374
|
+
const limit = Math.max(1, Number(args.limit ?? 50));
|
|
25375
|
+
const page = rows.slice(offset, offset + limit);
|
|
25376
|
+
const more = rows.length - offset - page.length;
|
|
25377
|
+
if (rows.length === 0) return "No tasks.";
|
|
25378
|
+
return [
|
|
25379
|
+
...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})` : ""}`),
|
|
25380
|
+
...more > 0 ? [`\u2026 ${more} more (offset: ${offset + page.length})`] : []
|
|
25381
|
+
].join("\n");
|
|
24734
25382
|
}
|
|
24735
25383
|
},
|
|
24736
25384
|
{
|
|
@@ -24805,8 +25453,13 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
24805
25453
|
inputSchema: schema({ task: str("Task key"), project: PROJECT }, ["task"]),
|
|
24806
25454
|
run: async (args) => {
|
|
24807
25455
|
const target = await project(args);
|
|
24808
|
-
const
|
|
24809
|
-
|
|
25456
|
+
const query = new URLSearchParams({ limit: String(Math.max(1, Number(args.limit ?? 50))) });
|
|
25457
|
+
if (args.before) query.set("before", String(args.before));
|
|
25458
|
+
const events = await api.get(`${taskPath(target.id, String(args.task))}/events?${query}`);
|
|
25459
|
+
if (events.length === 0) return "No history yet.";
|
|
25460
|
+
const lines = events.map((record2) => `${record2.createdAt.slice(0, 16).replace("T", " ")} ${record2.actor.name} ${JSON.stringify(record2.event)}`);
|
|
25461
|
+
const full = events.length >= Number(query.get("limit"));
|
|
25462
|
+
return [...full ? [`\u2026 earlier history: pass before: ${events[0].createdAt}`] : [], ...lines].join("\n");
|
|
24810
25463
|
}
|
|
24811
25464
|
},
|
|
24812
25465
|
{
|
|
@@ -24887,66 +25540,83 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
24887
25540
|
}
|
|
24888
25541
|
function readJson(path3) {
|
|
24889
25542
|
try {
|
|
24890
|
-
return JSON.parse(
|
|
25543
|
+
return JSON.parse(readFileSync13(path3, "utf8"));
|
|
24891
25544
|
} catch {
|
|
24892
25545
|
return {};
|
|
24893
25546
|
}
|
|
24894
25547
|
}
|
|
24895
|
-
function installClaude() {
|
|
25548
|
+
function installClaude(dir2) {
|
|
24896
25549
|
const done = [];
|
|
25550
|
+
const primary = dir2 === join12(homedir4(), ".claude");
|
|
25551
|
+
const env = { ...process.env };
|
|
25552
|
+
if (primary) delete env.CLAUDE_CONFIG_DIR;
|
|
25553
|
+
else env.CLAUDE_CONFIG_DIR = dir2;
|
|
25554
|
+
const where = primary ? "" : ` for ${dir2}`;
|
|
24897
25555
|
try {
|
|
24898
|
-
|
|
24899
|
-
done.push(
|
|
24900
|
-
} catch {
|
|
24901
|
-
|
|
25556
|
+
execFileSync5("claude", ["mcp", "add", "--scope", "user", "slop", "--", "slop", "mcp"], { stdio: ["ignore", "ignore", "pipe"], env });
|
|
25557
|
+
done.push(`Claude Code: MCP server "slop" registered (user scope${where})`);
|
|
25558
|
+
} catch (error62) {
|
|
25559
|
+
const said = String(error62.stderr ?? "") + String(error62.stdout ?? "");
|
|
25560
|
+
if (/already exists/i.test(said)) done.push(`Claude Code: MCP server "slop" already registered (user scope${where})`);
|
|
25561
|
+
else done.push(`Claude Code: run \`${primary ? "" : `CLAUDE_CONFIG_DIR=${dir2} `}claude mcp add --scope user slop -- slop mcp\` (${said.trim() || "the claude command was not found here"})`);
|
|
24902
25562
|
}
|
|
24903
|
-
const
|
|
24904
|
-
|
|
24905
|
-
mkdirSync6(dir, { recursive: true });
|
|
25563
|
+
const path3 = join12(dir2, "settings.json");
|
|
25564
|
+
mkdirSync8(dir2, { recursive: true });
|
|
24906
25565
|
const settings = readJson(path3);
|
|
24907
|
-
settings.hooks = mergeHooks(settings.hooks ?? {});
|
|
24908
|
-
settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "
|
|
24909
|
-
|
|
25566
|
+
settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
|
|
25567
|
+
settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "21600000" };
|
|
25568
|
+
writeFileSync8(path3, `${JSON.stringify(settings, null, 2)}
|
|
24910
25569
|
`);
|
|
24911
|
-
done.push(`Claude Code:
|
|
25570
|
+
done.push(`Claude Code: SessionStart hook and MCP_TOOL_TIMEOUT written to ${path3}; other slop hooks removed (its session socket wakes it)`);
|
|
24912
25571
|
return done;
|
|
24913
25572
|
}
|
|
24914
|
-
function mergeHooks(hooks) {
|
|
24915
|
-
const
|
|
24916
|
-
|
|
24917
|
-
|
|
25573
|
+
function mergeHooks(hooks, harness) {
|
|
25574
|
+
const ours = (hook) => typeof hook.command === "string" && /^slop hook /.test(hook.command);
|
|
25575
|
+
for (const event of Object.keys(hooks)) {
|
|
25576
|
+
const entries = hooks[event] ?? [];
|
|
25577
|
+
for (const entry of entries) entry.hooks = (entry.hooks ?? []).filter((hook) => !ours(hook));
|
|
25578
|
+
hooks[event] = entries.filter((entry) => (entry.hooks ?? []).length > 0);
|
|
25579
|
+
if (hooks[event].length === 0) delete hooks[event];
|
|
24918
25580
|
}
|
|
24919
|
-
const
|
|
24920
|
-
|
|
24921
|
-
|
|
25581
|
+
const add = (event, verb, timeout, statusMessage) => {
|
|
25582
|
+
const entries = hooks[event] ?? [];
|
|
25583
|
+
entries.push({ hooks: [{ type: "command", command: `slop hook ${verb} --harness ${harness}`, timeout, statusMessage }] });
|
|
25584
|
+
hooks[event] = entries;
|
|
25585
|
+
};
|
|
25586
|
+
add("SessionStart", "start", 5, "Slopware Tasks");
|
|
25587
|
+
if (harness === "codex") {
|
|
25588
|
+
add("UserPromptSubmit", "prompt", 5, "Slopware Tasks");
|
|
25589
|
+
add("Stop", "stop", 21600, "Listening on Slopware Tasks");
|
|
24922
25590
|
}
|
|
24923
|
-
return
|
|
25591
|
+
return hooks;
|
|
24924
25592
|
}
|
|
24925
|
-
function installCodex() {
|
|
25593
|
+
function installCodex(dir2) {
|
|
24926
25594
|
const done = [];
|
|
24927
|
-
|
|
24928
|
-
|
|
24929
|
-
const
|
|
24930
|
-
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
|
|
25595
|
+
mkdirSync8(dir2, { recursive: true });
|
|
25596
|
+
const configPath3 = join12(dir2, "config.toml");
|
|
25597
|
+
const current = existsSync11(configPath3) ? readFileSync13(configPath3, "utf8") : "";
|
|
25598
|
+
if (/^\[mcp_servers\.slop\]/m.test(current)) {
|
|
25599
|
+
if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync8(configPath3, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
|
|
25600
|
+
done.push(`Codex: MCP server already in ${configPath3}`);
|
|
25601
|
+
} else {
|
|
25602
|
+
writeFileSync8(configPath3, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}[mcp_servers.slop]
|
|
24934
25603
|
command = "slop"
|
|
24935
25604
|
args = ["mcp"]
|
|
24936
|
-
tool_timeout_sec =
|
|
25605
|
+
tool_timeout_sec = 21600
|
|
24937
25606
|
`);
|
|
24938
|
-
done.push(`Codex: MCP server "slop" added to ${
|
|
25607
|
+
done.push(`Codex: MCP server "slop" added to ${configPath3}`);
|
|
24939
25608
|
}
|
|
24940
|
-
const hooksPath =
|
|
24941
|
-
const
|
|
24942
|
-
|
|
24943
|
-
|
|
25609
|
+
const hooksPath = join12(dir2, "hooks.json");
|
|
25610
|
+
const file3 = readJson(hooksPath);
|
|
25611
|
+
file3.hooks = mergeHooks(file3.hooks ?? {}, "codex");
|
|
25612
|
+
writeFileSync8(hooksPath, `${JSON.stringify(file3, null, 2)}
|
|
24944
25613
|
`);
|
|
24945
|
-
done.push(`Codex:
|
|
25614
|
+
done.push(`Codex: SessionStart, UserPromptSubmit and Stop hooks written to ${hooksPath}`);
|
|
24946
25615
|
return done;
|
|
24947
25616
|
}
|
|
24948
|
-
async function trustCodexHooks() {
|
|
24949
|
-
const server =
|
|
25617
|
+
async function trustCodexHooks(codexHome) {
|
|
25618
|
+
const server = spawn3("codex", ["app-server"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
|
|
25619
|
+
const where = codexHome === join12(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
|
|
24950
25620
|
let buffer = "";
|
|
24951
25621
|
const pending = /* @__PURE__ */ new Map();
|
|
24952
25622
|
server.stdout.on("data", (chunk) => {
|
|
@@ -24964,7 +25634,7 @@ async function trustCodexHooks() {
|
|
|
24964
25634
|
}
|
|
24965
25635
|
});
|
|
24966
25636
|
let nextId = 1;
|
|
24967
|
-
const call = (method, params) => new Promise((
|
|
25637
|
+
const call = (method, params) => new Promise((resolve5, reject) => {
|
|
24968
25638
|
const id = nextId++;
|
|
24969
25639
|
const timer = setTimeout(() => {
|
|
24970
25640
|
pending.delete(id);
|
|
@@ -24973,7 +25643,7 @@ async function trustCodexHooks() {
|
|
|
24973
25643
|
pending.set(id, (message) => {
|
|
24974
25644
|
clearTimeout(timer);
|
|
24975
25645
|
if (message.error) reject(new Error(message.error.message ?? `${method} failed`));
|
|
24976
|
-
else
|
|
25646
|
+
else resolve5(message.result);
|
|
24977
25647
|
});
|
|
24978
25648
|
server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
24979
25649
|
`);
|
|
@@ -24981,48 +25651,64 @@ async function trustCodexHooks() {
|
|
|
24981
25651
|
const notify = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
24982
25652
|
`);
|
|
24983
25653
|
try {
|
|
24984
|
-
await call("initialize", { clientInfo: { name: "slop", version: "0.
|
|
25654
|
+
await call("initialize", { clientInfo: { name: "slop", version: "0.4.1" }, capabilities: { experimentalApi: true } });
|
|
24985
25655
|
notify("initialized", {});
|
|
24986
25656
|
const listed = await call("hooks/list", { cwds: [process.cwd()] });
|
|
24987
|
-
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command
|
|
24988
|
-
if (ours.length === 0) return
|
|
25657
|
+
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
25658
|
+
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`;
|
|
24989
25659
|
const untrusted = ours.filter((hook) => hook.trustStatus !== "trusted");
|
|
24990
|
-
if (untrusted.length === 0) return
|
|
25660
|
+
if (untrusted.length === 0) return `Codex: the slop hooks are already trusted${where}`;
|
|
24991
25661
|
const value = Object.fromEntries(untrusted.map((hook) => [hook.key, { trusted_hash: hook.currentHash }]));
|
|
24992
25662
|
await call("config/batchWrite", { edits: [{ keyPath: "hooks.state", value, mergeStrategy: "upsert" }], reloadUserConfig: true });
|
|
24993
|
-
return `Codex: trusted ${untrusted.length} slop hook(s), so it will not ask`;
|
|
25663
|
+
return `Codex: trusted ${untrusted.length} slop hook(s)${where}, so it will not ask`;
|
|
24994
25664
|
} catch (error62) {
|
|
24995
|
-
return `Codex: could not trust the hooks automatically (${error62 instanceof Error ? error62.message : String(error62)}); trust them once with /hooks in a Codex session`;
|
|
25665
|
+
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`;
|
|
24996
25666
|
} finally {
|
|
24997
25667
|
server.kill();
|
|
24998
25668
|
}
|
|
24999
25669
|
}
|
|
25000
|
-
async function
|
|
25001
|
-
const
|
|
25670
|
+
async function healCodexTrust() {
|
|
25671
|
+
const home = ownHarnessHome("CODEX_HOME");
|
|
25672
|
+
if (!home) return;
|
|
25673
|
+
const hooksPath = join12(home, "hooks.json");
|
|
25674
|
+
const configPath3 = join12(home, "config.toml");
|
|
25675
|
+
if (!existsSync11(hooksPath) || !existsSync11(configPath3)) return;
|
|
25676
|
+
if (!readFileSync13(hooksPath, "utf8").includes("slop hook stop")) return;
|
|
25677
|
+
if (readFileSync13(configPath3, "utf8").includes(`hooks.state."${hooksPath}:stop:`)) return;
|
|
25678
|
+
await trustCodexHooks(home);
|
|
25679
|
+
}
|
|
25680
|
+
async function hookInput(harness) {
|
|
25681
|
+
const input2 = await new Promise((resolve5) => {
|
|
25002
25682
|
let data = "";
|
|
25003
25683
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
25004
|
-
process.stdin.on("end", () =>
|
|
25005
|
-
setTimeout(() =>
|
|
25684
|
+
process.stdin.on("end", () => resolve5(data));
|
|
25685
|
+
setTimeout(() => resolve5(data), 2e3);
|
|
25006
25686
|
});
|
|
25687
|
+
let cwd = process.cwd();
|
|
25688
|
+
let conversation = null;
|
|
25007
25689
|
try {
|
|
25008
|
-
|
|
25690
|
+
const parsed = JSON.parse(input2);
|
|
25691
|
+
cwd = parsed.cwd ?? cwd;
|
|
25692
|
+
conversation = parsed.session_id ?? null;
|
|
25009
25693
|
} catch {
|
|
25010
|
-
return process.cwd();
|
|
25011
25694
|
}
|
|
25695
|
+
if (conversation && !nonInteractiveHarness()) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
|
|
25696
|
+
return { cwd, conversation };
|
|
25012
25697
|
}
|
|
25013
|
-
async function
|
|
25014
|
-
const
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "
|
|
25698
|
+
async function promptHook(harness) {
|
|
25699
|
+
const { cwd } = await hookInput(harness);
|
|
25700
|
+
markWake(cwd);
|
|
25701
|
+
}
|
|
25702
|
+
async function startHook(harness) {
|
|
25703
|
+
const { conversation } = await hookInput(harness);
|
|
25704
|
+
if (!conversation) return;
|
|
25705
|
+
const text = `On Slopware Tasks you are ${agentNameFor(conversation)}: that is how people and other agents address you (session who lists everyone).`;
|
|
25706
|
+
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } })}
|
|
25022
25707
|
`);
|
|
25023
25708
|
}
|
|
25024
|
-
async function stopHook() {
|
|
25025
|
-
|
|
25709
|
+
async function stopHook(harness) {
|
|
25710
|
+
if (nonInteractiveHarness()) return;
|
|
25711
|
+
const { cwd } = await hookInput(harness);
|
|
25026
25712
|
process.chdir(cwd);
|
|
25027
25713
|
const parts = inboxDrain(cwd).map(pushBlock);
|
|
25028
25714
|
const repo = findRepo();
|
|
@@ -25044,12 +25730,40 @@ ${parts.join("\n\n")}` })}
|
|
|
25044
25730
|
}
|
|
25045
25731
|
const general = await api.get(`/projects/${project.id}/threads?scope=general&unread=1&read=1`);
|
|
25046
25732
|
if (general.length) parts.push("Unread general threads (context):", ...general.map(threadLine));
|
|
25047
|
-
if (parts.length
|
|
25048
|
-
|
|
25733
|
+
if (parts.length) {
|
|
25734
|
+
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
25735
|
|
|
25050
25736
|
${parts.join("\n")}` })}
|
|
25737
|
+
`);
|
|
25738
|
+
return;
|
|
25739
|
+
}
|
|
25740
|
+
const sessionId = rememberedSession(cwd);
|
|
25741
|
+
if (!sessionId || harness !== "codex") return;
|
|
25742
|
+
const reachable = async () => (await api.get(`/projects/${project.id}/sessions`)).some((session) => session.id === sessionId && session.reachable);
|
|
25743
|
+
if (!await reachable()) return;
|
|
25744
|
+
const began = Date.now();
|
|
25745
|
+
takeWake(cwd, began);
|
|
25746
|
+
let lastCheck = began;
|
|
25747
|
+
while (Date.now() - began < STANDBY_HOLD_MS) {
|
|
25748
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
25749
|
+
const arrived = inboxDrain(cwd);
|
|
25750
|
+
if (arrived.length) {
|
|
25751
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: `A message for you on Slopware Tasks. Act or answer it (say reply), then stop.
|
|
25752
|
+
|
|
25753
|
+
${arrived.map(pushBlock).join("\n\n")}` })}
|
|
25754
|
+
`);
|
|
25755
|
+
return;
|
|
25756
|
+
}
|
|
25757
|
+
if (takeWake(cwd, began)) return;
|
|
25758
|
+
if (Date.now() - lastCheck > 6e4) {
|
|
25759
|
+
lastCheck = Date.now();
|
|
25760
|
+
if (!await reachable().catch(() => true)) return;
|
|
25761
|
+
}
|
|
25762
|
+
}
|
|
25763
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: "Still reachable on Slopware Tasks, nothing arrived. Say so in a word and stop; you will wait again." })}
|
|
25051
25764
|
`);
|
|
25052
25765
|
}
|
|
25766
|
+
var STANDBY_HOLD_MS = 5 * 60 * 60 * 1e3;
|
|
25053
25767
|
function tools() {
|
|
25054
25768
|
const fine = handlers();
|
|
25055
25769
|
const run2 = (name, args) => {
|
|
@@ -25072,13 +25786,13 @@ function tools() {
|
|
|
25072
25786
|
"board",
|
|
25073
25787
|
"The project: tasks on the board, columns, members, projects.",
|
|
25074
25788
|
{ 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" } }
|
|
25789
|
+
{ 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
25790
|
),
|
|
25077
25791
|
group(
|
|
25078
25792
|
"task",
|
|
25079
25793
|
"One task. show; create; update (only given fields change); move; link; archive/restore; history; finish (first done column, releases your session).",
|
|
25080
25794
|
{ 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") }
|
|
25795
|
+
{ 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
25796
|
),
|
|
25083
25797
|
group(
|
|
25084
25798
|
"plan",
|
|
@@ -25088,16 +25802,16 @@ function tools() {
|
|
|
25088
25802
|
),
|
|
25089
25803
|
group(
|
|
25090
25804
|
"session",
|
|
25091
|
-
"Being live. claim: take a task, go live on it, get what is unread (messages then reach you
|
|
25805
|
+
"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).",
|
|
25092
25806
|
{ 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
|
|
25807
|
+
{ task: str("Task key"), minutes: int2("wait: give up after (default 300)") }
|
|
25094
25808
|
),
|
|
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") }),
|
|
25809
|
+
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
25810
|
group(
|
|
25097
25811
|
"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
|
|
25812
|
+
"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
25813
|
{ post: "post", reply: "reply" },
|
|
25100
|
-
{ subject: str("post"), body: str("Markdown"), task: str("post: about this task"), thread: str("reply: thread id") }
|
|
25814
|
+
{ 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") }
|
|
25101
25815
|
)
|
|
25102
25816
|
];
|
|
25103
25817
|
}
|
|
@@ -25105,7 +25819,7 @@ function withInbox(list, version2) {
|
|
|
25105
25819
|
return list.map((tool) => ({
|
|
25106
25820
|
...tool,
|
|
25107
25821
|
run: async (args) => {
|
|
25108
|
-
const result = await tool.run(args);
|
|
25822
|
+
const result = bounded(await tool.run(args));
|
|
25109
25823
|
const parts = [result];
|
|
25110
25824
|
if (!(tool.name === "session" && (args.action === "check" || args.action === "wait"))) {
|
|
25111
25825
|
const arrived = inboxDrain(process.cwd());
|
|
@@ -25122,25 +25836,28 @@ function registerMcpCommands(program3, version2) {
|
|
|
25122
25836
|
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.");
|
|
25123
25837
|
mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(withInbox(tools(), version2), { name: "slop", version: version2 }));
|
|
25124
25838
|
mcp.command("install").description("Register the server and the two hooks with Claude Code and Codex on this machine.").action(async () => {
|
|
25125
|
-
const
|
|
25839
|
+
const homes = codexHomes();
|
|
25840
|
+
const lines = [...claudeHomes().flatMap((home) => installClaude(home)), ...homes.flatMap((home) => installCodex(home))];
|
|
25126
25841
|
let codexPresent = true;
|
|
25127
25842
|
try {
|
|
25128
|
-
|
|
25843
|
+
execFileSync5("codex", ["--version"], { stdio: "ignore" });
|
|
25129
25844
|
} catch {
|
|
25130
25845
|
codexPresent = false;
|
|
25131
25846
|
}
|
|
25132
|
-
if (codexPresent) lines.push(await trustCodexHooks());
|
|
25847
|
+
if (codexPresent) for (const home of homes) lines.push(await trustCodexHooks(home));
|
|
25133
25848
|
emit({ done: lines }, () => lines.join("\n"));
|
|
25134
25849
|
});
|
|
25135
25850
|
const hook = program3.command("hook").description("Harness hooks (installed by slop mcp install).");
|
|
25136
|
-
|
|
25137
|
-
hook.command("
|
|
25851
|
+
const harnessOf2 = (options) => options.harness === "claude" || options.harness === "codex" ? options.harness : options.harness ? "other" : null;
|
|
25852
|
+
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)));
|
|
25853
|
+
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)));
|
|
25854
|
+
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)));
|
|
25138
25855
|
}
|
|
25139
25856
|
|
|
25140
25857
|
// src/commands/sessions.ts
|
|
25141
|
-
import { readFileSync as
|
|
25858
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
25142
25859
|
var taskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
25143
|
-
var
|
|
25860
|
+
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
25861
|
function registerSessionCommands(program3, task) {
|
|
25145
25862
|
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
25146
25863
|
const session = program3.command("session").description("Claim a task and be reachable on it while you work.");
|
|
@@ -25171,7 +25888,8 @@ function registerSessionCommands(program3, task) {
|
|
|
25171
25888
|
`);
|
|
25172
25889
|
const outcome = await waitForMessage(api, project.id, key, {
|
|
25173
25890
|
timeoutMs: options.timeout ? Number(options.timeout) * 1e3 : 0,
|
|
25174
|
-
|
|
25891
|
+
claim: agentIdentity(process.cwd(), "directory"),
|
|
25892
|
+
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
25893
|
onMessage: options.follow ? (push) => {
|
|
25176
25894
|
emit(push, () => `${pushBlock(push)}
|
|
25177
25895
|
`);
|
|
@@ -25196,7 +25914,7 @@ function registerSessionCommands(program3, task) {
|
|
|
25196
25914
|
const api = authedApi();
|
|
25197
25915
|
const project = await resolveProject(api, options.project);
|
|
25198
25916
|
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(
|
|
25917
|
+
emit(sessions, () => sessions.length === 0 ? "Nobody is live." : table([["AGENT", "TASK", "WHO", "MACHINE", "WHERE", "SINCE", "STATE"], ...sessions.map(sessionLine2)]));
|
|
25200
25918
|
});
|
|
25201
25919
|
}
|
|
25202
25920
|
|
|
@@ -25222,13 +25940,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
|
|
|
25222
25940
|
}
|
|
25223
25941
|
|
|
25224
25942
|
// 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.
|
|
25943
|
+
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.1").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
25944
|
registerAuthCommands(program2);
|
|
25227
25945
|
registerProjectCommands(program2);
|
|
25228
25946
|
var taskCommand = registerTaskCommands(program2);
|
|
25229
25947
|
registerSessionCommands(program2, taskCommand);
|
|
25230
25948
|
registerThreadCommands(program2, taskCommand);
|
|
25231
|
-
registerMcpCommands(program2, "0.
|
|
25949
|
+
registerMcpCommands(program2, "0.4.1");
|
|
25232
25950
|
registerGuideCommand(program2);
|
|
25233
25951
|
registerSkillCommands(program2);
|
|
25234
25952
|
program2.parseAsync().catch(fail);
|