@jopqior/pi-subagents 1.0.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 (110) hide show
  1. package/CHANGELOG.md +2705 -0
  2. package/LICENSE +21 -0
  3. package/README.md +503 -0
  4. package/dist/public.d.ts +331 -0
  5. package/dist/settings.d.ts +82 -0
  6. package/docs/architecture/architecture.md +1566 -0
  7. package/docs/architecture/client-server-opportunities.md +127 -0
  8. package/docs/architecture/history/phase-1-api-boundary.md +8 -0
  9. package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
  10. package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
  11. package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
  12. package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
  13. package/docs/architecture/history/phase-14-strip-policy.md +49 -0
  14. package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
  15. package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
  16. package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
  17. package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
  18. package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
  19. package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
  20. package/docs/architecture/history/phase-20-result-delivery.md +245 -0
  21. package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
  22. package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
  23. package/docs/architecture/history/phase-4-implement-service.md +8 -0
  24. package/docs/architecture/history/phase-5-decompose-index.md +42 -0
  25. package/docs/architecture/history/phase-7-encapsulation.md +173 -0
  26. package/docs/architecture/history/phase-8-testability.md +103 -0
  27. package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
  28. package/docs/comparison-with-upstream.md +77 -0
  29. package/docs/configuration.md +364 -0
  30. package/docs/decisions/0001-deferred-patches.md +80 -0
  31. package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
  32. package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
  33. package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
  34. package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
  35. package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
  36. package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
  37. package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
  38. package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
  39. package/package.json +91 -0
  40. package/src/config/agent-types.ts +135 -0
  41. package/src/config/custom-agents.ts +151 -0
  42. package/src/config/default-agents.ts +121 -0
  43. package/src/config/invocation-config.ts +167 -0
  44. package/src/config/thinking-level.ts +58 -0
  45. package/src/debug.ts +14 -0
  46. package/src/handlers/index.ts +3 -0
  47. package/src/handlers/interrupt.ts +58 -0
  48. package/src/handlers/lifecycle.ts +71 -0
  49. package/src/handlers/widget-events.ts +49 -0
  50. package/src/index.ts +292 -0
  51. package/src/layered-settings.ts +105 -0
  52. package/src/lifecycle/child-lifecycle.ts +115 -0
  53. package/src/lifecycle/child-shutdown.ts +105 -0
  54. package/src/lifecycle/concurrency-limiter.ts +55 -0
  55. package/src/lifecycle/create-subagent-session.ts +335 -0
  56. package/src/lifecycle/parent-snapshot.ts +119 -0
  57. package/src/lifecycle/run-listeners.ts +37 -0
  58. package/src/lifecycle/selection-scope.ts +116 -0
  59. package/src/lifecycle/spawn-selection.ts +259 -0
  60. package/src/lifecycle/subagent-manager.ts +546 -0
  61. package/src/lifecycle/subagent-session.ts +347 -0
  62. package/src/lifecycle/subagent-state.ts +404 -0
  63. package/src/lifecycle/subagent.ts +885 -0
  64. package/src/lifecycle/turn-limits.ts +13 -0
  65. package/src/lifecycle/usage.ts +60 -0
  66. package/src/lifecycle/workspace-bracket.ts +76 -0
  67. package/src/lifecycle/workspace.ts +46 -0
  68. package/src/observation/composite-subagent-observer.ts +74 -0
  69. package/src/observation/notification.ts +430 -0
  70. package/src/observation/outcome-delivery.ts +239 -0
  71. package/src/observation/record-observer.ts +78 -0
  72. package/src/observation/renderer.ts +161 -0
  73. package/src/observation/subagent-events-observer.ts +148 -0
  74. package/src/runtime.ts +137 -0
  75. package/src/service/service-adapter.ts +201 -0
  76. package/src/service/service.ts +246 -0
  77. package/src/session/ask-parent-tool.ts +69 -0
  78. package/src/session/content-items.ts +53 -0
  79. package/src/session/context.ts +80 -0
  80. package/src/session/conversation.ts +49 -0
  81. package/src/session/env.ts +40 -0
  82. package/src/session/model-resolver.ts +126 -0
  83. package/src/session/notify-parent-tool.ts +83 -0
  84. package/src/session/package-exclusions.ts +75 -0
  85. package/src/session/prompts.ts +231 -0
  86. package/src/session/provider-inheritance.ts +56 -0
  87. package/src/session/selection-catalogue.ts +143 -0
  88. package/src/session/session-config.ts +202 -0
  89. package/src/session/session-dir.ts +38 -0
  90. package/src/settings.ts +447 -0
  91. package/src/tools/agent-tool.ts +305 -0
  92. package/src/tools/background-spawner.ts +83 -0
  93. package/src/tools/foreground-runner.ts +159 -0
  94. package/src/tools/get-result-renderer.ts +119 -0
  95. package/src/tools/get-result-report.ts +84 -0
  96. package/src/tools/get-result-tool.ts +192 -0
  97. package/src/tools/helpers.ts +118 -0
  98. package/src/tools/result-renderer.ts +153 -0
  99. package/src/tools/spawn-config.ts +192 -0
  100. package/src/tools/steer-tool.ts +109 -0
  101. package/src/types.ts +143 -0
  102. package/src/ui/agent-widget.ts +333 -0
  103. package/src/ui/bounded-lines.ts +45 -0
  104. package/src/ui/display.ts +180 -0
  105. package/src/ui/glyphs.ts +62 -0
  106. package/src/ui/session-navigation.ts +150 -0
  107. package/src/ui/session-navigator.ts +255 -0
  108. package/src/ui/subagents-settings.ts +179 -0
  109. package/src/ui/transcript-content.ts +374 -0
  110. package/src/ui/widget-renderer.ts +301 -0
