pi-pier 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +12 -11
  2. package/package.json +2 -2
  3. package/schemas/role-manifest.schema.json +7 -1
  4. package/skills/pi-herdr-workflow/SKILL.md +4 -5
  5. package/src/ansi-text.ts +62 -11
  6. package/src/ask-dialog.ts +308 -0
  7. package/src/ask-user.ts +113 -112
  8. package/src/bootstrap.ts +8 -12
  9. package/src/compact-coordinator.ts +278 -261
  10. package/src/compact-economics-core.ts +77 -145
  11. package/src/config-catalog-core.ts +77 -197
  12. package/src/config-command.ts +271 -64
  13. package/src/dashboard-command.ts +38 -19
  14. package/src/efficiency-config-core.ts +287 -484
  15. package/src/efficiency-store.ts +16 -29
  16. package/src/focus-poller.ts +14 -35
  17. package/src/gc-core.ts +10 -36
  18. package/src/herdr-client.ts +82 -208
  19. package/src/history-store.ts +36 -23
  20. package/src/index-gates.ts +124 -0
  21. package/src/index-locks.ts +2 -7
  22. package/src/index-master.ts +24 -18
  23. package/src/index-notices.ts +28 -15
  24. package/src/index-pipe.ts +17 -12
  25. package/src/index-roles.ts +306 -0
  26. package/src/index-runtime.ts +2 -6
  27. package/src/index-worker.ts +4 -8
  28. package/src/index.ts +305 -564
  29. package/src/jev-client.ts +12 -17
  30. package/src/jev-core.ts +137 -87
  31. package/src/ledger.ts +13 -21
  32. package/src/lock-core.ts +28 -56
  33. package/src/manifest-compose.ts +35 -20
  34. package/src/observation-core.ts +52 -100
  35. package/src/pane-title.ts +19 -38
  36. package/src/pi-surface.ts +34 -22
  37. package/src/pier-options.ts +54 -50
  38. package/src/pipe-channel.ts +27 -59
  39. package/src/platform-paths.ts +19 -52
  40. package/src/{core → plugins}/grid-shape.ts +8 -10
  41. package/src/{core → plugins}/heat-plan.ts +20 -22
  42. package/src/plugins/observation.ts +475 -0
  43. package/src/plugins/subagent.ts +728 -0
  44. package/src/{core → plugins}/terminal.ts +167 -127
  45. package/src/{core → plugins}/todo.ts +61 -100
  46. package/src/progress-core.ts +16 -53
  47. package/src/reconcile-core.ts +24 -33
  48. package/src/reducer-core.ts +24 -60
  49. package/src/reducer-invoker.ts +266 -239
  50. package/src/renderers.ts +76 -87
  51. package/src/role-loader.ts +33 -28
  52. package/src/role-manifest.ts +23 -28
  53. package/src/role-state.ts +119 -0
  54. package/src/routing-telemetry.ts +136 -0
  55. package/src/runtime-policy.ts +25 -40
  56. package/src/session-tail.ts +65 -41
  57. package/src/settle-wake-core.ts +52 -21
  58. package/src/slim-frame.ts +25 -122
  59. package/src/stale-core.ts +17 -24
  60. package/src/storage-layout.ts +14 -43
  61. package/src/subagent-core.ts +253 -118
  62. package/src/subagent-gc.ts +109 -67
  63. package/src/subagent-poller.ts +367 -24
  64. package/src/subagent-session.ts +516 -0
  65. package/src/subagent-spawn.ts +530 -52
  66. package/src/swallow.ts +7 -17
  67. package/src/terminal-core.ts +125 -106
  68. package/src/todo-core.ts +57 -109
  69. package/src/todo-read-hook.ts +23 -39
  70. package/src/todo-reminder-core.ts +26 -51
  71. package/src/todo-window.ts +11 -12
  72. package/src/todos-service.ts +15 -35
  73. package/src/tool-error.ts +6 -13
  74. package/src/tool-gate.ts +38 -21
  75. package/src/vocab.ts +52 -25
  76. package/src/ask-multi.ts +0 -260
  77. package/src/config-guide.ts +0 -361
  78. package/src/core/observation.ts +0 -555
  79. package/src/core/subagent.ts +0 -633
  80. package/src/efficiency-config-io.ts +0 -53
  81. package/src/git-adapter.ts +0 -113
  82. package/src/notice-buffer.ts +0 -23
  83. package/src/subagent-execute.ts +0 -328
  84. package/src/subagent-git-io.ts +0 -80
  85. package/src/subagent-launch.ts +0 -114
  86. package/src/subagent-list-action.ts +0 -69
  87. package/src/subagent-output-action.ts +0 -104
  88. package/src/subagent-output-adapter.ts +0 -27
  89. package/src/subagent-output-core.ts +0 -263
  90. package/src/subagent-poll-loop.ts +0 -305
  91. package/src/subagent-port.ts +0 -27
  92. package/src/subagent-ready.ts +0 -89
  93. package/src/subagent-registry.ts +0 -70
  94. package/src/subagent-resolution.ts +0 -41
  95. package/src/subagent-scope.ts +0 -41
  96. package/src/subagent-session-io.ts +0 -230
