pi-pier 0.3.1 → 0.4.2

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 (54) hide show
  1. package/README.md +7 -5
  2. package/package.json +3 -3
  3. package/schemas/efficiency-config.schema.json +6 -0
  4. package/src/ask-dialog.ts +9 -2
  5. package/src/ask-user.ts +142 -74
  6. package/src/bootstrap.ts +2 -1
  7. package/src/compact-coordinator.ts +207 -35
  8. package/src/compact-economics-core.ts +62 -29
  9. package/src/config-catalog-core.ts +1 -0
  10. package/src/cordis-services.ts +64 -0
  11. package/src/dashboard-command.ts +40 -15
  12. package/src/delegation-core.ts +115 -0
  13. package/src/efficiency-config-core.ts +17 -6
  14. package/src/efficiency-store.ts +43 -6
  15. package/src/guards.ts +6 -0
  16. package/src/herdr-client.ts +141 -36
  17. package/src/history-store.ts +2 -0
  18. package/src/index-gates.ts +6 -12
  19. package/src/index-master.ts +12 -16
  20. package/src/index-pipe.ts +6 -0
  21. package/src/index-runtime.ts +14 -0
  22. package/src/index-worker.ts +4 -10
  23. package/src/index.ts +310 -76
  24. package/src/jev-client.ts +32 -7
  25. package/src/jev-core.ts +71 -36
  26. package/src/observation-core.ts +13 -1
  27. package/src/panel-open.ts +47 -0
  28. package/src/pier-options.ts +1 -2
  29. package/src/pipe-channel.ts +9 -0
  30. package/src/plugins/heat-plan.ts +18 -3
  31. package/src/plugins/observation.ts +118 -46
  32. package/src/plugins/subagent.ts +119 -68
  33. package/src/plugins/terminal.ts +221 -57
  34. package/src/plugins/todo.ts +63 -63
  35. package/src/reducer-core.ts +3 -5
  36. package/src/reducer-invoker.ts +27 -13
  37. package/src/renderers.ts +25 -2
  38. package/src/role-manifest.ts +2 -3
  39. package/src/roles/worker-default.json +2 -2
  40. package/src/settle-wake-core.ts +17 -3
  41. package/src/subagent-core.ts +209 -3
  42. package/src/subagent-poller.ts +69 -19
  43. package/src/subagent-session.ts +67 -4
  44. package/src/subagent-spawn.ts +55 -7
  45. package/src/terminal-core.ts +178 -50
  46. package/src/todo-core.ts +47 -2
  47. package/src/todo-read-hook.ts +54 -17
  48. package/src/todo-reminder-core.ts +9 -5
  49. package/src/todo-window.ts +8 -4
  50. package/src/todos-service.ts +12 -0
  51. package/src/tree-view.ts +162 -0
  52. package/src/vocab.ts +7 -1
  53. package/src/worker-marker.ts +76 -0
  54. package/src/focus-poller.ts +0 -223
