@sgtbeatdown/wolfpack-tasks 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -2
- package/docs/protocol-reference.md +124 -0
- package/package.json +55 -4
- package/skills/task-context-summary/SKILL.md +34 -0
- package/skills/wolfpack-task-delegation/SKILL.md +107 -0
- package/src/abortable-sleep.ts +25 -0
- package/src/agent-task-runtime.ts +709 -0
- package/src/agent-task-tool-schemas.ts +30 -0
- package/src/claude/adapter.ts +336 -0
- package/src/claude/constants.ts +40 -0
- package/src/claude/environment.ts +54 -0
- package/src/claude/json-rpc.ts +50 -0
- package/src/claude/mcp-server.ts +144 -0
- package/src/claude/server.ts +37 -0
- package/src/claude/status-file.ts +66 -0
- package/src/claude/validation.ts +28 -0
- package/src/claude/wake.ts +215 -0
- package/src/claude/worker-gate-hook.ts +26 -0
- package/src/claude/worker-gate.ts +260 -0
- package/src/configured-task-core.ts +76 -0
- package/src/extension.ts +51 -0
- package/src/in-memory-task-relay.ts +115 -0
- package/src/index.ts +35 -0
- package/src/task-core.ts +925 -0
- package/src/task-errors.ts +111 -0
- package/src/task-inbox.ts +292 -0
- package/src/task-protocol.ts +243 -0
- package/src/task-relay-conformance.ts +33 -0
- package/src/task-store.ts +342 -0
- package/src/task-work-scope.ts +98 -0
- package/src/task-worker-mode.ts +9 -0
- package/src/volatile-task-session.ts +310 -0
- package/src/wolfpack-endpoint-view.ts +46 -0
- package/src/wolfpack-local-auth.ts +20 -0
- package/src/wolfpack-task-relay.ts +39 -0
- package/tsconfig.json +30 -0
package/README.md
CHANGED
|
@@ -1,3 +1,214 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wolfpack-tasks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**give another pi agent a task, exchange messages, and get a structured result back.**
|
|
4
|
+
|
|
5
|
+
wolfpack-tasks adds coordination tools to [pi](https://pi.dev). a parent agent can hand work to another session, keep working, and receive a follow-up when the worker asks a question or finishes. it works between sessions on one machine and, through wolfpack's trusted tailnet routing, across machines.
|
|
6
|
+
|
|
7
|
+
for example: an implementer works on a change while the parent investigates something else. the implementer reports its result; the parent checks the diff and tests, then acknowledges the task.
|
|
8
|
+
|
|
9
|
+
**wolfpack-tasks coordinates work. it does not create agent sessions or execute the assignment itself.** wolfpack manages sessions and carries messages; the agents do the work.
|
|
10
|
+
|
|
11
|
+
## how it works
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
parent agent ── assignment ──> worker agent
|
|
15
|
+
<── questions ──
|
|
16
|
+
─── answers ───>
|
|
17
|
+
<─── result ────
|
|
18
|
+
─ acknowledgment >
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
1. the parent sends instructions to the worker's **task endpoint**: an opaque address for that running session.
|
|
22
|
+
2. the relay carries the assignment. the worker's pi extension queues it as a follow-up, without interrupting an active turn.
|
|
23
|
+
3. the worker does the work, optionally exchanges messages, and submits a structured completion or failure.
|
|
24
|
+
4. the parent receives the result, verifies it, and acknowledges the task.
|
|
25
|
+
|
|
26
|
+
completion ends the **task**, not the **session**. keep a worker for another assignment, or close it through wolfpack when you no longer need it.
|
|
27
|
+
|
|
28
|
+
## use with wolfpack
|
|
29
|
+
|
|
30
|
+
this is the ready-made integration. you need pi, a compatible wolfpack memory-only server, and this package loaded in every participating pi session.
|
|
31
|
+
|
|
32
|
+
### 1. install the pi package
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pi install npm:@sgtbeatdown/wolfpack-tasks
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
start new pi sessions after installation. wolfpack supplies `WOLFPACK_SESSION_NAME` to its sessions; the extension uses it to register the current process. the local control port defaults to `18790` (`WOLFPACK_PORT` overrides it).
|
|
39
|
+
|
|
40
|
+
### 2. open a worker
|
|
41
|
+
|
|
42
|
+
from a pi parent running under wolfpack:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# choose an available pi model explicitly.
|
|
46
|
+
MODEL="openai-codex/gpt-6.1-sol:medium"
|
|
47
|
+
wolfpack agent spawn --project-dir /absolute/worktree \
|
|
48
|
+
--name task-implementation --model "$MODEL" \
|
|
49
|
+
--task-worker --readiness-timeout-ms 30000 --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
replace `/absolute/worktree` with the directory the worker should use. this starts a prompt-free worker and returns a stable `sessionId` and a registered `taskEndpoint`. registration means the session can receive mail—not that its model has executed anything.
|
|
53
|
+
|
|
54
|
+
for an existing session, use `wolfpack session status <sessionId> --json`. copy its returned `taskEndpoint` unchanged; never construct an endpoint from a session name.
|
|
55
|
+
|
|
56
|
+
### 3. assign work
|
|
57
|
+
|
|
58
|
+
ask the parent agent to call `agent_task_send` with that endpoint:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"to": {
|
|
63
|
+
"relay": "wolfpack-pi-tasks-v2",
|
|
64
|
+
"id": "target-opaque-endpoint-id"
|
|
65
|
+
},
|
|
66
|
+
"task": "implement the narrow change and run focused tests",
|
|
67
|
+
"timeoutMs": 3600000
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
replace `to` with the actual returned `{ relay, id }`. put the complete assignment in `task`; do not also give the worker a startup prompt. the timeout is a failure deadline, not a polling interval. allow at least an hour for coding work.
|
|
72
|
+
|
|
73
|
+
### 4. receive and verify the result
|
|
74
|
+
|
|
75
|
+
continue useful work after sending. when none remains, yield the turn: structured follow-ups bring questions and results back to the parent. do not repeatedly poll just to wait.
|
|
76
|
+
|
|
77
|
+
| tool | purpose |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `agent_task_send` | assign work to an endpoint |
|
|
80
|
+
| `agent_task_message` | send a question, answer, or progress update |
|
|
81
|
+
| `agent_task_done` | worker submits its final status and result |
|
|
82
|
+
| `agent_task_ack` | parent acknowledges a terminal task after verification |
|
|
83
|
+
| `agent_task_status` / `agent_task_inbox` | inspect locally known task state |
|
|
84
|
+
| `agent_task_cancel` | parent cancels its task |
|
|
85
|
+
| `agent_task_wait` | explicitly block for a terminal result when requested |
|
|
86
|
+
|
|
87
|
+
workers report changed source paths in `result.changedFiles`. artifact declarations point to files for the parent to inspect; they do **not** transfer file contents.
|
|
88
|
+
|
|
89
|
+
when finished with a session you created, run `wolfpack kill <sessionId> --json`, then verify that exact ID is absent from `wolfpack list --json`.
|
|
90
|
+
|
|
91
|
+
### across machines
|
|
92
|
+
|
|
93
|
+
select a configured tailnet peer with `wolfpack --machine <peer>`. remote child spawning requires a parent on that machine; for a worker coordinated from here, use remote top-level creation:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
wolfpack --machine <peer> session create --project-dir /absolute/remote/worktree \
|
|
97
|
+
--harness pi --task-worker --readiness-timeout-ms 30000 --json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
paths are on the remote host. the CLI qualifies the remote endpoint through your local coordinator and returns a locally routable `taskEndpoint`. use that returned address in the same send flow. if it reports `taskEndpointError`, inspect the retained `sessionId` rather than blindly creating another session. use the same `--machine` selector for cleanup.
|
|
101
|
+
|
|
102
|
+
see the [delegation workflow](skills/wolfpack-task-delegation/SKILL.md) for role reuse, worker restrictions, readiness errors, and complete lifecycle rules.
|
|
103
|
+
|
|
104
|
+
### claude code sessions
|
|
105
|
+
|
|
106
|
+
the same tools are available in claude code through a plugin in this repository. the plugin runs a small MCP server (`claude-plugin/dist/server.mjs`, run by node) over the same host-neutral runtime as the pi extension. tool names, parameters, validation messages, and the worker gate are the same.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
claude plugin marketplace add /absolute/path/to/wolfpack-tasks
|
|
110
|
+
claude plugin install wolfpack-tasks@wolfpack-tasks
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **identity gate.** the server activates only when both `WOLFPACK_SESSION_ID` and `WOLFPACK_SESSION_NAME` are set, as wolfpack does for its sessions. otherwise it registers nothing, contacts nothing, writes no files or output, and every task tool returns "not a wolfpack session". with identity present, an invalid `WOLFPACK_PORT`, `WOLFPACK_JWT_SECRET`, `WOLFPACK_DIR`, or `HOME` makes every tool return "misconfigured" with the variable name.
|
|
114
|
+
- **wake.** new task mail sends one `notifications/claude/channel` pointer (`wolfpack task mail: call agent_task_inbox`, with string `task_id` meta). the pointer never contains task content. the model calls `agent_task_inbox`, and the inbox result contains the messages. further mail does not send another pointer until that call.
|
|
115
|
+
- **fallback.** if the pointer stays unanswered for 15s while the session is idle, the server checks `wolfpack session status <id> --json` and types the pointer into the session once (`wolfpack session send`, then a separate submit). a failed status check is retried every 5s, up to 3 checks; after that the server releases the pointer and records `wakeStalledAt`, and the next mail starts a new wake. a failed or timed-out paste is never repeated or submitted, and the pointer expires after 120s. this is best effort: the text is appended to any unsubmitted prompt input. after one fallback the server uses fallback mode for the rest of the process lifetime. it never types into a working session. idle state comes from the plugin's `UserPromptSubmit`, `PreToolUse`, and `Stop` hooks.
|
|
116
|
+
- **status.** each session writes `<wolfpack state dir>/wolfpack-tasks/claude/<WOLFPACK_SESSION_ID>.json` (mode 0600; the state dir is `WOLFPACK_DIR` or `~/.wolfpack`). it records the wake mode, the negotiated MCP protocol version, pointer, inbox, and nudge timestamps, the status-check failure count, and `wakeStalledAt`. it is diagnostic only and never task state.
|
|
117
|
+
- **worker gate.** in a worker session (`WOLFPACK_TASK_WORKER=1` or `PI_TASK_WORKER=1`, with wolfpack identity), two `PreToolUse` hooks gate claude code's native tools. the `mcp_tool` hook asks the server and gives the full denial reason, but claude code allows the tool when that hook fails. the second hook is a `/bin/sh` command with `onFailure: "block"`. when neither variable is exactly `1`, it reads and discards the hook input and exits 0 without starting node. otherwise it runs `claude-plugin/dist/worker-gate.mjs` with node, which asks the running MCP server over a unix socket and allows a native tool only when the server answers with an authorization for this session. the socket is `<state dir>/wolfpack-tasks/claude/gate/<first 16 hex characters of sha256(WOLFPACK_SESSION_ID)>.sock` in a mode-0700 directory; the server listens only in worker mode, replaces a stale socket at start, and removes its socket on shutdown. the server answers from a fresh evaluation, or from its latest one when the evaluation takes more than 1s. it holds at most 16 connections and closes each one 2s after accept, so slow clients cannot exhaust its descriptors; a query it rejects is denied. no listener (a stopped or killed server), no answer within 3s, a malformed, oversized, or other-session answer, a socket path longer than 103 bytes, a crash, a timeout, or a missing node blocks the tool in a worker session. limits: claude code cannot enforce the gate when hooks are disabled (`disableAllHooks`, managed hook policy) or the plugin is disabled; every session with the plugin enabled starts `/bin/sh` for each native tool call.
|
|
118
|
+
- **memory only.** as with pi, restarting the claude session loses its active tasks.
|
|
119
|
+
|
|
120
|
+
the channel is a claude code research-preview surface. claude code must allow this plugin's channel (wolfpack provides a managed-settings drop-in); without it, the fallback is the wake path.
|
|
121
|
+
|
|
122
|
+
## use without wolfpack
|
|
123
|
+
|
|
124
|
+
**the task core is transport-independent; the default pi extension is not.** you can import the core into your own application and supply a `TaskRelay`. there is no bundled alternative production network relay or configuration switch that makes the default extension wolfpack-free.
|
|
125
|
+
|
|
126
|
+
### try the core in one process
|
|
127
|
+
|
|
128
|
+
with the package available to your bun project, save this as `task-demo.ts` and run `bun task-demo.ts`. it uses the exported in-memory relay fixture, so it needs neither wolfpack nor a model:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import {
|
|
132
|
+
createInMemoryTaskRelay,
|
|
133
|
+
createTaskCore,
|
|
134
|
+
createTaskStore,
|
|
135
|
+
} from "@sgtbeatdown/wolfpack-tasks";
|
|
136
|
+
|
|
137
|
+
const relay = createInMemoryTaskRelay("demo");
|
|
138
|
+
const parentStore = createTaskStore();
|
|
139
|
+
const workerStore = createTaskStore();
|
|
140
|
+
const parent = createTaskCore({
|
|
141
|
+
endpoint: { relay: relay.id, id: "parent" },
|
|
142
|
+
relay,
|
|
143
|
+
store: parentStore,
|
|
144
|
+
});
|
|
145
|
+
const worker = createTaskCore({
|
|
146
|
+
endpoint: { relay: relay.id, id: "worker" },
|
|
147
|
+
relay,
|
|
148
|
+
store: workerStore,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await parent.connect();
|
|
153
|
+
await worker.connect();
|
|
154
|
+
const { taskId } = await parent.createTask({
|
|
155
|
+
target: worker.endpoint,
|
|
156
|
+
task: "summarize the change",
|
|
157
|
+
timeoutMs: 60_000,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// a real application would consume the assignment and do the work here.
|
|
161
|
+
for (const item of await worker.receive()) {
|
|
162
|
+
if (item.kind === "relay") await worker.acknowledgeRelayDelivery(item.delivery.cursor);
|
|
163
|
+
}
|
|
164
|
+
await worker.submitIntent({
|
|
165
|
+
taskId,
|
|
166
|
+
type: "task.completed",
|
|
167
|
+
payload: { summary: "the change adds task coordination" },
|
|
168
|
+
});
|
|
169
|
+
// relay items carry peer mail; local items are the parent's own view of events it canonicalized.
|
|
170
|
+
for (const item of await parent.receive()) {
|
|
171
|
+
if (item.kind === "relay") await parent.acknowledgeRelayDelivery(item.delivery.cursor);
|
|
172
|
+
else parent.completeLocalPresentation(item.event.eventId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(parent.getTask(taskId)?.status); // completed
|
|
176
|
+
await parent.acknowledgeParent(taskId);
|
|
177
|
+
} finally {
|
|
178
|
+
parentStore.close();
|
|
179
|
+
workerStore.close();
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
this demonstrates task coordination, not agent execution. both endpoints share one in-process fixture; it does not connect separate processes or machines.
|
|
184
|
+
|
|
185
|
+
### integrate your own transport
|
|
186
|
+
|
|
187
|
+
implement the exported [`TaskRelay` interface](src/task-protocol.ts) to connect endpoints, resolve targets, send envelopes, receive mail, and acknowledge individual deliveries. provide a separate `createTaskStore()` and `createTaskCore()` for each endpoint. `runTaskRelayConformance` is available to check a relay implementation.
|
|
188
|
+
|
|
189
|
+
`receive()` returns items in order: `relay` items wrap a delivery you acknowledge with `acknowledgeRelayDelivery`, and `local` items carry an event the origin itself canonicalized, which you complete with `completeLocalPresentation` once presented.
|
|
190
|
+
|
|
191
|
+
`createTaskCore({ timers })` accepts a `TaskTimers` clock for the delivery budget (deadline, retry slices, cooldowns), which lets tests run those bounds on virtual time; task records still use `clock`. an origin can move an active task off a target whose loss the relay reported with `reassignTask`; the [reference](docs/protocol-reference.md#memory-owned-lifecycle) covers its preconditions and why the default extension does not call it yet.
|
|
192
|
+
|
|
193
|
+
your application owns polling, retries, timeout evaluation, delivery acknowledgments, and shutdown. it also owns how assignments reach an agent and how that agent runs them. the core alone does not install pi tools or wake a model.
|
|
194
|
+
|
|
195
|
+
## important limits
|
|
196
|
+
|
|
197
|
+
- **active state lives in ram.** a pi restart loses that endpoint's tasks; a relay restart can lose even accepted mail. pi session history remains readable, but does not restore tasks or authorize workers after restart.
|
|
198
|
+
- **accepted is not executed.** relay acceptance, receiver receipt, pi follow-up insertion, model execution, and task completion are separate stages.
|
|
199
|
+
- **the parent owns task status.** a worker submits a result; the origin processes it before its task becomes terminal. status reads are local snapshots.
|
|
200
|
+
- **use a trusted network.** the wolfpack integration assumes trusted tailnet machines. do not expose owner APIs to the public internet or an untrusted proxy.
|
|
201
|
+
|
|
202
|
+
## reference and development
|
|
203
|
+
|
|
204
|
+
- [protocol and operations reference](docs/protocol-reference.md): memory lifecycle, restart loss, authentication, worker gate, delivery evidence, and acknowledgment details.
|
|
205
|
+
- [wolfpack relay control API](https://github.com/almogdepaz/wolfpack/blob/main/docs/control-api-schema.md#pi-tasks-relay-v2-boundary): transport contract.
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bun install
|
|
209
|
+
bun test
|
|
210
|
+
bun run typecheck
|
|
211
|
+
bun run build:claude # rebuild the committed claude plugin bundle and skill copy; a test fails when they are stale
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
optional real-worker checks and their prerequisites are documented in the [reference](docs/protocol-reference.md#development-and-verification).
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# protocol and operations reference
|
|
2
|
+
|
|
3
|
+
start with the [readme](../README.md) for an overview, quickstart, and the transport-independent core example. this reference covers the default wolfpack integration.
|
|
4
|
+
|
|
5
|
+
Endpoint-owned task coordination for Pi over Wolfpack's memory-only relay. **Both relay mail and endpoint task bookkeeping live in RAM.** Pi's existing session history is the historical record; there is no SQLite database, task-history directory, migration, disk outbox or startup replay.
|
|
6
|
+
|
|
7
|
+
The [relay control API](https://github.com/almogdepaz/wolfpack/blob/main/docs/control-api-schema.md#pi-tasks-relay-v2-boundary) defines the transport boundary.
|
|
8
|
+
|
|
9
|
+
## endpoint-owned relay
|
|
10
|
+
|
|
11
|
+
The default extension requires a compatible Wolfpack memory-owned server. It negotiates `volatile-v1` at `POST /api/task-relay/volatile-v1`. No transport opt-in flag is needed. The old durable relay adapter is removed, not retained as a compatibility mode. The transport keeps at most 8 requests in flight, with 2 more slots reserved for ACK/connect; further requests wait for a slot in FIFO order instead of failing locally. A waiter aborted by its caller has sent nothing and fails with a retryable `ABORTED`; `RELAY_CAPACITY` comes only from the server.
|
|
12
|
+
|
|
13
|
+
Set `WOLFPACK_SESSION_NAME` for each Pi process. `WOLFPACK_PORT` defaults to `18790`; `WOLFPACK_SESSION_NAME` resolves the active Pi process to its relay endpoint. Read `taskEndpoint` from structured session creation/status and pass its opaque `{ relay, id }` unchanged. Never derive it from a name, terminal output or broker ID.
|
|
14
|
+
|
|
15
|
+
### minimal valid v2 send envelope
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"to": {
|
|
20
|
+
"relay": "wolfpack-pi-tasks-v2",
|
|
21
|
+
"id": "target-opaque-endpoint-id"
|
|
22
|
+
},
|
|
23
|
+
"task": "implement the narrow change and run focused tests"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `agent_task_send` schema is exactly `to`, `task`, and optional `timeoutMs`. Unsupported fields are rejected before admission: a pre-admission validation rejection creates no task. If an outcome is uncertain, idempotency remains necessary within the current lifetime; inspect the returned task ID rather than blindly creating another assignment.
|
|
28
|
+
|
|
29
|
+
## history versus live state
|
|
30
|
+
|
|
31
|
+
- Pi requests task-processing turns through its safe `deliverAs: "followUp"` queue. Pi session messages and tool calls/results preserve historical context. Parent ACKs, late-terminal facts and confirmed receiver-authored conversation echoes are archived as `pi-tasks-event-record` custom entries before transport ACK, without model insertion or wake. Received model-visible task events include the complete structured event in `pi-tasks-event` details, including results—not just a rendered summary.
|
|
32
|
+
- Active tasks, outbox intentions, individual ACK checkpoints, deduplication and delivery-blocked evidence are RAM-owned. Each endpoint has lower-only limits of 16,384 records/32MiB encoded state. Admission fails before exceeding those bounds; they are not a process-RSS guarantee.
|
|
33
|
+
- `agent_task_send` confirms relay acceptance, **not execution**. `receiver_recorded` confirms receiver RAM receipt, **not durable persistence**. Pi insertion and wake acceptance are separate evidence, never proof of model execution.
|
|
34
|
+
- Delivery receipts are origin-local status. The origin records each one in its task log without presenting, archiving or forwarding it to the worker. Stages are ordered `receiver_recorded` < `pi_inserted` < `wake_accepted`, and a later stage implies the earlier ones. Per event and refresh, a receiver sends only the highest stage it reached, plus `pi_insertion` `blocked` when Pi did not record the insertion, so an assignment received, inserted and woken in one refresh costs one receipt. The `wake_accepted` record is written before Pi emits the wake, which keeps it ahead of any intent the woken model sends; it is sent only after the wake appears in Pi's entries, and an unobserved or throwing wake withdraws the never-sent record. `wake_requested` remains in `TaskDeliveryStage` and is accepted from 0.1.15 receivers (status shows wake acceptance pending) but is no longer emitted.
|
|
35
|
+
- Session history is not an operational journal. A new process/lifecycle starts empty with a fresh endpoint/generation; it does not reconstruct tasks, replay envelopes, reuse old ACKs or reopen a worker gate from old history.
|
|
36
|
+
- A relay restart can lose even accepted mail. A Pi restart loses that endpoint's active task state. Relay-loss evidence marks affected work failed only in the discarded local lifetime; its remote outcome remains explicitly unknown and is never presented as canonical delivery or completion evidence.
|
|
37
|
+
|
|
38
|
+
## memory-owned lifecycle
|
|
39
|
+
|
|
40
|
+
`createConfiguredTaskCore()` owns its transport and `createTaskStore()` RAM state. Startup registers; background polling is one `setTimeout` chain that re-arms after each refresh: every 1s while a task is active, the outbox is pending, or a gate-enabled worker awaits an eligible assignment, otherwise every 5s. Successful send/message/done/ack/cancel tools, `turn_end`, agent end and agent settlement request an immediate single-flight refresh; requests made during a running refresh coalesce into one trailing refresh. `agent_task_wait` requests one refresh, then re-checks local state when the RAM store commits a change or after a backoff of 250ms doubling to 2s; it issues no relay requests of its own. Shutdown fences new work, aborts requests, drains active calls and discards state. Late callbacks cannot mutate a successor lifetime.
|
|
41
|
+
|
|
42
|
+
Background refresh records protocol facts in RAM even while Pi is busy or has queued messages. A valid completion within the fetched receive page can become canonical before its notification is presented. Model-visible insertion and the separate follow-up wake still wait for idle Pi with no queued messages; visible relay mail remains replayable until the required structured insertion/wake evidence is present. Intake alone does not incorporate an assignment or authorize worker tools. Parent acknowledgment before terminal presentation retains the event but suppresses its redundant wake. Origin-authored conversations do not echo to the origin. The origin's own view of other canonical events it creates, such as a completion canonicalized from a worker's raw intent, is queued in a RAM-only local presentation table instead of a self-addressed relay envelope. `receive()` merges those local items with relay mail by intake order. A held model-visible item blocks later visible items; non-visible items behind it are still archived and ACKed. A local item needs the same insertion and wake evidence before it completes, but records no relay delivery receipts. Receiver-authored conversation copies remain canonical replicas, but local outbound intent identity makes their presentation archive-only; payload author claims are not provenance. Missing or unmatched intent identity leaves peer conversations actionable. This correlation is RAM-only and is never recovered from session history.
|
|
43
|
+
|
|
44
|
+
The adaptive 1s/5s polling cadence does not change relay byte limits. A single-flight background refresh runs explicit phases in one work scope: lease connection, then intake (timeout canonicalization and relay receive persist and canonicalize without sending), presentation (Pi insertion/wake and receipt records, still without sending), shipping (required timeout fan-out, intake fan-out and receipts in persist order), acknowledgement (ACKs for presented items, then raw ACK repair) and finally old-work retry. The shared counts are: at most 56 scoped sends plus 8 rotating old-work sends, 48 ACK attempts and receive pages of up to 50 items while fewer than 200 relay deliveries are fetched but unacknowledged, with a 2s overall deadline. Failed attempts consume the shared counts; the same send/ACK identity is not attempted twice in that scope. Separate 250ms retry/ACK-repair slices stop admitting another attempt, not an already admitted request: each admitted request gets the remaining background window. These are scheduling bounds, not throughput guarantees. Foreground send/message/done retain the configured transport request deadline (default 12s) and caller cancellation instead of inheriting the background cutoff. Any speculative old-work retry after a foreground send uses a finite 2s background window, starting only when that share is reached.
|
|
45
|
+
|
|
46
|
+
Intake first re-drives tracked, unacknowledged relay deliveries from the local RAM inbox in cursor order, then fetches after the highest tracked cursor and follows `hasMore` while the pending set has room. At 200 fetched-but-unacknowledged deliveries fetching pauses until ACKs drain it; nothing is skipped or dropped. Each delivery is tracked and persisted on its own before fan-out, so an abort mid-page loses nothing and the next fetch resumes after the last tracked cursor. Byte limits can make a page shorter than 50 items, and shared send/ACK counts can defer work. When the ship phase accepts a raw intent's required canonical fan-out, the intent stages an authorized ACK request in the existing checkpoint; the acknowledge phase's repair pass sends it, and a staged delivery is not re-driven. Failed or pending required fan-out withholds only that intent's ACK and no longer stops intake of later deliveries. A presented relay item is ACKed only after the ship phase accepted its receipts or found them blocked for good (reported as outbox degradation); a failed receipt withholds only that item's ACK, and presentation, shipping and ACKs of later items continue. Receipts to one target still leave in creation order, so a failed receipt also defers younger receipts to the same origin. Because receipts ship before ACKs, an ACK never precedes the receipt that justifies it. The repair pass uses the same ACK budget, admission slice and overall background deadline; remaining raw ACK work reports local deferral on budget/slice/cooldown exhaustion. Presentation stops at the remaining ACK budget. The first fan-out, receipt, presentation, repair or deferral failure is collected and reported after the pass, so it no longer holds back items intake already produced. Core operations in a phased pass never start old-work retry or ACK repair on their own; only the final phase retries old work, after required sends and ACKs, even on empty, held/busy or failed passes when budget remains. Ignored-abort ACK responses cannot later advance local authority. A held visible event stops presentation of later visible events but not archive and ACK of later non-visible events; an ACK budget exhaustion stops the pass. An intent whose required fan-out is quarantined is never ACKed and keeps one pending slot. General backlog scanning beyond the pending bound is not provided.
|
|
47
|
+
|
|
48
|
+
Scoped operations attempt their own immutable IDs and cannot report acceptance for omitted or partial fan-out. Concurrent same-ID sends join existing delivery rather than retransmitting. Pending retry IDs rotate incrementally in RAM on attempts/actual skips, including failures and cooldown skips, without copying payloads or rotating an unattempted suffix past the retry deadline. A foreground packet's capacity refusal does not suppress smaller old packets to the same target; same-cycle target suppression starts only when speculative retry itself encounters capacity. Mail to one target leaves in creation order: an envelope is sent only after every older pending envelope to the same target is accepted, and a younger send waits for an older send already in flight. While the oldest envelope is cooling down or out of budget, younger mail to that target reports `TASK_DELIVERY_DEFERRED` (with `blockedBy`); a quarantined envelope leaves the queue and blocks nothing, and other targets are unaffected. Retry selection offers only each target's oldest pending envelope, and a reported send first offers older mail to its target from the bounded old-work share. Known capacity defers that envelope/ACK for at least 5s (or the validated longer `retryAfterMs`), without a retry timer. `TASK_DELIVERY_DEFERRED` is a retryable local budget/cooldown outcome, not relay acceptance or permanent quarantine. Raw intent replays retain their canonical fan-out reservation and are not ACKed while required sends remain pending; quarantined required sends report their blocked outcome instead of false acceptance. The exported `TaskStore` contract requires indexed lookup, fair rotation, cooldown, per-target FIFO selection (`pendingOutboxAhead`), withdrawal of a never-sent reservation (`discardUnsentOperation`), the RAM local presentation queue, post-commit `onChange` notification, the indexed `getQuarantine(envelopeId)` and `hasActiveTasks()` lookups and `setTarget(taskId, target)`, supplied by `createTaskStore()`; custom `TaskStore`/`TaskCore` implementations written against 0.1.15 must add these members (`TaskCore` also gains `hasPendingWork`, `onChange`, `completeLocalPresentation` and the phased-pass members `intakeInbox`, `shipRequired` and `repairAcknowledgements`, and its `WakeRequest` gains `discard()`; `createTaskWorkScope(signal, "phased")` makes a scope whose evidence and fan-out sends are queued for `shipRequired`, and `deliverTaskInbox` requires one); there is no legacy snapshot/fixed-prefix fallback. `TaskCore.receive()` returns an ordered array of `{ kind: "relay", delivery, order }` and `{ kind: "local", event, order }` items; a consumer ACKs relay items and completes local items with `completeLocalPresentation(eventId)`. The documented transport seam is unchanged, and origin-targeted canonical envelopes from older peers are still accepted.
|
|
49
|
+
|
|
50
|
+
`createTaskStore()` keeps each row as immutable encoded text and caches its parsed value on first read; events are indexed by task and active tasks are tracked as a set, and both indexes follow transaction rollback and `clear()`. Stored values are deep-frozen and shared between reads: outbox and quarantine records, task events, payloads and endpoints throw a `TypeError` on a strict-mode write. Task snapshots and pending presentations are fresh top-level objects around those frozen values, so a write to their own fields is lost. Neither reaches the store. The store's own read-modify-write of the ACK checkpoint works on a copy. `createTaskCore({ timers })` takes an optional `TaskTimers` (`now`, `elapsed`, `timeout`) for the delivery budget: the 2s deadline, the retry/ACK-repair slices and capacity cooldowns. It defaults to system time and is separate from `clock`, which stamps task records; tests use it to run deadline and cooldown cases on virtual time.
|
|
51
|
+
|
|
52
|
+
The default extension automatically replaces a lifetime only after a structured `RELAY_RESET` or `RELAY_REBIND_REQUIRED`. It fences and discards the unusable RAM state, records each active task in session history as locally failed with `RELAY_STATE_LOST` and an explicitly unknown remote outcome, then binds a fresh empty endpoint. The loss record does not wake the model by itself. Transient availability, authentication, capacity, abort, profile and malformed-response failures never trigger replacement. Historical tasks are not adopted into the successor lifetime.
|
|
53
|
+
|
|
54
|
+
An origin can move an active task off a lost target with the optional `TaskCore.reassignTask(taskId)`, which `createTaskCore()` provides. It requires a quarantined send to the current target whose relay refusal was `TARGET_NOT_REGISTERED`; otherwise it fails with non-retryable `REASSIGNMENT_REFUSED` (`NOT_ORIGIN` for a non-origin caller). It resolves the lost endpoint's reference through the relay and refuses when it resolves to itself; the loss and the target are re-checked inside the commit, so one loss allows one reassignment even when calls race. It then sets the task's target, appends a `task.reassigned` event (payload `{ task, previousTarget }`), presents it locally at the origin and sends the successor an assignment envelope carrying `{ task, event, history }`. History that would exceed `MAX_RELAY_PAYLOAD_BYTES` keeps the newest events that fit and sets `historyTruncated: true`; a task too large even without history fails with `PAYLOAD_TOO_LARGE`. The receiver validates the carried history against the task and records it like an original assignment: it sends a `receiver_recorded` receipt, presents a `## task assignment (reassigned)` message and opens the worker gate for that task. A known task follows the reassignment, and carried events it already holds are skipped. A 0.1.15 receiver rejects `task.reassigned` as `INVALID_ASSIGNMENT`.
|
|
55
|
+
|
|
56
|
+
The default extension does not call `reassignTask` yet. A current Wolfpack server issues a fresh random endpoint id on every rebind and answers `resolve` for a retired id with `TARGET_NOT_REGISTERED`, so the origin can detect the loss but cannot discover the successor; the client also rejects a `resolve` result whose id differs from the reference. Automatic reassignment needs the server to resolve a retired id (or a stable session-scoped reference that survives restart) to its live successor, and then a client change to accept the differing id.
|
|
57
|
+
|
|
58
|
+
Error codes have one internal registry (`src/task-errors.ts`) that gives each code a class, a default message and explicit retry, rebind, relay-loss and blocked-delivery flags; a per-throw `retryable` still wins. Every failure of a `createVolatileTaskSession` relay request carries the relay operation in `details.operation`, and failures built from relay responses use the registry message for their code (server-supplied text was never shown); codes, `retryable` and other details are unchanged, and nothing new goes on the wire. The status line names a failed relay operation as `tasks: <class> (<CODE>) on <operation>`, for example `tasks: capacity (RELAY_CAPACITY) on send`; an unregistered code has class `error`. Relay loss, a missing compatible profile and required authentication keep their dedicated labels, and failures without a named operation keep `tasks: outbox degraded` or `tasks: relay unavailable`. Event handling (model-visible, terminal and its status, conversational, origin-local evidence, accepted as a receiver intent) comes from one internal `TASK_EVENT_TYPES` table in `src/task-protocol.ts`; unknown event types are still rejected.
|
|
59
|
+
|
|
60
|
+
`createVolatileTaskSession({ url, callerSession, store })` is the lower-level API for a caller-owned RAM store. Transport reconnect within that same lifetime retains pending immutable retries; `rebind()` clears the store, while `close()` stops transport and the caller then closes its state. Persistence/path options are rejected. Old database files are neither read nor deleted.
|
|
61
|
+
|
|
62
|
+
Full immutable content, including the original creation timestamp, is reused for retries. Destination-confirmed acceptance is required. Conflicting content, exhausted delivery and reset cannot become successful acceptance. Individual ACKs preserve sparse bigint cursors and cannot skip earlier pending mail. Bounds/deadlines apply through complete response bodies, including noncooperative injected transports.
|
|
63
|
+
|
|
64
|
+
## trusted Tailnet and optional authentication
|
|
65
|
+
|
|
66
|
+
The intended deployment trusts its Tailnet machines; Tailscale access control is the network boundary. Do not expose owner APIs through the public internet/Funnel or an untrusted proxy. No additional JWT or peer-signature scheme is required by the task protocol.
|
|
67
|
+
|
|
68
|
+
Existing global Wolfpack JWT enforcement remains **optional**. If the owner configures `WOLFPACK_JWT_SECRET`, the configured Pi factory supports it with short-lived tokens only for HTTP loopback. It never discovers/distributes secrets or automatically forwards them to arbitrary HTTPS URLs. HTTP401 reports `RELAY_AUTH_REQUIRED`, not an epoch reset.
|
|
69
|
+
|
|
70
|
+
Remote task endpoints must be locally resolved aliases. The paired CLI qualifies a selected remote endpoint through the local coordinator; remote lists expose explicit remote metadata. Failed qualification preserves any created session/ID but removes unsafe `taskEndpoint` output and reports `taskEndpointError`.
|
|
71
|
+
|
|
72
|
+
## delegation workflow
|
|
73
|
+
|
|
74
|
+
Configure `WOLFPACK_IMPLEMENTER_MODEL` and `WOLFPACK_REVIEWER_MODEL`; defaults are `openai-codex/gpt-5.6-terra` and `openai-codex/gpt-5.6-sol`. Explicit user or project choices override those defaults.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
IMPLEMENTER_MODEL="${WOLFPACK_IMPLEMENTER_MODEL:-openai-codex/gpt-5.6-terra}"
|
|
78
|
+
REVIEWER_MODEL="${WOLFPACK_REVIEWER_MODEL:-openai-codex/gpt-5.6-sol}"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
1. Use one assignment mode: full-startup instructions, or an endpoint assignment—not both. For a task worker: `wolfpack agent spawn --project-dir /absolute/worktree --name <task-role> --model "$IMPLEMENTER_MODEL" --task-worker --readiness-timeout-ms 30000 --json` (or `"$REVIEWER_MODEL"`). This mode rejects prompts/plans and `--notify-parent`; put all instructions in `agent_task_send.task`.
|
|
82
|
+
2. Verify exact session ID/root/Pi harness and registered `taskEndpoint`. `TASK_WORKER_PREFLIGHT_FAILED` precedes creation. `TASK_WORKER_NOT_READY` retains `createdSession`/`cleanup`; inspect `unconfirmed` cleanup by exact ID before retry. Registration is not model execution.
|
|
83
|
+
3. Submit the assignment and continue useful work. When none remains, yield to structured follow-up. Do not poll `agent_task_status` or `agent_task_inbox` merely to wait; use `agent_task_wait` only when explicitly asked to block.
|
|
84
|
+
4. Use `agent_task_message` for questions, answers and information. The receiver's last action is `agent_task_done`, not completion prose.
|
|
85
|
+
5. Independently verify, then acknowledge the terminal task and deliberately retain or clean up the parent-owned role.
|
|
86
|
+
|
|
87
|
+
Endpoint assignments require terminal completion and one `agent_task_ack`; full-startup children have no task ID, so use explicit completion/block notification and parent verification instead. Close only sessions the parent spawned using `wolfpack kill <stable-session-id> --json`, then verify that ID absent from `wolfpack list --json`. Never use `wolfpack session send`, `/exit`, or `/quit` for cleanup.
|
|
88
|
+
|
|
89
|
+
## worker-only execution gate
|
|
90
|
+
|
|
91
|
+
The exact value `PI_TASK_WORKER=1` or the harness-neutral alias `WOLFPACK_TASK_WORKER=1` enables a fail-closed model tool gate; either variable alone is sufficient, and other values leave the gate off. The error codes keep their `PI_TASK_WORKER_` prefix because they are a stable contract. Before assignment only inbox/status/wait are allowed; other tools receive `PI_TASK_WORKER_ASSIGNMENT_REQUIRED`. Ordinary interactive Pi and explicit user shell commands are outside this gate.
|
|
92
|
+
|
|
93
|
+
`PI_TASK_WORKER=1` sessions are leaf roles. Generic role-orchestration guidance applies only to non-worker coordinators. The spawning coordinator owns each child's complete lifecycle.
|
|
94
|
+
|
|
95
|
+
Workers cannot call `agent_task_send`, `agent_task_cancel`, or `agent_task_ack` (`PI_TASK_WORKER_COORDINATION_FORBIDDEN`). `agent_task_message` must name their eligible incorporated assignment. A structured session event alone is insufficient: its task must also be active in this endpoint's current RAM state. Old session history never authorizes work after restart.
|
|
96
|
+
|
|
97
|
+
After confirmed relay loss discards active assignments, assignment-required denials keep the `PI_TASK_WORKER_ASSIGNMENT_REQUIRED` code as their prefix and add `RELAY_STATE_LOST`, up to 8 lost assignment IDs (JSON-quoted, each truncated after 128 characters, with additional IDs explicitly omitted), and parent recovery instructions. This bounded diagnostic comes only from the discarded lifetime's active tasks targeted to this worker, not session history; it survives failed fresh binding but is cleared on session replacement/shutdown. It is not authorization or a canonical task outcome. An ordinary unassigned worker still receives only the original denial code. Existing local-loss history records also include the recovery instructions, without a new wake.
|
|
98
|
+
|
|
99
|
+
The parent/coordinator must independently verify work and side effects, obtain the worker's fresh `taskEndpoint` from structured Wolfpack session status, then send a new explicit assignment for the remaining scope. Do not replay the old task ID or infer a canonical remote outcome from local failure. The worker must wait for the fresh assignment; diagnostics never restore, replay, reassign or weaken the gate.
|
|
100
|
+
|
|
101
|
+
Preflighting `agent_task_done` marks that task as closing before sibling calls are preflighted. Ordinary tools remain blocked for a closing, pending-terminal, accepted-terminal, or `delivery_blocked` task; an idempotent `agent_task_done` retry for that same assigned task remains allowed. Another independently active assignment can still authorize work.
|
|
102
|
+
|
|
103
|
+
## terminal delivery and acknowledgment
|
|
104
|
+
|
|
105
|
+
During the active lifetime `terminalDelivery` reports `not_submitted`, `pending`, `accepted`, or `delivery_blocked`. The blocked variant includes stable intent/envelope identities, origin endpoint, timestamp, and structured non-retryable relay error. Canonical task status is never changed to `delivery_blocked`. Retryable failures reuse the original intent; permanent failures are not rearmed. This evidence is lost on endpoint restart.
|
|
106
|
+
|
|
107
|
+
`agent_task_ack` is terminal-only and origin-owned. Sequential/concurrent retries in one lifetime reuse the logical event and envelope IDs. There is no restart recovery. Report changed source paths in `result.changedFiles`; artifacts are receiver-project-relative regular files, not the changed-file list.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{ "result": { "changedFiles": ["src/extension.ts"] }, "artifacts": [{ "path": "verification/task-2.md" }] }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## development and verification
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bun install
|
|
117
|
+
bun test
|
|
118
|
+
bun run typecheck
|
|
119
|
+
PI_TASKS_WOLFPACK_SOURCE=/absolute/clean/wolfpack-checkout \
|
|
120
|
+
PI_TASKS_WOLFPACK_REVISION=<full-commit-id> \
|
|
121
|
+
bun test tests/volatile-real-worker.test.ts tests/volatile-extension-worker.test.ts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Wolfpack worker tests require its supported Bun runtime (currently stable>=1.4.2). Optional source-selected checks use private HTTP/workers, not installed services. They test same-lifetime retry/ACK, automatic default-extension reset recovery, explicit lower-level session replacement, fresh endpoint state and history-only evidence. `createInMemoryTaskRelay` is a deterministic conformance fixture, never the default runtime transport. Physical two-machine qualification, final-path performance, release packaging and live activation remain separate gates.
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sgtbeatdown/wolfpack-tasks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Memory-only endpoint-owned agent delegation for Pi and Claude Code through Wolfpack relay v2.",
|
|
5
|
+
"module": "src/index.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/almogdepaz/wolfpack-tasks.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"pi",
|
|
16
|
+
"agent",
|
|
17
|
+
"tasks"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"README.md",
|
|
21
|
+
"docs",
|
|
22
|
+
"src",
|
|
23
|
+
"skills",
|
|
24
|
+
"tsconfig.json"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@earendil-works/pi-ai": "*",
|
|
31
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
32
|
+
"@earendil-works/pi-tui": "*",
|
|
33
|
+
"typebox": "*"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@earendil-works/pi-ai": "^0.80.10",
|
|
37
|
+
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
38
|
+
"@earendil-works/pi-tui": "^0.80.10",
|
|
39
|
+
"@types/bun": "^1.3.14",
|
|
40
|
+
"typebox": "^1.3.6",
|
|
41
|
+
"typescript": "^7.0.2"
|
|
42
|
+
},
|
|
43
|
+
"pi": {
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./src/extension.ts"
|
|
46
|
+
],
|
|
47
|
+
"skills": [
|
|
48
|
+
"./skills"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"test": "bun test",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"build:claude": "bun scripts/build-claude.ts"
|
|
55
|
+
},
|
|
56
|
+
"type": "module"
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: task-context-summary
|
|
3
|
+
description: Recover or reuse a bounded Wolfpack task context summary when the parent needs to reconstruct handoff context without copying a transcript.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# task context summary
|
|
7
|
+
|
|
8
|
+
parent authors normal summaries. use this skill only for recovery or reuse when the parent lacks enough already-loaded context.
|
|
9
|
+
|
|
10
|
+
## evidence order
|
|
11
|
+
|
|
12
|
+
1. inspect relevant docs/code first.
|
|
13
|
+
2. use an existing session summary next, if one exists.
|
|
14
|
+
3. inspect only the selective recent transcript needed to resolve remaining gaps.
|
|
15
|
+
|
|
16
|
+
do not copy a full transcript, infer unseen file contents, or automatically cache generated summaries. when evidence disagrees, state the disagreement explicitly instead of silently choosing a version.
|
|
17
|
+
|
|
18
|
+
## output
|
|
19
|
+
|
|
20
|
+
return concise Markdown, under 16KiB UTF-8, using any useful subset:
|
|
21
|
+
|
|
22
|
+
```md
|
|
23
|
+
## constraints and preferences
|
|
24
|
+
## progress
|
|
25
|
+
### done
|
|
26
|
+
### in progress
|
|
27
|
+
### blocked
|
|
28
|
+
## key decisions
|
|
29
|
+
## critical context
|
|
30
|
+
## failed approaches
|
|
31
|
+
## open questions
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
include selected refs separately in the task request. mention a ref's path, selector, and purpose only; do not read or duplicate ref contents merely to populate the summary.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wolfpack-task-delegation
|
|
3
|
+
description: Use when opening, selecting, delegating to, checking, or cleaning up a Wolfpack agent session (Pi or Claude Code) through the endpoint-owned agent task tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wolfpack task delegation
|
|
7
|
+
|
|
8
|
+
use `wolfpack-tailnet-control` for session control. this skill covers endpoint-owned v2 task lifecycle, structured handoffs, and parent verification. The cutover package default uses the epoch-bound `volatile-v1` transport with the unchanged `pi-tasks/v2` task protocol. It requires a coordinated compatible Wolfpack server; an old server is refused, never silently adopted.
|
|
9
|
+
|
|
10
|
+
## harnesses
|
|
11
|
+
|
|
12
|
+
The same `agent_task_*` tools, parameters, and lifecycle rules apply in every supported harness:
|
|
13
|
+
|
|
14
|
+
- **Pi** loads this package's default extension. Task mail enters session history and wakes the model through Pi's follow-up queue.
|
|
15
|
+
- **Claude Code** loads the `wolfpack-tasks` plugin. Its MCP server wakes the model with a short channel pointer (`wolfpack task mail: call agent_task_inbox`); the task content arrives only in the `agent_task_inbox` result. When you see a wolfpack task channel message, call `agent_task_inbox` and act on the returned events.
|
|
16
|
+
|
|
17
|
+
Harness-specific notes below are labeled. Everything else is harness-neutral.
|
|
18
|
+
|
|
19
|
+
## choose one assignment mode
|
|
20
|
+
|
|
21
|
+
choose exactly one mode for the initial assignment:
|
|
22
|
+
|
|
23
|
+
- **full-startup mode:** spawn with the complete `--plan`, `--prompt-file`, or concise `--prompt`. do not call `agent_task_send` for that same work.
|
|
24
|
+
- **endpoint mode:** spawn without a startup plan or prompt, obtain the structured `taskEndpoint`, then put the complete assignment in one `agent_task_send.task`.
|
|
25
|
+
|
|
26
|
+
never combine a plan-driven spawn with a narrower endpoint task. prefer one cohesive implementation handoff per approved PR or phase; split only at a real approval, design, isolation, or blocker boundary—not per issue, commit, finding, or verification checkpoint. the remaining v2 workflow below describes endpoint mode.
|
|
27
|
+
|
|
28
|
+
## role model selection
|
|
29
|
+
|
|
30
|
+
Role models are explicit and configurable at spawn time. The defaults below are Pi models:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
IMPLEMENTER_MODEL="${WOLFPACK_IMPLEMENTER_MODEL:-openai-codex/gpt-5.6-terra}"
|
|
34
|
+
REVIEWER_MODEL="${WOLFPACK_REVIEWER_MODEL:-openai-codex/gpt-5.6-sol}"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Pass `--model "$IMPLEMENTER_MODEL"` when spawning the editing implementer and `--model "$REVIEWER_MODEL"` when spawning the read-only reviewer. an explicit user or project model choice overrides the environment/default. never infer role or model from the session name, and never omit the resolved model because the current parent default may differ.
|
|
38
|
+
|
|
39
|
+
## v2 requirements and addressing
|
|
40
|
+
|
|
41
|
+
- load the task adapter in every participating session (Pi: this package's default extension; Claude Code: the `wolfpack-tasks` plugin) and set `WOLFPACK_SESSION_NAME`. Set `WOLFPACK_PORT` only when the local Wolfpack control port differs from `18790`.
|
|
42
|
+
- address `agent_task_send` targets only as `to: { relay, id }`. For the default Wolfpack adapter, the relay is `wolfpack-pi-tasks-v2` and the ID is opaque.
|
|
43
|
+
- create an endpoint worker with opt-in readiness and no initial assignment prompt: `wolfpack agent spawn --project-dir /absolute/worktree --name <task-role> --model "$IMPLEMENTER_MODEL" --task-worker --readiness-timeout-ms 30000 --json` (or `"$REVIEWER_MODEL"` for review). Use the actual explicit project root. This mode currently launches Pi workers only and rejects prompts/plans and `--notify-parent`; do not start a blocking “wait for assignments” prompt. Put the complete instructions in `agent_task_send.task`.
|
|
44
|
+
- read the returned `taskEndpoint` after exact live session/root and relay registration readiness succeeds. For a selected existing session, inspect structured status by its stable ID. Do not derive endpoint IDs from session names, broker IDs, terminal labels, output, or prose. Registration proves neither provider/model readiness nor task execution.
|
|
45
|
+
- inspect typed creation errors before retrying: `TASK_WORKER_PREFLIGHT_FAILED` precedes creation; `TASK_WORKER_NOT_READY` retains `createdSession` and `cleanup`. An `unconfirmed` cleanup requires exact-ID inspection, not an assumption that the session is gone. Do not silently fall back to ordinary spawn if the installed Wolfpack lacks readiness support; report the capability gap.
|
|
46
|
+
- pass the returned endpoint without translation:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"to": { "relay": "wolfpack-pi-tasks-v2", "id": "target-opaque-endpoint-id" },
|
|
51
|
+
"task": "implement the approved phase and run focused tests",
|
|
52
|
+
"timeoutMs": 3600000
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The default `agent_task_send` schema is exactly `to`, `task`, and optional `timeoutMs`. Unsupported fields are rejected rather than translated. use at least `3600000` milliseconds for coding assignments; timeout is a failure deadline, not a progress-poll interval.
|
|
57
|
+
|
|
58
|
+
## phase roles and handoffs
|
|
59
|
+
|
|
60
|
+
For a multi-step project phase, retain one persistent implementer and one persistent read-only reviewer. Give the implementer the whole approved phase, including required commit ordering and focused verification, rather than opening one task per issue or checkpoint. Reuse a healthy role session for corrections and follow-up review; a completed task finishes one assignment, not the underlying session. Do not rotate a healthy role session for routine corrections. Rotate only for phase completion, material context degradation, harness failure, or required specialist independence. Keep at most one active assignment per role unless the user explicitly approves parallel work.
|
|
61
|
+
|
|
62
|
+
A coordinator-capable agent may delegate further when the quality or throughput gain justifies it. For an endpoint assignment, the spawning coordinator owns task lifecycle: record the stable session ID, acknowledge its terminal task once, then deliberately retain the child or close it with `wolfpack kill <stable-session-id> --json` and verify that exact ID is absent from `wolfpack list --json`. Full-startup children have no endpoint task ID; use their explicit completion/block notification plus parent verification before the same retain-or-exact-ID-kill decision.
|
|
63
|
+
|
|
64
|
+
`PI_TASK_WORKER=1` sessions are leaf roles; `WOLFPACK_TASK_WORKER=1` is the harness-neutral alias with identical effect. Workers cannot call `agent_task_send`; workers cannot call `agent_task_cancel`; workers cannot call `agent_task_ack`. These coordinator-tool attempts are blocked with the stable reason `PI_TASK_WORKER_COORDINATION_FORBIDDEN`, not the pre-assignment reason. They may use `agent_task_message` only for the eligible incorporated assignment named by its input `taskId`. Generic role-orchestration guidance applies only to non-worker coordinators. Task workers must not create, rotate, or close Wolfpack sessions through shell or session-control tools.
|
|
65
|
+
|
|
66
|
+
After every verified and acknowledged terminal task, explicitly retain a parent-spawned session only when concrete follow-up is likely; otherwise close it through canonical Wolfpack session control. Workers never close their own sessions. terminal `send` is only for explicit human steering; it is not task state, completion evidence, or a substitute for `agent_task_message`.
|
|
67
|
+
|
|
68
|
+
## delegation lifecycle
|
|
69
|
+
|
|
70
|
+
| event | owner | command | session outcome |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| assignment completion | worker | `agent_task_done` | unchanged |
|
|
73
|
+
| task acknowledgment | parent | `agent_task_ack` | unchanged |
|
|
74
|
+
| role session retention | parent | `none` | retained for reuse |
|
|
75
|
+
| role session teardown | spawning coordinator | `wolfpack kill <stable-session-id> --json` | exact stable session id terminated |
|
|
76
|
+
| teardown verification | spawning coordinator | `wolfpack list --json` | exact stable session id absent |
|
|
77
|
+
| harness exit | none | `/exit` or `/quit` | not Wolfpack teardown |
|
|
78
|
+
|
|
79
|
+
Assignment completion ends the assigned task, not the reusable role session. When no further reuse is likely, the coordinator that spawned the role—not the worker itself—must pass the exact stable session ID to `wolfpack kill <stable-session-id> --json`, then inspect `wolfpack list --json` and verify that ID is absent. Never use `wolfpack session send`, `/exit`, or `/quit` for cleanup: they are terminal interaction, not Wolfpack teardown. Do not guess lifecycle commands. Workers never close themselves.
|
|
80
|
+
|
|
81
|
+
## v2 workflow
|
|
82
|
+
|
|
83
|
+
1. create a role session through the readiness path above, or select an existing role by stable ID and verify its structured liveness, canonical project, harness, and registered `taskEndpoint`. Never use terminal output as readiness evidence.
|
|
84
|
+
2. call `agent_task_send` with the opaque endpoint and concise instructions in `task`. The tool returns after relay acceptance only, not model-visible insertion or model execution. If submission reports a retryable delivery error after local persistence, use the structured task ID from the error and inspect it rather than creating an unrelated replacement.
|
|
85
|
+
3. after `agent_task_send`, continue useful independent coordinator work. When none remains, yield the current turn and let structured task follow-up wake the parent; do not poll `agent_task_status` or `agent_task_inbox` merely to wait. Reserve those tools for concrete progress evidence or recovery, and call `agent_task_wait` only when the user explicitly asks to block.
|
|
86
|
+
4. while continuing work or asking for input, use `agent_task_message` for intermediate `question`, `answer`, or `information` flow within the active endpoint lifetime. Do not use rendered text, terminal output, or logs as lifecycle evidence.
|
|
87
|
+
5. assignees call exactly one final `agent_task_done` with the assigned task ID, terminal status, and concise completion or blocker summary, plus optional structured result, error, and artifact declarations. Intermediate messages remain allowed; do not send a separate terminal information message before `agent_task_done`. Report source modifications in `result.changedFiles`; artifacts are receiver-project-relative regular files for a parent to inspect, not changed-file lists: `{ "result": { "changedFiles": ["src/extension.ts"] }, "artifacts": [{ "path": "verification/task-2.md" }] }`. No prose completion afterward.
|
|
88
|
+
6. the parent independently verifies files, diff, tests, and artifacts. Then call `agent_task_ack({ taskId })` for that one terminal task and explicitly retain or close the spawned role session.
|
|
89
|
+
|
|
90
|
+
## v2 delivery and recovery
|
|
91
|
+
|
|
92
|
+
Wolfpack is a memory-only, content-blind relay; each endpoint keeps active task state, event order, retries and receipts in RAM. There is no SQLite task database. Relay acceptance is not execution; receiver receipt is not durable persistence. Pi: model-visible `pi-tasks-event` entries include the complete structured event in session history; task wakes use Pi's safe `deliverAs: "followUp"` queue. Non-waking receipts, parent ACKs and late-terminal facts are archived as `pi-tasks-event-record` custom entries, not replayed. Claude Code: the wake is a content-free channel pointer and the structured events arrive in the `agent_task_inbox` result. History is for inspection, never reconstruction/replay or worker authorization after restart. A relay restart can lose even accepted mail; a harness restart loses its active task state and starts a fresh endpoint.
|
|
93
|
+
|
|
94
|
+
The default extension automatically replaces its endpoint lifetime only after structured `RELAY_RESET` or `RELAY_REBIND_REQUIRED` evidence. It fences and discards old RAM state, records every active old-lifetime task in session history as locally failed with `RELAY_STATE_LOST` and an explicitly unknown remote outcome, then binds a fresh empty endpoint. This local loss record is not a canonical remote terminal event and does not wake the model by itself. Historical tasks are never adopted or silently reassigned. Transient availability, authentication, capacity, abort, profile and malformed-response failures do not rotate the lifetime. Old database files are not read, migrated or deleted; no backward-compatible durable mode is supported.
|
|
95
|
+
|
|
96
|
+
Receiver terminal submission has one task-wide logical identity. Same-status retries reuse it; a conflicting terminal status fails closed. Canonical status remains origin-owned, while `terminalDelivery` separately exposes `not_submitted`, `pending`, `accepted`, or `delivery_blocked`. Parent acknowledgment is terminal-only and retries reuse one logical event and stable envelope identities.
|
|
97
|
+
|
|
98
|
+
Set `PI_TASK_WORKER=1` (or `WOLFPACK_TASK_WORKER=1`) only for task-only workers. Before a valid structured assignment matches a locally owned active task, the worker gate allows only inbox/status/wait inspection and blocks other model tools with `PI_TASK_WORKER_ASSIGNMENT_REQUIRED`.
|
|
99
|
+
|
|
100
|
+
## do not
|
|
101
|
+
|
|
102
|
+
- do not use terminal output, rendered prompts, logs, or error prose as task state.
|
|
103
|
+
- do not ask workers to complete in prose or close their own sessions.
|
|
104
|
+
- do not steer or interrupt an active turn with task context; use the adapter's wake path (Pi follow-up queue, Claude Code channel pointer).
|
|
105
|
+
- Do not copy plans, source contents, or transcripts into task context.
|
|
106
|
+
- do not derive or transform opaque endpoint IDs.
|
|
107
|
+
- do not promise JWT federation, artifact byte transfer, exactly-once model execution, or migration of historical tasks to successor endpoints.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Sleep until the timeout, an optional early wake, or abort; every exit path releases the timer and both subscriptions. */
|
|
2
|
+
export async function abortableSleep(milliseconds: number, signal: AbortSignal | undefined, cancellationError: () => Error, subscribeWake?: (wake: () => void) => () => void): Promise<void> {
|
|
3
|
+
if (signal?.aborted) throw cancellationError();
|
|
4
|
+
await new Promise<void>((resolve, reject) => {
|
|
5
|
+
let unsubscribeWake: (() => void) | undefined;
|
|
6
|
+
const release = (): void => {
|
|
7
|
+
clearTimeout(timeout);
|
|
8
|
+
signal?.removeEventListener("abort", abort);
|
|
9
|
+
unsubscribeWake?.();
|
|
10
|
+
};
|
|
11
|
+
const timeout = setTimeout(() => {
|
|
12
|
+
release();
|
|
13
|
+
resolve();
|
|
14
|
+
}, milliseconds);
|
|
15
|
+
const abort = (): void => {
|
|
16
|
+
release();
|
|
17
|
+
reject(cancellationError());
|
|
18
|
+
};
|
|
19
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
20
|
+
unsubscribeWake = subscribeWake?.(() => {
|
|
21
|
+
release();
|
|
22
|
+
resolve();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|