@sgtbeatdown/pi-tasks 0.1.6 → 0.1.8
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 +17 -121
- package/package.json +3 -5
- package/skills/wolfpack-pi-task-delegation/SKILL.md +8 -17
- package/src/task-store.ts +28 -141
- package/src/gateway-client.ts +0 -409
- package/src/legacy-extension.ts +0 -241
- package/src/legacy-task-inbox.ts +0 -148
- package/src/metrics-cli.ts +0 -20
- package/src/metrics.ts +0 -288
- package/src/task-artifacts.ts +0 -153
- package/src/task-board.ts +0 -365
- package/src/v1-compat-extension.ts +0 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# pi-tasks
|
|
2
2
|
|
|
3
|
-
`pi-tasks` is
|
|
3
|
+
`pi-tasks` is an endpoint-owned Pi extension for Wolfpack relay v2. It keeps task state in each Pi endpoint's local SQLite store and uses Wolfpack as a content-blind relay. It never writes assignments to terminals. Wolfpack's [relay v2 control-api contract](https://github.com/almogdepaz/wolfpack/blob/main/docs/control-api-schema.md#pi-tasks-relay-v2-boundary) is canonical for the transport boundary.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## endpoint-owned relay
|
|
6
6
|
|
|
7
|
-
The default extension uses the configured local Wolfpack relay
|
|
7
|
+
The default extension uses the configured local Wolfpack relay, never the in-memory conformance relay. It registers an opaque endpoint with `POST /api/task-relay/v2/connect`, stores task state at the deterministic per-session path `~/.pi/tasks/v2/sessions/<sha256(WOLFPACK_SESSION_NAME)>/tasks.sqlite`, and exchanges opaque relay envelopes through Wolfpack. The adapter requires a Wolfpack release exposing the stable [relay v2 control-api contract](https://github.com/almogdepaz/wolfpack/blob/main/docs/control-api-schema.md#pi-tasks-relay-v2-boundary).
|
|
8
8
|
|
|
9
9
|
Set `WOLFPACK_SESSION_NAME` for every Pi process. The adapter uses `WOLFPACK_PORT` when the local control port differs from `18790`; `WOLFPACK_SESSION_NAME` resolves the active Pi process to its relay endpoint. After the target extension registers, run `wolfpack session status <session> --json` and read its `taskEndpoint`. Pass that opaque `{ relay, id }` value unchanged; do not derive it from a session name, broker ID, terminal label, output, or prose.
|
|
10
10
|
|
|
@@ -20,116 +20,42 @@ Set `WOLFPACK_SESSION_NAME` for every Pi process. The adapter uses `WOLFPACK_POR
|
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
The
|
|
23
|
+
The `agent_task_send` schema is exactly `to`, `task`, and optional `timeoutMs`. Unsupported fields are rejected before persistence: a pre-persistence validation rejection creates no task. If creation status is uncertain after a transport failure, idempotency remains necessary; inspect the structured task ID rather than creating an unrelated replacement.
|
|
24
24
|
|
|
25
|
-
Wolfpack
|
|
25
|
+
Wolfpack owns durable mailbox delivery and peer forwarding; Pi owns lifecycle, logical event order, receipts, and local SQLite state. `agent_task_send` returns after relay acceptance only, not Pi insertion or model execution. The receiver inserts model-visible events as structured `pi-tasks-event` custom messages through Pi's safe `deliverAs: "followUp"` queue and records structured `{ taskId, eventId }` insertion evidence. Replay after Pi has structurally recorded an event cannot create another logical receipt. `createInMemoryTaskRelay` remains exported solely as a deterministic conformance fixture.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
## delegation workflow
|
|
28
28
|
|
|
29
|
-
Configure Pi role models with `WOLFPACK_IMPLEMENTER_MODEL` and `WOLFPACK_REVIEWER_MODEL`; they default to `openai-codex/gpt-5.6-terra` and `openai-codex/gpt-5.6-sol
|
|
29
|
+
Configure Pi role models with `WOLFPACK_IMPLEMENTER_MODEL` and `WOLFPACK_REVIEWER_MODEL`; they default to `openai-codex/gpt-5.6-terra` and `openai-codex/gpt-5.6-sol`. Explicit user or project choices override those defaults.
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
IMPLEMENTER_MODEL="${WOLFPACK_IMPLEMENTER_MODEL:-openai-codex/gpt-5.6-terra}"
|
|
33
33
|
REVIEWER_MODEL="${WOLFPACK_REVIEWER_MODEL:-openai-codex/gpt-5.6-sol}"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
1. create or select a role session. For a
|
|
37
|
-
2.
|
|
38
|
-
3. call `agent_task_send` with that endpoint and the complete
|
|
36
|
+
1. create or select a role session. For a new endpoint worker, use its explicit root and resolved role model without a startup assignment: `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). Put all worker instructions in `agent_task_send.task`. This Pi-only mode rejects prompts/plans and `--notify-parent`.
|
|
37
|
+
2. read the ready `taskEndpoint` from creation success, or verify an existing role's structured liveness, root, harness, and endpoint through `wolfpack session status <stable-session-id> --json`. `TASK_WORKER_PREFLIGHT_FAILED` precedes creation; `TASK_WORKER_NOT_READY` retains `createdSession` and `cleanup`. Inspect an `unconfirmed` cleanup by exact stable ID before retrying. Registration is not model/task execution evidence; report unsupported readiness instead of silently falling back.
|
|
38
|
+
3. call `agent_task_send` with that endpoint and the complete instructions. 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 them for concrete progress evidence or recovery, and call `agent_task_wait` only when explicitly asked to block.
|
|
39
39
|
4. use `agent_task_message` for durable questions, answers, and information. The receiver calls `agent_task_done` as its final action; no completion prose follows.
|
|
40
40
|
5. independently verify the result, call `agent_task_ack({ taskId })` once for that terminal task, then explicitly retain or close only the role sessions the parent spawned.
|
|
41
41
|
|
|
42
|
-
Coordinator-capable agents may delegate further when justified, and the spawning coordinator owns each child's lifecycle.
|
|
42
|
+
Coordinator-capable agents may delegate further when justified, and the spawning coordinator owns each child's lifecycle. Endpoint assignments require terminal completion and one `agent_task_ack`; full-startup children have no task ID, so use their explicit completion/block notification and parent verification instead. Then deliberately retain the child or run `wolfpack kill <stable-session-id> --json`, and verify that exact ID is absent from `wolfpack list --json`. Never use `wolfpack session send`, `/exit`, or `/quit` for cleanup; terminal input is not Wolfpack teardown.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
## worker-only execution gate
|
|
45
45
|
|
|
46
|
-
Set `PI_TASK_WORKER=1` only when launching a task-only worker. The exact value enables a fail-closed model `tool_call` gate; an absent value or any other value leaves ordinary interactive Pi behavior unchanged. Before assignment, only `agent_task_inbox`, `agent_task_status`, and `agent_task_wait` are allowed. Other current and future model tools are blocked with
|
|
46
|
+
Set `PI_TASK_WORKER=1` only when launching a task-only worker. The exact value enables a fail-closed model `tool_call` gate; an absent value or any other value leaves ordinary interactive Pi behavior unchanged. Before assignment, only `agent_task_inbox`, `agent_task_status`, and `agent_task_wait` are allowed. Other current and future model tools are blocked with `PI_TASK_WORKER_ASSIGNMENT_REQUIRED`. Explicit user `!`/`!!` shell commands are outside Pi's model `tool_call` event and are not intercepted.
|
|
47
47
|
|
|
48
|
-
`PI_TASK_WORKER=1` sessions are leaf roles. Workers cannot call `agent_task_send
|
|
48
|
+
`PI_TASK_WORKER=1` sessions are leaf roles. Workers cannot call `agent_task_send`, `agent_task_cancel`, or `agent_task_ack`; those attempts are blocked with `PI_TASK_WORKER_COORDINATION_FORBIDDEN`. `agent_task_message` is limited to the eligible incorporated assignment named by its input `taskId`. Generic role-orchestration guidance applies only to non-worker coordinators.
|
|
49
49
|
|
|
50
50
|
The gate opens only when the current Pi session contains a structured `pi-tasks-event` entry for `task.created` and the same event belongs to a locally persisted active task targeted to the current endpoint. Rendered prompt text, terminal output, unknown events, foreign tasks, and stale terminal tasks do not authorize execution. On restart, durable session evidence reopens the gate only while the matching local task remains eligible.
|
|
51
51
|
|
|
52
52
|
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.
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
## terminal delivery and acknowledgment
|
|
55
55
|
|
|
56
|
-
A receiver task snapshot reports terminal transport separately as `terminalDelivery`: `not_submitted`, `pending`, `accepted`, or `delivery_blocked`. The blocked variant includes
|
|
56
|
+
A receiver task snapshot reports terminal transport separately as `terminalDelivery`: `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` remains origin-owned and is never changed to `delivery_blocked`. Retryable failures keep the same pending terminal intent and envelope; permanent failures remain inspectable and are not rebound to a successor endpoint without a separate authenticated Wolfpack contract.
|
|
57
57
|
|
|
58
|
-
`agent_task_ack` is valid only for a terminal origin-owned task. Sequential, concurrent, and restart retries reuse one durable `task.parent_acknowledged` event and the same destination envelope identities while
|
|
59
|
-
|
|
60
|
-
## v1 gateway adapter (retained)
|
|
61
|
-
|
|
62
|
-
The v1 gateway client is available only through the published `@sgtbeatdown/pi-tasks/v1-compat-extension` entry. This is an explicit opt-in: after resolving the installed package root, add `src/v1-compat-extension.ts` as an extension path in Pi settings instead of loading the package default. The package manifest loads only `src/extension.ts` (v2); v2 does not fall back to v1 when its relay is unavailable or incompatible. Its v1 behavior and operational requirements are documented below.
|
|
63
|
-
|
|
64
|
-
## requirements and trust boundary
|
|
65
|
-
|
|
66
|
-
- a reachable local Wolfpack v1 task gateway on every participating machine;
|
|
67
|
-
- this package loaded in every participating Pi process;
|
|
68
|
-
- `WOLFPACK_SESSION_NAME` set by Wolfpack so the local gateway can resolve the caller to a stable broker ID; and
|
|
69
|
-
- `WOLFPACK_PORT` when the local control port is not the default `18790`.
|
|
70
|
-
|
|
71
|
-
The boundary is trusted local processes and trusted Tailnet machines. Pi only calls its own local gateway. The gateways perform direct fetch federation; Pi never fetches a peer directly. v1 adds no task capability authentication. Normal Wolfpack global JWT behavior is not bypassed, and JWT federation is unsupported: peer delivery fails clearly when credentials would be required.
|
|
72
|
-
|
|
73
|
-
## task address and send
|
|
74
|
-
|
|
75
|
-
A target is always `{ machine, sessionId }`, where `sessionId` is the stable opaque broker ID returned by Wolfpack session control. Use `machine: "local"` for same-machine work. For a peer, use the receiver's canonical HTTPS Tailnet origin, for example `https://worker.example.ts.net`; do not supply a hostname label, path, query, fragment, credentials, unexpected port, or arbitrary HTTPS URL.
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"to": { "machine": "local", "sessionId": "receiver-broker-id" },
|
|
80
|
-
"task": "implement the narrow change and run focused tests",
|
|
81
|
-
"context": {
|
|
82
|
-
"summary": "## constraints and preferences\n- keep scope narrow\n## key decisions\n- preserve the public contract",
|
|
83
|
-
"refs": [{ "path": "src/extension.ts", "purpose": "affected tool wiring" }]
|
|
84
|
-
},
|
|
85
|
-
"metadata": { "phaseId": "phase-1", "issueId": "task-3", "verificationTier": "focused" },
|
|
86
|
-
"onCompletePrompt": "review the receiver diff before reporting completion",
|
|
87
|
-
"timeoutMs": 1800000
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### v1 minimal valid send envelope
|
|
92
|
-
|
|
93
|
-
```json
|
|
94
|
-
{
|
|
95
|
-
"to": {
|
|
96
|
-
"machine": "local",
|
|
97
|
-
"sessionId": "receiver-broker-id"
|
|
98
|
-
},
|
|
99
|
-
"task": "implement the narrow change and run focused tests"
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`INVALID_REQUEST.error.path` is an optional RFC 6901 JSON Pointer to the rejected send field, for example `/preflight/requiredProject`; use it rather than parsing error prose. A pre-persistence validation rejection creates no task, so correct the field and retry. If creation status is uncertain, idempotency remains necessary.
|
|
104
|
-
|
|
105
|
-
Remote addressing changes only `to.machine`:
|
|
106
|
-
|
|
107
|
-
```json
|
|
108
|
-
{ "to": { "machine": "https://worker.example.ts.net", "sessionId": "receiver-broker-id" }, "task": "review the cited diff" }
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Context is optional, curated Markdown plus deliberately selected ref metadata. The extension renders refs but never reads or copies their contents. Relative refs resolve in the receiver project; absolute refs are same-machine only and must remain inside the authoritative parent or receiver project root. Missing refs are warnings, not copied content or transcript transfer.
|
|
112
|
-
|
|
113
|
-
Initial limits are 16 KiB UTF-8 each for task instructions and context summary, 48 KiB for the assignment envelope, and 64 KiB for an HTTP request body. The initial size limits require representative payload benchmarking before adjustment.
|
|
114
|
-
|
|
115
|
-
`agent_task_send` waits for durable gateway acceptance, not adapter insertion or task execution; delivery remains pending until `task.delivered`. A remote initial send has one initial attempt: the receiver persists a provisional receipt, then receives sender confirmation before Pi can see the assignment. Later peer confirmation, messages, terminal updates, cancellation, delivery notices, and parent acknowledgment have four total attempts: the initial attempt plus retries around 1, 2, and 4 seconds with jitter. Exhaustion is a visible local delivery failure, not a background queue or offline dispatch promise.
|
|
116
|
-
|
|
117
|
-
Normal delegation is fire-and-forget. Use `agent_task_wait` only when the user explicitly asks to block.
|
|
118
|
-
|
|
119
|
-
## tools, messages, and acknowledgment
|
|
120
|
-
|
|
121
|
-
| tool | purpose |
|
|
122
|
-
| --- | --- |
|
|
123
|
-
| `agent_task_send` | create a durable gateway assignment |
|
|
124
|
-
| `agent_task_status` | read task state, history, result, and warnings |
|
|
125
|
-
| `agent_task_wait` | explicitly poll a task until terminal or timeout |
|
|
126
|
-
| `agent_task_inbox` | read task events without acknowledgment |
|
|
127
|
-
| `agent_task_ack` | acknowledge one independently verified terminal task |
|
|
128
|
-
| `agent_task_message` | send a durable `question`, `answer`, or `information` event |
|
|
129
|
-
| `agent_task_cancel` | request cancellation |
|
|
130
|
-
| `agent_task_done` | receiver-only terminal completion; terminates the tool turn |
|
|
131
|
-
|
|
132
|
-
Use `agent_task_message` for clarification instead of terminal prose. Only one question can be unresolved per task; answers link to that question. Information is durable and does not change task state. An accepted receiver question terminates that receiver turn; a parent question does not.
|
|
58
|
+
`agent_task_ack` is valid only for a terminal origin-owned task. Sequential, concurrent, and restart retries reuse one durable `task.parent_acknowledged` event and the same destination envelope identities while retrying pending physical delivery.
|
|
133
59
|
|
|
134
60
|
Report source modifications in `result.changedFiles`. Artifacts are receiver-project-relative regular files for a parent to inspect, not a changed-file list:
|
|
135
61
|
|
|
@@ -137,36 +63,6 @@ Report source modifications in `result.changedFiles`. Artifacts are receiver-pro
|
|
|
137
63
|
{ "result": { "changedFiles": ["src/extension.ts"] }, "artifacts": [{ "path": "verification/task-2.md" }] }
|
|
138
64
|
```
|
|
139
65
|
|
|
140
|
-
See the canonical Wolfpack artifact contract for provenance, containment, and warning behavior.
|
|
141
|
-
|
|
142
|
-
The sender gateway owns canonical event order, timeout, and terminal state. The first accepted terminal event wins. Sender timeout triggers best-effort remote cancellation; late terminals remain diagnostics. After independently verifying one terminal result, call `agent_task_ack({ taskId })`. Remote acknowledgment is two-phase: the receiver durably confirms pending acknowledgment before the sender records final parent acknowledgment. If that delivery fails, the task remains visible and a later explicit acknowledgment repairs it with the same event ID.
|
|
143
|
-
|
|
144
|
-
## structured delivery and replay
|
|
145
|
-
|
|
146
|
-
The extension polls the local gateway every five seconds. With no pending message, it submits each visible event as a structured custom message using Pi's safe `deliverAs: "followUp"` mode: idle Pi starts immediately, while an active turn finishes before the queued event starts the next turn. It never steers or interrupts active work. Delivery remains pending until `task.delivered`. The `{ taskId, eventId }` details let task context participate in the session without parsing prose.
|
|
147
|
-
|
|
148
|
-
On start or resume it rebuilds incorporated IDs from the full durable session entry set, not only active prompt context. A session gets only missing events; a new Pi session in the same Wolfpack PTY gets complete active history. The receiver records gateway delivery only after the structured insertion exists. A restart after insertion and before acknowledgment therefore acknowledges the existing entry without duplicate injection. Unknown model-visible events fail closed without advancing the cursor. Gateway delivery is at-least-once; this does not claim exactly-once model execution.
|
|
149
|
-
|
|
150
|
-
## parent workflow
|
|
151
|
-
|
|
152
|
-
1. create or select a Wolfpack Pi session with the canonical session-control workflow and retain its stable broker ID. Spawn a disposable worker without an initial assignment prompt using `wolfpack agent spawn <project> --name <task-role> --model "$IMPLEMENTER_MODEL" --json` (or `"$REVIEWER_MODEL"` for review) so it reaches idle before assignment; put worker instructions in `agent_task_send.task`.
|
|
153
|
-
2. before remote dispatch, complete Wolfpack's [Live-peer readiness checklist](https://github.com/almogdepaz/wolfpack/blob/main/docs/task-gateway.md#live-peer-readiness-checklist) and retain operator-recorded package/reload evidence; only then check the target's local requirements and send compact instructions with curated context and selected refs when they save receiver investigation.
|
|
154
|
-
3. keep working; use `agent_task_status` or `agent_task_inbox` for structured follow-up, and `agent_task_message` for questions, answers, and decisions.
|
|
155
|
-
4. verify cited files, diffs, tests, and paths-only artifact metadata independently before reporting success.
|
|
156
|
-
5. acknowledge the accepted terminal result, then clean up only sessions this parent spawned. Keep a reusable implementer alive while review or correction remains possible; never make workers close their own sessions.
|
|
157
|
-
|
|
158
|
-
Isolated coverage is the deterministic acceptance gate, but a specific live peer still requires current readiness at the time of use. If the checklist cannot pass, stop before task creation and report fixture-only verification. Do not use direct peer URLs from Pi.
|
|
159
|
-
|
|
160
|
-
See `skills/wolfpack-pi-task-delegation/SKILL.md` for the operational workflow and `skills/task-context-summary/SKILL.md` for recovery/reuse summaries.
|
|
161
|
-
|
|
162
|
-
## deferred follow-up
|
|
163
|
-
|
|
164
|
-
These are documented operational debt, not production TODOs: exact Pi runtime registration and heartbeat/capability leases; durable offline initial dispatch; JWT/authenticated peer federation; artifact byte transfer and retention; representative payload benchmarking; automated recovery summaries; and summary caching only after measured repeated-generation waste. v1 has no queue, scheduler, artifact transfer, or transcript transfer.
|
|
165
|
-
|
|
166
|
-
## legacy historical metrics
|
|
167
|
-
|
|
168
|
-
`task-metrics` and the task board remain read-only analyzers for already-existing historical `.pi/tasks` directories. They are not part of the runtime task protocol and must not create, mutate, or deliver v1 gateway tasks. Replacing or retiring that historical reporting surface requires a product decision about gateway-wide query/metrics ownership.
|
|
169
|
-
|
|
170
66
|
## development
|
|
171
67
|
|
|
172
68
|
```bash
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sgtbeatdown/pi-tasks",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Durable Pi agent delegation through
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Durable endpoint-owned Pi agent delegation through Wolfpack relay v2.",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./src/index.ts"
|
|
8
|
-
"./v1-compat-extension": "./src/v1-compat-extension.ts"
|
|
7
|
+
".": "./src/index.ts"
|
|
9
8
|
},
|
|
10
9
|
"repository": {
|
|
11
10
|
"type": "git",
|
|
@@ -50,7 +49,6 @@
|
|
|
50
49
|
},
|
|
51
50
|
"scripts": {
|
|
52
51
|
"test": "bun test",
|
|
53
|
-
"task-metrics": "bun src/metrics-cli.ts",
|
|
54
52
|
"typecheck": "tsc --noEmit"
|
|
55
53
|
},
|
|
56
54
|
"type": "module"
|
|
@@ -31,8 +31,9 @@ Pass `--model "$IMPLEMENTER_MODEL"` when spawning the editing implementer and `-
|
|
|
31
31
|
|
|
32
32
|
- load this package's default extension in every participating Pi process and set `WOLFPACK_SESSION_NAME`. Set `WOLFPACK_PORT` only when the local Wolfpack control port differs from `18790`.
|
|
33
33
|
- 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.
|
|
34
|
-
- create
|
|
35
|
-
-
|
|
34
|
+
- 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 Pi-only mode rejects prompts/plans and `--notify-parent`; do not start a blocking “wait for assignments” prompt. Put the complete instructions in `agent_task_send.task`.
|
|
35
|
+
- 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.
|
|
36
|
+
- 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.
|
|
36
37
|
- pass the returned endpoint without translation:
|
|
37
38
|
|
|
38
39
|
```json
|
|
@@ -43,13 +44,13 @@ Pass `--model "$IMPLEMENTER_MODEL"` when spawning the editing implementer and `-
|
|
|
43
44
|
}
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
The default `agent_task_send` schema is exactly `to`, `task`, and optional `timeoutMs`.
|
|
47
|
+
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.
|
|
47
48
|
|
|
48
49
|
## phase roles and handoffs
|
|
49
50
|
|
|
50
51
|
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.
|
|
51
52
|
|
|
52
|
-
A coordinator-capable agent may delegate further when the quality or throughput gain justifies it.
|
|
53
|
+
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.
|
|
53
54
|
|
|
54
55
|
`PI_TASK_WORKER=1` sessions are leaf roles. 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.
|
|
55
56
|
|
|
@@ -70,9 +71,9 @@ Assignment completion ends the assigned task, not the reusable role session. Whe
|
|
|
70
71
|
|
|
71
72
|
## v2 workflow
|
|
72
73
|
|
|
73
|
-
1. create or select
|
|
74
|
+
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, Pi harness, and registered `taskEndpoint`. Never use terminal output as readiness evidence.
|
|
74
75
|
2. call `agent_task_send` with the opaque endpoint and concise instructions in `task`. The tool returns after relay acceptance only, not Pi 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.
|
|
75
|
-
3.
|
|
76
|
+
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.
|
|
76
77
|
4. use `agent_task_message` for durable `question`, `answer`, or `information` flow. Do not use rendered text, terminal output, or logs as lifecycle evidence.
|
|
77
78
|
5. assignees call `agent_task_done` as their final action with the assigned task ID, terminal status, concise summary, and optional structured result, error, and artifact declarations. 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.
|
|
78
79
|
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.
|
|
@@ -85,21 +86,11 @@ Receiver terminal submission has one task-wide logical identity. Same-status ret
|
|
|
85
86
|
|
|
86
87
|
Set `PI_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`.
|
|
87
88
|
|
|
88
|
-
## v1 compatibility
|
|
89
|
-
|
|
90
|
-
The retained v1 workflow is available only through the explicit `@sgtbeatdown/pi-tasks/v1-compat-extension` entrypoint. It does not load by default, and v2 never falls back or translates its targets.
|
|
91
|
-
|
|
92
|
-
v1 uses a reachable local Wolfpack task gateway and addresses targets as `to: { machine, sessionId }`. Use `machine: "local"` for same-machine work; remote work uses only the receiver's canonical HTTPS Tailnet origin. v1 trusts local processes and trusted Tailnet machines, has no JWT federation, and follows Wolfpack's [task gateway guide](https://github.com/almogdepaz/wolfpack/blob/main/docs/task-gateway.md).
|
|
93
|
-
|
|
94
|
-
Before v1 remote dispatch, complete the [Live-peer readiness checklist](https://github.com/almogdepaz/wolfpack/blob/main/docs/task-gateway.md#live-peer-readiness-checklist), including operator-recorded package/reload evidence. If it cannot pass, stop before task creation and report fixture-only verification. Isolated coverage is the deterministic acceptance gate, but it does not prove a specific live peer.
|
|
95
|
-
|
|
96
|
-
Only v1 supports curated `context.summary`/refs, role, metadata, preflight, idempotency keys, and completion prompts. Use `context.summary` only for constraints, decisions, and recovery state. Refs are metadata, not copied files or transcript. A v1 remote initial send gets one initial attempt; later peer events get four total attempts. Retry exhaustion is surfaced; v1 has no offline queue or background dispatch promise.
|
|
97
|
-
|
|
98
89
|
## do not
|
|
99
90
|
|
|
100
91
|
- do not use terminal output, rendered prompts, logs, or error prose as task state.
|
|
101
92
|
- do not ask workers to complete in prose or close their own sessions.
|
|
102
93
|
- do not steer or interrupt an active Pi turn with task context; use the adapter's follow-up queue.
|
|
103
94
|
- Do not copy plans, source contents, or transcripts into task context.
|
|
104
|
-
- do not
|
|
95
|
+
- do not derive or transform opaque endpoint IDs.
|
|
105
96
|
- do not promise JWT federation, artifact byte transfer, exactly-once model execution, or successor endpoint rebinding.
|
package/src/task-store.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { openSqliteDatabase } from "./sqlite-database";
|
|
6
6
|
import type { SqliteDatabase } from "./sqlite-database";
|
|
7
|
-
import {
|
|
7
|
+
import { TERMINAL_INTENT_OPERATION } from "./task-protocol";
|
|
8
8
|
import type { RelayEnvelope, TaskEndpoint, TaskEvent, TaskRecord, TaskSnapshot, TerminalDeliveryState, TerminalTaskIntentType } from "./task-protocol";
|
|
9
9
|
|
|
10
10
|
const SCHEMA_VERSION = 5;
|
|
@@ -71,8 +71,13 @@ export function createTaskStore(options: TaskStoreOptions = {}): TaskStore {
|
|
|
71
71
|
const path = options.path ?? join(homedir(), ".pi", "tasks", "v2", "tasks.sqlite");
|
|
72
72
|
preparePath(path);
|
|
73
73
|
const database = openSqliteDatabase(path);
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
try {
|
|
75
|
+
initializeSchema(database);
|
|
76
|
+
database.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
database.close();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
76
81
|
|
|
77
82
|
const store: TaskStore = {
|
|
78
83
|
transaction(operation) {
|
|
@@ -213,17 +218,6 @@ interface TaskOperationRow {
|
|
|
213
218
|
readonly envelope_ids: string;
|
|
214
219
|
}
|
|
215
220
|
|
|
216
|
-
interface LegacyIntentRow {
|
|
217
|
-
readonly intent_id: string;
|
|
218
|
-
readonly task_id: string;
|
|
219
|
-
readonly envelope_id: string;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
interface LegacyEnvelopeRow {
|
|
223
|
-
readonly envelope_id: string;
|
|
224
|
-
readonly envelope: string;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
221
|
interface EventRow {
|
|
228
222
|
readonly event_id: string;
|
|
229
223
|
readonly task_id: string;
|
|
@@ -237,10 +231,11 @@ interface EventRow {
|
|
|
237
231
|
readonly payload: string;
|
|
238
232
|
}
|
|
239
233
|
|
|
240
|
-
function
|
|
234
|
+
function initializeSchema(database: SqliteDatabase): void {
|
|
241
235
|
const version = database.query("PRAGMA user_version").get() as { readonly user_version: number };
|
|
242
|
-
if (version.user_version
|
|
243
|
-
if (version.user_version
|
|
236
|
+
if (version.user_version === SCHEMA_VERSION) return;
|
|
237
|
+
if (version.user_version !== 0) throw new Error("task store schema version is unsupported");
|
|
238
|
+
database.transaction(() => {
|
|
244
239
|
database.exec(`
|
|
245
240
|
CREATE TABLE tasks (task_id TEXT PRIMARY KEY, protocol_version TEXT NOT NULL, origin_relay TEXT NOT NULL, origin_id TEXT NOT NULL, target_relay TEXT NOT NULL, target_id TEXT NOT NULL, task TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, status TEXT NOT NULL);
|
|
246
241
|
CREATE TABLE events (event_id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(task_id), type TEXT NOT NULL, sequence TEXT NOT NULL, source_relay TEXT NOT NULL, source_id TEXT NOT NULL, target_relay TEXT NOT NULL, target_id TEXT NOT NULL, occurred_at INTEGER NOT NULL, payload TEXT NOT NULL);
|
|
@@ -248,137 +243,29 @@ function migrate(database: SqliteDatabase): void {
|
|
|
248
243
|
CREATE TABLE inbox (envelope_id TEXT PRIMARY KEY, cursor TEXT NOT NULL, envelope TEXT NOT NULL);
|
|
249
244
|
CREATE TABLE intents (intent_id TEXT PRIMARY KEY, task_id TEXT NOT NULL, envelope_id TEXT NOT NULL);
|
|
250
245
|
CREATE TABLE relay_state (name TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
251
|
-
PRAGMA user_version = 1;
|
|
252
|
-
`);
|
|
253
|
-
}
|
|
254
|
-
if (version.user_version <= 1) {
|
|
255
|
-
database.exec(`
|
|
256
|
-
CREATE INDEX IF NOT EXISTS tasks_origin_expiry ON tasks (origin_relay, origin_id, status, expires_at);
|
|
257
|
-
CREATE INDEX IF NOT EXISTS inbox_cursor ON inbox (cursor);
|
|
258
|
-
PRAGMA user_version = 2;
|
|
259
|
-
`);
|
|
260
|
-
}
|
|
261
|
-
if (version.user_version <= 2) {
|
|
262
|
-
database.exec(`
|
|
263
246
|
CREATE TABLE insertion_receipts (task_id TEXT NOT NULL REFERENCES tasks(task_id), event_id TEXT NOT NULL, PRIMARY KEY (task_id, event_id));
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
details TEXT NOT NULL DEFAULT '{}',
|
|
275
|
-
prior_state TEXT NOT NULL DEFAULT 'pending'
|
|
276
|
-
);`);
|
|
277
|
-
ensureColumn(database, "outbox_quarantine", "envelope", "TEXT NOT NULL DEFAULT '{}'");
|
|
278
|
-
ensureColumn(database, "outbox_quarantine", "reason", "TEXT NOT NULL DEFAULT 'operator quarantine'");
|
|
279
|
-
ensureColumn(database, "outbox_quarantine", "quarantined_at", "INTEGER NOT NULL DEFAULT 0");
|
|
280
|
-
ensureColumn(database, "outbox_quarantine", "error_code", "TEXT NOT NULL DEFAULT 'UNKNOWN'");
|
|
281
|
-
ensureColumn(database, "outbox_quarantine", "details", "TEXT NOT NULL DEFAULT '{}'");
|
|
282
|
-
ensureColumn(database, "outbox_quarantine", "prior_state", "TEXT NOT NULL DEFAULT 'pending'");
|
|
283
|
-
database.exec("PRAGMA user_version = 4;");
|
|
284
|
-
}
|
|
285
|
-
if (version.user_version <= 4) {
|
|
286
|
-
database.transaction(() => {
|
|
287
|
-
database.exec(`CREATE TABLE IF NOT EXISTS task_operations (
|
|
247
|
+
CREATE TABLE outbox_quarantine (
|
|
248
|
+
envelope_id TEXT PRIMARY KEY,
|
|
249
|
+
envelope TEXT NOT NULL,
|
|
250
|
+
reason TEXT NOT NULL,
|
|
251
|
+
quarantined_at INTEGER NOT NULL,
|
|
252
|
+
error_code TEXT NOT NULL,
|
|
253
|
+
details TEXT NOT NULL,
|
|
254
|
+
prior_state TEXT NOT NULL
|
|
255
|
+
);
|
|
256
|
+
CREATE TABLE task_operations (
|
|
288
257
|
task_id TEXT NOT NULL REFERENCES tasks(task_id),
|
|
289
258
|
operation TEXT NOT NULL,
|
|
290
259
|
logical_id TEXT NOT NULL,
|
|
291
260
|
logical_type TEXT NOT NULL,
|
|
292
261
|
envelope_ids TEXT NOT NULL,
|
|
293
262
|
PRIMARY KEY (task_id, operation)
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function backfillTaskOperations(database: SqliteDatabase): void {
|
|
302
|
-
const envelopes = legacyEnvelopes(database);
|
|
303
|
-
const terminalTasks = new Set<string>();
|
|
304
|
-
const intents = database.query("SELECT intent_id, task_id, envelope_id FROM intents ORDER BY rowid").all() as LegacyIntentRow[];
|
|
305
|
-
for (const intent of intents) {
|
|
306
|
-
if (terminalTasks.has(intent.task_id)) continue;
|
|
307
|
-
const serializedEnvelope = envelopes.get(intent.envelope_id);
|
|
308
|
-
if (!serializedEnvelope) throw new Error(`legacy intent ${intent.intent_id} has no durable envelope`);
|
|
309
|
-
const envelope = legacyEnvelope(intent.envelope_id, serializedEnvelope);
|
|
310
|
-
const payload = legacyEnvelopePayload(envelope);
|
|
311
|
-
if (envelope.kind !== "intent" || envelope.taskId !== intent.task_id || !isRecord(payload) || payload.intentId !== intent.intent_id || payload.taskId !== intent.task_id || typeof payload.type !== "string") {
|
|
312
|
-
throw new Error(`legacy intent ${intent.intent_id} has malformed structured state`);
|
|
313
|
-
}
|
|
314
|
-
if (!isTerminalIntentType(payload.type)) continue;
|
|
315
|
-
insertTaskOperation(database, { taskId: intent.task_id, operation: TERMINAL_INTENT_OPERATION, logicalId: intent.intent_id, logicalType: payload.type, envelopeIds: [intent.envelope_id] });
|
|
316
|
-
terminalTasks.add(intent.task_id);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const acknowledgments = database.query("SELECT * FROM events WHERE type = 'task.parent_acknowledged' ORDER BY task_id, CAST(sequence AS INTEGER), rowid").all() as EventRow[];
|
|
320
|
-
const acknowledgedTasks = new Set<string>();
|
|
321
|
-
for (const acknowledgment of acknowledgments) {
|
|
322
|
-
if (acknowledgedTasks.has(acknowledgment.task_id)) continue;
|
|
323
|
-
const envelopeIds = [...envelopes]
|
|
324
|
-
.flatMap(([envelopeId, serializedEnvelope]) => {
|
|
325
|
-
const envelope = optionalLegacyEnvelope(envelopeId, serializedEnvelope);
|
|
326
|
-
return envelope?.kind === "canonical_event" && envelope.taskId === acknowledgment.task_id && eventIdFromLegacyEnvelope(envelope) === acknowledgment.event_id ? [envelopeId] : [];
|
|
327
|
-
})
|
|
328
|
-
.sort();
|
|
329
|
-
if (envelopeIds.length === 0) throw new Error(`legacy parent acknowledgment ${acknowledgment.event_id} has no durable envelope`);
|
|
330
|
-
insertTaskOperation(database, { taskId: acknowledgment.task_id, operation: PARENT_ACKNOWLEDGMENT_OPERATION, logicalId: acknowledgment.event_id, logicalType: acknowledgment.type, envelopeIds });
|
|
331
|
-
acknowledgedTasks.add(acknowledgment.task_id);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function legacyEnvelopes(database: SqliteDatabase): Map<string, string> {
|
|
336
|
-
const envelopes = new Map<string, string>();
|
|
337
|
-
const live = database.query("SELECT envelope_id, envelope FROM outbox ORDER BY rowid").all() as LegacyEnvelopeRow[];
|
|
338
|
-
const quarantined = database.query("SELECT envelope_id, envelope FROM outbox_quarantine ORDER BY rowid").all() as LegacyEnvelopeRow[];
|
|
339
|
-
for (const row of [...live, ...quarantined]) {
|
|
340
|
-
if (row.envelope_id.length === 0) throw new Error("legacy durable envelope identity is malformed");
|
|
341
|
-
if (!envelopes.has(row.envelope_id)) envelopes.set(row.envelope_id, row.envelope);
|
|
342
|
-
}
|
|
343
|
-
return envelopes;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function legacyEnvelope(envelopeId: string, serializedEnvelope: string): RelayEnvelope {
|
|
347
|
-
const envelope = parseEnvelope(serializedEnvelope);
|
|
348
|
-
if (envelope.envelopeId !== envelopeId) throw new Error(`legacy envelope ${envelopeId} identity is malformed`);
|
|
349
|
-
return envelope;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function optionalLegacyEnvelope(envelopeId: string, serializedEnvelope: string): RelayEnvelope | undefined {
|
|
353
|
-
try {
|
|
354
|
-
return legacyEnvelope(envelopeId, serializedEnvelope);
|
|
355
|
-
} catch {
|
|
356
|
-
// Unrelated operator quarantine rows are audit data, not lifecycle state.
|
|
357
|
-
return undefined;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function legacyEnvelopePayload(envelope: RelayEnvelope): unknown {
|
|
362
|
-
try {
|
|
363
|
-
return JSON.parse(envelope.payload) as unknown;
|
|
364
|
-
} catch {
|
|
365
|
-
throw new Error(`legacy envelope ${envelope.envelopeId} payload is malformed`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function eventIdFromLegacyEnvelope(envelope: RelayEnvelope): string | undefined {
|
|
370
|
-
const payload = legacyEnvelopePayload(envelope);
|
|
371
|
-
return isRecord(payload) && typeof payload.eventId === "string" ? payload.eventId : undefined;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function insertTaskOperation(database: SqliteDatabase, input: TaskOperationRecord): void {
|
|
375
|
-
database.query(`INSERT INTO task_operations (task_id, operation, logical_id, logical_type, envelope_ids)
|
|
376
|
-
VALUES (?, ?, ?, ?, ?) ON CONFLICT(task_id, operation) DO NOTHING`).run(input.taskId, input.operation, input.logicalId, input.logicalType, JSON.stringify(input.envelopeIds));
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function ensureColumn(database: SqliteDatabase, table: string, name: string, definition: string): void {
|
|
380
|
-
const columns = database.query(`PRAGMA table_info(${table})`).all() as Array<{ readonly name: string }>;
|
|
381
|
-
if (!columns.some((column) => column.name === name)) database.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`);
|
|
263
|
+
);
|
|
264
|
+
CREATE INDEX tasks_origin_expiry ON tasks (origin_relay, origin_id, status, expires_at);
|
|
265
|
+
CREATE INDEX inbox_cursor ON inbox (cursor);
|
|
266
|
+
PRAGMA user_version = 5;
|
|
267
|
+
`);
|
|
268
|
+
})();
|
|
382
269
|
}
|
|
383
270
|
|
|
384
271
|
function preparePath(path: string): void {
|