@@ -1,230 +0,0 @@
1
- /**
2
- * Child-session JSONL I/O: path resolution, settlement text, liveness probes.
3
- *
4
- * Why: pollLoop, foreground wait, and revive all shared the same candidate
5
- * order (reported path/id before recent-file fallback, excluding the parent
6
- * session). Keeping it in one adapter prevents settlement text from crossing sessions.
7
- */
8
- import { accessSync, constants, statSync } from 'node:fs';
9
- import type { HerdrClientLike } from './herdr-client.ts';
10
- import {
11
- deriveSubSessionState,
12
- lastAssistantText,
13
- listSessionFiles,
14
- readSessionFile,
15
- sessionFileById,
16
- type SessionEntryLike,
17
- type SubSessionState,
18
- } from './session-tail.ts';
19
- import type { AliveProbe } from './subagent-core.ts';
20
-
21
- export interface SessionIoHost {
22
- client: HerdrClientLike;
23
- getSessionId: () => string;
24
- sessionsDir: () => string;
25
- }
26
-
27
- export interface SessionIo {
28
- resolveSessionFileCandidates(paneId: string, cwd: string): Promise<string[]>;
29
- resolveSessionFile(paneId: string, cwd: string): Promise<string | null>;
30
- collectFinalText(paneId: string, cwd: string, sinceTs: number, attempts?: number): Promise<string | null>;
31
- readAskFlag(paneId: string): Promise<string | null>;
32
- probeAlive(paneId: string, cwd: string): Promise<AliveProbe>;
33
- subSessionState(paneId: string, cwd: string, sinceTs: number): Promise<SubSessionState>;
34
- }
35
-
36
- /** Cheap change fingerprint of a session file; null when it does not exist. */
37
- interface FileStamp {
38
- size: number;
39
- mtimeMs: number;
40
- }
41
-
42
- function stampOf(file: string): FileStamp | null {
43
- try {
44
- const s = statSync(file);
45
- return { size: s.size, mtimeMs: s.mtimeMs };
46
- } catch {
47
- return null;
48
- }
49
- }
50
-
51
- /** Readable regular file check: parsing the file would read it in full (up to MBs). */
52
- function isReadableFile(file: string): boolean {
53
- try {
54
- if (!statSync(file).isFile()) return false;
55
- accessSync(file, constants.R_OK);
56
- return true;
57
- } catch {
58
- return false;
59
- }
60
- }
61
-
62
- /**
63
- * Memo of values derived from a session file, keyed by path and invalidated by (size, mtime).
64
- *
65
- * Why: worker sessions grow to tens of MB, and the poll loop re-derives the same idle state every
66
- * tick (collectFinalText re-derives up to `attempts` times). Measured on a 31.6MB session:
67
- * file read + JSON.parse + derive ≈ 227ms, so an unchanged file must not be parsed twice.
68
- * An append-only JSONL changes size on every write, so (size, mtime) is a sound fingerprint.
69
- */
70
- class DerivedCache<T> {
71
- private readonly entries = new Map<string, { stamp: FileStamp; sinceTs: number; value: T }>();
72
- private readonly limit: number;
73
-
74
- constructor(limit = 32) {
75
- this.limit = limit;
76
- }
77
-
78
- get(file: string, stamp: FileStamp, sinceTs: number): T | undefined {
79
- const hit = this.entries.get(file);
80
- if (!hit || hit.stamp.size !== stamp.size || hit.stamp.mtimeMs !== stamp.mtimeMs || hit.sinceTs !== sinceTs) {
81
- return undefined;
82
- }
83
- return hit.value;
84
- }
85
-
86
- set(file: string, stamp: FileStamp, sinceTs: number, value: T): void {
87
- this.entries.delete(file);
88
- this.entries.set(file, { stamp, sinceTs, value });
89
- while (this.entries.size > this.limit) {
90
- const oldest = this.entries.keys().next().value;
91
- if (oldest === undefined) break;
92
- this.entries.delete(oldest);
93
- }
94
- }
95
- }
96
-
97
- export function createSessionIo(h: SessionIoHost): SessionIo {
98
- const stateCache = new DerivedCache<SubSessionState>();
99
- const finalTextCache = new DerivedCache<string | null>();
100
-
101
- async function resolveSessionFileCandidates(paneId: string, cwd: string): Promise<string[]> {
102
- const out: string[] = [];
103
- try {
104
- const reported = await h.client.getAgentSessionPath(paneId);
105
- if (reported) {
106
- if (/\.jsonl$/.test(reported)) out.push(reported);
107
- else {
108
- const byId = sessionFileById(cwd, h.sessionsDir(), reported);
109
- if (byId) out.push(byId);
110
- }
111
- }
112
- } catch {
113
- /* Reports may be unavailable during startup. */
114
- }
115
- const ownSession = h.getSessionId();
116
- for (const f of listSessionFiles(cwd, h.sessionsDir(), 4)) {
117
- if (f !== ownSession && !out.includes(f)) out.push(f);
118
- }
119
- return out;
120
- }
121
-
122
- async function resolveSessionFile(paneId: string, cwd: string): Promise<string | null> {
123
- for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
124
- if (isReadableFile(file)) return file;
125
- }
126
- return null;
127
- }
128
-
129
- async function collectFinalText(
130
- paneId: string,
131
- cwd: string,
132
- sinceTs: number,
133
- attempts = 12,
134
- ): Promise<string | null> {
135
- for (let i = 0; i < attempts; i++) {
136
- for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
137
- const stamp = stampOf(file);
138
- if (!stamp) continue;
139
- const cached = finalTextCache.get(file, stamp, sinceTs);
140
- if (cached !== undefined) {
141
- if (cached) return cached;
142
- continue;
143
- }
144
- const entries: SessionEntryLike[] | null = readSessionFile(file);
145
- if (!entries) continue;
146
- const text = lastAssistantText(entries, { sinceTs })?.text ?? null;
147
- finalTextCache.set(file, stamp, sinceTs, text);
148
- if (text) return text;
149
- }
150
- const wait = Promise.withResolvers<void>();
151
- setTimeout(wait.resolve, 500);
152
- await wait.promise;
153
- }
154
- return null;
155
- }
156
-
157
- async function readAskFlag(paneId: string): Promise<string | null> {
158
- try {
159
- const a = (await h.client.listAgents()).find((x) => x.paneId === paneId);
160
- const v = a?.tokens?.['pi-ask'];
161
- return typeof v === 'string' && v ? v : null;
162
- } catch {
163
- return null;
164
- }
165
- }
166
-
167
- async function probeAlive(paneId: string, cwd: string): Promise<AliveProbe> {
168
- const probe: AliveProbe = { paneExists: false, agentStatus: null, lastActivityMs: null };
169
- let listed = false;
170
- try {
171
- const pane = (await h.client.listPanes()).find((p) => p.paneId === paneId);
172
- listed = true;
173
- probe.paneExists = pane != null;
174
- probe.agentStatus = pane?.agentStatus ?? null;
175
- if (pane?.foregroundCwd) probe.foregroundCwd = pane.foregroundCwd;
176
- } catch {
177
- /* pane.list unavailable — agent.list is a degraded fallback, not a death signal */
178
- }
179
- try {
180
- const a = (await h.client.listAgents()).find((x) => x.paneId === paneId);
181
- if (a) {
182
- if (!listed) probe.paneExists = true;
183
- probe.agentStatus = a.status ?? probe.agentStatus;
184
- if (a.foregroundCwd) probe.foregroundCwd = a.foregroundCwd;
185
- }
186
- } catch {
187
- /* agent.list is status-only; absence is not death */
188
- }
189
- for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
190
- const stamp = stampOf(file);
191
- // Session files can disappear during candidate scanning; a vanished file has no activity.
192
- if (!stamp) continue;
193
- if (probe.lastActivityMs == null || stamp.mtimeMs > probe.lastActivityMs) probe.lastActivityMs = stamp.mtimeMs;
194
- }
195
- return probe;
196
- }
197
-
198
- async function subSessionState(
199
- paneId: string,
200
- cwd: string,
201
- sinceTs: number,
202
- ): Promise<SubSessionState> {
203
- for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
204
- const stamp = stampOf(file);
205
- // A missing or vanished path is skipped: herdr often reports a .jsonl path before the worker
206
- // creates the file, and waitAgent(idle) returns immediately, which used to throw
207
- // `Cannot read properties of null (reading 'length')` (session 01a055c5).
208
- if (!stamp) continue;
209
- const cached = stateCache.get(file, stamp, sinceTs);
210
- if (cached) return cached;
211
- const entries = readSessionFile(file);
212
- // An empty/unparsable file has no state to cache; the next candidate may still have one.
213
- if (!entries?.length) continue;
214
- const state = deriveSubSessionState(entries, sinceTs);
215
- stateCache.set(file, stamp, sinceTs, state);
216
- return state;
217
- }
218
- return { text: null, pendingTool: false, activity: false, turnEnded: false };
219
- }
220
-
221
- return {
222
- resolveSessionFileCandidates,
223
- resolveSessionFile,
224
- collectFinalText,
225
- readAskFlag,
226
- probeAlive,
227
- subSessionState,
228
- };
229
- }
230
-