pi-agent-replicate 0.0.0-stage → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +147 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +239 -2
  6. package/RELEASING.md +130 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +20 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/public-subagent-tree-api.md +58 -0
  15. package/docs/settled-lifecycle-contract.md +71 -0
  16. package/docs/worktree-subagents.md +58 -0
  17. package/examples/role-pack/extension.ts +18 -0
  18. package/examples/role-pack/package.json +16 -0
  19. package/examples/role-pack/roles/example-reviewer.md +12 -0
  20. package/index.d.ts +116 -0
  21. package/index.js +1064 -0
  22. package/index.ts +155 -0
  23. package/package.json +70 -4
  24. package/pi-extension/subagents/activity.js +575 -0
  25. package/pi-extension/subagents/activity.ts +980 -0
  26. package/pi-extension/subagents/completion.js +184 -0
  27. package/pi-extension/subagents/completion.ts +229 -0
  28. package/pi-extension/subagents/delivery-drain.js +15 -0
  29. package/pi-extension/subagents/delivery-drain.ts +20 -0
  30. package/pi-extension/subagents/delivery-files.js +86 -0
  31. package/pi-extension/subagents/delivery-files.ts +105 -0
  32. package/pi-extension/subagents/delivery-log.js +51 -0
  33. package/pi-extension/subagents/delivery-log.ts +51 -0
  34. package/pi-extension/subagents/delivery-payload.ts +55 -0
  35. package/pi-extension/subagents/delivery.ts +219 -0
  36. package/pi-extension/subagents/execution-owner.js +30 -0
  37. package/pi-extension/subagents/execution-owner.ts +38 -0
  38. package/pi-extension/subagents/herdr.js +645 -0
  39. package/pi-extension/subagents/herdr.ts +927 -0
  40. package/pi-extension/subagents/index.ts +3400 -0
  41. package/pi-extension/subagents/launch.js +774 -0
  42. package/pi-extension/subagents/launch.ts +1124 -0
  43. package/pi-extension/subagents/lifecycle.js +477 -0
  44. package/pi-extension/subagents/lifecycle.ts +582 -0
  45. package/pi-extension/subagents/lineage.js +806 -0
  46. package/pi-extension/subagents/lineage.ts +841 -0
  47. package/pi-extension/subagents/replica-background.js +32 -0
  48. package/pi-extension/subagents/replica-background.ts +38 -0
  49. package/pi-extension/subagents/replica-branch.ts +45 -0
  50. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  51. package/pi-extension/subagents/replica-history.js +22 -0
  52. package/pi-extension/subagents/replica-history.ts +21 -0
  53. package/pi-extension/subagents/replica-work.js +326 -0
  54. package/pi-extension/subagents/replica-work.ts +336 -0
  55. package/pi-extension/subagents/replica-worker.js +74 -0
  56. package/pi-extension/subagents/replica-worker.ts +71 -0
  57. package/pi-extension/subagents/replication.ts +99 -0
  58. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  59. package/pi-extension/subagents/role-catalog.js +123 -0
  60. package/pi-extension/subagents/role-catalog.ts +24 -0
  61. package/pi-extension/subagents/runtime-routing.js +154 -0
  62. package/pi-extension/subagents/runtime-routing.ts +239 -0
  63. package/pi-extension/subagents/session-binding.js +346 -0
  64. package/pi-extension/subagents/session-binding.ts +339 -0
  65. package/pi-extension/subagents/session-work.js +219 -0
  66. package/pi-extension/subagents/session-work.ts +167 -0
  67. package/pi-extension/subagents/session.js +318 -0
  68. package/pi-extension/subagents/session.ts +448 -0
  69. package/pi-extension/subagents/settled-contract.js +47 -0
  70. package/pi-extension/subagents/settled-contract.ts +175 -0
  71. package/pi-extension/subagents/source-state.js +180 -0
  72. package/pi-extension/subagents/source-state.ts +184 -0
  73. package/pi-extension/subagents/status.ts +493 -0
  74. package/pi-extension/subagents/subagent-done.js +805 -0
  75. package/pi-extension/subagents/subagent-done.ts +878 -0
  76. package/pi-extension/subagents/terminal.js +111 -0
  77. package/pi-extension/subagents/terminal.ts +208 -0
  78. package/pi-extension/subagents/watchdog.js +122 -0
  79. package/pi-extension/subagents/watchdog.ts +171 -0