@@ -0,0 +1,104 @@
1
+ ---
2
+ status: amended by 0008
3
+ date: 2026-08-30
4
+ ---
5
+
6
+ # 0006 — A child inherits the parent prompt's identity, not its session-resolved tail
7
+
8
+ ## Status
9
+
10
+ Accepted, and amended by [ADR 0008].
11
+ Supersedes the equal-cwd exception recorded in [#640] and generalizes it into one rule for every layer Pi resolves per session.
12
+
13
+ The mechanism below is unchanged: a child still inherits the identity region and nothing after it.
14
+ [ADR 0008] amends what that placement *guarantees* — shared parts rather than shared bytes, scoped to hosts that reuse a prefix over the system text independently of the tool definitions — after [#890] found that an extension narrowing the `Available tools:` listing inside this region ended the shared prefix at offset 171.
15
+
16
+ ## Context
17
+
18
+ `buildAgentPrompt` embeds the parent session's effective system prompt verbatim as the child's leading content.
19
+ That placement is deliberate and load-bearing: [#180] and [#400] moved the shared parent text to the front precisely so a child's prompt shares a byte-identical prefix with its parent's, which prefix-caching providers and local inference engines reuse instead of reprocessing.
20
+ The reporter of [#180] measured 8,333 shared tokens costing roughly 40 seconds of prompt processing on a local model before that change.
21
+
22
+ What the placement overlooked is that the parent's *effective* prompt is not identity alone.
23
+ Pi's `buildSystemPrompt` writes four regions and extensions append a fifth:
24
+
25
+ | Region | Content | Written by |
26
+ | -------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
27
+ | Identity | preamble or `customPrompt`, tool snippets, guidelines, `<project_context>` | `buildSystemPrompt` |
28
+ | Catalogue | the skills heading and `<available_skills>` … `</available_skills>` | `formatSkillsForPrompt`, gated on the session's tool set including `read` |
29
+ | Footer | `Current working directory: <cwd>` | `buildSystemPrompt`, always last |
30
+ | Extension tail | further blocks | handlers returning `systemPrompt` from `before_agent_start` |
31
+
32
+ The last three are resolved against **one session**: its directory, its loaded skills, its bound extensions.
33
+ The extension tail is resolved against one *turn* — Pi rebuilds it from the base prompt on every turn and applies it as a single-turn override.
34
+
35
+ A child session rebuilds all three for itself and appends them after the inherited copy.
36
+ Inheriting them therefore hands the child a second, stale claim of each.
37
+ [#640] found this for the footer, where a worktree-isolated child followed the parent's directory back out of its workspace.
38
+ [#801] found it again for the catalogue, reported as a visibly duplicated `<available_skills>` block.
39
+
40
+ ## Decision
41
+
42
+ The inherited prompt contributes **only its identity region**.
43
+ `inheritedIdentity` cuts at the first per-session layer present — the catalogue, or the footer when the parent session resolved no skills — and returns what precedes it.
44
+
45
+ ### Truncate rather than excise
46
+
47
+ The alternative was to cut the catalogue and footer out while keeping the extension tail.
48
+ It was rejected on both criteria.
49
+
50
+ On accuracy, the inherited tail is the least defensible layer of the three.
51
+ It was built for the parent's directory and the parent's extension set: `@gotgenes/pi-nocd`'s block names the parent's cwd — the [#640] defect itself — and a package excluded from children through `excludedExtensionPackages` ([#696]) still reaches them through the inherited copy, which is the exact opposite of what that setting asks for.
52
+
53
+ On cost, excision is strictly worse for the constituency [#180] exists to protect.
54
+ Removing an interior span leaves the tail in the child but displaces it past the divergence point, moving it from cached to prefilled — measured at roughly 275 characters in [#640]'s environment.
55
+ Truncation deletes it instead, so a child's prefilled token count is unchanged from before this decision while its total prompt shrinks by the whole tail.
56
+
57
+ ### The equal-cwd exception is withdrawn
58
+
59
+ [#640] kept an inherited footer when the child's directory matched the parent's, on the grounds that removing an accurate duplicate would shorten the byte-identical prefix.
60
+ That reasoning depended on the footer being inside the shared prefix.
61
+ The catalogue precedes the footer, so once the catalogue is cut the footer is already past the divergence point and the exception preserves nothing.
62
+ It survives only for a parent session that resolved no skills, where it would save one line.
63
+ The footer is now stripped unconditionally.
64
+
65
+ ### The catalogue is located by position, not by document order
66
+
67
+ A project-context file may quote Pi's own prompt text, and so may a block an extension appended after the footer.
68
+ Any rule that picks the first or the last `<available_skills>` in the prompt is a guess about document order, and it is wrong in one direction or the other: the first loses to a quote in project context, the last loses to a quote in an appended block.
69
+
70
+ `buildSystemPrompt` writes the cwd footer immediately after the catalogue, in both of its branches and unconditionally.
71
+ Pi's own catalogue is therefore exactly the one whose closing tag is the line before the footer, which is a structural fact rather than a heuristic.
72
+ The heading is then found by searching back from that tag, so prose quoting the heading ahead of the section is not mistaken for it either.
73
+ A prompt with no footer has been rewritten by something downstream; there the last closing tag is the best remaining guess.
74
+
75
+ Both anchors match whole lines, which keeps a footer naming a directory that merely shares a prefix with the parent's from being mistaken for it.
76
+
77
+ ## Consequences
78
+
79
+ - A child's assembled prompt carries exactly one skills catalogue and one working-directory claim, both describing the child's own session.
80
+ - Extensions that append to the system prompt no longer reach children through inheritance.
81
+ Their handlers still run in the child — it binds the parent's extension set and its turn loop fires `before_agent_start` unconditionally — so an unconditional appender simply writes a block built for the child.
82
+ This is documented for extension authors in the README.
83
+ - **Accepted residual:** a handler that appends conditionally — gated on an interactive UI, or on state cached at `session_start` — contributes nothing in a child, which then carries less guidance than it did before this decision.
84
+ The mechanism is verified to fire in children and this package's own appender is unconditional, but the third-party population cannot be enumerated.
85
+ The trade is accepted because the alternative is inheriting a block built for another directory and another extension set, which is wrong rather than merely absent.
86
+ - The shared prefix a child holds with its parent is shorter by the three dropped regions.
87
+ Nothing that remains in the child's prompt moved out of that prefix, so no additional tokens require processing.
88
+ - `@gotgenes/pi-nocd` documents a rewrite path premised on subagents inheriting the prompt verbatim, which this decision ends.
89
+ Tracked as [#846].
90
+ - A consumer that recovers the inherited region by searching a child's prompt for the parent's *full* assembled prompt finds nothing, because truncation ends the containment it matches on.
91
+ `pi-claude-bridge` does exactly this to project a child's prompt onto another harness, so a child on that provider forwards Pi's base prompt where the parent forwards only its portable parts.
92
+ Reported as [#883]; a consumer-side matcher fix is proposed at [pi-claude-bridge#88].
93
+ A child whose cwd differs from its parent's is beyond any such fix: Pi's `useExtensionCacheCwd` clears the extension cache on a cwd change, so the consumer's capture of the parent never exists in the child's module instance at all.
94
+
95
+ [#180]: https://github.com/gotgenes/pi-packages/issues/180
96
+ [#400]: https://github.com/gotgenes/pi-packages/issues/400
97
+ [#640]: https://github.com/gotgenes/pi-packages/issues/640
98
+ [#696]: https://github.com/gotgenes/pi-packages/issues/696
99
+ [#801]: https://github.com/gotgenes/pi-packages/issues/801
100
+ [#846]: https://github.com/gotgenes/pi-packages/issues/846
101
+ [#890]: https://github.com/gotgenes/pi-packages/issues/890
102
+ [ADR 0008]: 0008-inherited-region-is-shared-parts.md
103
+ [#883]: https://github.com/gotgenes/pi-packages/issues/883
104
+ [pi-claude-bridge#88]: https://github.com/elidickinson/pi-claude-bridge/issues/88
@@ -0,0 +1,228 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-09-03
4
+ ---
5
+
6
+ # 0007 — The transcript viewer is a docked pane, not an overlay
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Closes Pi's overlay mount to this package until the upstream compositor changes.
12
+
13
+ ## Context
14
+
15
+ `/subagents:sessions` mounted its transcript viewer with `ui.custom(..., { overlay: true })`.
16
+ Operators reported fragments of the viewer's box — the `╭─` top rule, the `Subagent session` header, empty box rows — interwoven through the terminal's scrollback, repeating every few screens and surviving the viewer being closed ([#733]).
17
+
18
+ ### The mechanism
19
+
20
+ In pi-tui 0.84.4's `TuiMainScreen.doRender`, overlays are composited into the rendered lines **before** the differential compare (`packages/tui/src/tui-main-screen.ts:267`), and that composited array is then stored as `previousLines` — at every assignment site, not only the clearing one (`:314` full render, `:440` deleted-lines path, `:611` the ordinary differential path).
21
+ The renderer's model of the transcript at those rows *is* the overlay.
22
+
23
+ Differential rendering can only repair rows still on screen.
24
+ Anything the terminal has scrolled past is already committed to scrollback with the overlay's pixels in it.
25
+
26
+ `TuiAltScreen` composites into a bounded `screen` buffer with no scrollback behind it (`tui-alt-screen.ts:1321`), so this is a regular-mode defect only.
27
+
28
+ ### The precondition, measured
29
+
30
+ The obvious account — that every row under the overlay eventually scrolls off carrying chrome — is **wrong**, and the first two attempts at a reproduction correctly found nothing.
31
+ While output is appended a line at a time, the overlay band moves up through rows that are still on screen, and the differential renderer repairs each one before it scrolls away.
32
+
33
+ The failure needs a single append large enough to carry a row from under the overlay past the top of the screen in one frame.
34
+ That row is committed with the chrome still in it and can never be reached again.
35
+
36
+ Measured on a 24-row terminal with a 10-line centered overlay, by the script below:
37
+
38
+ | Lines appended per frame | Rows committed carrying chrome |
39
+ | ------------------------ | ------------------------------ |
40
+ | 1, 3, 6, 7 | 0 |
41
+ | 8 | 57 |
42
+ | 9 | 114 |
43
+ | 10 | 171 of 576 |
44
+ | 25 | 590 |
45
+
46
+ The threshold is geometry, not chance: a 10-line box centered in 24 rows sits at screen row `floor((24 - 10) / 2) = 7`, so a row beneath its top edge must travel 8 rows to leave the screen entirely — one more than the distance to the top.
47
+ An identical control run without the overlay commits none.
48
+
49
+ This is why the symptom is intermittent, and why [#733] reports it as worst during tool calls: tool output arrives in chunks.
50
+ The shipped viewer's box was 70% of the terminal height, putting its threshold around 7 lines, which nearly any tool call clears.
51
+
52
+ ### Upstream
53
+
54
+ The maintainer rejected an adjacent diagnosis in [earendil-works/pi#4785] — a report that the diff scan's start index forces a full redraw on off-screen spinner ticks — with "you are wrong. your clanker is wrong. as explained on twitter.com."
55
+ No technical reason is recorded in the tracker and the cited explanation is off-platform.
56
+ That report concerns the widget render loop ([#864]), not this mechanism, so nothing upstream has ruled on the claim here.
57
+
58
+ It is recorded because it is the reason not to plan around upstream fixing this.
59
+ By contrast [earendil-works/pi#2759], which arrived with a reproduction command against the repo's own example extension, was reproduced by the maintainer and fixed within the hour — which is why the script below exists.
60
+
61
+ ## Decision
62
+
63
+ Mount the transcript viewer through `ui.custom`'s **non-overlay** path, and do not use `overlay: true` in this package.
64
+
65
+ Pi's `showExtensionCustom` then clears the input-editor container, adds the component, and focuses it.
66
+ The pane renders at full terminal width with the conversation streaming above it, and `ui.custom` restores the editor and any typed text on close.
67
+ No compositing happens, so nothing can be baked into scrollback.
68
+
69
+ The rejected alternative was forcing a full repaint on close (`requestRender(true)`).
70
+ It erases the fragments already in scrollback but leaves the viewer shredding while open, and wipes the terminal's scrollback history as a side effect.
71
+
72
+ Fixing `compositeOverlays` upstream is the more general answer and is not ours to make.
73
+ The non-overlay path is entirely within our control and removes the failure mode rather than papering over it.
74
+
75
+ ## Consequences
76
+
77
+ - The viewer is a pane docked above the editor, not a floating window.
78
+ Its chrome is a header and a footer line: docked, it is already bracketed by the agents widget above and Pi's footer below, so a frame costs rows for nothing.
79
+ - It renders at full terminal width, so there is no overlay width to keep in sync and no compositor `maxHeight` slice.
80
+ - Its height is sized to its content, capped at a share of the terminal, because a docked pane that reserves a fixed 70% pushes the conversation off screen to show blank space.
81
+ - The path is well exercised rather than novel: it is what Pi runs for the editor on every turn, and what `pi-permission-system`'s permission prompt already uses with an explicit `{ overlay: false }`.
82
+ - This is consistent with [ADR 0004](0004-reconsider-ui-direction.md), which replaced the bespoke `ConversationViewer` overlay with native session navigation and framed the interaction as navigation rather than a live overlay.
83
+ - The repo's other `overlay: true` call site, `pi-permission-system`'s settings modal, has the same defect at much lower exposure and is tracked separately as [#874].
84
+ Its fix is a different question, because it asks for a fixed 82-column width the non-overlay path does not offer.
85
+
86
+ ## Reproduction
87
+
88
+ A dated claim about `@earendil-works/pi-tui` **0.84.4**, using only its public API (`TuiMainScreen`, `showOverlay`, `renderNow`, `captureRenderState`).
89
+ It needs no Pi monorepo checkout and no extension.
90
+ Nothing in CI runs it; it is preserved as evidence, not as a test, and it will stop being meaningful when the upstream compositor changes.
91
+
92
+ Run with `node repro.mjs <lines-appended-per-frame>`; the table above used 1 through 25.
93
+
94
+ ```javascript
95
+ import { TuiMainScreen } from "@earendil-works/pi-tui";
96
+
97
+ const COLUMNS = 80;
98
+ const ROWS = 24;
99
+ const FRAMES = 60;
100
+ const BURST = Number(process.argv[2] ?? 1);
101
+
102
+ function fakeTerminal() {
103
+ return {
104
+ start() {}, stop() {}, async drainInput() {}, write() {},
105
+ get columns() { return COLUMNS; },
106
+ get rows() { return ROWS; },
107
+ get kittyProtocolActive() { return false; },
108
+ moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {},
109
+ clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
110
+ };
111
+ }
112
+
113
+ /** The transcript: plain text only. Never emits a box-drawing glyph. */
114
+ class Chat {
115
+ constructor() { this.lineCount = 0; }
116
+ render() {
117
+ return Array.from(
118
+ { length: this.lineCount },
119
+ (_, i) => `chat line ${String(i).padStart(3, "0")}`,
120
+ );
121
+ }
122
+ }
123
+
124
+ /** A bordered overlay, exactly the shape a transcript viewer paints. */
125
+ class BoxOverlay {
126
+ render(width) {
127
+ const inner = width - 2;
128
+ const lines = [`╭${"─".repeat(inner)}╮`];
129
+ for (let i = 0; i < 8; i++) lines.push(`│${" ".repeat(inner)}│`);
130
+ lines.push(`╰${"─".repeat(inner)}╯`);
131
+ return lines;
132
+ }
133
+ }
134
+
135
+ const BOX_GLYPHS = /[╭╮╰╯│─]/;
136
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
137
+
138
+ function run({ withOverlay }) {
139
+ const tui = new TuiMainScreen(fakeTerminal(), false, "/tmp/pi733");
140
+ const chat = new Chat();
141
+ tui.addChild(chat);
142
+
143
+ if (withOverlay) {
144
+ tui.showOverlay(new BoxOverlay(), {
145
+ anchor: "center",
146
+ width: "90%",
147
+ maxHeight: "70%",
148
+ });
149
+ }
150
+
151
+ // What the terminal keeps for each row index, captured at the last frame in
152
+ // which that row was still on screen (and therefore still repairable).
153
+ const committed = new Map();
154
+
155
+ for (let i = 0; i < FRAMES; i++) {
156
+ chat.lineCount = (i + 1) * BURST;
157
+ tui.renderNow();
158
+ const { previousLines, previousViewportTop } = tui.captureRenderState();
159
+ for (let row = previousViewportTop; row < previousLines.length; row++) {
160
+ committed.set(row, previousLines[row]);
161
+ }
162
+ }
163
+
164
+ const finalTop = tui.captureRenderState().previousViewportTop;
165
+ return { committed, finalTop, fullRedraws: tui.fullRedraws };
166
+ }
167
+
168
+ function report(label, { committed, finalTop, fullRedraws }) {
169
+ const scrolledOff = [...committed.entries()].filter(([row]) => row < finalTop);
170
+ const contaminated = scrolledOff.filter(([, line]) => BOX_GLYPHS.test(line));
171
+
172
+ console.log(`\n--- ${label}`);
173
+ console.log(` full redraws: ${fullRedraws}`);
174
+ console.log(` rows scrolled into history: ${scrolledOff.length}`);
175
+ console.log(` ...committed WITH chrome: ${contaminated.length}`);
176
+ for (const [row, line] of contaminated.slice(0, 3)) {
177
+ console.log(` row ${String(row).padStart(2)}: ${JSON.stringify(stripAnsi(line).slice(0, 56))}`);
178
+ console.log(` the Chat component rendered: ${JSON.stringify(`chat line ${String(row).padStart(3, "0")}`)}`);
179
+ }
180
+ return contaminated.length;
181
+ }
182
+
183
+ const withOverlay = report("WITH overlay", run({ withOverlay: true }));
184
+ const control = report("WITHOUT overlay (control)", run({ withOverlay: false }));
185
+
186
+ console.log("\n=== Result");
187
+ console.log(
188
+ withOverlay > 0 && control === 0
189
+ ? `REPRODUCED: ${withOverlay} rows entered scrollback carrying overlay chrome.`
190
+ : `NOT REPRODUCED (with=${withOverlay}, control=${control}).`,
191
+ );
192
+ ```
193
+
194
+ At a burst of 10 it reports a row the `Chat` component rendered as `"chat line 013"` committed to history as `"chat╭─────…"` — the overlay having overwritten from column 4, its left margin.
195
+ That is the signature operators see in scrollback.
196
+
197
+ ## An upstream report, if one is filed
198
+
199
+ Nobody has filed this upstream, and this repository does not depend on anyone doing so.
200
+ The text below is kept ready rather than pending.
201
+
202
+ ```markdown
203
+ Title: Regular-mode overlays composite into the scrollback buffer, baking chrome into history
204
+
205
+ In `TuiMainScreen.doRender`, overlays are composited into `newLines` before the
206
+ differential compare, and that composited array becomes `previousLines`
207
+ (`tui-main-screen.ts:267`, assigned at `:314`, `:440`, and `:611`). The renderer's
208
+ model of the transcript at those rows is the overlay.
209
+
210
+ While output is appended a line at a time the band is repaired as it moves, so
211
+ nothing is lost. The failure needs a single append large enough to carry a row from
212
+ under the overlay past the top of the screen: that row is committed to scrollback
213
+ with the chrome in it, and differential rendering can no longer reach it.
214
+
215
+ Measured on 0.84.4, 24-row terminal, 10-line centered overlay: zero contamination
216
+ for appends of 7 lines or fewer; 57 rows at 8; 171 of 576 at 10. The threshold of 8
217
+ is one more than the distance from the overlay's top edge to the top of the screen,
218
+ which is the row count needed to carry a row off screen. An identical
219
+ control run without the overlay commits none.
220
+
221
+ Repro script, public API only, no monorepo checkout needed: <attached>
222
+ ```
223
+
224
+ [#733]: https://github.com/gotgenes/pi-packages/issues/733
225
+ [#864]: https://github.com/gotgenes/pi-packages/issues/864
226
+ [#874]: https://github.com/gotgenes/pi-packages/issues/874
227
+ [earendil-works/pi#2759]: https://github.com/earendil-works/pi/issues/2759
228
+ [earendil-works/pi#4785]: https://github.com/earendil-works/pi/issues/4785
@@ -0,0 +1,81 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-09-08
4
+ ---
5
+
6
+ # 0008 — The inherited region guarantees shared parts, not shared bytes
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Amends [ADR 0006], which stands: a child still inherits the parent prompt's identity region and nothing after it.
12
+ What this record changes is the *goal* that placement serves, and the claim the package makes about it.
13
+
14
+ ## Context
15
+
16
+ [#180] and [#400] moved the inherited parent prompt to the front of a child's prompt so the child's leading bytes would be **byte-identical** to the parent's, which prefix-caching providers and local inference engines reuse instead of reprocessing.
17
+ [#180]'s reporter measured 8,333 shared tokens costing roughly 40 seconds on a local model.
18
+ [ADR 0006] then narrowed what is inherited to the identity region, on the reasoning that everything after it is resolved per session.
19
+
20
+ [#890] found that the goal, stated that way, was not being met and in one important case could not be.
21
+
22
+ ### The tool list sits inside the region
23
+
24
+ `@gotgenes/pi-permission-system` narrows a session's `Available tools:` listing to the tools policy allows.
25
+ That listing is at offset 171 of Pi's preamble — inside the identity.
26
+ Rewriting it in place ended the shared prefix there for every child whose allowed set differed from its parent's: measured at **365 shared characters of a 57,423-character identity** in this repo's configuration, and reported from the field as a divergence at offset 412 of a 22,157-character prompt ([#890]).
27
+
28
+ Neither package was misbehaving.
29
+ A shared identity and an honest per-session tool list cannot coexist while the list lives inside the identity.
30
+
31
+ ### The byte-identical claim was unscoped
32
+
33
+ Anthropic builds its cache prefix in the order `tools`, `system`, `messages`, and modifying tool definitions "invalidates the entire cache".
34
+ The `tools` array precedes the system prompt, so **any** child whose tool array differs from its parent's gets no cache hit from a byte-identical system prompt — which is every child with a narrowed `tools:` list, and every child at all, since the core installs `ask_parent`/`notify_parent` that no parent has.
35
+
36
+ On the Anthropic OAuth path there is a second, independent break: `@gotgenes/pi-anthropic-auth` prepends a billing block as system block 0 whose hash is derived from the first user message, and a parent and child never share one.
37
+
38
+ The property pays where tool definitions are rendered *after* the system text — local inference engines whose chat template does so, which is [#180]'s own constituency, and API-key Anthropic with an identical tool array.
39
+ The package had never said so.
40
+
41
+ ## Decision
42
+
43
+ The inherited region guarantees **shared parts, not shared bytes**, and the benefit is scoped to hosts that reuse a prefix over the system text independently of the tool definitions.
44
+
45
+ Concretely:
46
+
47
+ 1. `buildAgentPrompt` continues to place the inherited identity first, verbatim, and to cut at the first per-session layer ([ADR 0006] is unchanged in mechanism).
48
+ 2. Per-session **prose about the tool surface does not belong in that region.**
49
+ An extension that must state a session's tools states them after the layers a child inherits, so each session speaks for itself and none edits another's bytes.
50
+ `pi-permission-system` implements this in [its ADR 0014].
51
+ 3. The package documents the property as a shared leading prefix whose value is host-dependent, not as an unconditional byte-for-byte guarantee.
52
+
53
+ ### The bridge block goes with it
54
+
55
+ The `<sub_agent_context>` block append mode injected is removed in the same change.
56
+ It named `read`, `edit`, `write`, `find`, and `grep` unconditionally — instructing an `Explore` child, which has neither `edit` nor `write`, to use them — and duplicated the `promptGuidelines` Pi's own tools already contribute to every child's prompt.
57
+ It was inherited verbatim from the upstream fork with no decision behind it.
58
+
59
+ It sat after the identity, so removing it costs no prefix.
60
+
61
+ ## Consequences
62
+
63
+ - The identity a child shares with its parent is now the full identity minus the relocated tool surface, rather than ending at the tool list.
64
+ A test pins it in this package (`buildAgentPrompt` opens with the inherited identity verbatim, in both modes), which the property had never had.
65
+ - Append mode differs from replace mode only in the `<agent_instructions>` wrapper.
66
+ - A child gets tool guidance for the tools it actually holds, rendered per session, instead of a fixed five-tool assertion.
67
+ - **Accepted residual:** with `pi-permission-system` absent, nothing restates a child's tool list and it still inherits its parent's.
68
+ Tracked as [#901]; this package has no `before_agent_start` handler today, and adding one is that issue's work.
69
+ - A consumer that projects a child's prompt by matching the parent's — `pi-claude-bridge` — is helped rather than hindered: the region it looks for is verbatim again.
70
+ That interaction is recorded, with what remains unverified, in [its ADR 0014].
71
+ - The region is still Pi's preamble, which a provider that re-homes the prompt into another harness carries into that harness's API.
72
+ [#883] is that case, and [ADR 0009] adds an opt-in `portable` strategy for it — scoped per provider, so the shared prefix this record restored stays the default everywhere else.
73
+
74
+ [#180]: https://github.com/gotgenes/pi-packages/issues/180
75
+ [#400]: https://github.com/gotgenes/pi-packages/issues/400
76
+ [#890]: https://github.com/gotgenes/pi-packages/issues/890
77
+ [#901]: https://github.com/gotgenes/pi-packages/issues/901
78
+ [#883]: https://github.com/gotgenes/pi-packages/issues/883
79
+ [ADR 0006]: 0006-inherited-prompt-is-identity-only.md
80
+ [ADR 0009]: 0009-portable-inheritance-is-provider-scoped.md
81
+ [its ADR 0014]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0014-tool-surface-is-node-local-prose.md
@@ -0,0 +1,116 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-09-08
4
+ ---
5
+
6
+ # 0009 — Portable inheritance is opt-in and scoped to the provider
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Extends [ADR 0006] and [ADR 0008], both of which stand: a child still inherits the parent prompt's identity region and nothing after it, and that region still guarantees shared parts rather than shared bytes.
12
+ What this record adds is a second strategy for the case where inheriting the region is not merely worthless but harmful, and it settles what selects between them.
13
+
14
+ ## Context
15
+
16
+ [#883] reported a `pi-subagents` child failing its first API call with `400 Third-party apps now draw from your extra usage`, while its parent — same machine, same OAuth token, same Claude Code binary — passed.
17
+
18
+ The chain, bisected by the reporter and independently corroborated by `pi-claude-bridge`'s own `diag/EXTRA-USAGE-400.md`:
19
+
20
+ 1. `buildAgentPrompt` places the parent's identity region first in the child's prompt.
21
+ That region opens with Pi's base preamble, which includes a documentation-routing line naming `custom providers (docs/custom-provider.md)` and `pi packages (docs/packages.md)`.
22
+ 2. `pi-claude-bridge` projects a session's system prompt into Claude Code's `--append-system-prompt`, so the child's copy of that line reaches Anthropic.
23
+ 3. Anthropic's subscription OAuth gate scores the appended text.
24
+ The two phrases **together** trip it; either alone passes, and substituting `banana packages` for `pi packages` passes.
25
+ 4. Classified as a third-party app, the request draws from extra usage — a hard 400 on an account with none.
26
+
27
+ The parent passes because the bridge's projection carries only its portable parts; Pi's base never reaches the API on that path.
28
+
29
+ ### Why the region cannot simply be narrowed further
30
+
31
+ The offending line is in Pi's base preamble, which `inheritedIdentity` keeps by construction — the cut is at the *first per-session layer*, and the preamble precedes all of them.
32
+ Cutting further would not be a boundary any principle draws; it would be a blocklist of phrases one provider currently dislikes.
33
+
34
+ ### The benefit being traded away is real, and recently restored
35
+
36
+ [#180] and [#400] placed the inherited region first so a child's leading bytes match its parent's, which prefix-reusing inference engines reuse instead of reprocessing — [#180]'s reporter measured 8,333 shared tokens at roughly 40 seconds on a local model.
37
+ [#890] found `@gotgenes/pi-permission-system` had been rewriting that region in place and moved the tool surface out, restoring the shared prefix from 365 characters to about 57,000 in this repo's configuration.
38
+
39
+ So a strategy that discards the region is correct for a re-homing host and a regression for a local-inference host.
40
+ The two cannot share a default.
41
+
42
+ ## Decision
43
+
44
+ A second strategy, `portable`, is available and **off by default**, selected per provider.
45
+
46
+ ### The identity is the operator's own text
47
+
48
+ A `portable` child's identity is composed from the parent's operator-authored prompt parts, in the order Pi's own `buildSystemPrompt` composes them: the custom prompt, the appended prompt, then the `<project_context>` block.
49
+ The result is what Pi would assemble for a session with a custom prompt and no tools or skills.
50
+
51
+ Three of Pi's `BuildSystemPromptOptions` fields are excluded, each for a reason already settled:
52
+
53
+ - `skills` — the child loads its own catalogue ([ADR 0006]).
54
+ - `promptGuidelines` — Pi derives it per session from the tools actually in the registry, so inheriting the parent's asserts guidance for tools the child may not hold.
55
+ That is the defect [ADR 0008] removed with the `<sub_agent_context>` block.
56
+ - `selectedTools` and `toolSnippets` — the tool surface is node-local prose ([its ADR 0014]).
57
+
58
+ Context files are **included**, and load-bearing: `createSubagentSession` builds the child's loader with `noContextFiles: true`, so this is the only way a portable child sees project instructions at all.
59
+
60
+ ### The provider selects the strategy, not the agent
61
+
62
+ ```json
63
+ { "promptInheritance": { "claude-bridge": "portable" } }
64
+ ```
65
+
66
+ Re-homing is a property of the **transport**, not of the agent.
67
+ An agent has no opinion about prompt inheritance; a provider has a requirement.
68
+
69
+ Keying on the agent — a frontmatter `inherit_prompt`, which the contributed reference implementation ranked above the provider rule — resolves wrongly under a per-spawn `model` override: an agent declaring `portable` because its own `model:` names a bridge model keeps that strategy when spawned onto the raw API, discarding the parent identity for a transport that never needed it.
70
+ Keying on the child's resolved provider is correct by construction, because the override changes the provider and therefore the strategy, with nothing to keep in sync.
71
+
72
+ A provider-**declared** policy would be better still — the provider knows whether it re-homes — but is not reachable: Pi's `ProviderConfigInput` carries no policy field.
73
+ The settings map is the available seam, not the preferred one.
74
+
75
+ ### The default does not change
76
+
77
+ Every provider the map does not list inherits `full`.
78
+ Flipping the default would change every existing child's prompt on a routine upgrade with no user edit, undo [#890]'s just-restored prefix for [#180]'s constituency, and — for a user without `pi-permission-system` — trade [#901]'s wrong tool list for no tool list at all, which is [#901]'s decision to make.
79
+
80
+ There is deliberately **no global default arm** on the setting.
81
+ A `"default": "portable"` would let one entry silently switch a local-model child, which is exactly the harm above; a re-homing host is per-provider by definition, so the map alone expresses every real case.
82
+
83
+ ### An unusable capture never falls back to the full prompt
84
+
85
+ An absent or whitespace-only portable capture falls back to the generic base, never to the parent's assembled prompt.
86
+ Opting into portable must never silently re-embed the harness base it exists to avoid.
87
+
88
+ ## Consequences
89
+
90
+ - A child on a listed provider carries none of Pi's base preamble, so a host that re-homes its prompt sees only text that host's own harness would have produced.
91
+ - `portable` is correct **only** where the host supplies its own base, and this is documented rather than enforced — Pi exposes no way to identify a re-homing provider.
92
+ Pointed at a provider that does not re-home, it is worse than `full`: `@gotgenes/pi-anthropic-auth` locates Pi's role line to shape the OAuth prompt and returns it unchanged when that line is absent, so such a child loses the neutral role prompt shaping would have substituted and gains nothing.
93
+ - The `full` path is unchanged, so [ADR 0008]'s guarantee and its pinning test are untouched.
94
+ - A child that resolved no model resolves to `full`.
95
+ This is defensive rather than reachable: Pi leaves the model unset only when no authenticated model exists at all, and such a parent cannot run a turn, emits no `before_agent_start`, and holds no capture to render.
96
+ - This package now registers a `before_agent_start` handler, its first.
97
+ It stores the capture and returns nothing, so [#901]'s planned handler extends it rather than competing with it.
98
+ - **Accepted residual:** a portable child whose parent has no context files, custom prompt, or append prompt falls back to `genericBase`, which claims write and exec capability a read-only child does not hold.
99
+ Tracked as [#904].
100
+ - `pi-claude-bridge` is fixing the projection on its own side ([bridge#89]), which would resolve [#883] for bridge users specifically.
101
+ `portable` remains the answer for any other re-homing host with no projection to fix.
102
+
103
+ The bisection and replay methodology that established the discriminator are `elidickinson`'s, in `pi-claude-bridge`'s `diag/EXTRA-USAGE-400.md`.
104
+ The capability, the capture seam, and the fail-safe fallback are @georgeharker's, contributed in [#884].
105
+
106
+ [#180]: https://github.com/gotgenes/pi-packages/issues/180
107
+ [#400]: https://github.com/gotgenes/pi-packages/issues/400
108
+ [#883]: https://github.com/gotgenes/pi-packages/issues/883
109
+ [#884]: https://github.com/gotgenes/pi-packages/pull/884
110
+ [#890]: https://github.com/gotgenes/pi-packages/issues/890
111
+ [#901]: https://github.com/gotgenes/pi-packages/issues/901
112
+ [#904]: https://github.com/gotgenes/pi-packages/issues/904
113
+ [ADR 0006]: 0006-inherited-prompt-is-identity-only.md
114
+ [ADR 0008]: 0008-inherited-region-is-shared-parts.md
115
+ [its ADR 0014]: https://github.com/gotgenes/pi-packages/blob/main/packages/pi-permission-system/docs/decisions/0014-tool-surface-is-node-local-prose.md
116
+ [bridge#89]: https://github.com/elidickinson/pi-claude-bridge/pull/89
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@jopqior/pi-subagents",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/public.d.ts",
8
+ "default": "./src/service/service.ts"
9
+ },
10
+ "./settings": {
11
+ "types": "./dist/settings.d.ts",
12
+ "default": "./src/layered-settings.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "src",
17
+ "dist",
18
+ "docs/*.md",
19
+ "docs/architecture",
20
+ "docs/decisions",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "imports": {
24
+ "#src/*": "./src/*",
25
+ "#test/*": "./test/*"
26
+ },
27
+ "description": "A focused, in-process sub-agent core for pi — autonomous agents plus a typed API and lifecycle events other extensions build on. Friendly fork of @tintinweb/pi-subagents.",
28
+ "author": {
29
+ "name": "Chris Lasher"
30
+ },
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Jopqior/gotgenes-pi-packages.git",
35
+ "directory": "packages/pi-subagents"
36
+ },
37
+ "homepage": "https://github.com/Jopqior/gotgenes-pi-packages/tree/main/packages/pi-subagents#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/Jopqior/gotgenes-pi-packages/issues"
40
+ },
41
+ "keywords": [
42
+ "pi-package",
43
+ "pi",
44
+ "pi-extension",
45
+ "subagent",
46
+ "agent",
47
+ "autonomous"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "peerDependencies": {
53
+ "@earendil-works/pi-ai": ">=0.75.0",
54
+ "@earendil-works/pi-coding-agent": ">=0.81.0",
55
+ "@earendil-works/pi-tui": ">=0.75.0"
56
+ },
57
+ "dependencies": {
58
+ "@sinclair/typebox": "^0.34.49"
59
+ },
60
+ "engines": {
61
+ "node": ">=22"
62
+ },
63
+ "devDependencies": {
64
+ "@biomejs/biome": "^2.5.11",
65
+ "@earendil-works/pi-ai": "0.84.4",
66
+ "@earendil-works/pi-coding-agent": "0.84.4",
67
+ "@earendil-works/pi-tui": "0.84.4",
68
+ "@types/node": "^22.15.3",
69
+ "rollup": "^4.63.1",
70
+ "rollup-plugin-dts": "^6.5.1",
71
+ "rumdl": "0.2.24",
72
+ "typescript": "^6.0.3",
73
+ "vitest": "^4.1.11"
74
+ },
75
+ "pi": {
76
+ "extensions": [
77
+ "./src/index.ts"
78
+ ],
79
+ "video": "https://github.com/gotgenes/pi-subagents/raw/main/media/demo.mp4",
80
+ "image": "https://github.com/gotgenes/pi-subagents/raw/main/media/screenshot.png"
81
+ },
82
+ "scripts": {
83
+ "check": "tsc --noEmit",
84
+ "build:types": "rollup -c rollup.dts.config.mjs",
85
+ "verify:public-types": "bash scripts/verify-public-types.sh",
86
+ "test": "vitest run",
87
+ "test:watch": "vitest",
88
+ "lint:md": "rumdl check *.md docs/**/*.md",
89
+ "lint": "biome check . && eslint . && pnpm run lint:md"
90
+ }
91
+ }