@@ -1,223 +0,0 @@
1
- /**
2
- * D-4: herdr 0.9.1+ delivers `pane.focused`, so default polling is off (0 ms) there — a sampler
3
- * would reflow a second time ~8 s after a click; older servers keep the 1500 ms sampler.
4
- * `PIER_FOCUS_POLL_MS` overrides either default, 0 disables.
5
- */
6
- import { spawn } from 'node:child_process';
7
- import { dirname, join } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- /** Poll cadence for older Herdr (< 0.9.1). */
11
- export const FOCUS_POLL_DEFAULT_MS = 1500;
12
- /** Herdr 0.9.1+: native pane.focused is authoritative; default poller off to avoid a delayed second reflow. */
13
- export const FOCUS_POLL_HERDR_091_MS = 0;
14
-
15
- export function isHerdr091OrLater(version?: string | null): boolean {
16
- if (!version) return false;
17
- const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
18
- if (!m) return false;
19
- const major = Number(m[1]);
20
- const minor = Number(m[2]);
21
- const patch = Number(m[3]);
22
- if (major > 0) return true;
23
- if (minor > 9) return true;
24
- if (minor === 9 && patch >= 1) return true;
25
- return false;
26
- }
27
-
28
- export function resolveDefaultFocusPollMs(herdrVersion?: string | null): number {
29
- return isHerdr091OrLater(herdrVersion) ? FOCUS_POLL_HERDR_091_MS : FOCUS_POLL_DEFAULT_MS;
30
- }
31
- /** Min gap between reflow triggers: re-clicking the same pane must not spawn a process per click (the
32
- * workbench debounces 150 ms per tab on top of this). */
33
- export const FOCUS_FIRE_MIN_INTERVAL_MS = 700;
34
-
35
- export interface FocusSample {
36
- /** Pane focused in the sampled tab (null when the server reported no focus). */
37
- readonly focusedPaneId: string | null;
38
- /** Panes currently in that tab, used to tell a click apart from a spawn auto-focus. */
39
- readonly paneIds: readonly string[];
40
- }
41
-
42
- export interface FocusPollerState {
43
- readonly lastFocusedPaneId: string | null;
44
- readonly lastPaneIds: readonly string[];
45
- readonly lastFireAt: number;
46
- }
47
-
48
- /** Minimal flatten of a herdr layout tree (`{type:'pane',pane_id}` / `{type:'split',first,second}`). */
49
- export function collectPaneIds(root: unknown): string[] {
50
- const out: string[] = [];
51
- const walk = (node: unknown): void => {
52
- if (node === null || typeof node !== 'object') return;
53
- const n = node as { pane_id?: unknown; first?: unknown; second?: unknown };
54
- if (typeof n.pane_id === 'string') {
55
- out.push(n.pane_id);
56
- return;
57
- }
58
- walk(n.first);
59
- walk(n.second);
60
- };
61
- walk(root);
62
- return out;
63
- }
64
-
65
- /** `layout.export` payload → sample. `focusedPaneId` is optional for older clients. */
66
- export function parseFocusSample(
67
- layout: { root: unknown; focusedPaneId?: string | null } | null | undefined,
68
- ): FocusSample | null {
69
- if (!layout || !('root' in layout)) return null;
70
- const paneIds = collectPaneIds(layout.root);
71
- const focused = typeof layout.focusedPaneId === 'string' ? layout.focusedPaneId : null;
72
- return { focusedPaneId: focused, paneIds };
73
- }
74
-
75
- export interface FocusTick {
76
- readonly fire: boolean;
77
- readonly cause: 'user' | null;
78
- readonly state: FocusPollerState;
79
- }
80
-
81
- /**
82
- * Decide whether one sample should trigger a reflow: only on a *transition* into "I am focused",
83
- * and at most once per `minIntervalMs`. `cause` is 'user' when the pane set did not change in the
84
- * same sample (a click moves focus and nothing else, a spawn auto-focus comes with a new pane), so
85
- * the workbench can keep its pane-age whitelist. Baseline sample never fires.
86
- */
87
- export function planFocusTick(opts: {
88
- myPaneId: string;
89
- sample: FocusSample;
90
- prev: FocusPollerState | null;
91
- now: number;
92
- minIntervalMs?: number;
93
- }): FocusTick {
94
- const minInterval = opts.minIntervalMs ?? FOCUS_FIRE_MIN_INTERVAL_MS;
95
- const prev = opts.prev;
96
- const next: FocusPollerState = {
97
- lastFocusedPaneId: opts.sample.focusedPaneId,
98
- lastPaneIds: opts.sample.paneIds,
99
- lastFireAt: prev?.lastFireAt ?? 0,
100
- };
101
- const noFire = (): FocusTick => ({ fire: false, cause: null, state: next });
102
-
103
- if (opts.myPaneId === '') return noFire();
104
- // First sample only records the baseline: a pi that starts in the focused pane must not reflow on
105
- // startup (that is not a click, and it would fight whatever layout the user had).
106
- if (prev === null) return noFire();
107
- if (opts.sample.focusedPaneId !== opts.myPaneId) return noFire();
108
- if (prev.lastFocusedPaneId === opts.myPaneId) return noFire();
109
- if (opts.now - prev.lastFireAt < minInterval) return noFire();
110
-
111
- const setUnchanged =
112
- prev.lastPaneIds.length === opts.sample.paneIds.length &&
113
- prev.lastPaneIds.every((id, i) => id === opts.sample.paneIds[i]);
114
- return {
115
- fire: true,
116
- cause: setUnchanged ? 'user' : null,
117
- state: { ...next, lastFireAt: opts.now },
118
- };
119
- }
120
-
121
- export interface FocusPollerDeps {
122
- /** Layout of the caller's own tab (the client already exposes `layout.export`). */
123
- sample(): Promise<FocusSample | null>;
124
- /** Replay a pane.focused reflow for `paneId`. Must never throw. */
125
- fire(paneId: string, cause: 'user' | null): void;
126
- myPaneId: string;
127
- intervalMs?: number;
128
- minIntervalMs?: number;
129
- now?: () => number;
130
- /** Diagnostics only: polling failures are expected while herdr restarts. */
131
- onError?: (err: unknown) => void;
132
- }
133
-
134
- export interface FocusPoller {
135
- /** One sampling pass (exposed for tests; the interval calls the same function). */
136
- tick(): Promise<void>;
137
- stop(): void;
138
- }
139
-
140
- export function startFocusPoller(deps: FocusPollerDeps): FocusPoller {
141
- const intervalMs = deps.intervalMs ?? FOCUS_POLL_DEFAULT_MS;
142
- const now = deps.now ?? Date.now;
143
- let state: FocusPollerState | null = null;
144
- let inFlight = false;
145
- let stopped = false;
146
- let handle: NodeJS.Timeout | null = null;
147
-
148
- async function tick(): Promise<void> {
149
- if (stopped || inFlight) return;
150
- inFlight = true;
151
- try {
152
- const sample = await deps.sample();
153
- if (sample === null) return;
154
- const decision = planFocusTick({
155
- myPaneId: deps.myPaneId,
156
- sample,
157
- prev: state,
158
- now: now(),
159
- minIntervalMs: deps.minIntervalMs,
160
- });
161
- state = decision.state;
162
- if (decision.fire) deps.fire(deps.myPaneId, decision.cause);
163
- } catch (err) {
164
- // A failed sample must not advance the baseline: the next tick re-reads the layout.
165
- deps.onError?.(err);
166
- } finally {
167
- inFlight = false;
168
- }
169
- }
170
-
171
- if (intervalMs > 0) {
172
- handle = setInterval(() => { void tick(); }, intervalMs);
173
- handle.unref?.(); // never keep pi's event loop alive on our account
174
- }
175
-
176
- return {
177
- tick,
178
- stop(): void {
179
- stopped = true;
180
- if (handle !== null) {
181
- clearInterval(handle);
182
- handle = null;
183
- }
184
- },
185
- };
186
- }
187
-
188
- /** The workbench's reflow entry point; overridable for relocated checkouts. */
189
- export function reflowScriptPath(env: NodeJS.ProcessEnv = process.env): string {
190
- const root = env.PIER_WORKBENCH_ROOT?.trim();
191
- if (root) return join(root, 'scripts', 'heat-reflow.mjs');
192
- // packages/pier-ext/src/focus-poller.ts -> packages/pier-workbench/scripts/heat-reflow.mjs
193
- return join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'pier-workbench', 'scripts', 'heat-reflow.mjs');
194
- }
195
-
196
- export interface SpawnReflowOpts {
197
- paneId: string;
198
- cause: 'user' | null;
199
- env?: NodeJS.ProcessEnv;
200
- /** Injection seam for tests (defaults to child_process.spawn). */
201
- spawnFn?: typeof spawn;
202
- }
203
-
204
- /** Fire-and-forget replay of the `pane.focused` hook as a child process — exactly how herdr runs it,
205
- * so the workbench script keeps its own event parsing, debounce and state file. */
206
- export function spawnReflow(opts: SpawnReflowOpts): void {
207
- const env = opts.env ?? process.env;
208
- const spawnFn = opts.spawnFn ?? spawn;
209
- const payload = JSON.stringify({
210
- event: 'pane_focused',
211
- type: 'pane_focused',
212
- data: { type: 'pane_focused', pane_id: opts.paneId, cause: opts.cause },
213
- });
214
- try {
215
- const child = spawnFn(process.execPath, [reflowScriptPath(env)], {
216
- env: { ...env, HERDR_PLUGIN_EVENT: 'pane.focused', HERDR_PLUGIN_EVENT_JSON: payload },
217
- stdio: 'ignore',
218
- });
219
- child.on?.('error', () => { /* Missing script / dead exec: polling must never break pi. */ });
220
- child.unref?.();
221
- } catch {
222
- }
223
- }