package/RELEASING.md ADDED
@@ -0,0 +1,130 @@
1
+ # Release guide
2
+
3
+ GitHub Actions publishes this package when the version in `package.json` changes on `main`. The release workflow reads the package name and version from `package.json`, validates the package, publishes to npm, creates a matching `vX.Y.Z` tag, and creates a GitHub Release. If `.github/release-notes/<version>.md` exists, the release uses that file. Otherwise GitHub generates the notes and adds the npm package link.
4
+
5
+ The published version must be unique on npm.
6
+
7
+ ## Public versioning
8
+
9
+ `pi-agent-replicate` is a new npm package name and needs its first publication. The upstream `pi-herdr-agents` package used `0.0.1` for its manual bootstrap and `0.0.2` for its first trusted-publishing release. Those versions are upstream history, not publications of this renamed package.
10
+
11
+ Do not design a release that creates a GitHub Release without a successful npm publish for a new version. The workflow publishes first, then tags and creates the GitHub Release.
12
+
13
+ ## Prerequisites
14
+
15
+ You need:
16
+
17
+ - Permission to manage this repository's GitHub Actions settings and npm package access for `pi-agent-replicate`
18
+ - A clean local `main` branch
19
+
20
+ Automated release gates (run by the workflow and required locally):
21
+
22
+ ```bash
23
+ npm ci
24
+ npm run lint
25
+ npm test
26
+ npm pack --dry-run
27
+ ```
28
+
29
+ Manual deterministic Herdr integration (required before you push a release commit; not run in GitHub Actions):
30
+
31
+ ```bash
32
+ npm run test:integration
33
+ ```
34
+
35
+ Run that suite from inside Herdr. The shipped command bootstraps a separate test-owned headless Herdr server with a private socket, config, state directory, and worktree directory. It clears inherited Herdr identity and fails closed if the private endpoint cannot start, so it never falls back to the persistent user server. Normal exit and `SIGINT`, `SIGTERM`, or `SIGHUP` use one idempotent shutdown that verifies child and server process groups have exited, escalates after bounded grace, and removes only owned paths. Concurrent runs use separate endpoints and need no shared Herdr test slot. It uses real Pi and Herdr processes with the local deterministic provider, so it needs no provider credentials or network access. A focused file can be passed after `--`, for example `npm run test:integration -- test/integration/mux-surface.test.ts`.
36
+ The optional live-provider smoke test is not a release gate:
37
+
38
+ ```bash
39
+ PI_TEST_MODEL="openai-codex/gpt-5.6-luna" PI_TEST_TIMEOUT=180000 npm run test:integration:live
40
+ ```
41
+
42
+ Do not release from skipped Herdr tests. Confirm the package preview includes `README.md`, `CHANGELOG.md`, `AGENTS.md`, `docs/`, the package-root API files, and the files under `pi-extension/subagents/`. Confirm it excludes plans, journals, sessions, prototypes, generated evidence, and local `config.json`, and that the worktree integration tests leave no test workspace behind.
43
+
44
+ ## npm authentication
45
+
46
+ ### Steady state: trusted publishing (tokenless)
47
+
48
+ After the package exists on npm, steady-state releases use npm trusted publishing (OIDC). No long-lived `NPM_TOKEN` is required.
49
+
50
+ 1. Open the package settings for `pi-agent-replicate` on [npmjs.com](https://www.npmjs.com/).
51
+ 2. Add a trusted publisher for GitHub Actions with:
52
+ - Organization or user: `alexshpunt`
53
+ - Repository: `pi-agent-replicate`
54
+ - Workflow filename: `publish.yml`
55
+ - Allowed action: `npm publish`
56
+ 3. Confirm the release job has `permissions.id-token: write` and runs on a GitHub-hosted runner (already set in `.github/workflows/publish.yml`).
57
+ 4. Confirm the workflow uses the exactly pinned Node `26.3.0`, whose bundled npm supports trusted publishing.
58
+ 5. Publish stays tokenless: `npm publish --access public --provenance`.
59
+
60
+ When the repository secret `NPM_TOKEN` is absent, the publish step unsets `NODE_AUTH_TOKEN` and relies on OIDC. Once the package exists, the workflow fails if `NPM_TOKEN` is still configured, so steady-state releases cannot silently keep using the bootstrap credential. Manual dispatch runs only from `main`; other refs are rejected.
61
+
62
+ ### Bootstrap history
63
+
64
+ The upstream `0.0.1` publication and documented trusted publisher for `giuseppecrj/pi-herdr-agents` do not establish the renamed npm package or authorize its releases.
65
+
66
+ Only during an explicitly requested first release, configure a temporary `NPM_TOKEN` if `pi-agent-replicate` does not yet exist on npm. After that publication, revoke and remove the token, then configure the trusted publisher above. The workflow rejects `NPM_TOKEN` once the package exists. Renaming the project does not publish a package or configure its trusted publisher.
67
+
68
+ ## Publish a release
69
+
70
+ Choose the semantic version increment:
71
+
72
+ - `patch`: compatible bug fixes, such as `0.0.2` to `0.0.3`
73
+ - `minor`: compatible features, such as `0.0.2` to `0.1.0`
74
+ - `major`: breaking changes, such as `0.0.2` to `1.0.0`
75
+
76
+ Save approved release notes as `.github/release-notes/<version>.md` and commit that file before the version commit. The workflow uses its contents verbatim for the matching release. An empty notes file stops release creation. Without a matching file, GitHub generates the release notes.
77
+
78
+ Create the version commit without a local tag:
79
+
80
+ ```bash
81
+ git fetch --tags --prune
82
+ npm version patch --no-git-tag-version
83
+ git add package.json package-lock.json CHANGELOG.md
84
+ git commit -m "chore: release v$(node -p \"require('./package.json').version\")"
85
+ git push origin main
86
+ ```
87
+
88
+ The `npm version` hook regenerates `CHANGELOG.md` with `auto-changelog`. Use `npm run changelog` to regenerate it without changing the version.
89
+
90
+ Replace `patch` with `minor` or `major` when appropriate. The push triggers the **Release** workflow, which installs dependencies, runs lint and unit tests, previews package contents, publishes to npm with provenance, creates and pushes the version tag, and creates the GitHub Release.
91
+
92
+ You can rerun a failed or incomplete release from **Actions → Release → Run workflow**. If npm already has `PACKAGE_NAME@VERSION`, the workflow reads that version's `gitHead` and continues only when it matches `GITHUB_SHA` (exact-commit retry). A foreign publish fails before tag or GitHub Release creation. Existing tags are verified to point at the release commit. The workflow does not create a GitHub Release for a version that still needs publish and failed to publish.
93
+
94
+ ## Verify the release
95
+
96
+ After the workflow succeeds, inspect the published package:
97
+
98
+ ```bash
99
+ npm view pi-agent-replicate
100
+ ```
101
+
102
+ Test installation through Pi:
103
+
104
+ ```bash
105
+ pi install npm:pi-agent-replicate
106
+ ```
107
+
108
+ The package should appear at <https://pi.dev/packages/pi-agent-replicate> after the gallery indexes the npm release.
109
+
110
+ ## Troubleshooting
111
+
112
+ ### Tag points to another commit
113
+
114
+ The workflow stops if the matching version tag already points to a different commit. Do not move or reuse release tags. Increment the package version and push a new release commit instead.
115
+
116
+ ### npm rejects authentication
117
+
118
+ Confirm that the trusted publisher matches owner `alexshpunt`, repository `pi-agent-replicate`, and workflow `publish.yml`, that the job has `id-token: write`, and that the runner is GitHub-hosted. If a release reports that `NPM_TOKEN` is bootstrap-only, remove the secret and use the trusted publisher.
119
+
120
+ ### npm reports that the version already exists
121
+
122
+ If the published `gitHead` does not match this commit, the workflow fails before tagging. npm versions are immutable: increment the package version and push a new release commit. If it is a retry of the exact same commit, the workflow skips publish and continues with tag/release.
123
+
124
+ ### Initial branch creation did not release
125
+
126
+ A clean repository's first push has `github.event.before` all zeroes. The workflow treats that as `release=false`. For the renamed package's first publication, follow the bootstrap instructions above. Once it exists on npm, use tokenless trusted publishing for later releases.
127
+
128
+ ### The package is absent from pi.dev
129
+
130
+ Confirm that npm published the package publicly and that `package.json` contains the `pi-package` keyword. Gallery indexing may take some time.
Binary file
@@ -0,0 +1,6 @@
1
+ {
2
+ "status": {
3
+ "enabled": true,
4
+ "quietThresholdMs": 120000
5
+ }
6
+ }
package/docs/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Documentation
2
+
3
+ ## Active contracts
4
+
5
+ - [Worktree subagents](worktree-subagents.md) — isolation, retained state, review, and cleanup.
6
+ - [Public subagent tree API](public-subagent-tree-api.md) — package-root API and ownership contract.
7
+ - [Settled lifecycle contract](settled-lifecycle-contract.md) — completion and delivery semantics.
8
+
9
+ ## Architecture decisions
10
+
11
+ - [ADR-0003](adr/0003-installable-role-packs.md) — installable custom role packs.
12
+ - [ADR-0008](adr/0008-adopt-pi-only-subagent-execution.md) — Pi-only child execution.
13
+ - [ADR-0009](adr/0009-durable-recursive-subagent-ownership.md) — durable recursive ownership.
14
+ - [ADR-0010](adr/0010-public-subagent-tree-registry.md) — public process-local tree registry.
15
+
16
+ Older workflow and command decisions were removed with the surfaces they described. Historical release notes remain in `CHANGELOG.md`.
17
+
18
+ ## Background research
19
+
20
+ Files under [research](research/) are evidence and alternatives, not shipped contracts. When research conflicts with the README or an accepted ADR, the active contract wins.
@@ -0,0 +1,29 @@
1
+ # ADR-0003: Installable role packs
2
+
3
+ - Status: Accepted
4
+
5
+ ## Context
6
+
7
+ Projects need reusable custom subagent roles without copying them into every repository. The extension itself no longer ships built-in roles.
8
+
9
+ ## Decision
10
+
11
+ Discover role files from three sources, in this order:
12
+
13
+ 1. project `.pi/agents`;
14
+ 2. global Pi agent directories;
15
+ 3. installable packages that contribute agent directories through `pi-herdr-subagents:roles:discover:v1`.
16
+
17
+ Project roles override global roles. Global roles override package roles. If two packages contribute the same role name, discovery reports a collision instead of choosing by package order.
18
+
19
+ A role is a Markdown file with frontmatter and an optional prompt body. Role frontmatter may own model, thinking, tools, skills, cwd, session mode, prompt mode, and interaction defaults. Legacy roles that request an external CLI fail closed before Herdr creates resources.
20
+
21
+ The runtime does not scan arbitrary `node_modules` folders. Installed packages register their directories through the discovery event.
22
+
23
+ ## Consequences
24
+
25
+ - Role packs are reusable and explicit.
26
+ - Local policy can override package defaults.
27
+ - Package collisions are visible and deterministic.
28
+ - There is no protected built-in namespace.
29
+ - Role packs contribute roles only; they do not add commands or executable workflow engines.
@@ -0,0 +1,22 @@
1
+ # ADR-0008: Adopt Pi-only subagent execution
2
+
3
+ - **Status:** Accepted (implemented)
4
+ - **Date:** 2026-08-10
5
+ - **Scope:** `giuseppecrj/pi-herdr-agents`
6
+ - **Supersedes:** The external CLI runtime-adapter provisions of ADR-0002
7
+
8
+ ## Decision
9
+
10
+ Make every public subagent run Pi-backed. Remove the Claude terminal adapter and its `cli`, `cli-model`, and `resumeSessionId` interface fields. Existing role files that request an external CLI must fail with a clear error instead of silently running through Pi.
11
+
12
+ A single deep launch module will own the complete launch transaction behind one small interface. Pi and Herdr remain implementation details of that module; do not add a runtime adapter seam without a second real execution path.
13
+
14
+ Completion delivery will use one custom message whose content contains the bounded result and triggers the parent turn. Completion detection will keep the Pi sidecar as primary evidence and the terminal exit marker as its best-effort fallback.
15
+
16
+ ## Why
17
+
18
+ The Claude terminal path duplicates launch, completion, transcript, cleanup, status, resume, and interruption behavior. It also requires `--dangerously-skip-permissions`, has no bundled role, and reduces locality and test leverage. Pi already provides Claude models through normal provider/model routing.
19
+
20
+ ## Consequences
21
+
22
+ This is an immediate breaking change with no deprecation adapter. Custom roles that request an external CLI fail closed before Herdr creates resources.
@@ -0,0 +1,23 @@
1
+ # ADR-0009: Durable recursive subagent ownership
2
+
3
+ - Status: Accepted
4
+
5
+ ## Context
6
+
7
+ A child may launch descendants and then settle before they do. Process-local maps cannot preserve correct ownership through extension reloads or independent child processes.
8
+
9
+ ## Decision
10
+
11
+ Publish immutable lineage events under the parent session artifacts. Register each child before Herdr creates its pane or worktree. Each node records its parent node and parent Pi session sink.
12
+
13
+ A node drains only after its own terminal result has been delivered and every recursive descendant has drained. Inbox materialization uses durable claims and acknowledgements so recovery can avoid duplicate delivery. Cancellation becomes terminal only after pane absence and process exit are proven.
14
+
15
+ Lineage identity belongs to the launched Pi process. Unrelated subprocesses that inherit environment variables do not become descendants.
16
+
17
+ ## Consequences
18
+
19
+ - Nested ownership survives extension reloads and independent processes.
20
+ - A parent cannot report final completion while a descendant remains undrained.
21
+ - Results route to the exact parent Pi session.
22
+ - Corrupt, cyclic, or unattributable state fails closed instead of guessing.
23
+ - Cleanup is separate from delivery, so retained evidence remains inspectable.
@@ -0,0 +1,16 @@
1
+ # ADR 0010: Public subagent tree registry
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-30
5
+
6
+ ## Decision
7
+
8
+ The package-root public subagent tree API uses a lazy process-local registry and one single-writer branch journal per Pi process. Tree artifacts live below the original caller session's artifact directory. Published node, terminal, and callback records use temporary files and same-filesystem rename; exclusive claims prevent competing owners.
9
+
10
+ Nested Pi children receive only a package-owned context-file path outside model task text. They attach with validated caller, tree, token, and parent identities. The existing `launchPiSubagent()` remains the only Pi/Herdr launch owner.
11
+
12
+ `/reload`, `/new`, and `/resume` rebind fresh context in the same process. `/fork` is accepted only when the fresh session's `parentSession` header matches the previously bound session and `previousSessionFile`; stale callbacks are rejected by binding generation. Terminal completion is one compare-and-set record, and cancellation is authoritative only after pane absence and captured process exit are confirmed.
13
+
14
+ ## Consequences
15
+
16
+ The API is intentionally for installed Pi extensions inside Herdr and does not promise arbitrary Node use or full process restart recovery. A process crash can leave retained evidence for inspection, but the filesystem protocol is not a general job engine. Clean and explicit-empty roots may complete while descendant error or abort panes remain open and are reported in bounded lifecycle rows.
@@ -0,0 +1,58 @@
1
+ # Public subagent tree API
2
+
3
+ `pi-agent-replicate` exports a registration-free API for installed Pi extensions running in Herdr. Importing the package root does not register model-facing tools.
4
+
5
+ ## Create and launch
6
+
7
+ The model-facing `replicate` tool uses source-snapshot worktrees and host-owned joins. This separate API still supports named roles, bare children and managed worktrees. Its ownership and runtime contracts are unchanged.
8
+
9
+ ```ts
10
+ import {
11
+ createSubagentTree,
12
+ type SubagentTreeResult,
13
+ } from "pi-agent-replicate";
14
+
15
+ const tree = createSubagentTree({ pi, ctx, metadata: { requestId: "search-1" } });
16
+ const child = await tree.launchChild({
17
+ parentId: tree.callerId,
18
+ name: "search-router",
19
+ task: "Find the requested records.",
20
+ tools: ["read"],
21
+ });
22
+ const childResult = await child.result;
23
+ const result: SubagentTreeResult = await tree.result;
24
+ ```
25
+
26
+ A newly created root caller launches with `parentId: tree.callerId`. An attached child branch launches its own descendants with `parentId: handle.ownerId`, where `handle` is that branch's own handle:
27
+
28
+ ```ts
29
+ const handle = attachSubagentTree({ pi, ctx });
30
+ const grandchild = await handle.launchChild({
31
+ parentId: handle.ownerId,
32
+ name: "detail-search",
33
+ task: "Search the narrower records.",
34
+ tools: ["read"],
35
+ });
36
+ ```
37
+
38
+ The package owns `callerId`, `treeId`, `ownerId`, node IDs, parent IDs, and the opaque `ownershipToken`. Consumer metadata is JSON-compatible and limited to 64 KiB of serialized UTF-8 bytes. Returned metadata, results, and the `children` map are read-only. Child result metadata carries those immutable identities plus the consumer value.
39
+
40
+ Launching accepts explicit `cwd` and `worktree` options and reuses the managed-worktree contract described in [Worktree subagents](worktree-subagents.md). Results carry a `SubagentWorktree` with the created path, branch, base, and observed Git state. The package keeps every worktree workspace, including after failures; the caller owns review, integration, and cleanup.
41
+
42
+ ## Reattach and fork
43
+
44
+ Internal Pi launches carry their child lifecycle binding in the session, so moving a child does not depend on copying a `.lineage.json` sidecar. This does not replace a consumer's explicit callback reattachment described below; automatic model-facing result delivery and consumer callback delivery are separate contracts.
45
+
46
+ A consumer calls `attachSubagentTree` from every fresh `session_start` with the current `pi`, `ctx`, and caller credentials, then calls `bindFinalCallback()` on the returned handle. For `/reload`, `/new`, and `/resume`, this replaces the stale session references. For `/fork`, pass `forkLineage: { previousSessionFile }`. The API checks the previous binding and the new session header's `parentSession` before replacing the active context. A failed check leaves the old context untouched.
47
+
48
+ The attached handle rebuilds its read-only `children` map. Each child keeps the same awaitable result identity, including answers that settled while the consumer was unbound. Only the original caller branch may call `bindFinalCallback`; callback bindings are versioned, and a successful callback is delivered once. A callback failure remains pending for a later binding.
49
+
50
+ ## Settlement and cancellation
51
+
52
+ `tree.result` resolves once the root has a clean or explicit-empty settled result and every queued or launched descendant has settled. A root provider error, launch failure, or unexpected abort fails the tree. Descendant errors remain in bounded lifecycle rows. Intentional Escape is not a final tree result. Node rows report actual pane evidence (`open`) and bounded session and error facts; full descendant outputs remain on child promises.
53
+
54
+ `cancel()` returns the one authoritative terminal record. If the tree already has a terminal record, `cancel()` returns that existing result and performs no further work. Otherwise it stops new reservations, waits for in-flight launches to settle, closes known descendant panes, and confirms both pane absence and captured process exit. A confirmed cancellation yields `state: "cancelled"`. A termination it cannot confirm — missing identity or surviving resources — yields `state: "failed"` with error code `cancel_termination_failed` and the retained recovery evidence. Rows inside a failed cancellation record carry conservative recovery flags, notably `open: true`; these are not verified current pane state. Late child records cannot replace a terminal record.
55
+
56
+ ## Public exports
57
+
58
+ The root exports `createSubagentTree`, `attachSubagentTree`, `SubagentTreeHandle`, `SubagentChildHandle`, launch/result/metadata types, `ForkLineage`, and the error classes `SubagentTreeError`, `SubagentTreeValidationError`, and `SubagentTreeOwnershipError`.
@@ -0,0 +1,71 @@
1
+ # Settled lifecycle contract
2
+
3
+ This document defines how child turns become one parent result. The TypeScript source for settled outcome shapes is `pi-extension/subagents/settled-contract.ts`.
4
+
5
+ ## Settled turns stay local
6
+
7
+ A settled assistant entry is still classified for child control:
8
+
9
+ - `clean`: non-empty assistant text with no final provider error.
10
+ - `empty`: a settled assistant entry with no usable text.
11
+ - `error`: `stopReason: "error"`.
12
+ - `intentional-abort`: `stopReason: "aborted"` after the parent recorded an interrupt request.
13
+ - `unexpected-abort`: `stopReason: "aborted"` without an interrupt request.
14
+
15
+ These boundaries do not publish parent results. A persistent child can settle many times while it remains open. Its earlier answers, empty turns, errors, and aborts stay in its own session.
16
+
17
+ For a replica, a clean nonempty response can record terminal intent only after the last descendant result is processed. Earlier responses stay local. A terminal provider error, Escape, an empty required summary or an unsaved close records failure, saves completion evidence and closes after descendant drain. That failure remains the terminal outcome across reload and later turns that process pending work, even if a later turn succeeds. User input does not suppress replica completion. A saved final response without settlement evidence is preserved as history, never promoted to success. Its original receives error facts and a file of all post-fork textual assistant answers, excluding inherited answers, tooling and hidden reasoning.
18
+
19
+ If history export fails, the terminal result reports that error instead of retrying completion forever. An unknown inherited-answer boundary never authorizes exporting the whole conversation. If the session file cannot be checkpointed, terminal facts and source checkpointing still proceed. The checkout stays available, integration is blocked without advertising a saved-copy continuation.
20
+
21
+ A queued self-reload is unfinished work. The acknowledgement before reload is not a terminal answer, and the stopped-replica timer stays suspended until reload continues the session or reports that it was blocked. An old runtime must not publish completion after shutdown, even if its descendant wait finishes later.
22
+
23
+ A stopped model does not imply finished session work. Replicas wait for active tools, background terminal completion messages, pending background request receipts, reported live Linux descendant PIDs and their observed children, and their own replicas. The copy processes completion messages before returning. New work invalidates an older shutdown decision, including one already waiting for descendants. The idle safety timer cannot bypass this guard. Terminal completion remains authoritative for its exit status; observing the same PID exit must not replace it with a status-less duplicate. Work receipts and process ownership are read from the finalized `tool_execution_end` result, after all `tool_result` transformations, including nested calls. A successful terminal deletion retires only that terminal's receipt because deletion suppresses its completion message. Independently tracked live processes and other work still block closure.
24
+
25
+ Search completions committed as atomic `custom_message` boundary entries drain their matching request before final settlement, even when Pi emits no `message_start`. A passive `web-search:cancelled` custom entry with `{ requestId, status: "cancelled", parentToolCallId? }` releases that request without waking a stopped model. A successful `wait_for_web_searches` also drains the known search set captured when that wait started, including searches cancelled during a provider retry. Failed waits, unrelated requests and searches launched after the wait started remain pending.
26
+
27
+ Receipt and process ownership persists for the exact session through reload. Unreported daemons, unsupported native process identities and remote jobs without completion receipts are not discoverable by this guard. It does not adopt unrelated processes or idle runtime helpers.
28
+
29
+ The settled handler returns its finalization promise. Pi must see the shutdown request before it checks that settled boundary; saving a result or starting detached finalization is not proof that the Pi process exited.
30
+
31
+ The retained non-replica lifecycle is unchanged: autonomous clean or empty boundaries can record terminal intent after the last descendant result; provider errors and aborts keep those children open.
32
+
33
+ Manual and automatic context compaction are active child work. `session_before_compact` protects the child from stale recovery until `session_compact` or `session_compact_failed` ends that state. Compaction never publishes or repeats a parent result.
34
+
35
+ ## Failed replica startup
36
+
37
+ The parent records the allocated pane's shell incarnation before starting Pi. If extension loading or an earlier launch step fails, cleanup can use that launch record without waiting for child session initialization. Only a verified owned idle shell is closed; a changed boot, reused shell or live process blocks that cleanup. Keep failed source and session evidence.
38
+
39
+ A terminal marker is process-exit evidence only after the replica process has stopped. Revalidate it after sidecar and diagnostic-read races. Report available startup diagnostics, not unrelated warnings or an inherited parent answer. A native launch error already saved in the exact parent counts as its outcome and is not replayed as another automatic result. A warning without process failure does not terminate a copy.
40
+
41
+ ## One terminal result
42
+
43
+ A child publishes only when it reaches a terminal lifecycle outcome, such as auto-exit, cancellation, confirmed pane loss, or recovery failure. No model-facing completion or help tool is registered.
44
+
45
+ The terminal path:
46
+
47
+ 1. reads the latest child result;
48
+ 2. waits for every recursive descendant to drain;
49
+ 3. writes one immutable result file and one durable inbox entry;
50
+ 4. materializes that inbox in the exact parent session once;
51
+ 5. records delivery before cleanup; and
52
+ 6. removes the child from active supervision as soon as delivery is recorded.
53
+
54
+ The stable terminal delivery ID is the child ID prefixed with `terminal:`. Delivery uses an exclusive durable claim. A successful send is acknowledged once. A send that throws remains retryable. A send that Pi only queued — the streaming parent has not written it to its session yet — is never repeated: the claim stays held until the parent session holds the record, so one result cannot become many copies. Compaction, reload, restored watchers, and later parent tool calls cannot materialize an acknowledged delivery again.
55
+
56
+ Old pending `settled` inbox entries are acknowledged without being sent. This prevents an upgrade or restored session from exposing an intermediate response after the terminal-only rule is active.
57
+
58
+ Cleanup is separate from delivery. An ordinary child pane closes after delivery. A worktree child keeps its review workspace. A delivered child leaves the widget immediately; a rejected Herdr close retries a few times with backoff and then stops with a delivery-log record. Its result is not sent again.
59
+
60
+ Replicas do not automatically restart after process loss or inactivity. Their parent receives a failure with available answer/error facts and can create a fresh copy. Normal settlement reports only after session work drains. Ten continuous seconds with Pi idle and no pending messages, movement or undrained descendants retry a missed completion. A saved settlement checkpoint survives reload; without settlement evidence, this safety net reports failure and preserves the answers. Active requests, tools, retries and compaction do not count as stopped time.
61
+
62
+ Retained non-replica recovery is bounded to three attempts across crash and stale causes. An exhausted child is not killed, resumed or re-delivered on later watchdog passes.
63
+ A recovery watcher starts only after the replacement registers its own session and pane. The old closed pane must not become a manual-cancellation result for the new process.
64
+
65
+ ## Descendants and exact-parent binding
66
+
67
+ A child drains only after its terminal outcome reaches its exact immediate-parent inbox and all descendants drain recursively. Failed inbox publication has no timeout and is never rerouted. A pending session inbox stays bound to the stable parent session ID until that exact session returns, even through a changed file path or pane.
68
+
69
+ Lineage identity belongs to the launched Pi session. A child lifecycle starts only after the runtime opens that session's persisted child binding. Reload startup checks, shell commands and test runners may inherit its environment variables, but that does not let them publish completion, update its activity or take ownership of its descendants. In particular, closing a no-session startup check must not report the live replica as closed.
70
+
71
+ A public `SubagentTree` owner is different. Its callbacks process child results inside the tree runtime. The tree result still waits for all reserved descendants, but ordinary model-facing `subagent_result` delivery follows the one-terminal-result rule above.
@@ -0,0 +1,58 @@
1
+ # Worktree subagents
2
+
3
+ The retained package-root API and internal launcher support opt-in isolated Git worktrees. This document describes those retained paths only. The model-facing `replicate` tool defaults to a source-snapshot worktree with automatic host-owned join and successful cleanup; it also supports a shared-directory mode. See the README for that separate contract.
4
+
5
+ For example, a tree consumer can launch:
6
+
7
+ ```typescript
8
+ tree.launch({
9
+ name: "issue-42-build",
10
+ agent: "implementer",
11
+ task: "Implement the approved change, test it, commit it, and report the SHA.",
12
+ worktree: { branch: "issue/42", base: "HEAD" },
13
+ });
14
+ ```
15
+
16
+ This example assumes an existing `implementer` role. Bare API children are also supported.
17
+
18
+ `cwd` selects the source repository. `base` resolves to an exact commit before creation and defaults to committed `HEAD`. The child starts at the Herdr-created worktree root.
19
+
20
+ ## Guarantees
21
+
22
+ 1. Worktrees are opt-in per launch.
23
+ 2. Parent uncommitted and untracked files are not copied.
24
+ 3. An ownership manifest is written before Herdr creates resources.
25
+ 4. Herdr creates the workspace without changing parent focus.
26
+ 5. Success, failure, and help outcomes retain the workspace.
27
+ 6. Git inspection failures are reported as unknown, never guessed clean.
28
+ 7. The extension never pushes, merges, cherry-picks, switches the parent checkout, or removes worktrees and branches.
29
+
30
+ The manifest lives under the parent session artifacts:
31
+
32
+ ```text
33
+ <session-dir>/artifacts/<session-id>/worktree-runs/<child-id>.json
34
+ ```
35
+
36
+ ## Review
37
+
38
+ After completion, inspect the reported path and Git state. Review the diff and commits before integrating. The parent owns merge, publication, and cleanup.
39
+
40
+ Useful Git commands:
41
+
42
+ ```bash
43
+ git -C <worktree-path> status --short
44
+ git -C <worktree-path> log --oneline <base-sha>..HEAD
45
+ git -C <worktree-path> diff <base-sha>...HEAD
46
+ ```
47
+
48
+ ## Recovery
49
+
50
+ If creation returns an ambiguous Herdr error, the extension may reconcile one exact branch match from Herdr's worktree list. It never guesses among multiple matches. A failed launch retains any verified worktree and records the error in its manifest.
51
+
52
+ The retained internal resume path opens an ordinary pane. It does not reattach ownership of a retained worktree.
53
+
54
+ A parent or child may move through Teleport without transferring or discarding the original worktree ownership. The session's binding still resolves the launch manifest and pending answer. The new session location is used for lifecycle observation and delivery; the retained checkout remains the original launch's review target. Herdr process loss does not authorize closing an unrelated pane that reuses an old ID.
55
+
56
+ ## Cleanup
57
+
58
+ Only remove a worktree after review and integration. Verify its path and branch first, then use your normal Git or Worktrunk cleanup procedure. Never delete `.git/worktrees` entries by hand.
@@ -0,0 +1,18 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const roles = fileURLToPath(new URL("./roles", import.meta.url));
5
+
6
+ export default function rolePack(pi: ExtensionAPI) {
7
+ const unsubscribe = pi.events.on(
8
+ "pi-herdr-subagents:roles:discover:v1",
9
+ (request) => {
10
+ const discovery = request as {
11
+ apiVersion: number;
12
+ register(path: string): void;
13
+ };
14
+ if (discovery.apiVersion === 1) discovery.register(roles);
15
+ },
16
+ );
17
+ pi.on("session_shutdown", unsubscribe);
18
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@example/pi-herdr-roles",
3
+ "version": "1.0.0",
4
+ "keywords": [
5
+ "pi-package"
6
+ ],
7
+ "type": "module",
8
+ "pi": {
9
+ "extensions": [
10
+ "./extension.ts"
11
+ ]
12
+ },
13
+ "peerDependencies": {
14
+ "@earendil-works/pi-coding-agent": "*"
15
+ }
16
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Reviews a bounded change and reports concrete, evidence-backed findings
3
+ tools: read, bash
4
+ spawning: false
5
+ auto-exit: true
6
+ system-prompt: append
7
+ ---
8
+
9
+ # Example Reviewer
10
+
11
+ Review only the requested change. Report concrete findings with file and line
12
+ references. Do not modify files.
package/index.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
3
+ export interface SubagentTreeMetadata {
4
+ readonly value: unknown;
5
+ /** Immutable package-owned identity; value is the consumer-owned opaque metadata. */
6
+ readonly callerId?: string;
7
+ readonly treeId?: string;
8
+ readonly ownerId?: string;
9
+ readonly nodeId?: string;
10
+ readonly parentId?: string;
11
+ }
12
+ export interface SubagentError {
13
+ readonly code: string;
14
+ readonly message: string;
15
+ }
16
+ export interface SubagentWorktree {
17
+ readonly path?: string;
18
+ readonly branch?: string;
19
+ readonly baseRef?: string;
20
+ readonly baseSha?: string;
21
+ readonly workspaceId?: string;
22
+ readonly paneId?: string;
23
+ readonly headSha?: string | null;
24
+ readonly commitsAhead?: number | null;
25
+ readonly clean?: boolean | null;
26
+ readonly conflicted?: boolean | null;
27
+ readonly changedFiles?: string[] | null;
28
+ readonly untrackedFiles?: string[] | null;
29
+ readonly manifestFile?: string;
30
+ }
31
+ export type SubagentChildOutcome = "clean" | "empty" | "error" | "intentional-abort" | "unexpected-abort";
32
+ export interface SubagentChildResult {
33
+ readonly resultId: string;
34
+ readonly assistantEntryId: string;
35
+ readonly answer: string | null;
36
+ readonly outcome: SubagentChildOutcome;
37
+ readonly sessionReference?: string;
38
+ readonly error?: SubagentError;
39
+ readonly worktree?: SubagentWorktree;
40
+ readonly metadata?: SubagentTreeMetadata;
41
+ }
42
+ export interface SubagentNodeResult {
43
+ readonly nodeId: string;
44
+ readonly parentId: string;
45
+ readonly outcome: SubagentChildOutcome;
46
+ readonly open: boolean;
47
+ readonly sessionReference?: string;
48
+ readonly error?: SubagentError;
49
+ }
50
+ export interface SubagentTreeResult {
51
+ readonly terminalId: string;
52
+ readonly state: "completed" | "failed" | "cancelled";
53
+ readonly rootResult: SubagentChildResult;
54
+ readonly callerMetadata: SubagentTreeMetadata;
55
+ readonly nodes: readonly SubagentNodeResult[];
56
+ readonly error?: SubagentError;
57
+ }
58
+ export interface SubagentChildHandle {
59
+ readonly nodeId: string;
60
+ readonly parentId: string;
61
+ readonly result: Promise<SubagentChildResult>;
62
+ }
63
+ export interface LaunchChildOptions {
64
+ readonly parentId: string;
65
+ readonly name: string;
66
+ readonly task: string;
67
+ readonly agent?: string;
68
+ readonly skills?: string;
69
+ readonly tools: readonly string[];
70
+ readonly cwd?: string;
71
+ readonly worktree?: {
72
+ readonly branch: string;
73
+ readonly base?: string;
74
+ };
75
+ readonly metadata?: unknown;
76
+ }
77
+ export interface SubagentTreeHandle {
78
+ readonly callerId: string;
79
+ readonly treeId: string;
80
+ readonly ownershipToken: string;
81
+ readonly ownerId: string;
82
+ readonly children: ReadonlyMap<string, SubagentChildHandle>;
83
+ readonly result: Promise<SubagentTreeResult>;
84
+ launchChild(options: LaunchChildOptions): Promise<SubagentChildHandle>;
85
+ bindFinalCallback(callback: (result: SubagentTreeResult) => void | Promise<void>): void;
86
+ cancel(): Promise<SubagentTreeResult>;
87
+ }
88
+ export interface CreateSubagentTreeOptions {
89
+ readonly pi: ExtensionAPI;
90
+ readonly ctx: ExtensionContext;
91
+ readonly metadata?: unknown;
92
+ }
93
+ export interface ForkLineage {
94
+ readonly previousSessionFile: string;
95
+ }
96
+ export interface AttachSubagentTreeOptions {
97
+ readonly pi: ExtensionAPI;
98
+ readonly ctx: ExtensionContext;
99
+ readonly callerId?: string;
100
+ readonly treeId?: string;
101
+ readonly ownershipToken?: string;
102
+ readonly forkLineage?: ForkLineage;
103
+ }
104
+ export declare class SubagentTreeError extends Error {
105
+ constructor(message: string);
106
+ }
107
+ export declare class SubagentTreeValidationError extends SubagentTreeError {
108
+ constructor(message: string);
109
+ }
110
+ export declare class SubagentTreeOwnershipError extends SubagentTreeError {
111
+ constructor(message: string);
112
+ }
113
+ /** Create a caller-owned tree. Importing this module performs no registration. */
114
+ export declare function createSubagentTree(options: CreateSubagentTreeOptions): SubagentTreeHandle;
115
+ /** Reattach a caller or claim a child branch using its package context. */
116
+ export declare function attachSubagentTree(options: AttachSubagentTreeOptions): SubagentTreeHandle;